■ space · prose lane
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
| 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 | PR stays attached to its commits |
| 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? 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.
end of the two histories · nothing fetched · everything local