Merge vs Rebase vs Squash-Merge
· Jerwin Arnado ·
You finished a feature branch and cleaned it up with rebase.
Now you have to land it on main, and your hosting provider offers a button with three
options: Create a merge commit, Rebase and merge, Squash and merge. They
produce three genuinely different histories. Picking blindly is how a main log becomes
either a tangle of merge bubbles or a flat wall of unrelated commits. Here’s what each
actually does.
The setup
Say main moved forward while you worked, and your branch has three commits:
main: A───B───C
\
feature: D───E───F
All three strategies get D, E, F onto main. They differ in the shape they leave.
1. Merge commit (git merge)
git checkout main
git merge feature
Git creates a new merge commit (M) with two parents — the tip of main and the tip
of feature — stitching the branch in without rewriting anything:
main: A───B───C───────M
\ /
feature: D───E───F
What you get: the complete, true history. Every commit is preserved with its original hash; the merge commit records exactly when and what was integrated. Branch structure is visible forever.
The cost: on a busy repo, main fills with merge commits and the graph looks like a
subway map. git log is noisy; following the mainline takes effort.
2. Rebase and merge (git rebase then fast-forward)
git checkout feature
git rebase main # replay D,E,F on top of C
git checkout main
git merge feature # fast-forward, no merge commit
Rebase replays your commits on top of the current main, then main fast-forwards to
include them. No merge commit:
main: A───B───C───D'───E'───F'
What you get: a perfectly linear history. Every commit is preserved individually (D’,
E’, F’ — note the new hashes from the replay), but there’s no branch bubble. git log
reads like a single story.
The cost: it rewrites the branch’s commits (new hashes), so it obeys the one rebase rule — only do it to un-shared branches. And you lose the explicit record that D, E, F were ever one unit.
3. Squash and merge (git merge --squash)
git checkout main
git merge --squash feature
git commit -m "feat(auth): add passkey login"
All of the branch’s changes are collapsed into a single new commit on main. D, E,
and F cease to exist as separate commits:
main: A───B───C───S (S = D+E+F as one commit)
What you get: the cleanest possible main — one feature, one commit, one
conventional-commit subject. The messy
intermediate commits never touch the shared history at all, which means you barely even
need to clean the branch first.
The cost: you throw away the branch’s internal granularity. If the feature later needs
a partial revert or a git bisect lands inside it, you have one big commit, not the
fine-grained steps. Attribution of individual changes is gone too.
A rule of thumb
| Strategy | History shape | Best when… |
|---|---|---|
| Merge commit | Full, branched | You want a complete audit trail; large team; long-lived branches |
| Rebase and merge | Linear, every commit kept | You value a clean and granular history; small commits each stand alone |
| Squash and merge | Linear, one commit per feature | Branch commits are noisy; you want main to read as one-feature-per-line |
My default for a feature branch: squash and merge. Most feature branches accumulate
“wip” and “fix typo” commits that nobody needs in main, and squash makes the whole
cleanup question moot — the noise
never lands. Reach for rebase-and-merge when the individual commits are each
meaningful and well-formed (a refactor done in deliberate steps). Reach for a merge
commit when the audit trail of “these things were integrated together, on this date”
genuinely matters.
Caveats and best practices
- Pick one per repo and configure it. On GitHub, Settings → repo → “Allow [strategy]” and disable the rest, so nobody picks the wrong button half-asleep. Consistency beats any individual choice.
- Squash kills the branch’s commit messages — so the one squashed message is the only
record. Make it a good conventional-commit subject; it’s all
mainwill remember. - Rebase-and-merge on a shared branch is a footgun. If the PR branch has been pulled by others, the rewrite bites them. Squash sidesteps this (it makes a brand-new commit).
- Delete the branch after. All three strategies leave the source branch behind; merge it, then remove it so stale branches don’t pile up.
Conclusion
Three buttons, three histories: a merge commit keeps the whole truth, rebase keeps a
linear truth, squash keeps a summary. There’s no universally right answer — but there
is a right answer per repo, so decide once, enforce it in settings, and stop thinking
about it. For most feature work, squash-and-merge buys the cleanest main for the least
ceremony.