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
o on that branch. Then from
o, I create branch
feature, and make commits
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
| (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 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
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.