■ space · 散文道
撤销最后一次提交并保留改动:git reset --soft HEAD~1(已暂存)或git reset HEAD~1(未暂存)。
已经推送了?git revert HEAD——用一个新提交把它撤销掉,在共享分支上安全无虞。
reset --hard 是唯一会删工作的一行——而且 git reflog 还记得,约 90 天。
三条车道
多了一个提交,三种出口。旗标决定你的改动落在哪——顺着车道走:
图示——唯一会删工作的车道是 --hard;reflog 是流过三条车道底下的排水沟
保留改动:那两条命令
最常见的情形:提交完才发现一个错别字、漏了一个文件,或者意识到这个改动应该属于另一个提交。什么都还没推送。撤销提交,把一切放回去:
# 提交消失——改动回到暂存区
git reset --soft HEAD~1
# 或者:改动回到工作区(未暂存)
git reset HEAD~1
HEAD~1 意思是“HEAD 当前指向往前一个提交”。两条命令任选其一之后,你的文件在磁盘上原封未动——移动的只是分支指针。用 git status: with --soft 看:改动处于 已暂存状态,可以带着修好的内容重新提交;不加旗标则是 未暂存,可以先自由编辑。
验证:git status → 已暂存(--soft)或未暂存(默认)· git log --oneline -1 → 显示上一个提交
如果你只是想往最后一次提交里补文件,更稳妥的习惯是根本不用撤销:
git add forgotten-file.txt
git commit --amend --no-edit
--amend 原位替换最后一次提交(这里不改提交信息)。注意:amend 一个已推送的提交等于改写历史——详见下文。
三个旗标,并排看
这段值得背下来:旗标决定你的改动最终落在哪里——以及它们会不会丢:
| 旗标 | 提交被撤销? | 磁盘上的改动 | 已暂存? | 典型用途 |
|---|---|---|---|---|
--soft | 是 | 保留 | Yes | 带着小修小补重新提交 |
--mixed(默认) | 是 | 保留 | No | 重新分组,选择性暂存 |
--hard | 是 | 删除 | — | 把工作整个扔掉 |
git revert | 否(新提交) | 保留 | — | 撤销已推送的提交 |
--hard 是唯一危险的那个:它同时丢弃提交和改动。任何 hard reset 之前,先把手头的 stash 或开个分支:
git branch backup-before-reset # 便宜的保险
git reset --hard HEAD~1 # 提交 + 改动一起消失
已经跑了危险版本?并非全盘皆输——git reflog 记得 HEAD 去过哪里。reflog 默认把悬空提交保留约 90 天,所以“我手滑 hard-reset 了”几乎总能救回来,只要你在垃圾回收之前动手:
git reflog # 或者:git reset --hard <hash>
git reset --hard HEAD@{1} # or: git reset --hard <hash>
验证:git reflog → 被丢弃的提交就在 HEAD@{1}
已经推送了?
共享历史只加新提交,永不重写。 如果提交在共享分支上——除了你自己的功能分支以外的任何分支——不要改写历史。git revert 会算出相反的改动并提交它。所有 pull 的人只是得到一个移除旧改动的新提交。没有强推,没有 colleagues 被坑:
git revert HEAD
git push
Undoing a run of commits is the same move over a range: 撤销一串提交,就是把同样的动作套在一个区间上:. The alternative — ——只在没人基于它构建的分支上才可接受: — is only acceptable on a branch nobody else builds on: --force-with-lease 会在远端自你上次 fetch 后有移动时拒绝推送;裸的 --force 会覆盖一切新出现的东西。强推共享分支,就是团队弄丢提交、CI 日志神秘地对不上任何人的 checkout 的原因。
验证:git log --oneline -2 → Revert "…" 压在被撤销的提交之上
好工作,投错门
有时提交本身没问题——分支选错了,或者推早了。与其撤销,不如搬走。把整个提交停到新分支上,然后清理你的
git branch stash-commit # 把提交停到新分支上
git reset --hard HEAD~1 # 然后清理当前分支
git cherry-pick <hash> # 在目标分支上执行
在 reset --soft, cherry-pick and revert 之间,没有任何提交是你挪不动或取消不了的——诀窍是在伸手去拿旗标之前,先决定要“搬”还是要“撤”。
动词就是命令
一半的困惑是词汇问题:人们在搜索框里敲六个动词,而真正存在的命令只有三条。把脑子里的词对上干这件事的旗标:
| 你输入的词 | 你想要的 | 命令 | 改动保留? |
|---|---|---|---|
| undo / uncommit | 提交消失,工作回来 | git reset --soft HEAD~1 | 是——已暂存 |
| undo,未暂存 | 提交消失,工作散着回来 | git reset HEAD~1 | 是——mixed |
| remove / drop | 提交和修改都消失 | git reset --hard HEAD~1 | 否——reflog 约 90 天 |
| revert / cancel,已推送 | 公开撤销,历史完整 | git revert HEAD | 是——新提交 |
在 Git 里这些动词不是同义词:revert 从不碰过去,reset 则改写过去。跟队友说错一个词,你就会得到错误的命令。整棵决策树,做成清单:
- 未推送,想修好后重新提交 →
git reset --soft HEAD~1 - 未推送,想选择性重新暂存 →
git reset HEAD~1(mixed) - 想让改动彻底消失 →
git reset --hard HEAD~1(reflog 知道,万一你后悔) - 已推送到共享分支 →
git revert HEAD - 提交属于另一条分支 →
cherry-pick,别撤销
想让别人替你做决定?git-undo 生成器会问这些问题并打印命令。最后一条运维提示:如果坏提交真的上了服务器——部署钩子翻车、强推后 CI runner 抽风——下一个该看的地方是机器的日志,不是 Git:在任何 systemd 机器上,journalctl -u <service> -n 100 shows you exactly what ran and when. And when Git refuses to run at all — fatal: not a git repository — that is a different problem from undoing anything.
撤销页到此为止 · 未取一分毫 · 一切皆本地