RSS Feed Subscribe to RSS Feed

 

Git rebase

Git rebase can be intimidating for newcomers, but it’s a powerful and versatile tool when you understand it.

Like git merge, rebase allows you to bring the changes from one branch into another. However instead of all those noisy commit merges you get with the merge command, rebase allows a tidier, linear commit history.

Technically, rebasing is the process of moving a branch to a new base commit, but if that isn’t clear, hopefully the diagrams and explanations below will illuminate.

Before we even start looking at rebase though, we will start with a quick review of merging.

Merge

Fast Forward Merge

Let’s start with the simplest possible merge – the “fast forward” merge.

Take this common scenario where you have a master branch:

git1

 

 

And you create a feature branch (“fb”) off it to work on a new feature, to which you add a few commits (git commit hashes fbbe782 and 1c2f866 in this case):

git2

 

After completing work on the feature branch fb, it is time to merge back in to master.

$ git checkout master
$ git merge fb

 

In this case, git will do what’s called a fast forward merge:

 Updating abea942..1c2f866
 Fast-forward

Git realizes that there is no real merge to be done. There have been no new commits to master so the feature branch and the master branch have not deviated.  It is really a linear history, and after the “merge”, master is updated to reflect that:

 

git3

 

(Note that with the --no-ff option, you can force a merge even when a fast forward could be done).

 

A ‘real’ merge

Next, let’s take a step back to look at a slightly more complex scenario, where a merge really is required. Imagine that while we were working on feature branch fb, there have been other commits on master:

git4

 

 

So, we have two divergent branches, with a common ancestor abea942.

Now, you want to merge the changes from master into fb.

 

How do you do that? git merge.

While on feature branch fb, you do “git merge master”

$ git checkout fb
$ git merge master

This will result in a merge commit, with a default message that looks something like this:

    Merge branch 'master' into fb
    # Please enter a commit message to explain why this merge is necessary,
    # especially if it merges an updated upstream into a topic branch.
    # Lines starting with '#' will be ignored, and an empty message aborts
    # the commit.

 

This looks something like this:

git5

 

Where the new commit 3bff65f on the feature branch fb is the “merge commit” that combines the histories of both branches.

 

In git terminology, this is sometimes referred to as a 3 way merge because git uses 3 commits for the merge – the tip of each of the 2 branches (1c2f866, 0d88aea), and their common ancestor (abea942).

 

On a side note, git by default uses something called a recursive merge. There is also something called an octupus merge, used when there are more then 2 branches to be merge. Learning more on that is left as an exercise for the reader.

For now, being aware of a fast-forward, or a merge made by recursive will cover 99.9% of what you need to know for git merges. That is, until, we get in to rebasing!

 

Rebase

In the above example, we merged from master to our feature branch. If we do this often enough, your commit history will become littered with merge commits.

git rebase

Now, let’s see how the same “merging” of two branches could have been accomplished using rebase.

Let’s step back to where we have a master branch and a fb:

git4

and we want to merge from master into our feature branch fb.

Instead of doing a git merge, let’s this time use git rebase.

$ git checkout fb
$ git rebase master

This will result in a merge commit, with a default message that looks

First, rewinding head to replay your work on top of it...
Applying: feature part a
Applying: feature part b

Where “feature part a” and “feature part b” were the commit messages for the original fbbe782 and 1c2f866 commits.

What rebase is doing in this scenario is basically putting all your commits on fb of to one side, apply the commits from master, then re-apply your changes again BUT with new commit hashes. This makes it look like you had branched from master in its current state, then made your changes, and so end up with a linear history. This means git can now do a fast forward merge, meaning we can avoid the ugliness of a merge commit. You will in short end up with something like this:

 

git6

 

Note that the 2 latest commits on fb now have brand new commit hashes: 219643c and e6f2048. These contain exactly the same changes as the original commit hashes of fbbe782 and 1c2f866 respectively.
To put it another way, even though the branch contains the same changes, those changes are now new commits.

Most importantly, instead of the (superfluous) merge commit we had with the git merge, we now have a completely linear commit history.

git pull –rebase

We have covered git rebase. Another variation of rebase is the pull –rebase.

