Git Merge, Rebase & Squash — Part 2: Squash Merge — One Feature, One Commit

On this page
Abstract
Maintaining a clean version control history is critical for teams seeking to align discrete features with single, understandable changes. While normal merge operations can pollute the main branch timeline with noisy history, a squash merge condenses multiple feature modifications into one cohesive commit. Executed locally via the git merge --squash command followed by git commit, this mechanism stages the combined alterations of an entire branch without automatically generating a commit or establishing an ancestry link. Consequently, the resulting commit is new, possessing a unique hash and a single parent. Because the original branch retains its independent history and remains unrecognized by ancestry-based checks like git branch --merged, developers must forcefully remove it using git branch -D. The operation contrasts with a normal three-way merge that preserves full developmental lineage, as well as an interactive rebase squash that rewrites history within a single branch. This article outlines these fundamental mechanics, equipping developers to intentionally curate project logs while navigating branch ancestry.

Part 1 mapped every kind of merge onto the commit graph — fast-forward, three-way, --no-ff and --ff-only. This part isolates one more way to bring a branch home: the squash merge, the move behind GitHub's popular Squash and merge button. The idea is deceptively simple — take a feature branch made of many small commits and land it on main as a single, clean commit.
By the end you will be able to run a squash merge locally, explain exactly what Git records — and, just as importantly, what it does not record — tell the two different "squash" operations apart, and see at a glance how a squash merge differs from an ordinary merge commit.
What a squash merge is
A squash merge answers a very human wish: one feature, one commit.
Squash means: take the changes from many commits and combine them into a single new commit.
Suppose your feature/login branch has grown four commits, several of which are just corrections:
D create login form
E fix typo
F fix login again
G final fix
None of E, F or G is a milestone anyone needs to see on main forever. A squash merge lets you keep all of their changes while discarding the noisy history.
Squash merge in action: four commits become one
Start on the branch you want to merge into, run the squash, then commit:
git switch main
git merge --squash feature/login
git commit -m "Add login feature"
The --squash flag is the key. Unlike a normal git merge, it does not create a merge commit and it does not move main on its own. Instead it stages the combined changes of the whole branch and stops, leaving you to write a single commit message. Here is the graph before:
feature/login carries D–E–F–G, branched off C on main.and after you run the three commands above:
The new commit S carries the combined result of D + E + F + G — the same final files a normal merge would have produced — but as one tidy commit on main.
What a squash merge does not do
This is where squash merges surprise people. S looks like the end of the story, but Git has quietly recorded much less than a normal merge would:
Sis not the same commit asD,E,ForG— it is a brand-new commit with a new hash.Sdoes not have the feature branch as a parent. Its only parent isC.- Git did not record
Sas a merge commit — as far as the graph is concerned, it is an ordinary single-parent commit. - The original commits
D–Gstill live onfeature/loginuntil you delete the branch.
The third and fourth points together have a practical consequence worth spelling out, because it trips up almost everyone the first time.
Why Git doesn't see the branch as "merged"
Because
Sdoes not have the feature branch tip as a parent, there is no ancestry link frommainback intofeature/login.
Git's built-in "is this branch merged?" tools work purely by ancestry — they ask whether the branch tip is reachable from your current commit. After a squash merge it is not. So:
git branch --merged # feature/login is NOT listed
git branch -d feature/login # refused: "not fully merged"
To remove the branch you have to delete it forcefully with git branch -D feature/login, confirming that you really are finished with those original commits.
GitHub side-steps this confusion. When you click Squash and merge, GitHub records that the pull request was merged as a first-class fact about the PR — not as a deduction from the commit graph — so the PR shows as Merged and GitHub offers to delete the branch, even though the underlying Git ancestry still looks "unmerged". The lesson: after a squash merge, trust the PR status, not git branch --merged, to tell you whether the work has landed.
Advantages and disadvantages
Why teams like it
- The history on
mainstays clean. - One feature equals one commit.
- Reverting the whole feature is trivial — a single
git revertofS. - Ideal for a PR full of messy
fix,fix again,oopscommits.
What it costs
- You lose the fine-grained commit history on
main. - You can no longer see how each step of the feature was developed.
- Git does not treat the branch as merged by ancestry — the caveat above.
Running it locally vs GitHub's button
Locally, the full sequence — including a look at what got staged — is:
git switch main
git merge --squash feature/login
git status
git commit -m "feat: add login system"
The git status step is worth keeping: it lets you confirm exactly which changes were staged before you seal them into S.
GitHub's Squash and merge button does the same thing at the server level:
the whole Pull Request → one commit on the target branch.
Everything above — the new hash, the single parent, the branch that looks unmerged by ancestry — applies equally to that button.
The two kinds of "squash" (don't confuse them)
"Squash" is one word doing two different jobs. Keep them apart.
Kind 1 — Squash merge
Used when you bring a whole branch into main:
git merge --squash feature/login
git commit
This is everything described above.
Kind 2 — Interactive rebase squash
Used to tidy commits within the same branch, before it ever reaches main:
git rebase -i HEAD~4
In the editor you mark which commits to fold into the one before them:
pick Add login
squash Fix typo
squash Fix validation
(Part 4 covers interactive rebase in full.) Both operations reduce the number of commits, but the mechanism is different: a squash merge collapses a branch as it lands on another branch, while an interactive-rebase squash rewrites the branch's own history in place.
Merge commit vs squash merge, side by side
Put the two outcomes next to each other. Take a feature branch of three commits:
D---E---F
Merge commit
A normal (three-way) merge keeps every commit and adds a merge commit M that ties the two histories together:
main.From main you can still walk back into D, E and F. The full development history is preserved.
Squash merge
A squash merge throws that structure away and leaves a single commit:
main sees only S; the individual commits are gone from this line of history.From main you see only S. The individual commits D, E and F are gone from this line of history — they survive only on the feature branch until it is deleted.
Same final files, very different history. Choosing between them is really a choice about what you want main's log to remember — a question Part 6 turns into a full decision table.
Key takeaways
git merge --squashfollowed bygit commitlands an entire branch as one new commitS;--squashstages the combined changes but does not create the commit or the merge for you.Shas a new hash and a single parent (C); Git does not record it as a merge, and the original commits stay on the feature branch until you delete it.- Because there is no ancestry link,
git branch --mergedwon't list the branch andgit branch -dwill refuse — use-D, or trust the PR status on GitHub. - Two different "squashes":
merge --squashcollapses a branch onto another;rebase -iwithsquashtidies commits within one branch. - A merge commit keeps
D,EandFvisible frommain; a squash merge leaves onlyS. Same files, different memory.