How does Git rebase work?

I use git’s rebase command daily, it’s an invaluable tool for maintaining a clean and sane Git history. However, most people find it difficult to understand, or use it incorrectly, as it’s not the clearest command to use.

The first thing to understand, is that rebasing typically refers to two different (but similar) operations:

  • “Rebasing a branch” is the most common use of rebase, and refers to pulling changes from an upstream branch (like master or develop) to a feature branch (rebasing your branch is cleaner than the upstream branch into your branch)
  • “Interactive rebasing” can refer to cleaning up your commit history, squashing commits, and editing commit messages. You typically do an interactive rebase before submitting a pull request/patch/branch for review

Rebasing your branch

Say you have a main branch called master. Of course, you don’t develop directly against master, as that would be bad. Instead, you create feature branches (e.g. feature/foo-widget), develop on those, and when they’re done, you merge them back into master (or submit a merge request).

Now, let’s say you created your feature branch a few days ago, and now there are changes on master that you need on your branch to continue (or maybe your company has a policy of rebasing before you create a merge request).

The Wrong Way:

You could simply merge master into your feature branch:

git merge master

This is what a lot of people do. However, this dirties up your git history and is not the correct solution. Let’s look at how the branch looks after you merge your feature branch into master:

Commits are represented by circles, and named in the order they were authored. Both colored circles represent a merge commit.

And here is the git log output (notice the extra merge commit, cluttering up my history):

f403759 Merge branch 'feature/foobar'
647f7a5 Merge branch 'master' into feature/foobar
62e8839 Updating master after my feature branch
b9287ba Adding a foobar feature
c026dc1 This is another commit on master
ef03c90 This is a master commit

This problem is compounded on large feature branches where you’re merging master into your branch many times.

The Right Way:

Instead, you should rebase:

git checkout master
git pull
git checkout feature/foo-widget
git rebase master
git push -f origin feature/foo-widget

Before rebasing, your commit graph looks like this:

After rebasing, your commit graph looks like this (your commit was reapplied on the tip of master, which is D):

And once your feature branch has been merged into master, your commit history should look like this:

The very basic explanation is that you rewind all of your commits on feature/foo-widget, fast foward feature/foo-widget to the latest commit on master, then you reapply each of your commits on top of the latest commit from master.

It’s important to note that you should never rebase a shared branch (like master or develop) as it rewrites history and requires a force push, which can disrupt other developers.

Here is a more detailed walkthrough:

  1. Branch develop has commits 1-20
  2. You fork off of develop to create feature/foobar, from commit #20
  3. You add commits 21-24 to feature/foobar
  4. Some other dev add commits 21-22 to develop
  5. You want to get the most recent changes from develop
  6. You run ‘git rebase develop’ on your branch and this happens:
    1. Git looks at the last shared commit between feature/foobar and develop which is commit #20
    2. Git rewinds feature/foobar to commit #20, storing your commits off the branch
    3. Git fast forwards feature/foobar to commit #22 FROM the develop branch
    4. feature/foobar is now the same as develop
    5. Git re-applies each commit you had before (21-24) on top of #22, becoming 23-26
  7. If you run git status, you’ll like see something like this:

    Your branch and ‘origin/feature/foobar’ have diverged, and have 6 and 4 different commits each, respectively.

    This is because commits are identified by their SHA1 hash, and your original 4 commits were rolled back and re-applied, which gives them a different SHA1 hash. Git now thinks you’re missing the original 4 commits, and sees you have 6 new commits, the 2 new ones from develop and the 4 new commit hashes from re-applying your original 4 commits.

  8. Now however, if you try and push it’ll fail because the upstream version of feature/foobar has commits #21-24 that you made, which aren’t the same as your local branch’s commits #21-24, so you have to force push like this: git push -f origin feature/foobar. It is critical to always specify the branch. -f will overwrite the remote branch allowing you to push your corrected branch

Git Pull With Rebase

