This morning I discovered a nasty little problem with git-rebase that can have pretty unexpected and unwanted results - how it handles a merge commit.
The TL;DR version is this: Always use git rebase -p
I think a lot of people are using git pull --rebase
as their default to avoid unnecessary merge commits when fetching the latest code from master. There are a few blog posts on the matter, such as [1] [2]
For me, it boils down to two simple cases:
In this case, pull
and pull --rebase
will simply fast-forward. No problems.
In this case, the default pull
will actually merge the remote changes into your branch, making a merge commit. This is bad for a couple of reasons, messiness is one, but I actually consider the problems it causes for git bisect
more compelling (I must remember to write about that one day).
With git pull --rebase
, you simply replay those commits on top of the new head. Now, if you push, you have linear history, rather than a divergence/merge. I'm a big fan of the 'always work on a branch' and 'merges are meaningful and good' mindset (partly inspired by [3]), but I believe linear history is exactly appropriate when you're talking about working with remotes - I consider master
and origin/master
to be semantically the same thing.
By and large, git pull --rebase
is what you want. To make it the default, see [4].
This is a bit of an understatement. But let's work through an example.
Given this simple repo:
[master] git pull --rebase
First, rewinding head to replay your work on top of it...
Applying: little fix
Applying: forgot to push this
[master] git push
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 526 bytes, done.
Total 5 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (5/5), done.
To /Users/glen/envato/demo-origin
f9c3cb8..e4a2e92 master -> master
Easy!
Say you've been working on your little feature for a while, like this:
Then you merge to master (using --no-ff
, of course)
[master] git merge --no-ff feature
Merge made by recursive.
b | 3 +++
1 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 b
Then you go to push, but somebody got in there first (origin/master
has moved on)
[master] git push
To /Users/glen/envato/demo-origin
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to '/Users/glen/envato/demo-origin'
To prevent you from losing history, non-fast-forward updates were rejected
Merge the remote changes (e.g. 'git pull') before pushing again. See the
'Note about fast-forwards' section of 'git push --help' for details.
Of course, trying to push hasn't updated our reference to origin/master
, we need to git fetch
to see the full picture
[master] git fetch
remote: Counting objects: 5, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /Users/glen/envato/demo-origin
49ab1cf..9f3e34d master -> origin/master
If we pull --rebase
here, DOOM OCCURS
[master] git pull --rebase
First, rewinding head to replay your work on top of it...
Applying: my work
Applying: my work
Applying: my work
Our merge commit has disappeared!
This is bad for a whole lot of reasons. For one, the feature commits are actually duplicated, when really I only wanted to rebase the merge. If you later merge the feature branch in, both commits will be in the history of master. And origin/feature
, which supposed to be finished and in master, is left dangling. Unlike the awesome history that you get from following a good branching/merging model, you've actually got misleading history.
Worst of all, you did everything 'right'. You used merge --no-ff
and git pull --rebase
. Sad face.
In case it's not obvious, this is the ideal outcome:
In the past, I've noticed the three 'Applying:' lines in the log of a pull -rebase
and realised something was up. Then I'd reset and reapply the merge:
[master] git reset --hard origin/master
HEAD is now at 9f3e34d sneaky extra commit
[master] git merge --no-ff feature
Merge made by recursive.
b | 3 +++
1 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 b
In the manpage for git-rebase
-p
--preserve-merges
Instead of ignoring merges, try to recreate them.
This uses the --interactive machinery internally, but combining it with the --interactive option explicitly
is generally not a good idea unless you know what you are doing (see BUGS below).
Or, to put it another way:
So, instead of using git pull --rebase
, use a git fetch origin
and git rebase -p origin/master
[master] git rebase -p origin/master
Successfully rebased and updated refs/heads/master.
And it does exactly what we want!
The -p
flag doesn't apply to git pull --rebase
, so you have to start explicitly fetching and rebasing. To be honest, I think this is more an upside. Fetching explicitly is good, since it refreshes your entire copy of the remote, and lists what branches have moved on (handy on a fast-moving project). But for those used to a single-step pull, this is slightly more work.
ORIG_HEAD, once you get used to using it, is really handy to undo a destructive operation. Sadly, something within git rebase -p
sets ORIG_HEAD more than once, so you can't use it to quickly undo a rebase, something I ran in to working on this post.
Unlike git pull --rebase
, which will fetch changes from the branch your current branch is tracking, git rebase -p
doesn't have a sensible default to work from. You have to give it a branch to rebase onto.
So, how about some aliases to make this all idiot-proof? I like gup
, and I've got it in a gist for bash, fish and zsh here
It'll do a fetch of origin and rebase -p
of the branch on origin with the same name as the current branch. For 99% of cases, this is exactly what I want.
This morning, I had no idea about the --preserve-merges
flag on rebase, and was about ready to cry foul on using rebase at all, considering how bad this problem can get on a big project. But, as with everything Git, once you understand it a bit better, there's usually a more complex way that sucks a whole lot less. Which is aliases like gup
are really handy.
I'd welcome comments and suggestions, you can either respond to this Gist or hit me up on twitter - @glenmaddern
Cheers,
-geelen.