When you're using Github's nice "Squash and Merge" button (or the GitLab
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
o on that branch. Then from
feature, and make commits
r, that implement some
new feature that depends on the previous fix. Visually, it'd look something like
| (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
| (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
master right now (without squashing), while
the file changes will be correct, suddenly the previously squashed commits of
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
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
One thing to note is that this exact command will cause the result to appear as
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
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.