Bakken & BaeckTech

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.