loading...

Rebasing to avoid merge commits

msanford profile image Michael Sanford Originally published at Medium on ・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](mailto:git@bitbucket.org):michaelsanford/rebase-example.git
 ! [rejected] master -> master (fetch first)
error: failed to push some refs to ‘[git@bitbucket.org](mailto: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!

Discussion

pic
Editor guide