How and Why to Squash Your Pull Request
- 6 minutes read - 1080 wordsMany pull requests go through a cycle: programmer opens pull request, maintainer gives feedback, programmer makes changes, repeat until ready to merge, maintainer merges. Prior to the merge, the pull request can be messy, full of reverts, fixups, and WIP commits. In the end, those commits are noise. We can tell a better story by squashing the branch.
Consider this Git branch history:
* 724d574 (HEAD -> issue/38) Good to merge!
* e2af5ac Refactoring the previous commit
* 3e36475 Test are passing here
* ea479cb Revert "Ooops"
* 77aee1d Ooops
* 4c7ead2 Test breaker
The final commit at the top, 724d574
, has been approved to merge by the
maintainer. But this branch is a mess. We have a commit that’s just refactoring
(e2af5ac
), two commits that cancel each other out (ea479cb
and 77aee1d
),
and one that has failing tests (4c7ead2
).
Some GitHub repos use squash merges, which squash this entire list down into one commit. That technique has its place, but you lose a ton of context by squashing 5, 10, or 20 commits into one. I’d prefer to squash it myself and choose what to highlight. So, let’s squash it!
⚠️ Disclaimer! This process alters history. Do it on a feature branch, not the default branch. If it’s part of a pull request, wait until the project maintainer asks you to do it– changing history on an open pull request can make an unfinished discussion difficult to follow.
The Squash
We’ll use git rebase
, interactive mode via the --interactive
flag, going back six commits to the
start of our branch:
$ git rebase --interactive HEAD~6
A pro alternative is to use the parent branch as a reference. Our feature
branch came from main
, so lets use that:
$ git rebase --interactive main
This opens a git-rebase-todo
file:
pick 4c7ead2 Test breaker
pick 77aee1d Ooops
pick ea479cb Revert "Ooops"
pick 3e36475 Test are passing here
pick e2af5ac Refactoring the previous commit
pick 724d574 Good to merge!
# Rebase dd6bb12..724d574 onto dd6bb12 (6 command(s))
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
Our goal is to make the log atomic. In short, we want each commit to contain distinct, complete changes, that pass all tests, described by a coherent message.
Here are my notes on these commits:
4c7ead2 Test breaker
: this contributes to the solution, but it breaks tests. We want to keep it, but it can’t stand on its own as a single commitpick 77aee1d Ooops
: this is useless… it was a mistake.pick ea479cb Revert "Ooops"
: this reverts the previous commit… also useless.pick 3e36475 Test are passing here
: this is important!pick e2af5ac Refactoring the previous commit
: same as abovepick 724d574 Good to merge!
: same as above
With this in mind, I’d edit this file as such:
pick 4c7ead2 Test breaker
d 77aee1d Ooops
d ea479cb Revert "Ooops"
s 3e36475 Test are passing here
s e2af5ac Refactoring the previous commit
s 724d574 Good to merge!
What’s going on here?
- Delete (
d
) the two commits that cancel each other out. Deleting the line from the manifest has the same effect - Squash (
s
) the three useful commits together ‘up’. Because we’re deleting everything that’s ‘up’, the parent will be4c7ead2
, AKATest Breaker
An alternative to squash
is fixup
or f
, which squashes the four commits
and automatically picks the first commit message in the list to cover them
all. I don’t like that solution here, because the first commit message
(Test breaker
) has become inaccurate.
Save and close the temporary file, and you’ll be in a new commit message editing window, with the three squash messages and the parent’s message all together:
# This is a combination of 4 commits.
# The first commit's message is:
Test breaker
# This is the 2nd commit message:
Test are passing here
# This is the 3rd commit message:
Refactoring the previous commit
# This is the 4th commit message:
Good to merge!
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Sun Jul 3 14:31:38 2016 -0500
#
# interactive rebase in progress; onto dd6bb12
# Last commands done (6 commands done):
# s e2af5ac Refactoring the previous commit
# s 724d574 Good to merge!
# No commands remaining.
# You are currently editing a commit while rebasing branch 'main' on 'dd6bb12'.
Now it’s time to write the squash message. If the issue at hand is issue #38, ‘Back button is broken’, then I’d change the first line of this file to this:
Customer goes 'back' via back button
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
Here’s our squashed history:
* 4e763d3 (HEAD -> issue/38) Customer goes 'back' via back button
This is much better. Instead of sharing my messy thought process with the team, I’m just sharing the finished product. Force push it to GitHub, which is permissible because we’re on a personal feature branch:
$ git push --force
Now we have a clean pull request that tells a coherent story and is ready to merge.
Fixing Mistakes
Is your rebase going wrong? Don’t panic. You can abort the rebase with:
$ git rebase --abort
Even with the rebase is complete, you can always travel back in time with git reflog. There is very rarely a situation with a rebase that you can’t undo.
Conclusion
Keeping the history of a big project tidy is a constant process. I’m always trying to make commits smaller, better organized, more coherent. Do your favorite project maintainer and your own projects a favor and work on a feature branch, and when finished, squash your commits. It will make your code and the project better.