Rebasing to avoid merge commits

Michael Sanford
Aug 15, 2014 · 4 min read

Everyone likes to keep the source control history linear, preferring fast-forward merges to merge commits. It’s really not that hard. Promise.

Git’s commit structure is a linked list of snapshots whose commits point to one or several ancestors. This means that you must “base” your work on a previous commit.

What’s a merge commit?

Merge commits can be useful when merging topic/feature branches together to indicate, in the history, where branches were combined.

When working on a single branch, however, they are ugly, unnecessary, clutter the history, and look like this:

Here, I worked on master in two places,

This is the result of doing work — not necessarily on the same file just in the same branch — in two places at once. Here, eada9ae and eac49bb are both children of 7036724, but they contain different snapshots which were merged at 53ad270.

Rebasing for great success

Let’s start with a history like this:

Nice, linear history (so far)

Now, let’s work on the same file in two places at once (and also simulate a merge conflict). To do this, I’ll edit the file locally in the working copy, and I’ll edit it in the BitBucket editor (which gets committed and pushed immediately):

BitBucket edit appears in the history.

Now, edit and commit the file locally:

msanford@Tenjin:/tmp/rebase-example $ git commit -m “Fourth commit — working copy”
[master 25dd301] Fourth commit — working copy
1 file changed, 1 insertion(+)

Our branches are now out of sync: HEAD in origin/master points to 4ff5d2 whereas it points to 25dd301 in the working copy:

msanford@Tenjin:/tmp/rebase-example $ git log --oneline
25dd301 Fourth commit — working copy
e8a2bb8 Third commit — working copy
988eb03 Second commit — working copy
4168ee0 Initial commit — working copy

At this point, if you git push from the working copy, you’ll see this familiar message, suggesting that you git pull:

msanford@Tenjin:/tmp/rebase-example $ git push
To git@bitbucket.org:michaelsanford/rebase-example.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to ‘git@bitbucket.org:michaelsanford/rebase-example.git’
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., ‘git pull …’) before pushing again.
hint: See the ‘Note about fast-forwards’ in ‘git push --help’ for details.

Note that the hint also suggests consulting “the fast-forward section of the manpage” which, of course, nobody has ever done. It suggests a more elegant course of action.

msanford@Tenjin:/tmp/rebase-example $ git pull --rebase
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From bitbucket.org:michaelsanford/rebase-example
e8a2bb8..4ff45d2 master -> origin/master
First, rewinding head to replay your work on top of it…
Applying: Fourth commit — working copy
Using index info to reconstruct a base tree…
M README.md
Falling back to patching base and 3-way merge…
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Failed to merge in the changes.
Patch failed at 0001 Fourth commit — working copy
The copy of the patch that failed is found in:
/private/tmp/rebase-example/.git/rebase-apply/patch
When you have resolved this problem, run “git rebase —continue”.

Rebase succeeded (no error messages)? No merge conflicts? You can push right away!

Handling Merge Conflicts

Oh no, a merge conflict in the file we were working on!

README.md:

Edited locally in the working copy.
Second edit locally from the working copy.
Third commit locally from the working copy.
<<<<<<< HEAD
Fourth commit from BitBucket.
=======
Fourth commit locally from the working copy.
>>>>>>> Fourth commit — working copy

No problem: we can edit the file in the working copy, resolve the conflict, and rebase continue.

msanford@Tenjin:/tmp/rebase-example $ vi README.md
msanford@Tenjin:/tmp/rebase-example $ git add README.md
msanford@Tenjin:/tmp/rebase-example $ git rebase —continue
Applying: Fourth commit — working copy
msanford@Tenjin:/tmp/rebase-example $ git status
On branch master
Your branch is ahead of ‘origin/master’ by 1 commit.
(use “git push” to publish your local commits)

What happened?

Git rebase has, as the name suggests, changed the base of our working copy’s commit history to match origin/master’s base. Since both the local working copy and the remote have the same base commit (after fixing the merge conflict) it has allowed us to fast-forward our work as a patch on top of the remote’s commit and maintain a linear history.

This results in a nice, straight line that contains all of the work done in a semantically-sensible way (because the merged changes are topical):

In the first example (with the merge commit) if our working copy held the blue line with eac49bb, git pull rebase would pull eada9ae, then create a patch and replay the work contained in eac49bb on top of that commit (instead of using 7036724 as the base), fast-forwarding it and preserving a linear topical history.

Teams

In this example I have used only one developer, but that is not relevant: you can do this in teams exactly the same way!

    Michael Sanford

    Written by

    code := <-☕

    Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
    Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
    Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade