Every now and then I run into a situation where I’m branched off of a topic branch and the topic branch gets rebased off of main or squashes commits. How do I cleanly rebase my branch off the topic branch? This situation looks like this:
a b c main
| \
| d' e' branch-a (after being rebased off main)
|
d e branch-a-outdated (before rebasing main)
\
f g branch-b (based off outdated branch-a)
In the above, branch-b
had branched off of branch-a
, then branch-a
rebased off of main. For the sake of this discussion, let’s also assume there
were merge conflicts from main that were resolved during this rebase.
The goal is to now rebase branch-b
so its two commits, f
and g
, branch
off of e'
from branch-a
. This might appear trivial at first, but is trickier
than it seems.
Let’s try a regular rebase
Many times, I have attempted to solve this situation with a regular rebase, only to be met with a confusing situation:
branch-b % git rebase branch-a
First, rewinding head to replay your work on top of it...
Applying: d
Using index info to reconstruct a base tree...
M b.txt
Falling back to patching base and 3-way merge...
Auto-merging b.txt
CONFLICT (content): Merge conflict in b.txt
error: Failed to merge in the changes.
Patch failed at 0001 d
Use 'git am --show-current-patch' to see the failed patch
Resolve all conflicts manually, mark them as resolved with...
“Applying: d”?? Our branch started at commit f
, so why is git trying to apply
d
?
The short answer is that git doesn’t know that d
is already in branch-a
in
this case because when branch-a
was rebased off of main, the d
commit
changed. This can be due to a merge conflict or because it was squashed or
changed for any other reason.
When we look at the merge conflict, it’s for a file that wasn’t
even touched in branch-b
. We are forced to resolve the same conflicts that
we already resolved when rebasing branch-a
off of main
! This inserts
room for error, especially if we aren’t familiar with the changes from
branch-a
.
If you’ve ever rebased and wondered why you are having to resolve conflicts for files not in your current branch, there’s a chance this is what was occurring – the branch you are rebasing off of had rewritten history in some way that modified the content of its commits, and git no longer recognizes that the commit in your branch is from the branch off of which you are rebasing.
How about a cherry-pick?
Before we go on with the proposed solution, here is the commit diagram again so you don’t have to scroll up:
a b c main
| \
| d' e' branch-a (after being rebased off main)
|
d e branch-a-outdated (before rebasing main)
\
f g branch-b (based off outdated branch-a)
Remember that our goal is to have f
and g
branched off of e'
from
branch-a
. Since we just saw that git rebase branch-a
doesn’t work as we’d
like, what else could we try? One solution that I used for years before I knew
about the technique I’m about to show was to use git cherry-pick
to play f
and g
directly on top of branch-a
. This absolutely works but requires some
confusing branch shuffling:
branch-b % git log
ABC123 g (HEAD -> branch-b)
XYZ321 f
...
branch-b % git branch -m branch-b-bak # not necessary but nice if you're scared of git reflog like me
branch-b-bak % git checkout -b branch-b branch-a # check out new branch-b from branch-a
branch-b % git cherry-pick XYZ321^..ABC123 # copy the range of commits we want from the late branch-b
branch-b % git branch -D branch-b-bak # delete that backup
The above works just fine, and I’m sure there are some improvements that could be made to the process, but there is still a much easier way to get the exact same results. Read on!
git rebase --onto
for the win!
git, in its wisdom, knew that this issue would come up, so it provides a special
kind of rebase where we can explicitly specify which commit we want the rebase
to start from. That means that from branch-b
, we can effectively say “rebase
off of branch-a but only play my commits starting after commit X”. Here’s a
simplified look at the syntax:
git rebase --onto branch-i-want-to-be-based-off
branch-or-hash-that-my-changes-are-currently-based-off branch-im-rebasing
So in our case, from branch-b
, we would do:
% git log
ABC123 g (HEAD -> branch-b)
XYZ321 f
QWE123 e (branch-a-outdated)
...
% git rebase --onto branch-a QWE123 branch-b
First, rewinding head to replay your work on top of it...
Applying: f
Applying: g
This performed the same thing as git rebase branch-a
, but we were able to
specify which commit to start rebasing from and only apply f
and g
. Notice
that the QWE123
SHA we specified is the commit directly before the two
commits we want from branch-b
. We could also have achieved the same results
with:
% git rebase --onto branch-a branch-a-outdated branch-b
or:
% git rebase --onto branch-a HEAD~2 branch-b
Now, here are the results. Just what we wanted, and with one command.
% git log
g (HEAD -> branch-b)
f
e' (branch-a)
d'
c (main)
b
a
A Little Syntactic Sugar
I find it redundant to have to specify the current branch as the last parameter
to git rebase --onto
, since I always run it from the branch I’m rebasing, so
I have an alias and function I’ve set up in my dotfiles:
alias gcurrent="git rev-parse --abbrev-ref HEAD"
function gro() { git rebase --onto $1 $2 $(gcurrent) }
This allows me to accomplish the above with just: gro branch-a QWE123
. Simple!
Be sure to check out (pun) the git rebase docs for more info on all this.