Relative Sanity

a journal

No fast forward

10 October, 2013

I feel bad. I have something to confess.

I lied to you.

Hey, it doesn’t happen often, and it was for your own good. It was just a little white lie. I told you that when I merge branches with git, I merge by saying:

git checkout master
git merge awesome_feature

I missed something out. See, how I actually merge is:

git merge --no-ff awesome_feature

What’s this --no-ff nonsense, then? The straightforward answer is that it means I want git to merge the branch with “no fast-forward”. The question then is “what’s a fast-forward”?

Good question, glad you asked.

Two parents? Unnatural!

In git, each commit you make generally has two things that identify it: a SHA (a name, if you like), and a parent. The parent is the SHA of the commit that this one is based on. You can think of it a bit like this:

As you know, a branch is just a label on one of the commits. Let’s say we have our awesome branch:

We’ve made some commits to it, and we’re ready to merge to master. But lo, someone else in our team has beaten us to the punch: they’ve already made some commits to master:

Whatever are we to do? Let’s assume here that there are no conflicts (for simplicity). We can still merge. By default, git will perform the merge like this:

It creates a new commit—a merge commit—which has two parents: the head of our awesome branch, and the previous head of master. Note that the commits in neither the master nor awesome branches have changed their parents. In addition, the contents of the merge commit’s commit message will by default record the fact that I merged awesome into master.

The practical effect of this is twofold: first, there’s a log of the fact that these commits were part of a branch. If you’re developing features on branches, this means that in six months, when you find that there’s a bug and git blame points you to a commit on this branch, you’ll be able to see that it was merged into master as part of a feature. That might give you a clue as to why you wrote the code that way.

The other thing is that, if you decide you want to get back on to that branch and continue working, it’s dead easy: create a branch with that name, reset it back to the SHA before the merge, and you’re off to the races.

Oh, and you also get access to the awesome git log --merges, which plucks each merge from the log for your currently checked-out commit (thanks to Murray for taking me to school on that).

But…

Of course there’s a but. Consider this, again:

Our branch is ready to merge into master, but wait: this time, nobody beat you to it. By default, git does this:

That’s right, it performs the “merge” by renaming your awesome branch to master. Six months from now, all traces of that branch (where it started, where it ended and so on) are gone.

This is the “fast-forward” merge, and while it may seem neat and tidy at the time, it’s throwing away essential historical information. And the whole point of using a revision control system in the first place is to record historical information.

See the light!

So “fast-forward” merges should be considered harmful. We can force git to always perform a non-fast-forward merge by passing the --no-ff option to the merge command. That way, we end up with something like this:

In six months, the fact that this was a branch remains intact, which makes for happy debugging.

Typing’s hard. Let’s use CONFIGURATION!

I’d suggest you always want to use non-fast-forward merges: you always want those precious merge commits, those markers of branches that were. To make sure you never forget, say this in a shell session:

git config --global --add merge.ff false

Now, simply saying git merge awesome will work as expected. Which insider knowledge lets you know that I probably didn’t lie to you last time around after all.

Frankly, I’m a little hurt that you ever doubted my honesty.