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
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
topic branch is ahead of
master. But the
master branch does not have any additional commits.
If you merge
git checkout master git merge --ff topic
…you will have the following result:
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
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
master should always be done with
Merges away from the main line could be done using
--ff but continue reading to learn about an even cleaner approach.
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
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
git checkout topic git rebase master
The result will be:
A'--B'--C' topic / D---E---F---G master
Your old commits
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
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
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
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.
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
- Merge topic branches with
git merge --no-ff topictowards the mainline.
git rebase master topicto catch up with the latest changes from
- 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
If you want to dive deeper into git here are some recommendations:
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/ ↩