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
--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.
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
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
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
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
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).
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.
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.
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.