Imagine the scenario where you are working on a shared branch and making commits, while at the same time, another developer makes changes to the same branch. If you do a git pull from the branch, you may well end up with merge commits, similar to the ‘real merge’ scenario above.

This is because a git pull is a merge, or at least part of it. Specifically, a git pull is shorthand for git fetch followed by git merge.

So when the other developer makes commits to the remote repo, and you git pull (or an explicit git fetch then git merge), you may well get one of those messy merge commits because you are doing a merge.

If however, you do a git fetch, then git rebase you may be able to avoid the merge commit and end up with a linear commit history, just like in the above rebase example. This is still possible using git pull too; although the git pull command by default performs a merge, you can make it use a rebase instead: git pull –rebase

git fetch, then git rebase == git pull –rebase

I personally recommended using ‘git pull –rebase’ by default, particularly from dev to feature branch. You can do this by using this command

    git config --global pull.rebase true

Or by adding this to your ~/.gitconfig file:

[pull]
    rebase = true

The advantages and disadvantages are the same as when using git rebase instead of git merge; it eliminates superfluous merge commits and results in a more linear commit history, but is more complicated and get messy for conflicts.

git rebase -i

While the primary purpose of a git rebase is to maintain a linear project history (free from noisy commit merges), the primary purpose of an interactive git rebase (git rebase -i) is to tidy up your commit history and combine multiple commits.

This is particularly useful while you are on a feature branch, before merging to dev.

Some good examples on this can be found here:

https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase-i/

http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html

Guidelines and Gotchas

There are downsides to using git rebase:

  • Basically more work – merge is easier
  • Cannot do with unstaged changes
  • If you have conflicts it can get messy

But there are also times when you just plain shouldn’t rebase, issues or not.

Don’t Rebase Public History!

Your definition of public can vary though. If it is a public repo that anyone (including folks you are not even aware of!) can clone or pull from, then you should absolutely not rebase. If someone pulls your commit history, then you rebase, and they pull again, they will have completely contradictory commit histories. It will be like crossing the streams in Ghostbusters – “Try to imagine all life as you know it stopping instantaneously and every molecule in your body exploding at the speed of light.”. OK, that may be an exaggeration, but you will certainly end up in a git hell that will be very difficult to pull yourself out of.

If however, the branch is shared by you and another colleague, you could rebase, then tell your colleague to delete his local copy and re-pull. That is still risky, but something I have done in the past. Tread carefully. YMMV.

Don’t rebase your feature branches to master

I personally think that a git rebase instead of git merge is not recommended
from your feature branch to you main/master/development branch. This is because I think the the merge commit in this scenario is a good way to show that work that was being done on a FB was merged. You can in fact always force a commit merge to happen, even when doing a git merge that could be handled by a fast forward merge, but specify the no-ff flag: git merge –no-ff

If you use GitHub Pull Requests, these basically do the same thing – they always result in a git merge commit. And again, I think that is a good thing as the resulting merge commit acts as a form of documentation, documenting when a feature branch was merged to the master branch.

Flow

Here is the flow I try to use when working on branches, when it comes to merging and rebasing.

  • Use “pull –rebase” by default

In general, whether on the master or a feature branch, use pull –rebase, rather than a regular pull. This minimizes superflous and noisy merge commits.

  • Rebase to feature branches

When on a feature branch and wanting to update from master, rebase rather than merge. Again, this minimizes superfluous and noisy merge commits and leaves a tidier, more linear commit history.

  • Pull request to master

    When merging from a feature branch to the master branch, ideally do a pull request.

    If doing a regular merge, do a no fast forward (–no-ff) merge.

    The no fast forward merge is a way to document that you have merged from a feature branch. Don’t use a rebase from a feature branch to master since master is likely public.

  • Use interactive rebase before a Pull Request

Use an interactive rebase (git rebase -i) on your feature branches to tidy your commit history before a pull request.

Useful links

https://www.atlassian.com/git/tutorials/merging-vs-rebasing/conceptual-overview

https://www.atlassian.com/git/tutorials/using-branches/git-merge

https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase

 

 

 

Tags: , ,

Leave a Reply