Rebase branch after GitHub "Squash and merge" onto master
Asked Answered
S

1

14

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?

Shakeup answered 25/1, 2017 at 18:48 Comment(2)
IIRC you can cherry pick a range of commits e.g. cherry-pick D..FGymnosophist
@Whymarrh: you can, but then you must name D^ so as to copy D itself. The range notation is akin to a half-open interval, excluding the start and including the end. (Though actually it's a set-subtraction!)Hickie
H
27

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.

Hickie answered 25/1, 2017 at 19:3 Comment(2)
Thanks for the great diagrams, I hadn't heard of --onto before and it does exactly what I want. I noticed that I got different commit SHAs using git rebase --onto master branch`` than when I used git reset --hard master && git cherry-pick branch1..branch2`. Is this doing something fundamentally different than a series of cherry-picks?Shakeup
A non-interactive git rebase will often pipe git format-patch to git am to make the commits, but the effect is mostly the same. Commits made at least one second apart will get different hashes (unless carefully controlled, as by, e.g., git filter-branch) as they get the 1 second resolution time-stamp of "now" recorded and is part of the data being hashed.Hickie

© 2022 - 2024 — McMap. All rights reserved.