Squash Merging in Git: What It Does to Your History
Squash merging takes every change on a branch and applies it to the target as one new commit. The branch’s individual commits are not added to the target’s history, and no merge commit is created.
The result is a target branch where each unit of work is exactly one commit. Whether that is an improvement or a loss depends entirely on whether the branch’s individual commits were worth keeping.
What it produces
Section titled “What it produces”A trunk lane labelled main with commits A, B and S. A separate branch lane labelled feature has commits X, Y and Z branching from A, but it does not join the trunk. Commit S on main contains the combined changes from X, Y and Z as a single new commit.
The branch remains where it was; nothing connects it to main. Commit S has one parent (B) and
contains the same file changes the three branch commits produced together.
The Git command
Section titled “The Git command”git switch maingit merge --squash featureWhat it doesComputes the merge as normal, applies the result to your working tree and index, then stops before committing.
Why we run itIt gives you the merged content staged and ready, so the next git commit records it as a single ordinary commit rather than a merge.
Expected resultA message saying it stopped before committing. Nothing is committed yet — git status shows staged changes.
Automatic merge went well; stopped before committing as requestedSquash commit -- not updating HEADNote the second line: HEAD was not updated. Unlike a normal merge, this leaves you mid-operation with changes staged:
git status --shortM f.txtYou then commit yourself:
git commit -m "Add parser"The resulting commit has one parent:
git cat-file -p HEAD | grep -c parent1The branch is not “merged”
Section titled “The branch is not “merged””This is the consequence that surprises people most, and it is not a bug.
git branch --merged main* mainfeature is absent. git branch --merged tests reachability, and the squashed commit is a new object
that has no link to the branch. As far as the graph is concerned, feature was never integrated.
git branch -d featureerror: the branch 'feature' is not fully merged.If you are sure you want to delete it, run 'git branch -D feature'The changes did land. Verify with git log or by checking the files, then delete with -D. Hosting
platforms handle this for you — they know the pull request was merged, so their delete-branch button works
regardless.
The platform button is not the Git command
Section titled “The platform button is not the Git command”GitHub’s “Squash and merge” achieves a comparable result by a different route. Treating them as identical causes confusion when the details differ.
git merge --squash | Platform squash button | |
|---|---|---|
| Where it runs | Your machine | The server |
| Commit created | By you, in a second step | Automatically |
| Default message | .git/SQUASH_MSG — a list of the squashed commits | Usually the pull request title and description |
| Author of the result | You, the person merging | Typically the branch author, with the merger as committer |
| Branch marked merged | No | Yes, in the platform’s own records |
| Links to review | None | The commit message usually references the pull request |
The authorship difference matters. Locally, squashing several people’s commits into one attributes the whole thing to you. The platform generally preserves the branch author as the commit’s author, which is fairer and keeps contribution statistics meaningful.
What you gain
Section titled “What you gain”One commit per unit of review. main’s history has exactly one entry per pull request, which makes it
readable as a list of changes rather than a stream of intermediate steps.
Working notes stay out of main. “wip”, “fix typo”, “actually fix it”, “address review comments” —
these are useful while developing and noise afterwards.
Every commit on main is complete. Because each one is a whole reviewed change, every commit is a
plausible build. This makes git bisect unusually effective.
No merge commits. Linear history without needing to rebase anything.
Freedom to commit messily. Developers can commit as often as they like, knowing the result is collapsed. That is genuinely liberating on a branch.
Simple reverts. Undoing a whole change is one git revert, with no -m and no ambiguity.
What you lose
Section titled “What you lose”Granular history. If the branch had well-structured commits — a refactor, then a behaviour change,
then tests — that structure is gone. git bisect can no longer isolate which part broke something; it can
only tell you the whole change did.
Large commits. A branch that lived two weeks becomes one enormous commit. Reviewing it later means reading the entire change at once.
git blame gets less useful. Every line from the branch is attributed to the squash commit, with its
message. “Add parser” tells you nothing about why that particular line looks the way it does.
Reverting is all-or-nothing. Individual commits cannot be reverted independently.
Integration timing is lost. No merge commit means no record of when the branch was integrated, only when the squash commit was authored.
Attribution flattens. Covered above.
When squash merging works well
Section titled “When squash merging works well”Short-lived branches. A branch that lived four hours and has three commits loses nothing meaningful. This is the dominant case, and it is why squash merging pairs so naturally with short-lived branches and GitHub Flow.
Branches whose commits are working notes. If nobody would benefit from reading them, discarding them is a gain.
Teams that value a readable main above all. One commit per change, no merge commits, strictly linear.
Repositories with many contributors of varying experience. Squashing normalises commit hygiene without requiring everyone to master interactive rebase.
Open source projects taking external contributions. A contributor’s twelve exploratory commits become one clean commit in the project’s history.
When preserving commits is better
Section titled “When preserving commits is better”Genuinely structured branches. If the author deliberately separated “move the function”, “change its
behaviour” and “add tests”, that structure is valuable — reviewers can check the refactor is behaviour-
preserving by seeing it in isolation, and git bisect can pinpoint which step broke something.
Large changes that could not be split. A migration that has to land as one branch but has distinct internal phases is worse as a single 4,000-line commit.
When git blame matters. Codebases where understanding why a line exists is a frequent activity
benefit from finer-grained commits.
Multi-author branches, where attribution should be preserved.
When you want the integration recorded, for audit or release-note purposes.
Squash merge versus squash rebase
Section titled “Squash merge versus squash rebase”Two different operations are both called “squashing”, and conflating them causes real confusion.
| Squash merge | Interactive rebase squash | |
|---|---|---|
| Command | git merge --squash or platform button | git rebase -i with squash/fixup |
| Where the result lands | The target branch | The same branch |
| When | At integration time | Before review, usually |
| Branch history afterwards | Unchanged | Rewritten |
| Force push needed | No | Yes |
| Granularity | Always exactly one commit | However many you choose |
Squash merging is a decision about what main receives. Interactive rebase squashing is a decision about
what your branch looks like. You can do both, or either, or neither.
Squashing Commits covers the rebase form.
Effect on release notes and debugging
Section titled “Effect on release notes and debugging”Release notes get easier. One commit per change means git log --oneline main between two tags is
close to a changelog already, especially if commit messages come from pull request titles.
Bisecting gets coarser but more reliable. Every commit on main is a complete change, so every commit
should build — but a failure identifies the whole change rather than the specific step within it. For most
teams this is a good trade.
Reverting gets simpler. One commit, one revert, no -m.
Archaeology gets harder. Six months later, git blame points at “Add parser” for two hundred lines,
and the reasoning behind any specific line is only recoverable from the linked pull request — assuming the
platform still exists and the link still resolves.
That last point is the strongest argument for writing good squash commit messages. The message is now the only explanation in the repository itself.
Stacked branches and squashing
Section titled “Stacked branches and squashing”Squash merging interacts badly with stacked branches — where branch B was created from branch A because B’s work depends on A’s.
When A is squashed into main, the squash commit contains A’s changes but shares no commits with A. Branch
B still has A’s original commits in its history. Merging B into main now presents Git with A’s changes
twice: once as the squash commit, once as B’s inherited copies of A’s commits.
The usual symptom is a conflict-heavy merge where every line A touched conflicts with itself.
The fix is to rebase B onto the new main and drop the commits that are now redundant:
git switch feature-bgit rebase --onto main feature-a feature-b--onto takes three arguments: the new base (main), the old base to stop at (feature-a), and the
branch to move. It replays only the commits unique to B, discarding the inherited copies of A’s work.
Common mistakes
Section titled “Common mistakes”Assuming git merge --squash commits for you. It stages and stops. Forgetting the git commit leaves
you with staged changes and no commit.
Accepting the default squash message. “Squashed commit of the following:” plus a list of “wip” is worse than no message. Write a real one.
Being alarmed that git branch -d refuses. Expected. The changes landed; the branch tip is simply not
reachable.
Squashing a long-lived branch. Collapsing three weeks into one commit produces something nobody can review or bisect.
Squashing a branch someone else has based work on. Their branch now shares no commits with main, and
their next merge will conflict extensively.
Mixing methods without deciding. A main where some changes are squashed, some are merge commits and
some are rebased is harder to read than any consistent choice.
Mental Model
Section titled “Mental Model”Squash merging asks: what did this branch change, in total?
It ignores how the author got there — the false starts, the fixups, the “address review comments” commits — and records only the destination. The journey stays on the branch, and the branch is discarded.
Whether that is right depends on whether the journey was worth recording.
What You Learned
Section titled “What You Learned”- Squash merging applies a branch’s combined changes to the target as one new commit with one parent.
git merge --squashstages the result and stops; you make the commit yourself.- The branch is not reachable from the target afterwards, so
git branch --mergedand-ddo not see it. - Platform squash buttons differ from the Git command in message, authorship and branch bookkeeping.
- It suits short-lived branches with working-note commits, and suits structured or long branches badly.
- Squash merging affects the target branch; squash rebasing affects your own branch.
- Because the squash message is the only surviving explanation, writing a good one matters more.
Try It Yourself
Section titled “Try It Yourself”- Create a repository, commit, then create
featureand make three commits with deliberately poor messages (“wip”, “fix”, “more”). - On
main, make one unrelated commit so the branches diverge. - Run
git merge --squash feature. Predict: doesgit logshow a new commit yet? - Run
git status --shortand confirm the changes are staged. - Look at
.git/SQUASH_MSG. - Commit with a real message. Confirm one parent:
git cat-file -p HEAD | grep -c parent. - Run
git branch --merged main. Predict: isfeaturelisted? - Try
git branch -d feature, read the refusal, then verify the changes are onmainbefore using-D.
Steps 3 and 7 are the two behaviours that catch people out.
Next Lesson
Section titled “Next Lesson”Rebase-and-merge is the third integration method: linear history, but every commit preserved.