Squashing Commits in Git
Good Git practice encourages developers to create a history of useful commits. This essay is a ‘recipe’ for squashing multiple… uh… less than helpful commits into a single commit using the Git’s interactive rebase command.
Why? Consider this abbreviated history from a typical project:
In a “best case” each commit assume a reader familiar with the purpose
and background of the change. In a “worse case”, it shows some pretty
sloppy commits. Either way, git log
is completely useless with these
sorts of commit messages.
Note: I am not advocating for you to change your development style and wait for perfection before committing. On the contrary, make many commits, but squash the results before pushing the changes.
To follow along with this squashing exercise, let’s start out with a
fresh git project…follow along at home by first creating a directory on
your file system, and issuing a git init
within it.1 Then do:
$ git checkout -b TICKET-123 # Create a local dev branch Switched to a new branch 'TICKET-123' $ # edit src/some-code.py $ git add src/some-code.py $ git commit -m 'Added debugging code. To be removed.' 1 file changed, 1 insertion(+) $ # edit src/some-code.py $ git commit -a -m 'Step 2' 1 file changed, 1 insertion(+) $ # edit src/some-code.py $ git commit -a -m 'Found bug. Really? A tab vs. space issue?' 1 file changed, 1 insertion(+) $ # edit src/some-code.py $ git commit -a -m 'Oops. Forgot to remove debugging code.' 1 file changed, 1 insertion(+)
Issue a git log
and notice that we have four commits that should be
one, with the last commit at the top. Let’s start the squashing:
$ git rebase -i HEAD~4
This command2 will pull up your editor … and here is where it gets interesting. The commits to squash are shown at the top, along with some commentary to help refresh your memory:
Squash all but the top one by changing the word “pick” to “squash”:3
Note: Marking a line as squash
will merge it with the commit above it
(further back in the history). In our example, we are squashing
everything, so we just need one pick
line.
Save the file and exit from your editor, and your editor will be brought back…this time to let you clean up the commit messages. Notice that all the messages are shown, allowing you to include all the interesting bits from your past development session:
Clean up and leave only one commit message. Exit from your editor, and
issue a git log
to see that you have a nice Git history ready for
framing on the wall, and even submitting for review.
Footnotes:
Here is a cheat-sheet for starting up a blank Git project to play with:
cd /tmp mkdir gitstuff cd gitstuff git init # Initialized empty Git repository in /tmp/gitstuff/.git/ touch README git add README git commit -m "Initialization." # [master (root-commit) f91be73] Initialization.
At this point, you are ready to experiment.
The rebase
command is how we rewrite history, and the -i
option
says that we would like to interactively change things. The last
option specifies the parent of the oldest commit we want to squash.
Huh?
Allow a brief demonstration. Suppose our git log
looked like this:
commit 86de7aebe91cdee0ab91ef65c611962c3eec61b2 Author: Howard Abrams <howard.abrams@gmail.com> Date: Thu Oct 23 22:40:36 2014 -0700 Oops. Forgot to remove debugging code. commit 464b4adf81f29f85fbf45db1ccf19ebfcc80614f Author: Howard Abrams <howard.abrams@gmail.com> Date: Thu Oct 23 22:40:10 2014 -0700 Found bug. Really? A tab vs. space issue? commit 7657715763598aefb4956bc3893ad14ae89da3e9 Author: Howard Abrams <howard.abrams@gmail.com> Date: Thu Oct 23 22:39:47 2014 -0700 Step 2 commit 2b2b2169d20756a4977cf85ededb3d4b9438eb8d Author: Howard Abrams <howard.abrams@gmail.com> Date: Thu Oct 23 22:39:18 2014 -0700 Added debugging code. To be removed. commit 55bf21460610c52b5662250c2509a1cd4725a905 Author: Howard Abrams <howard.abrams@gmail.com> Date: Thu Oct 23 22:36:37 2014 -0700 Initializing the project
To squash the top four commits, we need to specify the parent of the
oldest commit, 2b2b2169d...
, which is commit labeled “Initializing
the project”, and has an ID: 55bf21460610c5...
Sure, we could copy and paste the ID, but we could also specify the commit by its location from the top.
HEAD
refers to the top of the commit logHEAD~1
refers to the commit just below itHEAD~2
refers to the commit just below that oneHEAD~3
refers to2b2b2169d...
, but we want its parentHEAD~4
refers to the parent
Essentially this is 0-based indexing, and HEAD~4
is an easy way to
remember that we want to squash the top four commits.
While you could use the abbreviations, like p
and s
, if I
press s
in my nifty little editor, it automatically replaces the
entire word for me…so no, I didn’t type anything more just for a
good screen-shot.