写代码最常遇到的事:改了一下午发现方向全错;提交完才发现把密码文件也提交了;push 之后线上出 bug。别慌,Git 的设计者早就备好了后悔药。本文按"文件还没提交 → 已提交未推送 → 已推送"三个战场,讲清楚 restore、reset、revert 各自的适用场景。

1. 后悔药的三种原理

先建立整体认知。Git 的撤销命令可以分成三类:

命令作用对象典型场景
git restore(旧版 git checkout --)工作区 / 暂存区改乱了,想回到上次提交或撤销 add
git reset本地提交历史提交错了,想撤销或重写提交
git revert已推送的提交线上出 bug,安全地"反做"某次提交

核心判断标准只有一个:这个错误是否已经 push 到远程、别人是否已经基于它工作。没推送,随便 reset;推送了,优先 revert,因为它不会改写历史。按破坏性从低到高排:restore 最温和,reset 三个模式依次递进,revert 则永远只增不改——心里有这张谱,关键时刻就不会乱。

2. 场景一:文件还没提交

你改了 main.py,越改越乱,想放弃这些改动回到最近一次提交的状态:

git restore main.py
# 旧版写法:git checkout -- main.py

这条命令用暂存区的内容覆盖工作区,所有未暂存的修改全部丢弃。git restore 是 Git 2.23 之后推荐的新命令,功能与 git checkout -- 相同。危险提醒:这个操作不可恢复,被覆盖的改动直接消失,执行前确认这不是你想要的版本。

如果文件已经被 git add 进暂存区,想撤销"暂存"这个动作、但保留文件内容:

git restore --staged main.py
# 旧版写法:git reset HEAD main.py

--staged 表示只把文件从暂存区"退回去",工作区里的改动原封不动。这个操作非常安全,相当于反悔了 git add。

3. 场景二:已提交但未推送

提交之后才发现提交信息写错了,或者漏了一个文件。先处理最简单的——修改最后一次提交:

git add 漏掉的文件.py
git commit --amend -m "修正后的提交信息"

git commit --amend 会"重做"上一次提交:把当前暂存区的内容并进去,同时替换提交信息。注意它生成的是新提交,旧的提交对象还在,只是没人引用了。

如果想撤销最近几次提交,用 git reset,它有三个模式:

git reset --soft HEAD~1    # 撤销提交,改动留在暂存区
git reset --mixed HEAD~1   # 撤销提交,改动回到工作区(默认)
git reset --hard HEAD~1    # 撤销提交,改动全部丢弃

HEAD~1 表示上一个提交。三个模式的区别是撤销之后改动去哪:--soft 留在暂存区,适合"提交拆得太碎想重新组织";--mixed 回到工作区,适合"提交了不该提交的东西,想重新 add";--hard 直接扔掉,适合"这一版提交完全是垃圾"。--hard 最危险,慎用。

还有一种常见情况:合并或变基到一半,发现冲突太多、或者压根不该合,想直接退出。merge 和 rebase 都提供了"一键放弃":

git merge --abort   # 放弃本次合并,回到合并前
git rebase --abort  # 放弃本次变基,回到变基前

这两条命令会干净地退出冲突现场,工作区恢复到操作之前的样子。记住它们,遇到"合并到一半发现方向错了"的时刻,比硬着头皮解冲突明智得多。

4. 场景三:已经推送

一旦提交被 push 到远程、尤其别人还拉取过,就不要再 reset 了——改写历史会让队友的仓库变得不一致。正确的做法是 git revert,它新增一个提交来抵消目标提交的改动:

git revert 提交哈希
git push

git revert 会打开编辑器让你写新提交的说明,默认是 Revert "原来的提交信息"。执行后历史变成:原提交 → 新提交(反做)。历史只增不减,团队其他人 pull 时毫无压力。如果 revert 时发生冲突,解决方式跟普通合并冲突一样,解完 git add 再 git commit。

5. reflog:最后一颗后悔药

万一刚才手滑执行了 git reset --hard,改动全没了怎么办?Git 有个隐藏日志 reflog,记录了你每一次 HEAD 的移动:

git reflog
# 输出类似:
# a1b2c3d HEAD@{0}: reset: moving to HEAD~1
# f9e8d7c HEAD@{1}: commit: 完成登录功能

git reflog 会显示最近所有"HEAD 去过哪里",包括被 reset 掉的提交。HEAD@{1} 表示"上一次的 HEAD"。找到目标提交的哈希,直接切回去:

git reset --hard f9e8d7c

reflog 默认保留 90 天,相当于 Git 自带的"回收站"。所以哪怕 --hard 了也不用绝望:只要在 90 天内,提交对象都还在,reflog 能帮你找回来。

6. 总结与练习

撤销的决策树:未提交用 git restore;已提交未推送用 git reset(拿不准就 --mixed);已推送用 git revert;什么都救不回来时查 git reflog。练习:

💡 黄金法则:未推送的提交随便折腾,已推送的提交用 revert。改写历史之前先问自己一句——别人会不会已经基于这个提交干活了?