■ 空间 · 散文 lane
有没有别人 pull 过这个提交? 有 → git revert <sha>。没有 → git reset,任意模式。
--soft 让改动保持 staged,--mixed(默认)unstaged,--hard 把它们删掉——唯一的碎纸机.
只有一个文件被搞坏? git restore <file> ——别去动分支。
一次性仓库演示
在一个临时仓库里花两分钟,差别就不再是一段定义——而是你亲眼见过的 log:
搭一个两个提交的仓库 — 2 commits · 错误已提交,树干净
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 — 历史变长
git revert --no-edit HEAD
git log --oneline
# 1a2b3c4 (HEAD -> main) Revert "two" ← the cancellation
# 5d6e7f8 two ← still in the log
# 9f8e7d6 one
reset — 历史变短
git reset --hard HEAD~1
git log --oneline
# 9f8e7d6 one ← "two" is unreachable
verify: revert 之后 git log --oneline → 3 commits · reset 之后 → 1 commit — 不对称本身就是全部答案
reset 的三种模式
这些模式控制 你的改动在指针 移动之后活在哪里——而这张表里只有一行删除工作:
| 模式 | 分支指针 | 暂存区 | 工作树 | 你的改动 |
|---|---|---|---|---|
--soft | 后退 | 保留并保持 staged | 不受碰 | 完整保留 |
--mixed(默认) | 后退 | unstaged | 不受碰 | 保留,unstaged |
--hard | 后退 | 重置 | 重置 | 删除 |
git reset --soft HEAD~1——「我提交得太早。」一切都回到 staged,随时重新提交。git reset HEAD~1(mixed) ——「我暂存错了文件。」改动在工作树里活下来,什么都没 staged。想顺手清掉走失的文件,就再配一个 clean。git reset --hard HEAD~1——「这个提交和我没提交的改动都是垃圾。」git 不会问第二遍;工作树那部分没有撤销,除非你先 stash 或先提交过它。
verify: git reset --soft HEAD~1 → git status 显示改动「to be committed」 · --mixed → 「changes not staged」
revert 没有各种模式,因为它从不销毁任何东西——只是补出一个对冲提交。
那一个问题
有没有别人 pull 过这个提交? 有的话,reset 出局。reset 一条别人在其上盖了楼的分支,等于重写共享历史:他们下一次 pull 会迎面撞上分叉的分支,「修复」通常是 force-push,把你的失误变成所有人的麻烦。revert 追加一个普通提交,所以一条普通的 git pull 能为所有人干净地合并——这就是 revert 是 main 和 GitHub pull request 上默认答案的原因,那里专门有「Revert」按钮,正是因为重写一个已合并 PR 的历史不是选项。
reset 留给共享 之前 的窗口:三十秒前的提交、暂存失误、本地实验分支。如果这份工作的唯一副本就在你机器上,你可以随意重排历史——这就是 push 前窗口的全部意义。中间情形:你 push 到了自己的 feature 分支、没人从它往上盖楼——reset 之后跟一个 force-push 在那里社会性可接受,但 revert 仍省协调。把 force-push 只留给独自拥有的分支。
revert 还更安全,是 结构上的,不只因惯例:revert 产出的是一个普通提交——CI 在它上面跑,diff 可供审阅,而 git log 向未来之你解释那个改动为何消失。reset 无声地丢弃中间状态;后来审阅的人看不见什么被撤销、何时被撤销。
revert 的两处锋利边缘
- revert 一个合并提交需要
git revert -m 1 <sha>——合并有两个父提交,所以「撤销它」是含糊的;-m 1保持你并进去的那条线。不带它,git 直接拒绝。后续陷阱:之后重新合并同一分支会被 git 视作已经应用过,所以想重新落地那些工作,就得 revert 那个 revert。 - revert 不是时光机 ——它只撤销 一个提交的 diff。如果之后的提交碰过同样的行,你也许要解冲突:git 在告诉你撤销缠住了——这是有用信息,不是故障。
restore 放在哪儿
git restore(还有 git switch,它的搭档)在 git 2.23 登场,接管 reset 因名字过载而做不好的那些活:
| 任务 | 老办法 | 清楚的办法 |
|---|---|---|
| 丢弃一个文件在工作树里的改动 | git checkout -- file / git reset --hard | git restore file |
| 把文件移出暂存区 | git reset HEAD file | git restore --staged file |
| 把分支移到另一个提交 | git reset <sha> | git reset <sha>(没有替代者——这个留着) |
现代分工:restore 修文件,reset 动分支,revert 撤公开提交。 它们是同一个心理模型切成三条命令。老的 git checkout 形式处处仍能用;新形式只是阻止你把整条分支送进碎纸机,而你本意只是把一个文件移出暂存区。
回顾
- 提交已公开(已 push、别人 pull 过) →
git revert <sha>. - 提交只在本地,想重来一遍 →
git reset --soft或--mixed然后重新提交。 - 只在本地、想连同未提交的烂摊子一起清掉 →
git reset --hard,之前先诚实地看一眼工作树里还装着什么。 - 只有一个文件被搞坏 →
git restore <file>,别去动分支。
verify: git reflog → HEAD@{n} 列出 reset 前的位置 — git reset --hard HEAD@{1} 走回去
那份名单里唯一真正危险的条目是 --hard ——其余的都能用 git reflog 救回来——它把脱队的提交记约 90 天。git 里的永久损失范围很窄,而且大多要求你亲手显式敲出来;值得养成的习惯是在 --hard 前顿半秒,而不是把 git 的锋利工具一概敬而远之。如果整份撤销菜单让你觉得是一场仪式,这批评是公道的——jujutsu,这个 git 兼容的 VCS,重做的正是这一块。
撤销二重奏到此为止 · 未取任何远程内容 · 全程本地