■ space · prose lane

■ the answer first

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 the bench below, commit 5d48940 came back as 6b2be0a.

Rebase went sideways? git reflog + git reset --hard HEAD@{1} puts the branch back. Nothing is gone.

THE TWO HISTORIES

Both commands land the same code; they disagree about what the history should say afterwards. 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. Same tree, different history:

fig — the primes are the same changes with new commit IDs: c3 went in as 5d48940 and came out as 6b2be0a

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

WHEN MERGE BECOMES 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 — five commits, zero knots:

* 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

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

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 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 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 git pull --rebase is a sane default for private branches — it replays your local commits on top of whatever came in, instead of weaving a merge knot every time you sync. Check the branch, not your gut: git fetch origin first, git status -sb for the ahead/behind pair.

WHAT BREAKS ON 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; unwinding that costs an afternoon. 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. 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"

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.

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

THE SIX SITUATIONS

SituationCommandWhy it wins
Your feature branch, never pushedgit rebase mainLinear history, one clean sequence for review
A branch others pull fromgit mergeNo SHA rewrite — no duplicate commits in anyone's clone
Diverged local main, nothing pushedgit pull --rebaseSyncs without a knot; your commits stay on top
Fork catching up with upstreamgit merge upstream/mainPR stays attached to its commits
Hotfix that must land nowgit merge --no-ffVisible marker of where the fix entered, even on a fast-forwardable branch
Rebase went wronggit reset --hard HEAD@{1}Reflog still holds the pre-rebase tip (~90 days)

Where does --squash fit? It 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, for throwing a branch away after landing its net change; it rewrites nothing, it just refuses to record the branch's internal steps. And can a team just pick one? Plenty do: rebase-local-work, merge-into-shared is the most common policy, and a repo-wide "rebase everything" 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.

■ honest stamp Tested on: git 2.47.3 in a throwaway bench — every graph and SHA above ran there. First published 2026-10-05. The SHA-rewrite behaviour, the fast-forward mechanics and the reflog recovery are the bench's own output, retyped; the Pro Git warning is quoted from the official book. Git itself was written from scratch in a ten-day sprint in April 2005 — the merge-vs-rebase argument has been running ever since, and it never needed to.

end of the two histories · nothing fetched · everything local