Git

How to Squash Git Commits

How to Squash Commits in Git to Keep Your History Clean

When you are working with Git, it’s a good idea to commit often, so you can always go back to the state of the code if you mess up. However, committing all those mini-changes to the main branch is not always a good idea. It makes the history messy and hard to follow.

Git provides a way to squash a bunch of your commits using the rebase command. Once you have locally made your changes to a particular file or for a particular feature, you can always use the squash method to combine the changes together before you commit to the main branch. This will help other understand your changes better.

Warning: Even though you can pull from external repositories and squash commits together, it’s a bad idea. It can create conflicts and confusion. Avoid changing history that is already public. Only stick to squashing commits that are local to your work.

Let’s work through an example case.

Suppose, we have two files a.py and b.py. Let’s first go through the process of creating the files and making the modifications:

$ mkdir myproject
$ cd myproject/
$ git init
$ echo "print("hello A")" > a.py
$ git add -A && git commit -m "Added a.py"
$ echo "print("hello B")" > b.py
$ git add -A && git commit -m "Added b.py"
$ echo "print("hello BB")" > b.py
$ git add -A && git commit -m "b.py Modification 1"
$ echo "print("hello BBB")" > b.py
$ git add -A && git commit -m "b.py Modification 2"

If we check the history of commits, we will see the following:

$ git log --oneline --graph --decorate
* dfc0295 (HEAD -> master) b.py Modification 2
* ce9e582 b.py Modification 1
* 7a62538 Added b.py
* 952244a Added a.py

After we are done with our work, we decide to put all the changes to the b.py into a single commit for clarity. We count that there are 3 commits on b.py from the HEAD. We issue the following command:

git rebase -i HEAD~3

The -i option tells Git to use the interactive mode.

It should pop up a window on your Git text editor:

pick 7a62538 Added b.py
pick ce9e582 b.py Modification 1
pick dfc0295 b.py Modification 2
 
# Rebase 952244a..dfc0295 onto 952244a (3 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
#
# 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 commits are listed chronologically on the top from the earliest to the most recent. You can choose which commit to “pick” and which commits to squash. For simplicity, we will pick the first commit and squash the rest into it. So we will modify the text like this:

pick 7a62538 Added b.py
squash ce9e582 b.py Modification 1
squash dfc0295 b.py Modification 2
 
# Rebase 952244a..dfc0295 onto 952244a (3 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
#
# 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

As soon as you save and close the text file, another text window should pop up that looks like this:

# This is a combination of 3 commits.
# The first commit's message is:
Added b.py
 
# This is the 2nd commit message:
 
b.py Modification 1
 
# This is the 3rd commit message:
 
b.py Modification 2
 
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Fri Mar 30 21:09:43 2018 -0700
#
# rebase in progress; onto 952244a
# You are currently editing a commit while rebasing branch 'master' on '952244a'.
#
# Changes to be committed:
#       new file:   b.py
#

Save and close this file too. You should see something like this:

$ git rebase -i HEAD~3
[detached HEAD 0798991] Added b.py
Date: Fri Mar 30 21:09:43 2018 -0700
1 file changed, 1 insertion(+)
create mode 100644 b.py
Successfully rebased and updated refs/heads/master.

If you check the commit history now:

$ git log --oneline --graph --decorate
* 0798991 (HEAD -> master) Added b.py
* 952244a Added a.py

All the commits for b.py have been squashed into one commit. You can verify by looking at the b.py file:

$ cat b.py
print("hello BBB")

It has the content of Modification 2.

Conclusion

The rebase is a powerful command. It can help you keep your history clean. But avoid using it for already public commits as it can cause conflicts and confusion. Only use it for your own local repository.

Further Study:

About the author

Zak H

Zak H. lives in Los Angeles. He enjoys the California sunshine and loves working in emerging technologies and writing about Linux and DevOps topics.