Merge vs Rebase
Date: 2026-08-17
Two ways of combining lines of work: merge records that they diverged, rebase pretends they never did. The argument is about whether history should record what happened or explain what changed — and there’s one hard rule that ends most of it.
Merge creates a new commit with two parents, joining two lines of history. Rebase replays your commits on top of another branch, creating new commits with new hashes.
The shapes
BEFORE
A ── B ── C main
\
D ── E feature
AFTER MERGE
A ── B ── C ── M main
\ /
D ── E
AFTER REBASE
A ── B ── C ── D' ── E' main
↑
new commits, new hashes
Rebase produces D' and E', not D and E. They contain the same changes and are different objects — which is the source of both the benefit and the hazard.
The trade
| Merge | Rebase | |
|---|---|---|
| History | What actually happened | A tidy linear story |
| Original commits | Preserved | Replaced |
| Conflicts | Once, at the merge | Potentially once per commit |
git bisect | Harder — merge commits | Cleaner — Git Bisect |
| Safe on shared branches | Yes | No |
| Context of divergence | Recorded | Lost |
The one hard rule
Never rebase commits that others have pulled.
you rebase a shared branch
→ the old commits still exist in
their clone
→ their next pull tries to reconcile
two histories containing the same
changes
→ duplicate commits, or a merge that
resolves nothing
The safe boundary is simple: rebase your own unpushed work freely; never rewrite anything anyone else has. That single rule prevents nearly every rebase disaster — Rewriting History.
The workflow most teams land on
# keep your branch current, tidily
git switch feature
git rebase main # your own work, unpushed
# integrate into main, recording the merge
git switch main
git merge --no-ff featureRebase to update your branch; merge to integrate it. You get a readable branch history and a main that records when each piece of work landed.
Fast-forward, and why to disable it
When a branch hasn’t diverged, git can just move the pointer:
A ── B ── C ── D ── E main
↑
no merge commit — the branch
is invisible afterwards
git merge --no-ff feature--no-ff forces a merge commit, keeping the fact that a branch existed. Worth it — the merge commit is where the pull request number, the review and the grouping live, and without it a feature’s commits are indistinguishable from anything else on main.
Squash merging
The third option, and the most common in web teams:
git merge --squash featureAll the branch’s commits become one commit on main.
- For:
maingets one clean commit per feature, and the messy work-in-progress commits disappear - Against: the individual commits are lost, so bisect granularity drops and a large feature becomes one enormous commit
The pragmatic position: squash when the branch’s commits are noise (“wip”, “fix typo”); preserve them when each commit is a genuine, self-contained step — which is an argument for Commit Hygiene rather than against squashing.
When rebase actively helps
- Before opening a pull request, so the reviewer sees your changes against current
mainrather than against a week-old base - Cleaning up your own branch — reordering, squashing fixups, rewriting messages — via
git rebase -i - Keeping a long-lived branch current without a series of merge commits from
main
When merge is the only answer
- Anything already pushed and shared
- Recording that two streams of work genuinely diverged and rejoined — a release integration, a long collaboration
- When conflicts are extensive. Resolving once at a merge is far less painful than resolving repeatedly as each commit replays