DEV Community

Cover image for git merge vs rebase — decide in 10 seconds
Mr Say Nothing
Mr Say Nothing

Posted on Originally published at mrsaynothing.dev

git merge vs rebase — decide in 10 seconds

Originally published on mrsaynothing.dev — receipts and interactive figures live there.

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 is the decision, not the theology. Every graph and SHA below ran on git 2.47.3 in a throwaway bench.

tl;dr

  • One question decides it: did the commit leave your machine? Unpushed work → rebase. Anything others may have pulled → merge.
  • Merge records what happened — one commit, two parents, no SHA changes. Rebase rewrites: in my bench run, commit 5d48940 came back as 6b2be0a.
  • Rebase went sideways? git reflog + git reset --hard HEAD@{1} puts the branch back. Nothing is gone.

The five-minute checklist

  1. git fetch origin — look at the real remote, not yesterday's memory of it
  2. git status -sb — the ahead/behind pair shows who has what
  3. Ask the one question: did any of these commits leave this machine?
  4. Shared → git merge; yours alone → git rebase
  5. git log --oneline --graph — run it to confirm the shape you chose

The whole undo map — undo, revert, reset, stash, cherry-pick — lives in one place on the site: Undo Anything in Git.

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                          after rebase

*   8e1f74b merge · 2 parents        * 67c0442 c4'
|\
| * 772bc59 c4                       * 6b2be0a c3'
| * 5d48940 c3
* | 2ff7bfa c5                       * 1398836 c5
|/
* 4e9d509 c2                         * facd5a4 c2
* 28fa4bd c1                         * f45b000 c1
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 the next section 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. 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 fork-sync variant of this decision lives 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
Enter fullscreen mode Exit fullscreen mode

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?

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
Rebase went wrong git reset --hard HEAD@{1} Reflog still holds the pre-rebase tip (~90 days)

Where does --squash fit? 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. Squash is the third answer in "merge vs rebase vs squash", for throwing a branch away after landing its net change.

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.

The full post — interactive decision tree, history diagrams, filterable table — is on mrsaynothing.dev. Which git command did you get wrong the longest — and did the reflog save you?

Top comments (1)

Collapse
 
mrsaynothing profile image
Mr Say Nothing •

The rewrite is the part people underestimate: in my bench run the feature commit went in as 5d48940 and came out as 6b2be0a — same change, new identity. Everyone holding the old SHA (review comments, CI logs, a link in internal docs) now points at a ghost. Whats the oldest ghost SHA your team still cites somewhere — and did anyone ever click it?