Does this (messy) branch history look familiar to you?
Keeping a clean history in git comes down to knowing when to use merge vs. rebase. Great quote describing when to use each:
Rebases are how changes should pass from the top of hierarchy downwards and merges are how they flow back upwards.
Rule of thumb:
- When pulling changes from origin/develop onto your local develop use rebase.
- When finishing a feature branch merge the changes back to develop.
Use git pull --rebase when pulling changes from origin
Difference between git pull & git pull --rebase:
Situation #1: You haven’t made any changes to your local develop branch and you want to pull changes from origin/develop.
In this case, git pull and git pull --rebase will produce the same results. No problems.
Situation #2: You’ve got one or two small changes of your own on your local develop branch that have not yet been pushed. You want to pull any changes you are missing from origin/develop to your local develop before you can push.
A regular git pull will often result in the following:
Unfortunately you no longer have a linear commit history. And after pushing your git history looks like:
Instead use git pull –rebase:
During the rebase your local commits C & D are played in order on top of the changes you pulled from origin/develop. These commits are replaced with C’ & D’ as you solve local conflicts one commit at a time when they are replayed. Now pushing to origin/develop results in a fast-forward and a nice clean git history:
Use git merge when finishing off a feature branch.
Example of merging a feature branch back into develop before pushing:
After pushing to origin:
Hold on, git pull --rebase isn’t all gravy!
While it is possible to set your system to default to git pull --rebase over using the regular git pull you will occasionally find you run into problems such as the following scenario:
You slave away working on a local feature branch and finish it off by merging it back into develop (using the --no-ff flag of course) resulting in the same history as the previous example:
However after running git fetch you notice that origin/develop is 2 commits of head of develop:
You run git pull –rebase as told and all of a sudden something strange has happened:
Your merge commit has disappeared!
What we really wanted was:
The problem is…
Rebasing Deletes Merge Commits!
Welcome the –preserve-merges flag to center stage:
From the git-rebase manpage:
So, instead of using git pull –rebase, use:
git fetch origin and git rebase -p origin/develop
Downsides to git rebase -p:
Git pull is dead!
Unfortunately the -p flag cannot be used in conjunction with git pull ( git pull –rebase -p doesn’t work!) and as a result you have to explicitly fetch & rebase changes from origin.
ORIG_HEAD is no longer preserved
ORIG_HEAD can be quite handy for multiple scenarios (If you want to review all changes you’ve just merged: git log -p –reverse ORIG_HEAD). Unfortunately, git rebase -p sets ORIG_HEAD for each commit being rebased.
(Check out this google+ conversation for more tips RE: how to use ORIG_HEAD)
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 from (which is why we specify origin/develop in the above example).
Conclusion
To avoid messy merge commits and help keep a relatively clean git commit history use the following workflow when fetching upstream changes:
git fetch origin
git rebase −p origin/develop
For further reading about the inner workings of Git, I found the Git Internals section of the Pro Git book very helpful!