git rebase on rebased branches and other fun, redux

2021-03-05

 | 

~6 min read

 | 

1153 words

Intro

I’ve previously looked into different strategies for reconciling branches once an underlying branch has been rebased (see Git Rebase —Onto And Other Fun), but the results left something to be desired: specifically, that the branches I created were new. So, I’m returning with fresh eyes and new inspiration. Let’s see if we can’t remedy the situation!

The Setup

Let’s imagine the following gitlog. Same idea as before: feat-a is cut from master, feat-b is cut from feat-a.

git-log
* c790bdf 2021-03-05 | 6 (HEAD -> feat-b) [Stephen]
* 0910aef 2021-03-05 | 5 [Stephen]
* 2fbd812 2021-03-05 | 4 (feat-a) [Stephen]
* 2f43e6a 2021-03-05 | 3 [Stephen]
* f62fd43 2021-03-05 | 2 (master) [Stephen]
* b436e94 2021-03-05 | 1 [Stephen]
* d0223b1 2021-03-05 | Initialize project using Create React App [Stephen]

Now, something comes up and we need to change feat-a which means that the ground is going to move underneath feat-b if we care that it’s still built on top of feat-a (we do).

% gco feat-a
# ... make some changes to feat-a and save them
% git add .
% git commit --fixup 2fbd812 # assuming that we're making a change that affected 4
% git rebase -i --autosquash 2f43e6a^ # let's rebase the entire branch, i.e. all the way to 3's parent
% git log

For simplicity, the new commits will be 4' and 3' respectively after the rebase. The new git log looks great!

git-log
* 77581b1 2021-03-05 | 4' (HEAD -> feat-a) [Stephen]
* adad331 2021-03-05 | 3' [Stephen]
* f62fd43 2021-03-05 | 2 (master) [Stephen]
* b436e94 2021-03-05 | 1 [Stephen]
* d0223b1 2021-03-05 | Initialize project using Create React App [Stephen]

But this is really a fairly narrow view. What happens if we expand the view to look at all branches?

git-log-all
* 77581b1 2021-03-05 | 4' (HEAD -> feat-a) [Stephen]
* adad331 2021-03-05 | 3' [Stephen]
| * c790bdf 2021-03-05 | 6 (feat-b) [Stephen]
| * 0910aef 2021-03-05 | 5 [Stephen]
| * 2fbd812 2021-03-05 | 4 [Stephen]
| * 2f43e6a 2021-03-05 | 3 [Stephen]
|/
* f62fd43 2021-03-05 | 2 (master) [Stephen]
* b436e94 2021-03-05 | 1 [Stephen]
* d0223b1 2021-03-05 | Initialize project using Create React App [Stephen]

It’s a little more complicated! feat-b is still pointing at the old feat-a commits even while feat-a itself has been rebased (notice how the SHAs are different between 3 and 3', 4 and 4'?).?

Now we’re in the situation where we need to be able to rebase feat-b onto the new feat-a.

As discussed in my first attempt on rebasing on rebased branches, we have multiple options:

  1. Git rebase’s --onto
  2. Cherry-picking

Let’s look at each of these in turn, but do it in a way that doesn’t result in a new branch!

Git Rebase —Onto

As a reminder, the API for git rebase --onto looks like:

git rebase --onto <new-parent> <old-parent> [until]

Note: until will default to head, so if you’re on feat-b, this can be left off, but I’ll be explicit for now.

In our case:

  • new-parent will be 77581b1 or feat-a
  • old-parent will be 0910aef^ or 2fbd812
  • until will be c790bdf or feat-b
% git checkout feat-b
% git rebase --onto feat-a 0910aef^
% git log -a

This produces the exact outcome we were hoping for (after reconciling any conflicts):

git-log
* 87dd25b 2021-03-05 | 6 (HEAD -> feat-b) [Stephen]
* 5c78184 2021-03-05 | 5 [Stephen]
* 77581b1 2021-03-05 | 4' (feat-a) [Stephen]
* adad331 2021-03-05 | 3' [Stephen]
* f62fd43 2021-03-05 | 2 (master) [Stephen]
* b436e94 2021-03-05 | 1 [Stephen]
* d0223b1 2021-03-05 | Initialize project using Create React App [Stephen]

Note the commit messages for 5 and 6 weren’t updated, but the SHAs were.

It appears that the simplest way to avoid the detached HEAD state is to checkout feat-b before rebasing.

Cherry Pick: A Better Way

In my previous attempt at using git cherry-pick, I simply went to the feat-a, cut a new branch and then brought over my commits from feat-b.

An alternative approach with git cherry-pick is to combine it with git reset. Since resetting doesn’t actually delete the reference to the commit, we can reuse it after we cherry pick the commits we want into our branch.

For example, let’s start with a new example (references to feat-a and feat-b removed for clarity):

git-log
* 3c4ad0f 2021-03-05 | 9 (HEAD -> feat-d) [Stephen]
* 33616f2 2021-03-05 | 8 (feat-c) [Stephen]
* 5e06b07 2021-03-05 | 7 (master) [Stephen]
* f62fd43 2021-03-05 | 2 [Stephen]
* b436e94 2021-03-05 | 1 [Stephen]
* d0223b1 2021-03-05 | Initialize project using Create React App [Stephen]

Now each feature branch only has one commit - but other than that, our example is starting off the same.

Once we rebase feat-c, we have something like this:

git-log--all
* 81d3b8c 2021-03-05 | 8' (HEAD -> feat-c) [Stephen]
| * 3c4ad0f 2021-03-05 | 9 (feat-d) [Stephen]
| * 33616f2 2021-03-05 | 8 [Stephen]
|/
* 5e06b07 2021-03-05 | 7 (master) [Stephen]
* f62fd43 2021-03-05 | 2 [Stephen]
* b436e94 2021-03-05 | 1 [Stephen]
* d0223b1 2021-03-05 | Initialize project using Create React App [Stephen]

So, how can we use cherry-pick to resolve this situation?

% git checkout feat-d
% git reset --hard head~2
HEAD is now at 5e06b07 7
% git cherry-pick 81d3b8c 3c4ad0f

At this point, our git log looks like this:

git-log
* dd86e60 2021-03-05 | 9 (HEAD -> feat-d) [Stephen]
* ff99b2e 2021-03-05 | 8' [Stephen]
| * 81d3b8c 2021-03-05 | 8' (feat-c) [Stephen]
|/
* f62fd43 2021-03-05 | 2 [Stephen]
* b436e94 2021-03-05 | 1 [Stephen]
* d0223b1 2021-03-05 | Initialize project using Create React App [Stephen]

Not quite what we want as we have two separate SHAs for 8'. But this is a great example of where to use rebase:

% git rebase feat-c
git-log
* 309ad8f 2021-03-05 | 9 (HEAD -> feat-d) [Stephen]
* 81d3b8c 2021-03-05 | 8' (feat-c) [Stephen]
* 5e06b07 2021-03-05 | 7 (master) [Stephen]
* f62fd43 2021-03-05 | 2 [Stephen]
* b436e94 2021-03-05 | 1 [Stephen]
* d0223b1 2021-03-05 | Initialize project using Create React App [Stephen]

Note In the case of my specific example, I ended up with “empty” commits because each commit effectively changed the same property. As a result, after rebasing, I lost 9 (as it was empty). I cherry picked it back in by using the --allow-empty option. My understanding is that this is an edge case which is the result of my example and will not come up in normal practice.

Conclusion

Just like my previous attempts, I end up with the rewritten history the way I was hoping. The advantage of these approaches over my previous attempts, however, is that I’ve retained the relationship with my remote branches!


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!