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 or develop.
  • 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#L32

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.