■ space · prose lane
Has anyone else pulled the commit? Yes → git revert <sha>. No → git reset, any mode.
--soft keeps edits staged, --mixed (default) unstaged, --hard deletes them — the only shredder.
One mangled file? git restore <file> — leave the branch alone.
THE THROWAWAY-REPO DEMO
Two minutes in a scratch repo, and the difference is not a definition any more — it's a log you've seen:
build a two-commit repo — 2 commits · the mistake is committed, the tree is clean
git init /tmp/revert-vs-reset && cd /tmp/revert-vs-reset
echo one > file.txt && git add . && git commit -m "one"
echo two > file.txt && git add . && git commit -m "two"
revert it — history grows
git revert --no-edit HEAD
git log --oneline
# 1a2b3c4 (HEAD -> main) Revert "two" ← the cancellation
# 5d6e7f8 two ← still in the log
# 9f8e7d6 one
reset it — history shrinks
git reset --hard HEAD~1
git log --oneline
# 9f8e7d6 one ← "two" is unreachable
verify: after the revert git log --oneline → 3 commits · after the reset → 1 commit — the asymmetry is the whole answer
THE THREE RESET MODES
The modes control where your changes survive after the pointer moves — and only one row of this table deletes work:
| Mode | Branch pointer | Staging area | Working tree | Your edits |
|---|---|---|---|---|
--soft | moves back | kept staged | untouched | fully kept |
--mixed (default) | moves back | unstaged | untouched | kept, unstaged |
--hard | moves back | reset | reset | deleted |
git reset --soft HEAD~1— "I committed too early." Everything goes back to staged, ready to recommit.git reset HEAD~1(mixed) — "I staged the wrong files." Edits survive in the working tree, nothing staged. Pair it with a clean when you also want stray files gone.git reset --hard HEAD~1— "This commit and my uncommitted edits are garbage." Git will not ask twice; there is no undo for the working-tree part unless you stashed or committed it first.
verify: git reset --soft HEAD~1 → git status shows the change "to be committed" · --mixed → "changes not staged"
revert has no modes because it never destroys anything — it only ever adds a compensating commit.
THE ONE QUESTION
Has anyone else pulled this commit? If yes, reset is off the table. Resetting a branch others have based work on rewrites shared history: their next pull meets divergent branches, and the "fix" is usually a force-push that turns your mistake into everyone's problem. Revert appends a normal commit, so a plain git pull merges cleanly for everyone — that's why revert is the default answer on main and on GitHub pull requests, where the "Revert" button exists precisely because rewriting a merged PR's history is not an option.
Reset is for the window before sharing: the commit from thirty seconds ago, the staging mistake, the local experiment branch. If the only copy of the work lives on your machine, you are free to rearrange history — that is the entire point of the pre-push window. Middle case: you pushed to your own feature branch and nobody builds on it — a force-push after reset is socially acceptable there, but revert still costs less coordination. Reserve force-pushes for branches you own alone.
Revert is also safer structurally, not just by convention: the revert produces an ordinary commit — CI runs on it, the diff is reviewable, and git log explains to future-you why the change disappeared. Reset silently discards the intermediate state; nobody reviewing later can see what was undone or when.
TWO SHARP EDGES ON REVERT
- Reverting a merge commit needs
git revert -m 1 <sha>— a merge has two parents, so "undo it" is ambiguous;-m 1keeps the line you merged into. Without it, git refuses. Follow-up trap: re-merging the same branch afterwards looks already-applied to git, so re-landing that work means reverting the revert. - Revert is not a time machine — it undoes one commit's diff. If later commits touched the same lines, you may resolve conflicts: git telling you the undo is entangled, which is useful information, not a malfunction.
WHERE RESTORE FITS
git restore (and git switch, its pair) arrived in git 2.23 to take over jobs reset was doing badly by name overload:
| Task | Old way | Clear way |
|---|---|---|
| Discard working-tree edits in a file | git checkout -- file / git reset --hard | git restore file |
| Unstage a file | git reset HEAD file | git restore --staged file |
| Move branch to another commit | git reset <sha> | git reset <sha> (no replacement — this stays) |
The modern split: restore fixes files, reset moves branches, revert undoes published commits. They are one mental model split across three commands. The old git checkout forms still work everywhere; the new forms just stop you from sending a whole branch to the shredder when you meant to unstage one file.
THE RECAP
- Commit is public (pushed, others pulled) →
git revert <sha>. - Commit is local-only, want to redo it →
git reset --softor--mixedand recommit. - Local-only, want it gone with the uncommitted mess →
git reset --hard, after one honest look at what else the working tree holds. - One file got mangled →
git restore <file>and leave the branch alone.
verify: git reflog → HEAD@{n} lists the pre-reset position — git reset --hard HEAD@{1} walks it back
The only genuinely dangerous entry in that list is --hard — everything else can be talked back with git reflog — which remembers the detached commits for ~90 days. The permanent losses in git are narrow and mostly require you to type it explicitly; the habit worth building is pausing half a second before --hard, not avoiding git's sharp tools altogether. And if the whole undo menu feels like ceremony, that's a fair criticism — jujutsu, the git-compatible VCS, redesigns exactly this part.
end of the undo pair · nothing fetched · everything local