How to Fix Your Git Branches After a Rebase

Have you ever branched off of a branch, then rebased? Your second branch is all messed up. I never knew the right way to fix this. Until now. Read on so you can know how to fix it, too!

Let me tell you about a problem I sometimes have.

I have a feature branch in code review—let's call it feature-1. While I'm waiting for approval, I want to start working on a new feature, so I create a new feature branch off of feature-1—let's call that one feature-2. This might look something like:

a---b---c---d---e---f  master
     \
      g---h---i  feature-1
               \
                j---k---l---m  feature-2

When feature-1 is approved, I rebase it off master to prepare for merging. Now, I check my git tree and uh-oh! feature-2 is all busted. It has its commits, plus the feature-1 commits that were just rebased.

a---b---c---d---e---f  master
    |                \
    |                 g'--h'--i'  feature-1
     \
      g---h---i---j---k---l---m  feature-2

We can see j, k, l, and m like we expect, but we still have g, h, and i in there. We can't fix it by rebasing feature-2 off feature-1.

This is a super annoying problem. What we ultimately want to end up with is this:

a---b---c---d---e---f  master
                     \
                      g'--h'--i'  feature-1
                               \
                                j'--k'--l'--m'  feature-2

How can we get there? Usually, I solve this in a couple of workable but tedious and annoying ways.

Until now. Today, I possess the real solution. Take my hand and let's go on a trip through git rebase and unearth the secrets.

What is a rebase?

The first thing we need to understand is what a rebase even is. If we check the docs for git-rebase, we get the following definition:

Reapply commits on top of another base tip

The important thing here is the word base. A commit's base is its previous (or parent) commit. For example, given this git history:

a---b---c---d---e---f  master
     \
      g---h---i  feature-1

We could make the following assertions:

  1. a is b's base
  2. b is c's base
  3. b is g's base
  4. etc

To rebase, then, is to take a commit (or series of commits) and change its base. In other words, we move it to somewhere else in the history.

The simplest way to do this (and the form everyone knows) is git rebase <newbase>. git-rebase works on the current HEAD (which is almost always the currently checked out branch), so this form takes the current branch and changes its base to be the commit at <newbase>.

Given the following history:

a---b---c---d---e---f  master
     \
      g---h---i  feature-1

If we check out feature-1 and run git rebase master, git will find feature-1's base—which is b—and change it to be the commit at master— which is f:

a---b---c---d---e---f  master
                     \
                      g'--h'--i'  feature-1

(I label these g', h', etc. to reflect that these are actually different commits with different SHAs, even though the content of each is the same.)

This works perfectly for simple cases, but it can't help us with our original problem. This simple form receives the new base, but we can't provide it with the old base—git calculates that for us. We need to be able to tell git both.

Enter the --onto option.

--onto the next section of the article

The --onto option allows us to specify both the old and new bases. It has the form git rebase --onto <newbase> <oldbase>, where <oldbase> is the current base of the tree we want to move.

Let's revisit our previous example:

a---b---c---d---e---f  master
     \
      g---h---i  feature-1

We want to rebase feature-1 off of master. We know our new base—that's master. What is the old base? To find that, we need to trace through feature-1 and find where feature-1 intersects with master. That's our old base. In this example, it's b.

With feature-1 checked out, running:

git rebase --onto master b

Gives us:

a---b---c---d---e---f  master
                     \
                      g'--h'--i'  feature-1

This looks exactly the same as when we did git rebase master. That's because they do the same thing. git rebase master is just shorthand for git rebase --onto master <oldbase>, where git automatically determines <oldbase> for us based on the most common use case.

Now that we have --onto in our toolbelt, we can use it to solve more complicated rebase problems. Suppose we have the same git history as in our last example:

a---b---c---d---e---f  master
     \
      g---h---i  feature-1

We want to rebase feature-1 off of master. This time, though, commit g is no good and we want to omit it. After the rebase, we want feature-1 to be based off of master but only contain commits h and i.

No problemo: just set the new base to master and the old base to g. With feature-1 checked out:

git rebase --onto master g

Gives us:

a---b---c---d---e---f  master
                     \
                      h---i  feature-1

Neat!

A common mistake is to set <oldbase> to the first commit you're trying to move—g, in the previous example. Remember, we're working with bases, not with the commits to be moved. If you're not sure, find the first commit you want to move, then look for its parent. That's your <oldbase>.

Solution

Now, we circle back to our original problem. We have this:

a---b---c---d---e---f  master
     \
      g---h---i  feature-1
               \
                j---k---l---m  feature-2

And we want to move the entire tree with feature-1 and feature-2 to be based off master.

You've probably figured it out already:

git checkout feature-1
git rebase master

Gives us:

a---b---c---d---e---f  master
    |                \
    |                 g'--h'--i'  feature-1
     \
      g---h---i---j---k---l---m  feature-2

Then:

git checkout feature-2
git rebase --onto i' i

Gives us:

a---b---c---d---e---f  master
                     \
                      g'--h'--i'  feature-1
                               \
                                j'--k'--l'--m'  feature-2

We did it!

Bonus Round

Surprise! Suppose you have this history:

a---b---c---d---e---f  master
     \
      g---h---i---j---k---l---m  feature-1

And you want to rebase such that feature-1 is based off of master, but only contains commits i, j, k, and l. How would you do that?

Turns out, the git rebase --onto form takes a third argument, which is the ending commit: git rebase --onto <newbase> <oldbase> <end>. This form will do the rebase but will only take the commits up to (and including) <end>.

To make this happen:

git rebase --onto master h l

Gives us:

a---b---c---d---e---f  master
                     \
                      i---j---k---l  feature-1

Groovy.


I hope this makes your git branching a little smoother. Thanks for reading! Keep reaching for those stars.

What's your favorite git tip? Is there a better way to do this that I don't know about? What's the deal with airline food? Ask me anything in that comments box down there.

Chris Jones

Chris is a development director who designs and builds clean, maintainable software from our Durham, NC office. He works with clients such as the Wildlife Conservation Society, World Wildlife Fund, and Dick's Sporting Goods.

More articles by Chris