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:

bad-git-commit-example.png

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:

git-squash-1.png

Squash all but the top one by changing the word “pick” to “squash”:3

git-squash-2.png

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:

git-squash-3.png

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:

1

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.

2

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 log
  • HEAD~1 refers to the commit just below it
  • HEAD~2 refers to the commit just below that one
  • HEAD~3 refers to 2b2b2169d..., but we want its parent
  • HEAD~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.

3

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.