Tags: web-dev concept

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

MergeRebase
HistoryWhat actually happenedA tidy linear story
Original commitsPreservedReplaced
ConflictsOnce, at the mergePotentially once per commit
git bisectHarder — merge commitsCleaner — Git Bisect
Safe on shared branchesYesNo
Context of divergenceRecordedLost

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 feature

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

All the branch’s commits become one commit on main.

  • For: main gets 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 main rather 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