How to squash commits with interactive rebase
Squashing collapses several commits into one. It rewrites them, so every hash changes and pushed history is off limits.
Squashing combines several commits into one. The usual reason is a branch full of "fix typo", "actually fix it" and "wip" commits that nobody needs to read.
git rebase -i HEAD~3
That opens an editor listing the last three commits. Change pick to squash on all but the first, save, and they collapse into one.
Measured on git 2.50.0:
before: 8865559 commit 4
a7f9a25 commit 3
88a25ae commit 2
e78be7e first
after: 150c029 commit 2
e78be7e first
Four commits became two, the file contents were preserved exactly, and the hash changed from 8865559 to 150c029. That last point governs everything else on this page.
The editor
git rebase -i HEAD~3 opens something like:
pick 88a25ae commit 2
pick a7f9a25 commit 3
pick 8865559 commit 4
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the message
# s, squash = use commit, but meld into previous commit
# f, fixup = like squash, but discard this commit's message
# d, drop = remove commit
Two things about this screen surprise people.
The order is oldest first, which is the opposite of git log. The top line is the oldest of the three.
You never mark the first line. Squash means "meld into the commit above", so the top commit is the one everything else melds into. Marking it produces an error, because there is nothing above it in the selection.
So the edit looks like:
pick 88a25ae commit 2
squash a7f9a25 commit 3
squash 8865559 commit 4
Save and close. Git then opens a second editor with all three messages combined, for you to write the final one.
squash versus fixup
squash keeps the commit's message and asks you to combine them. fixup discards it silently.
Use fixup for the commits that were never meant to be read: "typo", "oops", "wip". Use squash when each commit says something worth folding into the final message.
Mixing them in one rebase is normal:
pick 88a25ae add login form
fixup a7f9a25 typo
squash 8865559 add validation
Choosing how many commits
HEAD~3 means "the last three". Counting wrong is the usual mistake, so check first:
git log --oneline -5
Better, when you are squashing a feature branch, is to name the base rather than count:
git rebase -i main
That selects every commit on your branch since it diverged from main, which is almost always what you actually mean and cannot be miscounted.
The rule about pushed commits
Squashing rewrites commits, so every hash after the squash point changes. Anyone who already has the old commits now has a history that disagrees with yours.
Squash before you push, not after.
If you must squash something already pushed, and you are certain nobody else has it:
git push --force-with-lease
--force-with-lease refuses if the remote has commits you have not seen, which makes it meaningfully safer than --force. It protects against the case where a colleague pushed while you were rebasing, and it is the only force push worth typing by default.
On a shared branch such as main, do not do this. The recovery involves everyone who has the branch, and the fix for a bad commit on shared history is revert, which adds a commit rather than rewriting one. This is the same constraint described in merge versus rebase.
The alternatives that need no rebase
Squash merging. Most of the time, the reason to squash is to keep a tidy main, and the merge can do it for you:
git switch main
git merge --squash feature
git commit
That applies every change from feature as one uncommitted lump, which you then commit. The branch's own history stays as it was. GitHub, GitLab and Bitbucket all offer this as a merge button setting, and turning it on removes the need for most manual squashing.
Note the side effect: the branch will not show as merged in git branch --merged, because none of its commits ended up in main. That is covered in deleting a branch.
Amending the last commit. When there is only one commit to fix:
git commit --amend
No rebase required. It replaces the most recent commit, which is still a rewrite, so the same pushed-history rule applies.
The other things interactive rebase can do
Squashing is one line in a menu that does considerably more. The same git rebase -i screen supports:
| Command | Effect |
|---|---|
pick |
Keep the commit as it is |
reword |
Keep the changes, edit the message |
squash |
Merge into the commit above, combine messages |
fixup |
Merge into the commit above, discard this message |
drop |
Remove the commit entirely |
edit |
Stop here so you can amend the commit |
Two of those are worth knowing beyond squashing.
reword fixes a bad commit message without touching anything else. It is the multi-commit version of git commit --amend.
Reordering. You can move the lines themselves, and git replays the commits in the order you leave them. That is how you group related commits together before squashing, or move an unrelated fix to the front so it can be split off.
edit pauses the rebase at that commit with it checked out, so you can change the files, git commit --amend, and then git rebase --continue. This is how you split one commit into two, by amending it down and committing the remainder separately.
All of these carry the same rewriting caveat as squashing.
A worked example: cleaning a branch before review
The typical state of a feature branch after a few days:
a1b2c3 wip
d4e5f6 add validation
7890ab typo
cdef01 fix the test I broke
234567 add login form
Read bottom-up, the story is: add the form, add validation. Everything else is noise from how the work happened.
git rebase -i main
pick 234567 add login form
fixup cdef01 fix the test I broke
pick d4e5f6 add validation
fixup 7890ab typo
fixup a1b2c3 wip
Two pick lines and three fixups, with the lines reordered so each fixup sits under the commit it belongs to. The result is two clean commits, each of which is a change a reviewer can read on its own.
That is the outcome worth aiming for, and it is why "squash everything into one" is a habit rather than a rule. The goal is a history that explains the change, not the shortest possible history.
When it goes wrong
A conflict during the rebase. Each squashed commit is replayed in turn, so a conflict can appear partway through:
# fix the files, then
git add .
git rebase --continue
Or back out entirely:
git rebase --abort
--abort returns the branch exactly as it was. It is completely safe, and it is the right move whenever a rebase becomes confusing.
You squashed the wrong commits. The reflog has the old position:
git reflog
git reset --hard HEAD@{1}
The pre-rebase commits still exist and are reachable through the reflog, which is the same recovery route described in detached HEAD.
How much to squash
Squashing everything into one commit is common and not always right. A commit should be a change that makes sense on its own, so a branch that genuinely did three things reads better as three commits than as one.
The rule worth applying: squash the commits that exist because of how you worked ("wip", "typo", "fix review comment"), and keep the ones that exist because of what changed. A reviewer reading history in six months cares about the second kind.
Marking fixups as you go
The tidy version of this workflow, for when you already know a commit is a fix to an earlier one:
git commit --fixup a1b2c3
That creates a commit whose message is fixup! <the original message>. It sits in your history looking obvious, and later:
git rebase -i --autosquash main
--autosquash reads those markers, moves each fixup commit directly under the commit it belongs to, and pre-sets it to fixup. The editor opens with the whole plan already correct, and you save and close.
This removes the two fiddliest parts of the manual process: reordering lines and remembering which commit each fix belonged to. Turn it on permanently:
git config --global rebase.autosquash true
Three common mistakes
Marking the first line as squash. There is nothing above it to meld into, so git stops with an error. The top commit in the list is always the one everything else folds into.
Counting the wrong number of commits. HEAD~3 when you meant four leaves a stray commit behind; when you meant two it pulls in one you did not intend to touch. Naming the base branch (git rebase -i main) removes the arithmetic entirely.
Squashing after pushing, then using --force. Plain --force overwrites whatever is on the remote, including commits a colleague pushed while you were rebasing. --force-with-lease refuses in exactly that case, and there is no good reason to type the other one.
Quick reference
git rebase -i HEAD~3 # last three commits
git rebase -i main # everything since main
# pick / squash / fixup / reword / drop
git rebase --continue # after fixing a conflict
git rebase --abort # cancel, restore the branch
git merge --squash feature # squash without rebasing
git commit --amend # fix just the last commit
git push --force-with-lease # after rewriting your own branch
git reflog # undo a rebase
Want to reshape history somewhere mistakes cost nothing? Work through Git basics in a real repository in the browser.
More from the blog
Objects are not valid as a React child: what to render
Objects are not valid as a React child, and React tells you which one by listing its keys. In React 19 the promise version throws a different error.
Read morenpm install errors: read the code, not the stack
Every npm ERR code names one cause. ERESOLVE, E404, EJSONPARSE and the rest, with what each one means, measured against real installs.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.