I am a fan of a clean git history. It should be easy to spot which commits belong to which feature, which feature was merged when, and what changes were made by whom. I’ve seen several git histories that look more like a pile of spaghetti and it takes a lot of effort to understand the history. I will explain how you can keep things neat and clean by using rebase and how to avoid the pitfalls I ran into.
If your git history looks like this and this bothers you this article is for you. With a better understanding of git and it’s features you can make your history look like this.
If you want to know how read more!
Understand merging with --ff
or --no-ff
In git there are two strategies to merge branches. Fast-Forward merges and Non-Fast-Forward merges. Let’s take a look at those two strategies, when you can use which strategy and how the result differs.
Fast-forward merges are possible if you have a situation like this:
D---E---F topic
/
A---B---C master
The topic
branch is ahead of master
. But the master
branch does not have any additional commits.
If you merge topic
with --ff
to master
…
git checkout master
git merge --ff topic
…you will have the following result:
A---B---C---D---E---F topic/master
Now master
and topic
are identical. If you delete the topic
branch nobody can tell anymore that there was a topic
branch once.
Let’s take a look what the result looks like if you merge with the --no-ff
strategy:
git checkout master
git merge --no-ff topic
Git will create an extra commit for the merge itself:
D---E---F topic
/ \
A---B---C---------- G master
This way it is transparent for everyone what commits belonged to the feature branch and what was merged. It also makes it easier to un-merge the branch. Because you only need to revert the merge commit and you are done.
So my recommendation is if you merge towards the main line use --no-ff
. This means merges from topic
to develop
or develop
to master
should always be done with --no-ff
.
Merges away from the main line could be done using --ff
but continue reading to learn about an even cleaner approach.
Understand rebasing
With rebase
you can move commits. Strictly speaking you do not move the commits, but you reapply the commits somewhere else. New commits (with new commit ids) are created, your old commits are removed. This can be used to “catch up” on the changes of master
or any other branch.
Let’s assume the following situation:
A---B---C topic
/
D---E---F---G master
Now you want to update your topic
branch to include the latest changes from master
. You could merge master
to topic
but this would create a useless merge commit (--no-ff
is not possible because both branches have additional commits). A cleaner solution is to rebase your topic
branch onto the current master
.
git checkout topic
git rebase master
The result will be:
A'--B'--C' topic
/
D---E---F---G master
Your old commits A
, B
and C
have been reapplied on top of master
. They have new commit ids and your topic
branch contains now the latest changes from master
. If your commits don’t apply cleanly on master
you have to resolve your conflicts while rebasing. Afterwards you can merge your rebased topic
branch conflict free with --no-ff
to master
. Your merge will cause no conflicts and you don’t have to fix several errors in a big, complicated, bloated merge commit. This will make the history cleaner and better to read.
My second recommendation is to rebase your topic
branches regularly to stay up to date and solve conflicts as soon as possible. The fresher the changes are the more likely it is that you still remember why you introduced a change and this will make resolving rebase conflicts easier.
Working with a shared repository
When you work with a shared repository and you start to rebase branches you need to communicate some extra knowledge to your team. Otherwise you clutter you git history with a lot of useless merge commits and it will blow up in your face. That’s basically the reason why people tell you “don’t rebase pushed branches”. Let’s narrow this principle down to the first rule: “don’t rebase your pushed main branches”. It depends on your branching model what your main branches are called but commonly these are master
and develop
.
By default, when pulling from a remote branch and both, your local and remote branch have additional commits, git performs a --no-ff-
merge. This creates an additional merge commit as you learned earlier. To avoid this you can use rebasing while pulling:
git checkout topic
git pull --rebase origin topic
Remembering to add --rebase
for every pull is annoying so you can set the default strategy for the topic
branch to rebase:
git config branch.topic.rebase true
You can even tell git to set this automatically for every new branch you create:
git config --global branch.autosetuprebase = always
After rebasing your branch locally you need to push it back to the remote repository. By default git will reject the push as “diverged”. But you can force the push with --force-with-lease
1:
git push --force-with-lease origin topic
This is a potentially destructive operation because it will overwrite the remote branch and you will loose any changes not included in your local copy of the branch. So always be careful which branch you push.
Common Pitfalls
Make sure you do not rebase your main branches. If you do this it will almost certainly cause unwanted results. Like inlined merges (like with --ff
). So make sure rebase is disabled for your main branches:
git config branch.master.rebase false
git config branch.develop.rebase false
If you rebase on master
(or any other main branch) make sure your master
is up to date. So usually you run the following command sequence:
git checkout master
git pull origin master
git checkout topic
git rebase master
TL;DR
- Merge topic branches with
git merge --no-ff topic
towards the mainline. - Use
git rebase master topic
to catch up with the latest changes frommaster
. - Update your topic branch from remote with
git pull --rebase origin topic
. - Push your topic branches with
git push --force-with-lease origin topic
.
Read the TL;DR version of this post at http://stevenharman.net/git-pull-with-automatic-rebase
Further reading
If you want to dive deeper into git here are some recommendations:
- https://ctoinsights.wordpress.com/2012/06/29/git-flow-with-rebase/
- https://www.atlassian.com/git/articles/git-team-workflows-merge-or-rebase/
- https://www.atlassian.com/git/tutorials/merging-vs-rebasing/
Provide Feedback!
This workflow is what works well for me and my teams and after some initial explaining is well accepted. What is your workflow? How do you deal with feature branches and master
diverging? Do you care about the transparency of your git history?
-
Learn more about
--force-with-lease
, how it works, and why you should use it at https://developer.atlassian.com/blog/2015/04/force-with-lease/ ↩