
Squash git commits
August 24, 2026
I often review git pull/merge requests (PRs) both for work and open source projects. Many of those PRs have a number of commits with commit messages like “fix”, “actual fix”, “fix again”, or other similar forms. I understand why this happens, even more in repositories where it is far easier to trigger the continuous integration (CI) pipeline, rather than execute the tests manually. Having multiple commits in a PR is not problematic by itself, since it might make sense to slice the accomplished work further than 1 commit per PR, but it needs to be done in a logical way.
This problem can be easily fixed by squashing the commits. Squashing means taking a sequence of commits and combining them into a single commit, so the result is one commit on the target branch that contains all the changes, with one commit message, instead of the original chain. The history you keep while working on a branch is useful to you, but it is rarely useful to anyone reading the result.
There are two main ways to get to a clean history in git:
git merge --squash, which is what most forges do under the hood when you click “Squash and merge”- interactive rebase, which gives you fine-grained control over each commit
Let’s see them one by one.
git merge –squash
This is the simplest one to learn and use, and that’s why it is a good solution to start with. The idea is simple: instead of a regular merge, which preserves the full commit history of the branch, you tell git to take all the changes from the source branch and stage them on the target branch as a single set of changes, without creating a merge commit.
git checkout main
git merge --squash feature-branch
git commit
After git merge --squash, the changes are staged but not committed.
Git leaves it to you to write the commit message, which is the whole point: you get to write one clean message that describes the feature as a whole, rather than inheriting the chain of messages you produced while hacking on it.
This is also what most forges (GitHub, GitLab, and so on) do under the hood when you pick “Squash and merge” on a PR, so you do not have to do anything locally; the forge handles it for you. Though, in this case, any cryptographical signature on the git commits is lost.
The downside is that you lose the individual commit history entirely. For most features that is fine and usually what you want, but for larger changes where the intermediate steps are meaningful, you might prefer the next approach.
Interactive rebase
Interactive rebase gives you much finer control.
git checkout feature-branch
git rebase -i main
This opens an editor with a list of all the commits on your branch since it diverged from main, and for each commit you can choose what to do: pick it as-is, reword its message, squash it into the previous one, drop it, and a few other options.
pick 1a2b3c4 Add initial implementation
pick 5d6e7f8 Fix typo in variable name
pick 9a0b1c2 Handle edge case with empty input
pick 3d4e5f6 Update documentation
To fold the typo fix and the edge case fix into the initial implementation, you would change it to:
pick 1a2b3c4 Add initial implementation
squash 5d6e7f8 Fix typo in variable name
squash 9a0b1c2 Handle edge case with empty input
pick 3d4e5f6 Update documentation
You can also use s instead of squash, which is very convenient if you have many commits to manage.
After saving, git prompts you to write a new commit message for the squashed commits. The documentation commit stays separate, which is the point: with interactive rebase, you decide which commits to combine and which to keep.
This is more work than merge --squash, but it gives you control over the final shape of the history.
I tend to always use this approach since the additional effort is, in my opinion, very modest and I prefer to always be intentional in the refinement of the git history.
Also, this allows to re-create the cryptographical signature of the newly created git commit.
Conclusions
Squashing is one of those topics that sounds like a git technicality but is really about who the history is for. The history you produce while working is for you; the history that lands on the target branch is for everyone else, including a future you who will have forgotten the details.
Interactive rebase is the right tool when you care about the structure of the history, or when you want to clean up a branch before opening a PR so the reviewer does not have to wade through “fix”, “fix fix”, and “actually fix”.
For the other cases, you might want to see what you prefer between the interactive rebase and the merge --squash.
By cleaning up your git history before you push commits to the PRs, you are making it simpler for the reviwer to review your change and, at the same time, you are ensuring that whoever will try to pinpoint a problem or a change of behavior will have more ability to quickly understand when was changed and why.