mrsaynothing.dev
git merge vs rebase — decide in 10 seconds

> git merge vs rebase — decide in 10 seconds▋

Merge or rebase, one question decides: did the commit leave your machine? The decision table, the fast-forward case, and the reflog fix when a rebase goes wrong.

mrsaynothing· 5 October 2026· 7 min read

✦ fixgit log --oneline --graph

You will type one of these two commands thousands of times in a career, and half the advice online makes it a religion — rebase everything, history must be clean on one side, never rebase, it destroys work on the other. Both camps skip the actual rule, which fits in ten seconds. This post is the decision, not the theology, and it belongs to the undo anything in Git cluster — same family as git revert vs reset, the other "two commands, one question" pair. Git itself was written from scratch in a ten-day sprint in April 2005; the merge-vs-rebase argument has been running ever since.

The five-minute checklist

git merge vs rebase — the one question a commit is ready to land git status -sb shows ahead/behind does anyone else pull this branch? check the branch, not your gut git log origin/feature..feature others may have pulled → yours alone → git merge records what really happened git rebase linear history, review-ready history looks wrong after? git reflog → reset --hard HEAD@{1}
fig 1 — the merge-or-rebase tree. Click your branch.

What is the difference between git merge and git rebase?

Both commands land the same code; they disagree about what the history should say afterwards. Merge writes the disagreement down: one new commit with two parents, the divergence preserved for anyone reading the graph later. Rebase pretends you started from today's base: your three commits are replayed as three new commits with new SHAs, and the old ones become unreachable from any branch.

Ran both in a throwaway bench on git 2.47.3, same five commits each time. The merge run kept every original SHA and added one knot; the rebase run changed the feature commits' identities — c3 went in as 5d48940 and came out as 6b2be0a. Same tree, different history:

after merge c1 c2 c3 · c4 c5 merge commit · 2 parents after rebase c1 c2 c5 c3' 6b2be0a c4' 67c0442 one line — no knot, old SHAs gone
fig 2 — same tree, two histories. The primes are the same changes with new commit IDs.

verify: git log --oneline --graph → a |\ knot means merge, a straight line means rebase won

When does a merge become a fast-forward?

If main has not moved since you branched, there is nothing to merge — git just slides the label forward. That is a fast-forward, and it is why rebase people get their clean line without forcing anything: rebase your feature onto main, then merge becomes a fast-forward, and the graph stays one straight line. From the bench:

* 67c0442 c4 feature 2      # rebased, then merged — fast-forward
* 6b2be0a c3 feature 1      # new SHAs — 5d48940 before the rebase
* 1398836 c5 main moved on
* facd5a4 c2 main work
* f45b000 c1 base

Five commits, zero knots. The trade: those prime SHAs no longer exist anywhere except your machine. Push them and a teammate's clone still holds the originals — two versions of "the same" commit, which is the exact mess section five is about.

verify: git merge --ff-only feature → Fast-forward in the output, no merge commit created

Which commits are still yours alone?

The answer is one command, not a feeling. git log origin/main..HEAD lists the commits origin has never seen — those are rebaseable. The mirror, git log origin/main..origin/main is empty by definition; the command that matters is checking the branch others build on. If the list includes a commit that another clone might already have — a push, an open pull request, a branch you pushed and force-pushed before — it is not yours alone, whatever your local graph claims.

Unpushed work is exactly what rebase was made for: replay it onto the updated base and your pull request reads as one clean sequence. This is also why "pull with rebase" is a sane default for private branches — git pull --rebase replays your local commits on top of whatever came in, instead of weaving a merge knot every time you sync. The stashed-work corner case lives in git stash a single file; the fork-sync variant in sync fork with upstream.

What breaks when you rebase a shared branch?

Every clone that holds the old commits now disagrees with yours about reality. Rebase gives your commits new SHAs; your pushed branch and your rewritten branch share no ancestry, so the next push needs --force — and everyone who pulled the old version gets to merge their copy of commits that "no longer exist". Their duplicates resurface in every future merge. This is not a hypothetical: the duplicate-commit merge is the classic aftermath, and unwinding it costs an afternoon.

The rule is one sentence long, and the official book carries it as a warning, not a suggestion:

Do not rebase commits that exist outside your repository and that people may have based work on.

— Pro Git, 2nd edition, "Git Branching — Rebasing"

Reviewer comments pinned to specific SHAs detach in the same move — rebase an open pull request and the review context evaporates with the old commit IDs.

How do you undo a rebase?

The old commits survive the rebase — reflog is where they hide. Rebase moves branch pointers; it does not delete objects. The bench recovery, after a reset --hard threw two commits away:

$ git reflog | head -4
1398836 HEAD@{0}: reset: moving to HEAD~2
67c0442 HEAD@{1}: merge feature: Fast-forward
1398836 HEAD@{2}: checkout: moving from feature to main
67c0442 HEAD@{3}: rebase (finish): returning to refs/heads/feature

$ git reset --hard HEAD@{1}
HEAD is now at 67c0442 c4 feature 2

One line, branch restored, staged and unstaged work with it. Reflog entries age out after about 90 days by default, so the rescue has a clock on it — same mechanism as in git undo last commit, and the fuller revert/reset decision lives in git revert vs reset.

verify: git reflog | head -3 → the pre-rebase tip sits at HEAD@{1}, one reset away

Which command does your situation need?

The table is the whole post in six rows — filter by your branch's situation.

all feature shared local fork recovery
Situation Command Why it wins
Your feature branch, never pushed git rebase main Linear history, one clean sequence for review
A branch others pull from git merge No SHA rewrite — no duplicate commits in anyone's clone
Diverged local main, nothing pushed git pull --rebase Syncs without a knot; your commits stay on top
Fork catching up with upstream git merge upstream/main Matches the fork sync flow; PR stays attached
Hotfix that must land now git merge --no-ff Visible marker of where the fix entered, even on a fast-forwardable branch
Rebase went wrong git reset --hard HEAD@{1} Reflog still holds the pre-rebase tip (~90 days)
Where does --squash fit in?

git merge --squash takes a branch's changes and stages them as one uncommitted lump — no merge commit, no link to the branch's history. Autocomplete says people search "merge vs rebase vs squash" daily: squash is the third answer, for throwing a branch away after landing its net change. It rewrites nothing; it just refuses to record the branch's internal steps.

Can a team just pick one?

Plenty do: rebase-local-work, merge-into-shared is the most common policy, and GitHub's own merge button keeps merge as the default. A repo-wide "rebase everything" policy works too — as long as no shared branch is ever rewritten, which is the one sentence that survives every workflow debate.

The ten-second version, one last time: did the commit leave your machine? No → rebase. Yes → merge. When the wrong one wins anyway, reflog has the exit — and the rest of the undo map lives in the git undo hub.

faq

— mrsaynothing

$ Get the next how-to by email

One email per post. Fix it and move on.

self-hosted · no third parties · one-click unsubscribe

what is this?