I’m one of the devs here at Envato, and this is my first post to the Notes blog. Having found myself in a role I would cautiously describe as ‘resident Git expert’, it’s only fitting that my first post would be about a fairly technical aspect of working with Git in a team environment.
The TL;DR version is this: When rebasing, always use the -p flag,
First, though, a small diversion – why rebase is part of my normal git workflow.
Why I starting using pull –rebase
Using git pull --rebase is becoming more popular 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]
I’ll give a brief summary of why this convinced me. For me, it boils down to two simple cases:
1. You haven’t made any changes to your branch
In this case, pull and pull --rebase will simply fast-forward. No problems.
2. You’ve got one or two small changes you forgot to push
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 think this is a better result. Usually, I follow the ‘always work on a branch’ and ‘merges are meaningful and good’ practice (partly inspired by [3]), but there’s no semantic difference between master and origin/master, so linear history makes sense.
So in general, git pull --rebase is better than a git pull. To make it the default, see [4].
There is one major problem with it, though – merge commits.
Rebasing deletes merge commits
This is best explained with an example
First, one that doesn’t fail
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

Linear history, just as we wanted!
All aboard the failboat
Say you’ve been working on your little feature for a while, like this:

Then you merge to master (using --no-ff, of course [3])
[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

Ah, yes. Someone has pushed a commit ‘sneaky extra commit’ before we were able to push our commit (merging in of branch feature). So, we would normally just git pull --rebase to get ready to push, but if we do that, the merge commit gets deleted!
[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 again, 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.
For example, if someone looks at the branches on origin, it’ll appear that origin/feature hasn’t been merged into master, even though it has! Which can cause all kinds of problems if that person then does a deploy. It’s just bad news all round.
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 what we wanted to happen:

You can recover from this situation (if you discover it before you push) by resetting and redoing 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
The solution!
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 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

[master] git rebase -p origin/master
Successfully rebased and updated refs/heads/master.

Win!
Downsides
Git pull is dead
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 is no longer preserved
ORIG_HEAD, once you get used to using it, is really handy to undo a destructive operation. Sadly, git rebase -p sets ORIG_HEAD for each commit being rebased, so you can’t use it to quickly return to the start of a rebase, something I ran in to working on this post.
Branch tracking is not used
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. With a good alias, however, that can be made painless.
Aliases
So, how about some aliases to make this all idiot-proof? I’ve decided to call mine gup (I’ve taken to calling it gee-up), and I’ve got it in a gist for bash, fish and zsh here. I’ve also included my gpthis alias for pushing without branch tracking.
gup will 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.
Conclusion
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 why aliases like gup are really handy – you can keep changing what your aliases mean without having to learn a new habit.
I’d welcome comments and suggestions, you can either reply here or hit me up on twitter – @glenmaddern







You mentioned you need to write about the problems with merge commits and git-bisect. That was my primary point at http://darwinweb.net/articles/the-case-for-git-rebase
FWIW, I’ve come to the conclusion that I prefer rebasing whenever convenient (ie. for topic branches too) because if a bug shows up later, I’d rather have git-bisect show me exactly where it is introduced. The fact that some series of commits was once a topic branch is obfuscated by rebasing, but there are still clues in the dates of the commits. It’s true that this may make it harder to piece together developer intent after the fact, but in practice I haven’t found it worth it to perform the mental exercise of reconciling a bunch of branches to figure out what someone was thinking.
Great post. The sooner we can get everybody to rebase their commits properly before pushing, the better.
I posted my own version of `gup` a little while ago at http://jasoncodes.com/posts/gup-git-rebase which also handles stashing any local unstaged changes before rebasing. Prompted by your post, I have just added a note about merge commits.
Cheers.
Nice work, gives me more confidence towards moving off svn and onto Git soon.
Sounds like you don’t trust Git too much. Keep in mind that even though this annoyance that he’s describing is a legitimate concern, it’s one of those problems that come with a system that satisfies much more than SVN could ever. Git’s much more reliable in a million ways, and I could honestly never see myself going back.
You do realize that you’re going through a huge amount of work to make Git behave an awful lot like CVS.
Not that this is wrong. It’s a perfectly reasonable thing to do. But it seems like the fundamental data model which all modern VCSs use doesn’t quite map to what people really want to do.
I don’t see how he’s trying to make Git act more like CVS.
But I can see why you’d say that. Yes, rebasing before pushing is something Git users do to keep their local history clean (somewhat like CVS). However, it’s just a courteous thing to do, like making sure a UI drawing has clean lines. It does have its tangible benefits (easier bisecting, easier reverting if necessary), but in the end I do it mainly because it’s easier to read and understand the commit history.
In this blog post, he’s trying to make sure that the merge commit _does_ exist (which CVS/SVN do not have) despite the intermediate commit made before pushing. Git users may be trying to make sure the history is clean and easy to read and understand, but they don’t want it to be linear and have no branch history like CVS either. There’s a balance in there somewhere that I suspect users strive for if they use Git a lot.
If you don’t care what happens to the history as long as your changes are recorded either way (if you treat it like CVS), then this becomes a non-issue.
I’ve introduced Git to two companies and taught it to several individuals so far, and usually at first people don’t care about the commit history. They treat it like CVS. But when someone starts enjoying Git, they start paying more attention to what happens before they push a bunch of commits to the central repository. For me it’s also a pride thing.
That’s why there are tools such as git-flow to help people like myself feel like we’re keeping a clean-looking commit history.
You mention (and link to a blog post talking about) using the –no-ff switch to preserve the branch. Have you experimented at all with using –squash on merge? In this way you can add the commit into your master as a single commit.
That can give you a problem when trying to git bisect your way back to when a problem was introduced.
Often these merges represent over a week of work, and having the ability to narrow down where a bug came from beyond “it’s a problem from that project we merged in last week” is pretty important.
Thanks for this helpful post. I ran into one problem when using your Bash commands from https://gist.github.com/590895, though: I get
-bash: git_current_branch: command not foundBuyer beware, I did this with a large complicated merge and it created a mess of conflicts to resolve during interactive rebasing that didn’t seem to make any sense. I’m not sure I would recommend this over simple doing the pull as a merge instead of a rebase.
Ignore my ranting, it worked fine the second time, must have done something wrong the first time around. Still a small problem is that it appears to just be re-merging, and not applying any changes made in the merge. RERERE helps a bit, but if you made changes that were not part of conflict resolution you are SOL.
Yeah, when the chain gets way too complicated to rebase, that’s when pulling is more equitable. I never bothered with rerere.
I’ve been on the fence about merge with –no-ff, cause some say its really not that important to keep track of those topical branches.
And it gets harder to maintain them once you introduce a bug/issue tracking system, reason being branch naming convention takes on the following:
issue-X or bug-x, or issue/x, bug/x i.e. issue-123 or bug-8, issue/123 bug/8
Really the only important thing is having a clean history and not being bothered with what was on on what branch.
Do you branch using bug/issue numbers or just with a descriptive name?
Honestly I’m on the fence between nested branches, merges with –no-ff and naming branches based on issue/bug numbers.