Skip to content

Squash Merging in Git: What It Does to Your History

Lesson 4 of 7Beginner → Intermediate9 min readModern Git Workflows · MergingVerified: Git 2.43.0 on Ubuntu 24.04; platform behaviour checked against GitHub's merge-methods documentation

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.

Squash: one new commit on main, branch commits not added

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.

ABSXYZmainfeatureS contains X+Y+Z's combined changes. The branch is not connected to main — S is a new, unrelated 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.

Terminal window
git switch main
git merge --squash feature

What 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 requested
Squash commit -- not updating HEAD

Note the second line: HEAD was not updated. Unlike a normal merge, this leaves you mid-operation with changes staged:

Terminal window
git status --short
M f.txt

You then commit yourself:

Terminal window
git commit -m "Add parser"

The resulting commit has one parent:

Terminal window
git cat-file -p HEAD | grep -c parent
1

This is the consequence that surprises people most, and it is not a bug.

Terminal window
git branch --merged main
* main

feature 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.

Terminal window
git branch -d feature
error: 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 --squashPlatform squash button
Where it runsYour machineThe server
Commit createdBy you, in a second stepAutomatically
Default message.git/SQUASH_MSG — a list of the squashed commitsUsually the pull request title and description
Author of the resultYou, the person mergingTypically the branch author, with the merger as committer
Branch marked mergedNoYes, in the platform’s own records
Links to reviewNoneThe 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.

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.

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.

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.

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.

Two different operations are both called “squashing”, and conflating them causes real confusion.

Squash mergeInteractive rebase squash
Commandgit merge --squash or platform buttongit rebase -i with squash/fixup
Where the result landsThe target branchThe same branch
WhenAt integration timeBefore review, usually
Branch history afterwardsUnchangedRewritten
Force push neededNoYes
GranularityAlways exactly one commitHowever 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.

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.

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:

Terminal window
git switch feature-b
git 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.

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.

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.

  • Squash merging applies a branch’s combined changes to the target as one new commit with one parent.
  • git merge --squash stages the result and stops; you make the commit yourself.
  • The branch is not reachable from the target afterwards, so git branch --merged and -d do 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.
  1. Create a repository, commit, then create feature and make three commits with deliberately poor messages (“wip”, “fix”, “more”).
  2. On main, make one unrelated commit so the branches diverge.
  3. Run git merge --squash feature. Predict: does git log show a new commit yet?
  4. Run git status --short and confirm the changes are staged.
  5. Look at .git/SQUASH_MSG.
  6. Commit with a real message. Confirm one parent: git cat-file -p HEAD | grep -c parent.
  7. Run git branch --merged main. Predict: is feature listed?
  8. Try git branch -d feature, read the refusal, then verify the changes are on main before using -D.

Steps 3 and 7 are the two behaviours that catch people out.

Rebase-and-merge is the third integration method: linear history, but every commit preserved.