1. Overview
We can often hear the word “squash” when we talk about Git workflows.
In this tutorial, we’ll briefly introduce what Git squashing is. Then we’ll talk about when we need to squash commits. Finally, we’ll take a closer look at how to squash commits.
2. What’s Git Squashing?
When we say “squash” in Git, it means to combine multiple continuous commits into one.
Let’s look at an example:
┌───┐ ┌───┐ ┌───┐ ┌───┐
... │ A │◄─────┤ B │◄────┤ C │◄─────┤ D │
└───┘ └───┘ └───┘ └───┘
After Squashing commits B, C, and D:
┌───┐ ┌───┐
... │ A │◄─────┤ E │
└───┘ └───┘
( The commit E includes the changes in B, C, and D.)
In this example, we’ve squashed the commits B, C and D into E.
Next, we’ll discuss when we should squash commits.
3. When to Squash Commits?
Simply put, we use squashing to keep the branch graph clean.
Let’s imagine how we implement a new feature. Usually, we’ll commit multiple times before we reach a satisfactory result, such as some fixes and tests.
However, when we’ve implemented the feature, those intermediate commits look redundant. So, we may want to squash our commits into one.
Another common scenario where we want to squash commits is merging branches.
Very likely, when we start working on a new feature, we’ll start a feature branch. Let’s say we’ve done our work with 20 commits in our feature branch.
So, when we merge the feature branch to the master branch, we want to do a squashing to combine the 20 commits into one. This way, we keep the master branch clean.
4. How to Squash Commits?
Today, some modern IDEs, such as IntelliJ and Eclipse, have integrated support for common Git operations. This allows us to squash commits from a GUI.
For example, in IntelliJ, we can select the commits we want to squash and choose “Squash Commits” in the right-click context menu:
However, in this tutorial, we’ll focus on squashing with Git commands.
We should note that squash is not a Git command, even if it’s a common Git operation. That is, “git squash … ” is an invalid Git command.
We’ll address two different approaches to squashing commits:
- Interactive rebase: git rebase -i …
- Merge with the –squash option: git merge –squash
Next, let’s see them in action.
5. Squashing by Interactive Rebase
Before we start, let’s create a Git alias slog (stands for short log) to show Git commit logs in a compact view:
git config --global alias.slog "log --graph --all --topo-order --pretty='format:%h %ai %s%d (%an)'"
We’ve prepared a Git repository as an example:
$ git slog
* ac7dd5f 2021-08-23 23:29:15 +0200 Commit D (HEAD -> master) (Kai Yuan)
* 5de0b6f 2021-08-23 23:29:08 +0200 Commit C (Kai Yuan)
* 54a204d 2021-08-23 23:29:02 +0200 Commit B (Kai Yuan)
* c407062 2021-08-23 23:28:56 +0200 Commit A (Kai Yuan)
* 29976c5 2021-08-23 23:28:33 +0200 BugFix #1 (Kai Yuan)
* 34fbfeb 2021-08-23 23:28:19 +0200 Feature1 implemented (Kai Yuan)
* cbd350d 2021-08-23 23:26:19 +0200 Init commit (Kai Yuan)
Git’s interactive rebase will list all relevant commits in the default editor. In this case, those are the commits we want to squash.
Then we can control each commit and commit message as we want and save the change in the editor.
Next, let’s squash the last four commits.
It’s worth mentioning that when we say “the last X commits,” we’re talking about the last X commits from the HEAD.
So, in this case, these are the last four commits:
* ac7dd5f ... Commit D (HEAD -> master)
* 5de0b6f ... Commit C
* 54a204d ... Commit B
* c407062 ... Commit A
Moreover, if we’ve squashed already pushed commits, and we would like to publish the squashed result, we have to do a force push.
It’s worth mentioning that force push to a public repository could be a dangerous operation, as it may overwrite others’ commits.
Further, when we really want to do a force push, we should make sure only to force push the required branch.
For example, we can set the push.default property to current so that only the current branch will be pushed/force-pushed to the remote repository.
Alternatively, we can force push to only one branch by adding a “+” in front of the refspec to push. For example, git push origin +feature will force a push to the feature branch.
5.1. Squash the Last X Commits
Here’s the syntax to squash the last X commits using interactive rebase:
git rebase -i HEAD~[X]
So, this is what we should run:
git rebase -i HEAD~4
After we execute the command, Git will start the system default editor (the Vim editor in this example) with the commits we want to squash and the interactive rebase help information:
As we can see in the screenshot above, all four commits we want to squash are listed in the editor with the pick command.
There’s a detailed guideline on how to control each commit and commit message in the commented lines that follow.
For example, we can change the pick command of commits into s or squash to squash them:
If we save the change and exit the editor, Git will do the rebase following our instructions:
$ git rebase -i HEAD~4
[detached HEAD f9a9cd5] Commit A
Date: Mon Aug 23 23:28:56 2021 +0200
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/master.
Now here’s what we’ll see if we check the Git commit log once again:
$ git slog
* f9a9cd5 2021-08-23 23:28:56 +0200 Commit A (HEAD -> master) (Kai Yuan)
* 29976c5 2021-08-23 23:28:33 +0200 BugFix #1 (Kai Yuan)
* 34fbfeb 2021-08-23 23:28:19 +0200 Feature1 implemented (Kai Yuan)
* cbd350d 2021-08-23 23:26:19 +0200 Init commit (Kai Yuan)
As the slog output shows, we’ve squashed the last four commits into one new commit, f9a9cd5.
Now if we have a look at the complete log of the commit, we can see that the messages of all squashed commits are combined:
$ git log -1
commit f9a9cd50a0d11b6312ba4e6308698bea46e10cf1 (HEAD -> master)
Author: Kai Yuan
Date: 2021-08-23 23:28:56 +0200
Commit A
Commit B
Commit C
Commit D
5.2. When X Is Relatively Large
We’ve learned that the command git rebase -i HEAD~X is pretty straightforward to squash the last X commits.
However, counting a larger X number can be a pain when we have quite a lot of commits in our branch. Moreover, it’s error-prone.
When the X is not easy to count, we can find the commit hash we want to rebase “onto” and run the command git rebase -i hash_onto.
Let’s see how it works:
$ git slog
e7cb693 2021-08-24 15:00:56 +0200 Commit F (HEAD -> master) (Kai Yuan)
2c1aa63 2021-08-24 15:00:45 +0200 Commit E (Kai Yuan)
ac7dd5f 2021-08-23 23:29:15 +0200 Commit D (Kai Yuan)
5de0b6f 2021-08-23 23:29:08 +0200 Commit C (Kai Yuan)
54a204d 2021-08-23 23:29:02 +0200 Commit B (Kai Yuan)
c407062 2021-08-23 23:28:56 +0200 Commit A (Kai Yuan)
29976c5 2021-08-23 23:28:33 +0200 BugFix #1 (Kai Yuan)
34fbfeb 2021-08-23 23:28:19 +0200 Feature1 implemented (Kai Yuan)
cbd350d 2021-08-23 23:26:19 +0200 Init commit (Kai Yuan)
As the git slog shows, in this branch, we have some commits.
Now let’s say we would like to squash all commits and rebase onto the commit 29976c5 with the message: BugFix #1.
So, we don’t have to count how many commits we need to squash. Instead, we can just execute the command git rebase -i 29976c5.
We’ve learned that we need to change the pick commands into squash in the editor, and Git will do the squashing as we expected:
$ git rebase -i 29976c5
[detached HEAD aabf37e] Commit A
Date: Mon Aug 23 23:28:56 2021 +0200
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/master.
$ git slog
* aabf37e 2021-08-23 23:28:56 +0200 Commit A (HEAD -> master) (Kai Yuan)
* 29976c5 2021-08-23 23:28:33 +0200 BugFix #1 (Kai Yuan)
* 34fbfeb 2021-08-23 23:28:19 +0200 Feature1 implemented (Kai Yuan)
* cbd350d 2021-08-23 23:26:19 +0200 Init commit (Kai Yuan)
6. Squashing by Merging With the –squash Option
We’ve seen how to use Git interactive rebase to squash commits. This can effectively clean the commit-graph in a branch.
However, we sometimes make many commits in our feature branch while working on it. After we’ve developed the feature, we usually want to merge the feature branch to the main branch, say “master”.
We want to keep the master branch graph clean, for example, one feature, one commit. But we don’t care about how many commits are in our feature branch.
In this case, we can use the commit git merge –squash command to achieve that.
Let’s understand it through an example:
$ git slog
* 0ff435a 2021-08-24 15:28:07 +0200 finally, it works. phew! (HEAD -> feature) (Kai Yuan)
* cb5fc72 2021-08-24 15:27:47 +0200 fix a typo (Kai Yuan)
* 251f01c 2021-08-24 15:27:38 +0200 fix a bug (Kai Yuan)
* e8e53d7 2021-08-24 15:27:13 +0200 implement Feature2 (Kai Yuan)
| * 204b03f 2021-08-24 15:30:29 +0200 Urgent HotFix2 (master) (Kai Yuan)
| * 8a58dd4 2021-08-24 15:30:15 +0200 Urgent HotFix1 (Kai Yuan)
|/
* 172d2ed 2021-08-23 23:28:56 +0200 BugFix #2 (Kai Yuan)
* 29976c5 2021-08-23 23:28:33 +0200 BugFix #1 (Kai Yuan)
* 34fbfeb 2021-08-23 23:28:19 +0200 Feature1 implemented (Kai Yuan)
* cbd350d 2021-08-23 23:26:19 +0200 Init commit (Kai Yuan)
As the output above shows, in this Git repository, we’ve implemented “Feature2” in the feature branch.
In our feature branch, we’ve made four commits.
Now we want to merge the result back to the master branch with one single commit to keep the master branch clean:
$ git checkout master
Switched to branch 'master'
$ git merge --squash feature
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
Unlike a regular merge, when we execute the command git merge with the –squash option, Git won’t automatically create a merge commit.
Instead, it turns all changes from the source branch (the feature branch in this scenario) into local changes in the working copy:
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: readme.md
In this example, all changes of the “Feature2” are about the readme.md file.
We need to commit the changes to complete the merge:
$ git commit -am'Squashed and merged the Feature2 branch'
[master 565b254] Squashed and merged the Feature2 branch
1 file changed, 4 insertions(+)
Now let’s check the branch graph:
$ git slog
* 565b254 2021-08-24 15:53:05 +0200 Squashed and merged the Feature2 branch (HEAD -> master) (Kai Yuan)
* 204b03f 2021-08-24 15:30:29 +0200 Urgent HotFix2 (Kai Yuan)
* 8a58dd4 2021-08-24 15:30:15 +0200 Urgent HotFix1 (Kai Yuan)
| * 0ff435a 2021-08-24 15:28:07 +0200 finally, it works. phew! (feature) (Kai Yuan)
| * cb5fc72 2021-08-24 15:27:47 +0200 fix a typo (Kai Yuan)
| * 251f01c 2021-08-24 15:27:38 +0200 fix a bug (Kai Yuan)
| * e8e53d7 2021-08-24 15:27:13 +0200 implement Feature2 (Kai Yuan)
|/
* 172d2ed 2021-08-23 23:28:56 +0200 BugFix #2 (Kai Yuan)
* 29976c5 2021-08-23 23:28:33 +0200 BugFix #1 (Kai Yuan)
* 34fbfeb 2021-08-23 23:28:19 +0200 Feature1 implemented (Kai Yuan)
* cbd350d 2021-08-23 23:26:19 +0200 Init commit (Kai Yuan)
We can see that we’ve merged all the changes in the feature branch into the master branch, and we have one single commit, 565b254, in the master branch.
On the other hand, in the feature branch, we still have four commits.
7. Conclusion
In this article, we talked about what Git squashing is and when we should consider using it.
We also learned how to squash commits in Git.