Let's say I've developed a feature on branch1
and sent it out for code review using a GitHub Pull Request. While it's being reviewed, I do some follow-on work on branch2
.
branch2 -> D --> E --> F
/
branch1 -> A --> B --> C
/
master M
My reviewer loves my work! No changes are required. I merge the pull request for branch1
using GitHub's Squash and merge feature.
After running git pull
on master
and deleting branch1
, I'm left with this situation:
branch2 -> A --> B --> C -> D --> E --> F
/
master M --> S
To send out a clean-looking PR for branch2
, I'd like to get my commit tree looking like this:
branch2 -> D' --> E' --> F'
/
master M --> S
The code at S
(the commit generated by "Squash and merge" for branch1
) is identical to C
, since it's just the squashed version of A --> B --> C
.
One way to achieve this would be to run a sequence like this on branch2
:
git reset --hard S
git cherry-pick D E F
But listing all the commits out this way gets tedious, and this really feels like a rebase. git rebase master
won't work, of course, since commits A
, B
and C
need to disappear.
What's the best way to rebase a branch off a squashed version of one of its ancestor commits?
Any changes from other developers need to be incorporated with git merge instead of git rebase . For this reason, it's usually a good idea to clean up your code with an interactive rebase before submitting your pull request.
Before rebasing such branches, you may want to squash your commits together, and then rebase that single commit, so you can handle all conflicts at once. Here's how to do that. Imagine you've been working on the feature branch show_birthday , and you want to squash and rebase it onto main .
When should I rebase and when should I squash? It does not matter which you use but I recommend rebase. Rebase changes the parent node of the feature branch but merge does not and I recommend it because it keeps the commit structure simpler but as a git user, it makes not different.
In case you are using the Tower Git client, using Interactive Rebase to squash some commits is very simple: just select the commits you want to combine, right-click any of them, and select the "Squash Revisions..." option from the contextual menu.
Use git rebase
with --onto
. This is still a bit tricky, so to make it easy you will want to do one thing different, earlier.
I think it's better, by the way, to draw these graphs with the branch names at the right side, pointing to one specific commit. This is because in Git, commits are on multiple branches, and branch names really do just point to one specific commit. It's also worth reversing the internal arrows (because Git really stores them that way) or just using connecting lines so as not to imply the wrong direction.
Hence:
D--E--F <-- branch2
/
A--B--C <-- branch1
/
M <-- master
Commits A
through C
really are on both branch1
and branch2
, while commits D
through F
are only on branch2
. (Commits M
and earlier are on all three branches.)
What git rebase upstream
does is select all1 commits reachable from the current branch, but not reachable from the upstream
argument, then copy them (with git cherry-pick
or equivalent) so that they come right after the upstream
commit.
After the squash-"merge" (not really a merge), if you run git fetch
and then fast-forward your master
, you have the same thing you drew, but I leave branch1
in and put the labels on the left and add origin/master
here:
D--E--F <-- branch2
/
A--B--C <-- branch1
/
M--S <-- master, origin/master
(Or, if you don't fast-forward your master
yet, only origin/master
points to commit S
).
You now want to tell Git to copy D-E-F
with cherry-pick, then move the label branch2
to point to the last commit copied. You don't want to copy A-B-C
as they're incorporated in S
. You want the copies to go after S
, to which origin/master
now points—whether or not you've updated master
. Hence:
git checkout branch2
git rebase --onto origin/master branch1
The upstream
is now branch1
instead of master
, but the --onto
tells Git where to place the copies: branch1
is only serving to delimit what not to copy. So now Git copies D-E-F
and changes branch2
to point there:
D--E--F [abandoned]
/
A--B--C <-- branch1
/
M--S <-- master?, origin/master
\
D'-E'-F' <-- branch2
and now you can delete the name branch1
. (And now you can fast-forward master
if you didn't yet—it does not really matter when you do it, and in fact you don't need your own master
at all.)
1More precisely, rebase selects commits that are (a) not merge commits and (b) do not have the same git patch-id
as some commit in the excluded set, using a symmetric difference. That is, rather than upstream..HEAD
, Git actually runs git rev-list
on upstream...HEAD
, with --cherry-mark
or similar, to pick out commits. The implementations vary slightly depending on the particular kind of rebase.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With