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:
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:
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):
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
! [rejected] master -> master (fetch first)
error: failed to push some refs to ‘email@example.com: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.
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…
Falling back to patching base and 3-way merge…
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/patchWhen 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!
Edited locally in the working copy.
Second edit locally from the working copy.
Third commit locally from the working copy.
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 copymsanford@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)
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.
In this example I have used only one developer, but that is not relevant: you can do this in teams exactly the same way!