Another useful trick I use is git pull --rebase instead if regular git pull.

Let’s say you have some local changes on feature/foobar that you haven’t pushed yet, and your co-worker just pushed his local changes to feature/foobar. If you do a regular git pull, git will do a merge, and you’ll end up with ugly git history and a commit message like this:

Merged ‘feature/foobar’ into ‘feature/foobar’

You want to avoid this clutter, so if you run git pull --rebase, it’ll do the following:

  • Rewind your local branch to the last commit that is shared with the remote
  • Pull down the latest changes from the remote branch and apply them using a fast-foward
  • Re-apply your local changes

Then you can do a regular git push. Since you are only modifying history of commits you haven’t yet pushed, you do not need a force push.

Rebase Merge Conflicts

When rebasing of any sort, it isn’t uncommon to run into merge conflicts. It’s important to understand how to resolve conflicts and continue your rebase.

What happens is that Git has rolled back your commits, fast forwarded your branch to a specified point, and is now re-applying commits one by one. Sometimes this will work fine, but sometimes your commits will now conflict with the updated branch, and Git will pause the rebase and ask you to resolve the conflicts.

[email protected] ~/Projects/dotfiles (test-branch ✔)
± git rebase master
First, rewinding head to replay your work on top of it...
Applying: Commit I'm rebasing
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 Commit I'm rebasing
The copy of the patch that failed is found in:
 /home/brandon/Projects/dotfiles/.git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

So here, I branched off of master, then I created a new commit on master changing line 1 of README.md. Then on my branch, I created a new commit changing line 1 of README.md. Then I rebased my test branch on to master, and got a merge conflict.

To see which files have an error, you can just read the error message, or run git status:

[email protected] ~/Projects/dotfiles (b6afd87...|REBASE ⚡)
± git status
rebase in progress; onto b6afd87
You are currently rebasing branch 'test-branch' on 'b6afd87'.
 (fix conflicts and then run "git rebase --continue")
 (use "git rebase --skip" to skip this patch)
 (use "git rebase --abort" to check out the original branch)

Unmerged paths:
 (use "git reset HEAD <file>..." to unstage)
 (use "git add <file>..." to mark resolution)

both modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")

So here, git is telling me that both of my branches modified README.md. If I open README.md, I’ll see the merge conflict indicators:

<<<<<<< HEAD
These are the changes from my master branch
=======
These are the changes from my test branch
>>>>>>> Commit I'm rebasing
==================

