Git: Squash Multiple Commits in to One Commit

One of the nice things about Git is it's flexibility, allowing you to perform just about any task on a source tree that you'd need. In this case I'm referring to cleaning up the history of a source tree by squashing commits.

When you squash commits, you're combining 2 or more commits in to a single commit. This can be done for many reasons, one of which being that the source history needs to be cleaned up before sharing with your team or submitting a pull request to an open source project. For example, let's say your recent commit history looks something like this:

$ git log --oneline
b7c864c Seriously, #421 is fixed now  
7729f48 Fixed typo  
cc4f2b5 Didn't work, trying something else  
b1339db Fixed issue #421  
c9f9e96 Updated docs for feature ABC  
4eeb10f Added feature ABC  
...

As you can see from the logs, issue #421 took a few tries to get fixed. Although it's great the solution was eventually found, this isn't exactly something you'd want to share with the rest of your team. All they really care about is the final solution to issue #421, but not necessarily how you got there. In cases like this you may want to squash commits together to create one nice, clean commit for this issue.

In order to squash the commits you'll need to use the rebase command like this:

$ git rebase -i HEAD~4

This tells Git to re-apply the last 4 commits on top of another base tip. The -i flag is short for --interactive, which will bring up your default text editor so you can edit the commands before rebasing. For our example above we'd see a text editor with the last 4 commits in reverse order, like the following:

pick b1339db Fixed issue #421  
pick cc4f2b5 Didn't work, trying something else  
pick 7729f48 Fixed typo  
pick b7c864c Seriously, #421 is fixed now  

Any commit with the "picK" keyword will remain in the source tree. However if you replace "pick" with "squash" then that commit will be combined with the previous one. Continuing with our example, we would want to combine the commits like this:

pick b1339db Fixed issue #421  
squash cc4f2b5 Didn't work, trying something else  
squash 7729f48 Fixed typo  
squash b7c864c Seriously, #421 is fixed now  

Saving your edits to this file will result in a single commit that is the combination of changes from all four, with the commit message being a combination of all 4 as well. To specify a new commit message you should use "reword" instead of "pick" on the first commit.

Or, if you want to keep just the "picked" commit message, you can used "fixup" instead of "squash" for the others, which will do the same as squash but discard the commit's message.

Here are all of the commands that can be used when rebasing:

  • pick (or p): use commit
  • reword (or r): use commit, but edit the commit message
  • edit (or e): use commit, but stop for amending
  • squash (or s): use commit, but meld into previous commit
  • fixup (or f): like "squash", but discard this commit's log message
  • exec (or x): run command (the rest of the line) using shell
  • drop (or d): remove commit

And a few other important notes from Git regarding the interactive mode when rebasing commits:

  • The lines can be re-ordered - they are executed from top to bottom.
  • If you remove a line there that commit will be lost.
  • However, if you remove everything, the rebase will be aborted.
  • Note that empty commits are commented out

Fixing up your commits in this way is good practice before sharing with your team members or before pushing the changes to a remote repository. It's much easier to read through a source tree and understand what bugs have been fixed when a single commit fixes a single bug, for example. However, if any of these commits have already been pushed to the remote repository then it is not recommended to squash commits since you'd be rewriting history.

For more information I highly recommend reading over the Git docs for the rebase command here.