Bakken & Baeck logo

Rebasing Onto A Squashed Commit | Ellen Shapiro

When you're using Github's nice "Squash and Merge" button (or the GitLab equivalent, or git merge --squash) to squash all the commits in a pull request rather than simply merging them, your commit history can get really screwy if you try to rebase off of the merged commit.

Let's say I have master branch, that I make branch fix. off of to fix some small bugs. I make commits m, n, and o on that branch. Then from o, I create branch feature, and make commits p, q, and r, that implement some new feature that depends on the previous fix. Visually, it'd look something like this:

        | (branch feature)
        | commit r
        | commit q
        | commit p
       /
      /
     /
     | (branch fix)
     | commit o
     | commit n
     | commit m
    /
   /
  /
-/
|
(master)

Next, I'd make a PR from branch fix into master:

          | (branch feature)
          | commit r
          | commit q
 PR       | commit p
  \      /
   \    /
    \ /
     | (branch fix)
     | commit o
     | commit n
     | commit m
    /
   /
  /
-/
|
(master)

Once that PR is merged, you wind up with something which looks something like this:

 | (branch feature)
 | commit r
 | commit q
 | commit p
 | commit o
 | commit n
 | commit m
 \
  \     (master)
   \   / including the squashed commits of `fix` branch: m, n, and o
    \-/
     |
     |
  (master~1)

If I merge branch feature into master right now (without squashing), while the file changes will be correct, suddenly the previously squashed commits of the fix branch will show up again in the master branch history. This is unfortunate, as I wanted to get rid of those.

Alternatively, if I merge branch feature with squashing, the commit messages of the squashed commits of the fix branch will be included in the commit message of the squashed commit in master. While I can remove that manually, it's a hassle and error-prone.

If I try to do a vanilla rebase before the merge, rebase will attempt to apply the changes from every single commit - including commits m, n, and o, which have already been squashed and merged.

This is because the metadata about the individual commits that made up the squashed merge are gone. In fact, this is the only difference between a squashed merge and a normal merge: Both put the merged commits on top of the destination branch, but while a normal merge does this in a special merge commit that includes metadata about the commit hashes of the branches that have been merged, a squash merge omits that metadata, and "pretends" the merge commit is a normal commit.

Now, if you've got a branch which you created off of the commits which were squashed and merged, using a plain vanilla rebase command will attempt to apply every one of those commits again, sequentially.

This gets annoying very quickly. I asked in our Slack if anyone knew a good way around this. A few people banged their heads together, and we wound up with an answer that takes the commits at the end of branch feature, which haven't been merged yet, and make them do something like this:

 | PR
 |
 | (branch feature)
 | commit r
 | commit q
 | commit p
 \
  \
   \
    \-
     |
     |
  (master) including squashed commits of `fix` branch

There's a slightly obscure git rebase sub-command for this. git checkout the branch you want to pick a bunch of commits off of and plop them onto master. Then, you can pass in the magic commands:

git rebase --onto master [hash for commit o] [hash for commit r]

What this does is tell Git that it should rebase a range of commits on to master. Note that you actually need to start with the commit before those you wish to pick up and move over onto master, so that all the commits are picked up.

One thing to note is that this exact command will cause the result to appear as a detached HEAD rather than as HEAD of the branch you have checked out. You'll need to create an updated branch from there, as your original feature branch will still be as it was (which is slightly annoying, but VERY helpful if you mess this up). Alternatively, you can checkout feature again, and reset --hard that branch to the hash of the detached head.

Now, you're able to make a PR that looks like the most recent ASCII art above, and have a clean commit history while still taking advantage of branching, squashing, and merging.