Git feature branches
How to use Git to manage new features in your app – branching, merging, rebasing.
Preliminary
At GRRR we use the OneFlow model for new projects, but we support legacy projects using the Git Flow model as described in A Successful Git Branching Model.
The branching workflow is more or less the same for both models. Git Flow will branch mostly to and from develop
, with OneFlow this all happens on master
.
In summary:
- Feature branches merge to and from
master
ordevelop
. - Within your branch you commit changes however you like.
- When your feature is done, submit a Pull Request.
- A reviewer will review your code and might propose changes.
- When you’re done, rebase your branch to ensure an informative commit history.
- Merge your feature into the main branch, preferably with fast-forward.
- Delete the feature branch.
Feature branches
What is a good way to manage feature branches? And in a broader sense, what do you want your Git history to look like? Let’s answer the latter question first to get an idea for the former question.
Git commit messages
90% of the time, when I look at the Git history of any repository, I want to quickly see which features are enabled by the given (groups of) commits. I want to answer the question “What was intended here?”.
And if the commit message answers that question I can checkout the commit hash and look at how the intented behavior was achieved.
Ideally I would be able to switch features on and off by going back and forth in the commit history.
A bad example would be this:
- start email feature
- WIP: added sending of notifications
- Fixed missing subject line
- Changed variable names to camelcase
- Fixed undefined var from previous commit
- Allow multiple recipients
Where a good example would just be:
- Send email notifications
If I ever want to go back to before the app sent email notifications, I could just go a step back into history and checkout the situation. Note that there’s no hard and fast rule. You, the developer has to decide which commits are individually interesting to retain for future reference. Most of the time though, I think a handful commits will suffice.
We’re all humans. Sometimes we make mistakes. Sometimes we forget to update the documentation for our feature. How do we prevent those commits from muddying the commit history?
Lifetime of a feature branch
Ideally, when working in a team, your process looks like this:
- You branch off
master
and create a new feature branch:feature/email-notification
. - You create commits that form your new feature.
- You rebase your commits into a package that adds value to your commit history. Honestly, most of the time this should probably be just a single commit describing the feature it adds.
- You publish your feature branch and receive a code review.
- You process the feedback given in the review, rebase the new commits to not muddy your neat package and push again.
- Your branch gets accepted and merged into
master
(We use GitHub’s Rebase and merge, but use whichever you prefer)
This will result in a clean git history that describes only what features were added when and by whom and not so much how you did it in ways that are inappropriate for a git commit message.
Starting a branch
Create a new branch, forking off master
:
$ git checkout -b email-notifications master
Committing
You create commits, however they make sense to you. A feature branch is your personal workspace, it doesn’t matter what you do as long as it’s contained to your own branch.
If you wish to create a hundred small commits so you can skip back and forth in your process, do it. Whatever makes you productive. It doesn’t matter at this point, because in the next step we’re going to prepare our glorious mess into a pretty package.
Having said that, it’s probably a good idea to get familiar with the amend
flag:
$ git commit --amend
This allows you to merge your current and previous commits, optionally tweaking the commit message. It’s the easiest way to merge two commits and super helpful whenever you forgot something in the previous commit, or made a typo or whatever.
💡 Pro-tip: grab this git alias to quickly amend unto the previous commit: https://github.com/harmenjanssen/dotfiles/blob/master/gitconfig#L27
Rebasing
Your feature is done! Let’s have a look at the commit history:
0f75d8c Added HTML email template
4b7a292 Fixed typo in docs
b61b840 Added documentation
692e21e Allow admin to modify email subject
e006f04 Add email notifications
Alright, that’s five separate commits. We can do better and make this a bit more presentable for the reviewer. Let’s use interactive rebase to do just that:
$ git rebase HEAD~5 -i
In this command, -i
means interactive, HEAD~5
means to rebase the last 5 commits.
Your $EDITOR
will be opened and you’re presented with the following document:
pick e006f04 Add email notifications
pick 692e21e Allow admin to modify email subject
pick b61b840 Added documentation
pick 4b7a292 Fixed typo in docs
pick 0f75d8c Added HTML email template
In the commented section of the document is an explanation of all the rebase options:
- p, pick = use commit
- r, reword = use commit, but edit the commit message
- e, edit = use commit, but stop for amending
- s, squash = use commit, but meld into previous commit
- f, fixup = like “squash”, but discard this commit’s log message
- x, exec = run command (the rest of the line) using shell
- d, drop = remove commit
You can now change the list of commits using the various rebase options. For instance, if you would save and quit with the following list:
pick e006f04 Add email notifications
fixup 692e21e Allow admin to modify email subject
fixup b61b840 Added documentation
fixup 4b7a292 Fixed typo in docs
pick 0f75d8c Added HTML email template
Git will squash the middle three commits into the top one, and discard their commit messages. You will be left with a new history looking like this:
0f75d8c Added HTML email template
e006f04 Add email notifications
The first three commits have become one, leaving only the first and last commits. Great! Much better for code review, and much easier to parse next year when you just want an overview of added features.
I don’t like the way those verbs are used though. One commit says Added
, another says Add
. Sloppy! Let’s correct that.
$ git rebase HEAD~2 -i
If we save the file like this:
pick e006f04 Add email notifications
reword 0f75d8c Added HTML email template
We will be dropped into an editor to correct the Added HTML email template
message. If we save this as Add HTML email template
, the history looks consistent again:
0f75d8c Add HTML email template
e006f04 Add email notifications
💡 Pro-tip: grab this git alias to drop into interactive rebase starting at the first commit of your branch: https://github.com/harmenjanssen/dotfiles/blob/master/gitconfig#L49
For example git rbi master
would find the commit where you branched off master
and allows you to rebase all commits from that point on.
Code review
Great! Push your work (git push -u origin my-feature
) to the remote, log into GitHub and create a new Pull Request.
The reviewer checks your code and alas, finds a typo.
Pushing
Now there’s a problem: your code is sent to the remote, meaning you can no longer safely amend or rebase. This would change the history of your repository and changing shared history is a cardinal sin.
So you’re left with a history looking like this:
1fed98c Fix typos
0f75d8c Add HTML email template
e006f04 Add email notifications
Damn. You will notice when you rebase this, or add --amend
to the commit, git tells you you have diverged and should both pull and push. It’s annoying and can be quite confusing:
Your branch and 'origin/my-feature' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
The answer is two-pronged: if you’re the only developer working on the branch, force your push. Change history!
$ git push origin my-feature -f
But if you’re collaborating with others on the same branch, refrain from amending or rebasing pushed commits. Wait until you’re merging, and right before merging into the parent branch, do a quick rebase to tidy up.
Merging
All done? Great. Either do a last rebase, as described in the previous session to tidy up, or go ahead and merge the commits as they are.
Note that you can (and maybe should) git rebase master
to grab any commits that were added to the master
branch since you branched off on your feature branch.
At GRRR we prefer merging with fast-forward, to avoid creating merge commits. Merge commits can be noisy and don’t really add anything to a git history.
A way to ensure this is the --ff-only
flag. This ensures a branch is up-to-date before it can be merged into master
.
$ git checkout master
$ git merge --ff-only my-feature
💡 This corresponds to GitHub’s Rebase and merge option when merging a Pull Request in its web interface.
Cleaning up
That’s it! You’re done. You can remove your branch, local and from the remote:
$ git br -d my-feature
$ git push origin :my-feature
Note the colon :
in the last line: it tells git to delete the branch from the remote.
Further rebase tricks
Rebasing when pulling
To avoid merge commits when pulling, use the --rebase
flag:
$ git pull --rebase
This rewinds your commits, pulls the remote and replays your work, avoiding a merge commit and adding your work to the top of the timeline.
Changing commit order
When rebasing interactively, you can change the order of the lines to change commit order. Just remember that you might conflict with yourself when you change into an impossible order. (for instance when a commit references code from a previous commit)
Auto-squash and auto-fixup
git commit
supports --squash
and --fixup
flags. They will mark commits as such when rebasing interactively.
See https://robots.thoughtbot.com/autosquashing-git-commits for more information.