Squish your Git Commit(s)

It's sometimes useful, especially if you're like me and make many small commits, to squish a number of commits together before submitting your changes to a public repository, before merging an experimental branch into your master branch, or simply to fix your Git history.

Note

The command used here, git rebase -i, can actually remove history from your Git repository, so you should be careful when using it. However, it is also quite easy to use and to understand, and Git will help you along the way, so it's nothing to shy from, either!

Find the commits you'd like to squish

You can get a listing of the latest commits in your branch by using the git log command. For example:

$ git log --oneline
b9bbc95 Ooops, changed the filename in the README, too.
b9eb949 Renamed the test program.
df62bfc Made the message more uplifting.
2748a22 Added info about test.py
117d212 fixed some punctuation
9ca66d5 Initial commit. Welcome to your test repository!

Before pushing my repo to the public, I want to squish some of these commits into one, making the history more significant and more readable. For example, what if I wanted to get rid of the lines about fixing punctuation, and the "Ooops" commit?

Thankfully, this is easy to do.

Rebase... interactively!

Once you've identified which commits you want to squish, use the -i switch with git rebase to modify your commit history. This is a powerful command, so comes with instructions included. For example, if I wanted to squish the last four commits into a single one, I could use:

git rebase -i HEAD~4

Note

The character between the HEAD and number 4 in the example is a tilde character. For more information about specifying a range of commits, see Revision Selection in Pro Git by Scott Chacon (available online).

You are the Ministry of Truth

You can now rewrite history as you see fit. This is a grave responsibility, but git wants you to succeed, and provides a helpful set of instructions in the resulting editor window:

pick 117d212 fixed some punctuation
pick 2748a22 Added info about test.py
pick df62bfc Made the message more uplifting.
pick b9eb949 Renamed the test program.
pick b9bbc95 Ooops, changed the filename in the README, too.

# Rebase 9ca66d5..b9bbc95 onto 9ca66d5
#
# 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
#
# 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

The default tag for each of these commits is pick, which accepts the commit as normal. You can also edit commit messages with reword, reject commits (which also removes any changes they made to the source) by deleting lines, run an external program (sed, for example) between commits, and so on.

In this case, I only want to squish some commits so that I have a clean history. I want all of the changes that I made (so I won't be deleting any lines), but I don't want my history polluted with many one-line changes of small significance.

There are two tags of interest here: squash and fixup. Both of these merge the tagged commit's changes into the previous one, but handle comments differently: squash also merges your commit comments, whereas fixup will remove the commments (but will still keep the changes).

Keep these points in mind as you edit the history:

  • Commits are presented from oldest (the first line) to the newest*.

  • The squash and fixup tags merge history with the previous (older) commit.

  • You can't squash or fixup the first line, but you can reword it.

Here are the significant lines again:

pick 117d212 fixed some punctuation
pick 2748a22 Added info about test.py
pick df62bfc Made the message more uplifting.
pick b9eb949 Renamed the test program.
pick b9bbc95 Ooops, changed the filename in the README, too.

I want to merge the oldest three commits (117d212, 2748a22, and df62bfc) into one, and I would also like to merge the final two commits (b9eb949 and b9bbc95) into one. Here's what the rewritten history looks like:

reword 117d212 fixed some punctuation
fixup 2748a22 Added info about test.py
fixup df62bfc Made the message more uplifting.
reword b9eb949 Renamed the test program.
fixup b9bbc95 Ooops, changed the filename in the README, too.

I'm using reword to change the commit messages for 117d212 and b9eb949, and merging the rest of the commits into these, using fixup to remove their comments. I haven't actually modified the commit messages that I want to reword here, I've just tagged the commits to let git know that I want to.

Note

Exiting your text editor now without saving anything will abort the rebase. Git may tell you that something was changed, but you can use git log to see that everything is just as it was.

After saving and exiting, git brings up the editor again to let me reword the commit messages. For example:

Renamed the test program.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# rebase in progress; onto a537049
# You are currently editing a commit while rebasing branch 'master' on 'a537049'.
#
# Changes to be committed:
#   modified:   README.md
#   renamed:    test.py -> message.py
#

This also shows you what the resulting commit looks like. You can see that the previous change ("Ooops!") has been merged into this one, just as I wanted. Now, I reword my commit, changing the line "Renamed the test program" to something more descriptive, such as "Renamed test.py to message.py".

Nobody needs to know that I forgot to update README.md and then later changed it--to the outside world, it will seem as if these operations were done in one commit, which is what I want anyway.

Finally, after I save and exit from the last commit that I'm rewording, git provides me with a synopsis of the changes:

[detached HEAD bb9458c] fixed some punctuation
 1 file changed, 1 insertion(+), 1 deletion(-)
[detached HEAD d021eb6] fixed some punctuation
 2 files changed, 7 insertions(+), 1 deletion(-)
[detached HEAD 48a3378] Renamed the test program.
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename test.py => message.py (100%)
[detached HEAD f4c0b9f] Renamed the test program.
 2 files changed, 1 insertion(+), 1 deletion(-)
 rename test.py => message.py (100%)
Successfully rebased and updated refs/heads/master.

History is yesterday (as told today)

Typing git log will show me the history as it is now:

$ git log --oneline
8d3c9ee Renamed test.py as message.py
a537049 Added info about test.py to the README
9ca66d5 Initial commit. Welcome to your test repository!

Note

These changes are permanent. I've effectively changed the history of the repository, and there's no going back. So when using this technique, be clear about what you're doing and why before you use it on a real repository. I also advise that if you haven't followed along so far--create a test repository and try it for yourself.

As you can see, this commit history is definitely more meaningful than my original history, and all of my changes have been preserved. I can now push my changes without fear of polluting the public history of the repo with the minutiae of my work habits. Only the significant commits are represented; your developer peers will be grateful.

You've rewritten history with no-one the wiser... well, except for you, that is!