This repository isn't really for other people, it's mostly for all my personalized configuration files so I can easily install them on any computer or environment (I'm frequently spinning up new VMs, which I like to install my dotfiles on). Feel free to use them or look through them to see what I've done.

Merge conflicts are always marked with “<<<<<<<“. The first part of the merge conflict, as separated by “=======”, indicate the conflicting line from your current working branch. The second half indicates the conflicting line from whatever your merging in (e.g. another branch, or in this case, or commit that was rewound). To pick the correct code, you can just edit the file normally. You don’t even have to pick one or the other. Make sure to remove the merge conflict indicators though!

These are the changes from BOTH of my branches
==================

This repository isn't really for other people, it's mostly for all my personalized configuration files so I can easily install them on any computer or environment (I'm frequently spinning up new VMs, which I like to install my dotfiles on). Feel free to use them or look through them to see what I've done.

Now that I’ve resolved the conflict, I have to add the file to my staging area using git add README.md, then I can continue my rebase:

[email protected] ~/Projects/dotfiles (b6afd87...|REBASE ⚡)
± git add README.md 
[email protected] ~/Projects/dotfiles (b6afd87...|REBASE ⚡)
± git rebase --continue
Applying: Commit I'm rebasing

And since that was my only commit, the rebase finished successfully. If I look at my git history, I see the commit from my test branch AFTER the commit from the master branch:

commit d331fa3648a4534e2f39a99365f60fa17045e7f1
Author: Brandon Wamboldt <[email protected]>
Date: Thu Aug 27 13:20:35 2015 -0300

    Commit from my test branch that I'll rebase

commit b6afd872152a7b404bff13bfc01da05a668b3def
Author: Brandon Wamboldt <[email protected]>
Date: Thu Aug 27 13:20:11 2015 -0300

    Commit from my master branch after I created my test branch

And if I looked at the diff for d331fa3, I can see how the rebase affected it:

diff --git a/README.md b/README.md
index 87e2d96..6a47c12 100755
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-These are the changes from my master branch
+These are the changes from BOTH of my branches
==================

This repository isn't really for other people, it's mostly for all my personalized configuration files so I can easily install them on any computer or environment (I'm frequently spinning up new VMs, which I like to install my dotfiles on). Feel free to use them or look through them to see what I've done.

And that concludes the section on merge conflicts.

Interactive Rebasing

Interactive rebasing is the use of git rebase with the -i flag. This tool lets you edit commit history which can be very useful when you have a feature branch that you want to clean up before submitting it. It’s very common to squash commits (combine multiple commits), edit commit messages, amend commits (roll back to a specific commit, make a change, then re-apply later commits), all of which you can do with interactive rebasing.

To start an interactive rebase session, you must specify the range of commits you wish to deal with. The most common way of doing this is using HEAD~n where n is some number of commits:

git rebase -i HEAD~5

The above command will start an interactive rebase session with the last 5 commits. After running that command, you’ll likely see vim (or some other editor) pop up with something like the following:

pick 1f7036f More useful terminal title
pick d61a3b3 Useful git aliases
pick 8439627 Add git pushu
pick cc8ba32 Tweak default email
pick daadd1b Add install script

# Rebase df47733..daadd1b onto df47733
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

At the top you see the 5 commits you are dealing with, from oldest to newest. In the comments, you see various actions. For example, if you modify the text to put this:

pick 1f7036f More useful terminal title
pick d61a3b3 Useful git aliases
s 8439627 Add git pushu
pick cc8ba32 Tweak default email
pick daadd1b Add install script

Git will combine d61a3b3 and 8439627 into a single commit (this is called squashing a commit). Like rebasing your branch, an interactive rebase alters git history. Each commit you modify will get a new commit hash, so as far as git is concerned, it is a completely different commit. This means that if you have previously pushed the commits you altered, you’ll have to do a force push on your branch. This also means you should never interactively rebase a shared branch like master or develop.

5 Replies to “How does Git rebase work?”

  1. This is really good explanation for best practice, thanks! good work

  2. Hamish Atkinson says: Reply

    He’s quite clear here – never rebase master or develop, because other developers pull these branches and base their work of it. Rebasing either of these branches will change the history under their feet and cause all sorts of problems that only git experts would be comfortable resolving..

  3. Thank you for this article. It’s bar far the best explanation i’ve read about git rebase. Very easy to understand. Well done!

  4. Since github so popular and i watching more repos, now i know thats a lot of people using rebase to clean their commits ..

    Thanks for your explanation, its helped me 🙂

  5. Hey Brandon, great article. I have been doing research into git recently in an attempt to bring a form of git-flow to our enterprise project. I have a particular scenario that I had hoped your article could have answered for me but I am not quite there yet.
    Lets say I have a master branch
    A dev branch off master which will be used as the integration hub for all the feature branches e.g.
    Master <- Dev <- Feature1
    So everything is going great in the sprint and at some point a hotfix is required. So we branch off master like so
    Master <- hotfix
    Once the hotfix has been completed and merged back into master etc I want to pass this change to master down to the dev branch and its children (for lack of a better word i.e. the feature branches)
    Is it appropriate for me to rebase master onto dev where dev is a shared branch that features are merged up to? If that is correct, is it then also correct to rebase dev onto each of the feature branches to give the impression in the git history that the dev/features branches all began from the point the hotfix was applied?

Leave a Reply