Three ways to squash, and when each is right
Interactive rebase, soft reset, and merge --squash all collapse commits into fewer commits. They differ in granularity, on which branch they leave the result, and how much history they preserve.
On this page
“Squash this branch before merging” is a common review request — and there are three ways to do it that look superficially identical but are different operations with different surface area. Pick the one that matches what you actually want preserved.
The three methods at a glance
| Axis | Interactive rebase (`rebase -i`) | Soft reset + recommit |
|---|---|---|
| Granularity | Per-commit — keep, squash, fixup, drop one at a time. | All-or-nothing — everything becomes one commit. |
| Edits the branch | Yes — rewrites the feature branch. | Yes — rewrites the feature branch. |
| Preserves intermediate commit messages | Yes (squash); no (fixup); editable. | No — you write a single new message. |
| Best for | Curating a story before merging — keep meaningful steps, drop WIP noise. | Collapsing a messy spike or experiment into one clean commit. |
merge --squash is the third option and operates on a different branch entirely — see below.
Method 1: interactive rebase
The right call when you want some commits to survive (the meaningful ones) and others to disappear (the WIP, the typo fixes, the “actually that was wrong” follow-ups).
git switch feature
git rebase -i origin/main
In the editor, change pick to squash or fixup on the lines you want collapsed:
squash(ors) — combine into the previous commit, open the editor to merge messages.fixup(orf) — combine into the previous commit, discard this message.
Autosquash for the disciplined workflow
If you commit fixes with --fixup, Git can sort them automatically:
git commit --fixup=<sha-of-commit-to-fix>
# ... more work ...
git rebase -i --autosquash origin/main
The editor opens with fixup commits already moved adjacent to their targets and marked fixup. You skim, save, done.
Method 2: soft reset and recommit
The right call when you don’t care about the intermediate commits at all — the branch’s net change is the only thing you want to preserve.
git switch feature
git reset --soft origin/main
git commit -m "feat: one clean commit"
Why it works: reset --soft moves the HEAD pointer back to origin/main without touching the index or working tree. Every line your branch added is now staged for one fresh commit.
Method 3: merge --squash on the target branch
The right call when you don’t want to rewrite the feature branch at all — you want the target branch to receive a single integration commit with no merge-commit metadata.
git switch main
git merge --squash feature
git commit -m "feat: squash merge feature"
After this, main has one new commit containing every change from feature. The feature branch is unchanged, and there’s no merge commit pointing back to it — from main’s perspective, the feature branch never existed.
Decision shortcut
When the branch has any commits worth keeping (a real refactor step before the feature, a test commit, a migration), interactive rebase preserves the story. Soft reset is for spike branches; merge --squash is for teams that have standardised on one-commit-per-PR at the integration layer.
- Keep meaningful intermediate commits
- Drop WIP noise selectively
- Fine control over the resulting story
- More work to drive
- Rewrites the feature branch
- Fast — three commands
- One commit, one message, done
- Loses all intermediate history
- Wrong base = giant accidental commit
- Linear history on the target branch
- Feature branch untouched
- Clean default for one-PR-one-commit policies
- No merge commit — loses the link back to the feature branch
- Reverts must rewrite the whole squash
Pushing the result
After methods 1 and 2, the feature branch has been rewritten. The remote will reject a plain push:
git push --force-with-lease origin feature
After method 3, only the target branch has a new commit — push that normally; the feature branch wasn’t touched. See 0025 Git rebase without losing work · Git for why --force-with-lease is the right form.
Recovery
A squash that produced the wrong result is reachable via the reflog:
git reflog
git reset --hard HEAD@{n}
Every reset, rebase, and commit leaves an entry. The pre-squash tip stays reachable for the reflog expiry window (default 90 days), giving you time to recover from any normal mistake.
Anti-patterns
Interactive rebase on a long-lived branch with multiple contributors — picking fixup on a commit you didn’t author.
The author saw “WIP” in the message and assumed it was noise to clean up.
Only squash commits you authored. Other people’s WIP commits are signals about their workflow you don’t have visibility into — and a force-push that flattens their work is the first step of an apology email.