Squash Your Commits

Juan Cabrera
April 22, 2020

After having worked on many projects with different Git workflows, I've come to appreciate the importance of a clean branch history. Unfortunately the usual way people use Git is not conducive to that goal.

Often a developer's workflow looks something like this:

  1. Pick an issue to work on
  2. Create a new feature branch
    git checkout -b my-feature
  3. Work
    git commit -m "Did X"
  4. Work some more
    git commit -m "Did Y"
  5. Is it done yet?
    git commit -m "Did Z"
  6. Push
  7. Create pull-request

What happens then is that the pull request is reviewed by other people and merged into the master or some other branch. The target branch will then have a new merge commit added and a bunch of work-in-progress commits accompanying the merge.

I've never found any use for all those commits remaining in the repository. They rarely provide any useful insight or interesting information regarding the feature. They usually don't build properly, nor do they pass the automated tests suites. Furthermore, they make it harder to follow the repository's history, so what's the point? Another issue with this approach is that it encourages developers not to commit their work-in-progress code for fear of leaving useless commits behind.

The solution to these problems is to squash your commits and rebase onto your long lived branches. When you squash your commits, you modify the Git history and replace all your work-in-progress commits with a single one that contains all the changes; then you rebase that commit onto the target branch. You will end up with a commit history containing only useful commits, with good titles and descriptions. These commits will also build properly and pass all tests.

Here's an example:

I have a Git repository with an empty test.txt file in it and one commit:

commit 15e184a2ff855d833d6456cd1cd0745205b3152d (HEAD -> master)
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:22:44 2020 -0300

  Add text.txt

Let's create a new feature branch called my-feature and add a few commits.

commit 2d2a2e9ee5a055653c990ee1e0a32d2e85cbf45e (HEAD -> my-feature)
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:25:37 2020 -0300

  Change test.txt

commit cd11a3ddf29aaeb85fd9d4aec7ca0c27eaa9346c
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:25:27 2020 -0300

  Change test.txt

commit 15e184a2ff855d833d6456cd1cd0745205b3152d (master)
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:22:44 2020 -0300

  Add text.txt

Now let's squash both commits into one. We need to take note of the hash of the commit just before the first commit from our feature branch. In this case, it's:

15e184a2ff855d833d6456cd1cd0745205b3152d

$ git rebase --interact
ive 15e184a2ff855d833d6456cd1cd0745205b3152d

Git will open your text editor with a message similar to this:

pick cd11a3d Change test.txt
pick 2d2a2e9 Change test.txt

# Rebase 15e184a..2d2a2e9 onto 15e184a (2 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

I'm going to leave the first one as pick and the second one as squash, save the file, and exit the editor.

Git will now open a new editor window with the new commit message. Make sure to change this so that you end up with a good title and description, save, and exit.

Let's see the log again:

commit 6bacb3d302b7cf25e179e67e0c32aec1711a2d7c (HEAD -> my-feature)
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:25:27 2020 -0300

  This is a great short message

  This is a great long message.

commit 15e184a2ff855d833d6456cd1cd0745205b3152d (master)
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:22:44 2020 -0300

  Add text.txt

You can now push your branch to your remote. You might need to use --force if this isn't your first push. You can then do a fast-forward merge of the feature into master. If this is not an option, you will need to rebase the feature onto master.

All of the well known Git provides (e.g; GitLab, Bitbucket, Github) allow you to force this behavior on PRs for all branches (or a subset of branches of your choosing), which eases this process significantly (consult your provider's manual on how to activate this feature). All you have to do is to create a new feature branch, commit as much as you want, push, create a pull-request, and merge. The provider will then squash all your commits into one, use the message you provided, and do a fast-forward merge onto the target branch.

Your Git history will be much cleaner and more useful by following this workflow. This is also the way that most big open-source projects are handled.

"Squash Your Commits" by Juan Cabrera is licensed under CC BY SA. Source code examples are licensed under MIT.

Photo by Viviana Rishe.

Categorized under research & learning.

We are Sophilabs

A software design and development agency that helps companies build and grow products by delivering high-quality software through agile practices and perfectionist teams.