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