How to Squash Commits in Git
Squashing combines several commits into one. In an interactive rebase you mark a commit squash or
fixup, and Git melds it into the commit above it.
This is squashing on your own branch, before anyone sees it. It is a different operation from squash merging, which collapses a branch as it lands on the target. The two are frequently confused, and the distinction is covered below.
Why squash
Section titled “Why squash”Fold in corrections. “fix typo” and “address review comment” are not steps in a story; they are corrections to a step. Folding them into the commit they correct leaves the story intact.
Remove noise. Debugging commits, commented-out experiments, “wip” checkpoints.
Make each commit reviewable. One coherent change per commit, rather than the sequence in which you happened to arrive at it.
Make git bisect meaningful. A commit that is a complete change is a useful bisect step; a “wip”
commit that does not build is not.
Keep git blame informative. A line attributed to “Add input validation” explains itself. One
attributed to “fix” does not.
squash versus fixup
Section titled “squash versus fixup”Both meld a commit into the one above. They differ in what happens to the message.
squash (s) | fixup (f) | |
|---|---|---|
| Changes combined | Yes | Yes |
| Message | Editor opens with both messages | Keeps only the first commit’s message |
| Use when | Both messages contain information | The second is a correction with nothing to say |
fixup is the more common choice in practice, because most commits being folded in are corrections.
There are two variants worth knowing: fixup -C keeps this commit’s message instead of the previous
one’s, and fixup -c does the same but opens the editor so you can adjust it.
Doing it
Section titled “Doing it”A branch with a correction that belongs to an earlier commit:
git log --onelinef195a48 WIP debug outputc413def Add validation1aff95b fix typode4df70 Add parser9fc05fd Initial commitgit rebase -i HEAD~4pick de4df70 Add parserpick 1aff95b fix typopick c413def Add validationpick f195a48 WIP debug outputFold the typo fix into the parser commit, and drop the debug commit:
pick de4df70 Add parserfixup 1aff95b fix typopick c413def Add validationdrop f195a48 WIP debug outputSuccessfully rebased and updated refs/heads/main.git log --oneline00039a8 Add validationb02ce4b Add parser9fc05fd Initial commitVerify the folded change is genuinely present rather than lost:
git show HEAD~1:parser.pyptypo fixSquashing everything into one
Section titled “Squashing everything into one”To collapse an entire branch to a single commit, mark every line after the first as fixup:
pick de4df70 Add parserfixup 1aff95b fix typofixup c413def Add validationfixup f195a48 WIP debug outputThen reword the survivor to describe the whole change.
An alternative that avoids the todo list entirely, using a soft reset:
git reset --soft maingit commit -m "Add input parser with validation"What it doesMoves the branch pointer back to the named commit while leaving the index and working tree exactly as they are.
Why we run itAll the branch's changes end up staged as one set, ready to be committed as a single new commit. It is often quicker than editing a long todo list.
Expected resultNo output. git status shows every change from the branch staged and ready to commit.
--soft is the key. It moves only the branch ref; --mixed would unstage everything, and --hard would
discard the work entirely.
Autosquash: deciding at commit time
Section titled “Autosquash: deciding at commit time”Working out later which commit a fix belongs to is harder than recording it at the time.
git commit --fixup=1d51084This creates a commit whose message is fixup! Add parser — a marker naming its target. Later:
git rebase -i --autosquash HEAD~4Git produces the todo list already arranged:
pick 1d51084 Add parserfixup 3b54d21 fixup! Add parserpick febcef5 Add validationpick f347f23 Add testsThe fixup has been moved next to its target and its verb changed. Save without editing and the history collapses correctly.
Make it the default:
git config --global rebase.autoSquash truegit commit --squash=<commit> is the equivalent that keeps both messages.
Squash rebase versus squash merge
Section titled “Squash rebase versus squash merge”These are different operations with similar names, and conflating them causes real confusion.
| Squash in a rebase | Squash merge | |
|---|---|---|
| Command | git rebase -i with squash/fixup | git merge --squash, or a platform button |
| What is rewritten | Your branch | Nothing — a new commit is added to the target |
| When | Before sharing, usually | At integration time |
| How many commits result | However many you choose | Always exactly one |
| Force push needed | Yes | No |
| Who does it | The branch author | Whoever merges |
Squash rebasing is about shaping your branch. Squash merging is about what main receives. You can do
either, both, or neither.
If your team squash-merges everything, squashing your own branch first buys you little — main gets one
commit regardless. It can still be worth doing so reviewers see a clean series, but the final history is
the same.
If your team merges or rebases branches whole, squashing beforehand is how you control what lands. Squash Merging covers the integration side.
Squashing only part of a branch
Section titled “Squashing only part of a branch”You rarely want to collapse everything. More often a branch has two or three coherent changes, each of which accumulated its own corrections.
The todo list handles this naturally — group the lines and mark the corrections:
pick a1b2c3d Add parser interfacefixup e4f5g6h fix parser typofixup i7j8k9l parser: handle nullspick m1n2o3p Add validationfixup q4r5s6t validation: fix off-by-onepick u7v8w9x Add testsThree commits result, each carrying its own corrections. This is usually a better outcome than one giant commit, and it costs no more effort than squashing everything.
If the corrections are not adjacent to their targets, either move the lines first or let
--autosquash do it.
Writing the combined message
Section titled “Writing the combined message”When you use squash, Git opens an editor containing every message it is combining:
# This is a combination of 3 commits.# This is the 1st commit message:
Add parser
# This is the commit message #2:
fix typo
# This is the commit message #3:
parser: handle nullsEverything not commented out becomes the final message. The default — all three concatenated — is almost never what you want.
Replace it with a single message describing the combined change:
Add parser with null handling
Parses the input into tokens. Returns None on empty input rather thanraising, which the caller in report.py relies on.When to keep granular commits
Section titled “When to keep granular commits”Squashing is not automatically an improvement.
Deliberately structured branches. “Add the interface”, “add the implementation”, “switch the caller”, “delete the old code” is far more reviewable as four commits than as one. A reviewer can verify the refactor is behaviour-preserving by looking at it in isolation.
Large changes. Collapsing a 3,000-line branch into one commit produces something nobody can review or bisect usefully.
Separable mechanical and behavioural changes. A rename across forty files, then the logic change. Folded together, the real change is invisible among the noise.
When git blame matters. Finer commits carry more explanation per line.
Multi-author branches. Squashing collapses attribution to one author. Co-authored-by: trailers can
preserve credit, but only if someone adds them.
The rule of thumb: squash corrections, keep steps. If a commit represents a decision someone made, it is probably a step. If it fixes a mistake in the commit before it, it is a correction.
Squashing commits already pushed
Section titled “Squashing commits already pushed”Squashing a branch you have pushed is routine, provided nobody else is using it.
git rebase -i main# … mark fixups, save …git push --force-with-lease--force-with-lease refuses if the remote has moved since your last fetch, which protects against
overwriting a colleague’s push. It is not a substitute for knowing whether anyone is working on the branch —
it only detects new commits, not the fact that someone has a local copy of the old ones.
Squashing commits already on a shared mainline is a different proposition entirely. Rewriting main
means every clone in existence is now inconsistent with it, and everyone must reset. That is an incident
response, not a cleanup, and is covered in
When Not to Rebase.
Verifying
Section titled “Verifying”Squashing changes history, not code. Confirm that:
git branch backup # before startinggit rebase -i maingit diff backup HEAD # afterEmpty output means the squash preserved the result exactly. Any output means something was lost — most likely a conflict resolved incorrectly during the replay.
Common mistakes
Section titled “Common mistakes”Marking the wrong line. squash/fixup fold upwards. Mark the commit being absorbed, not its
target.
Squashing a shared branch. Every commit gets a new ID; anyone who pulled it is orphaned.
Using git reset --hard to squash. Discards the work. --soft moves the pointer and keeps everything
staged.
Accepting the combined message unedited. A squash produces a message containing both originals,
often with “wip” in it. Write a real one.
Squashing before review rather than after. If reviewers commented on individual commits, rewriting during review destroys the anchors. Squash before opening, or after approval.
Squashing everything by reflex. A well-structured branch is worth more than a tidy one.
Forgetting --force-with-lease. After squashing a pushed branch, the remote copy has diverged.
Mental Model
Section titled “Mental Model”Squashing asks: were these separate decisions, or one decision and its corrections?
Separate decisions deserve separate commits — a reviewer can evaluate each, and
git blameexplains each line. A decision plus its corrections is one commit that took a few attempts, and the attempts are not worth preserving.
What You Learned
Section titled “What You Learned”squashkeeps both messages for editing;fixupdiscards the absorbed commit’s message.- Both fold into the commit above them in the todo list.
git reset --soft <base>followed by a commit squashes an entire branch without the todo list.--hardin that position destroys the work;--softis the only safe form.git commit --fixup=<id>plusgit rebase -i --autosquashrecords and applies fold-in intent.- Squash rebasing rewrites your branch; squash merging adds one commit to the target.
- Corrections are worth squashing; deliberate steps usually are not.
git diff backup HEADmust be empty afterwards.
Try It Yourself
Section titled “Try It Yourself”- Create a repository and four commits: a real change, a “fix typo” for it, another real change, and a “wip” commit.
- Back it up:
git branch backup. - Run
git rebase -i HEAD~4. Mark the typo commitfixupand the wip commitdrop. - Confirm two commits remain, and that the typo fix is inside the first:
git show HEAD~1:<file>. - Run
git diff backup HEAD. Predict the output. - Reset:
git reset --hard backup. - Now try the autosquash route. Note the first commit’s ID, make a change, and commit it with
git commit --fixup=<that-id>. - Run
git rebase -i --autosquash HEAD~5and inspect the generated list before saving. Where did Git place the fixup, and what verb did it use? - Finally, squash everything with
git reset --soft backup~4 && git commit -m "One commit". Confirm the working tree still contains all the changes.
Step 5 should print nothing. Step 8 shows autosquash doing the arranging for you — which is why recording the intent at commit time is worth the habit.
Next Lesson
Section titled “Next Lesson”Squashing, reordering and amending are all history editing. The next lesson is the decision framework for choosing between them — and for knowing when to revert instead.