Rebase vs Merge
When each is right, why the consensus answer is 'rebase your branch, merge to main'.
Rebase vs Merge
The most-debated topic in git, with surprisingly clean answers once you separate the two cases that come up.
Analogy
Merging is stitching two embroidery patterns together — you can see exactly where the thread of one design crosses into the other, and the seam is part of the finished cloth. Rebasing is unpicking the second pattern and sewing it back onto the first as if it had been there from the start — the thread is the same, the picture is identical, but the embroidery now reads as one continuous design. The first preserves history; the second pretends history was simpler than it was. Neither is wrong. But once a teammate has admired the embroidery and started copying it, unpicking is rude.
The visual difference
Both operations integrate changes from one branch into another. They write history differently.
Merge keeps both histories. A new merge commit M sits at the top with two parents:
main: A ── B ── C ──── M
╱
feat: ↳ D ── E ──╯ (M has parents C and E)
Rebase replays your commits on top of the new tip. The original commits are gone (well — orphaned in the reflog); the new ones (D′, E′) have new SHAs:
Before:
main: A ── B ── C
feat: ↳ D ── E
After (git checkout feat; git rebase main):
main: A ── B ── C
feat: ↳ D' ── E'
The diff is identical. The history is rewritten.
When to use which
Two clean rules covering 95% of the situation:
Rule 1 — Use rebase to update YOUR branch with changes from main. Your branch is yours. Keeping it linear on top of main is good hygiene — clean PR review, clean bisect, no zigzag history when the PR lands.
Rule 2 — Use merge (not rebase) on branches OTHER PEOPLE share. The moment a branch is shared, rebasing it is hostile. Their next pull diverges; their commits are now based on commits that no longer exist.
For most teams, this collapses to: rebase your feature branch, merge to main.
The combo workflow
Here's the workflow most modern teams converge on:
# 1. branch from main
git checkout main && git pull
git checkout -b feat/x
# 2. work, commit freely
# ... edits ...
git commit -am "wip"
# ... edits ...
git commit -am "fix typo"
# 3. daily, before review: rebase onto latest main
git fetch origin
git rebase origin/main
# fix any conflicts:
# git status, edit, git add, git rebase --continue
# 4. force-push your branch (safely)
git push --force-with-lease
# 5. open PR, get review
# 6. when ready, merge to main (NOT rebase main onto branch — that
# would rewrite main's history, which is the rule we just made up)
# On GitHub, choose "Rebase and merge" or "Squash and merge".
The merge-to-main step gives you three options:
| Strategy | Result on main | When to use |
|---|---|---|
| Merge commit | All your commits + a merge commit M |
Topology matters; you want to see "this PR landed here" |
| Rebase and merge | All your commits, replayed onto main | You want flat linear history; each commit is a clean unit |
| Squash and merge | One commit on main containing all your changes | Your branch had noisy WIP commits |
Most teams pick one and enforce it via branch protection.
Why "never rebase published commits"
You pushed feat/x to origin. Bob pulled it down to review. You both have:
feat/x: A ── B ── C
Now you rebase your local feat/x onto a new base. Your local has new SHAs:
feat/x (yours): A ── B' ── C'
feat/x (Bob's): A ── B ── C (still the old)
You force-push. Bob pulls. Git tells him his branch and origin diverged. If Bob made his own commit on top of C, he's now stranded:
feat/x (Bob's): A ── B ── C ── D (his D points at C, which doesn't exist on origin anymore)
feat/x (origin): A ── B' ── C'
Bob has to either rebase his D onto C′ (annoying), or reset and recreate (loses his commit message, breaks any in-progress review). It's recoverable, but it's social cost. Don't make your teammates pay for your rebase.
The escape hatch — when you really do need to rebase a shared branch — is to coordinate. Tell the team in chat. Wait for everyone to push their work. Rebase. Tell them again so they reset.
--force vs --force-with-lease
After rebasing your branch, you need to force-push (the remote sees diverged history). Two flags:
git push --force # YOLO — overwrites whatever's on origin
git push --force-with-lease # safe — refuses if origin has commits you haven't fetched
--force-with-lease is the right default. If a teammate pushed to your branch since your last fetch (maybe they ran git push --force-with-lease from their own work-in-progress), --force-with-lease will refuse rather than nuke their commit. --force will silently overwrite it.
Make --force-with-lease your muscle memory. Some teams alias it:
# in ~/.gitconfig
[alias]
pf = push --force-with-lease
Conflicts during rebase
A rebase replays your commits one at a time. If commit D conflicts with main's C, git stops mid-rebase:
$ git rebase main
First, rewinding head to replay your work on top of it...
Applying: add user search
CONFLICT (content): Merge conflict in src/search.ts
error: Failed to merge in the changes.
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
Three options at this point:
# Resolve and continue
# edit src/search.ts to fix the <<< === >>> markers
git add src/search.ts
git rebase --continue
# Skip this commit (rare — only if it's now a no-op)
git rebase --skip
# Bail out completely, return to pre-rebase state
git rebase --abort
--abort is the underrated escape hatch. If a rebase is going badly, abort, breathe, try a different approach (maybe a merge instead, or smaller rebase chunks).
When merge IS the right answer
Merge is the right answer for:
- Long-running feature branches that integrate a release-sized chunk of work. The merge commit is documentation.
- Multi-engineer feature branches where rebasing would be a nightmare.
- Pull requests at the org level where you want clear "this PR landed here" markers in the history.
- Open-source projects where contributors don't have permission to force-push — maintainers merge their PRs as-is.
Merge commits are not a sign of bad hygiene. They're a sign that integration happened.
When rebase IS the right answer
- Updating your personal branch with main's latest. Rebase, don't merge — avoid the "merge main into my branch" zigzag in your branch's history.
- Cleaning up a messy branch before PR review.
git rebase -i HEAD~10lets you squash, reword, drop, reorder commits. Make the PR's history readable before review starts. - Splitting a big commit.
git rebase -iwithediton the offending commit, then reset and re-commit in pieces.
Interactive rebase — the underused power tool
git rebase -i origin/main
Drops you into an editor with your commits listed:
pick a1b2c3d add user search
pick e4f5g6h fix typo
pick i7j8k9l add tests for search
pick m0n1o2p wip
pick q3r4s5t fix tests
Edit the lines:
pick→rewordto change the commit messagepick→editto stop and amend the commitpick→squashto combine into the previous commitpick→fixupto combine without editing the messagepick→dropto remove the commit entirely- Reorder lines to change commit order
Save and exit. Git replays the script.
This is how you turn 12 WIP commits into 3 clean ones for review.
What to internalise
- Rebase = rewrite history. Merge = preserve history.
- Rebase your own branch onto main (clean update). Merge your branch into main (clean integration).
- Never rebase commits other people have based work on.
- Force-push?
--force-with-lease, never--force. - Interactive rebase before PR review pays off ten-fold during the review.
Tools in the wild
4 tools- cligitfree tier
The DVCS itself — both `merge` and `rebase` ship with every install.
- cliGitButlerfree tier
Branch-juggling client that makes rebase chains visible and recoverable.
- cliLazygitfree tier
TUI for git that makes interactive rebase actually pleasant.
- serviceGitHubfree tier
PR settings let you enforce rebase-merge or squash-merge per repo.