Git 撤销操作 — reset / revert / restore 救命指南

"我刚才提交错了!""这段代码改坏了想退回去!""把密钥提交进去了怎么办!"——这是 Git 最常救场的场景。撤销操作根据"改动的阶段"和"是否已 push"有不同的方法。这一章把所有撤销套路整理成一张清晰的地图,记牢它能在关键时刻救命。

先搞清楚:你在哪个阶段?

撤销操作的关键是判断改动现在在哪个阶段,不同阶段用不同命令:

1. 撤销工作区与暂存区的改动

这两个阶段用 git restore(Git 2.23 引入,替代了 checkout 的混乱用法):

# 阶段 1:还没 add — 撤销工作区的修改
# 场景:改了 file.txt,发现改错了,想恢复成最近一次提交的样子

git restore file.txt            # 新命令(推荐)
git checkout -- file.txt        # 老命令(等价)

# 一次性恢复整个工作区(危险!所有未提交改动都没了)
git restore .

# 恢复成某个历史版本(不是最近一次提交)
git restore --source=HEAD~3 file.txt

# ---
# 阶段 2:已经 add 还没 commit — 从暂存区撤回
# 场景:误把 .env 加进暂存区了,想拿出来

git restore --staged file.txt   # 新命令(推荐)
git reset HEAD file.txt         # 老命令(等价)

# 撤回所有暂存的文件
git restore --staged .

# 注意:这只会让文件"退出暂存区",工作区的内容不变
# 想彻底丢弃改动,再 git restore file.txt

2. git reset — 撤销提交(还没 push 时)

reset 是"移动分支指针"的命令。把它往后移,就等于"撤销"了那些提交。它有三种模式,区别在于被撤销的改动留在哪里:

# 阶段 3:已经 commit 但还没 push — 用 reset 撤销提交
# reset 有三种模式,区别在于"改动留在哪里"

# soft:撤销提交,但改动完整留在暂存区(可重新 commit)
git reset --soft HEAD~1
# 适合:刚 commit 完发现 message 写错了,想重新写

# mixed(默认):撤销提交,改动留在工作区(变未暂存)
git reset HEAD~1               # 不写模式就是 mixed
# 适合:想重新挑选哪些改动进这次提交

# hard:撤销提交,改动也丢弃(彻底!)
git reset --hard HEAD~1
# 适合:这次提交完全不要了,代码也回退
# 警告:工作区未提交的改动也会丢!

# ---
# reset 到指定提交(不是只回退一个)
git reset --hard a1b2c3d        # 回到 a1b2c3d 那个状态

# reset 后想"反悔"?用 reflog 找回!
git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~1
# e4f5g6h HEAD@{1}: commit: feat: 添加登录   ← 这个被 reset 掉了
git reset --hard e4f5g6h        # 又回来了!

一张速查表:

3. git revert — 撤销已 push 的提交

这是最容易被新手搞错的地方:已经 push 到共享分支的提交,绝对不能用 reset 撤销——因为那会改写历史,别人 pull 时会爆炸。正确做法是用 git revert,它会生成一个反向提交(把目标提交的改动反过来做一遍),历史是"增加"的,不改写:

# 阶段 4:已经 push 到共享分支 — 用 revert(不要 reset!)
# 场景:已经 push 的提交发现有问题,要撤销
# 但别人可能已经 pull 走了,不能改写历史

# revert 会生成一个"反向提交",效果是抵消掉指定提交
git revert HEAD                # 撤销最近一次提交
git revert a1b2c3d             # 撤销指定的某次提交

# 工作流:
# 1. Git 自动算出"反向改动"
# 2. 打开编辑器让你写 revert message(默认就行)
# 3. 生成一个新的"撤销提交"
# 4. git push 推上去

# 历史效果:
#   原来: A - B - C            (C 是有问题的)
#   revert C 后:A - B - C - C' (C' 是反向提交,内容等于撤销 C)
#   历史是"增加"的,没有改写,别人 pull 不冲突

# 撤销一段连续提交(范围)
git revert HEAD~3..HEAD

# 撤销一个合并提交(高级,需要 -m 指定保留哪个父)
git revert -m 1 a1b2c3d

黄金法则(最重要)

已经 push 到共享分支的提交,绝不 reset --hard / rebase / amend。

原因:这些操作会改写提交哈希。同事已经基于旧哈希在工作,你改写后他们 pull 时,Git 会认为这是两份不同的历史,产生无法自动解决的冲突——轻则大家浪费时间,重则代码丢失。

4. git commit --amend — 修改最近一次提交

这是日常最高频的"小撤销":刚 commit 完发现 message 写错、漏了文件、多加了文件。amend 把当前暂存区的改动并入上一个提交(并可改 message):

# 修改最近一次提交(还没 push 时最常用)

# 场景 A:刚 commit 完,发现 message 写错了
git commit --amend -m "feat: 正确的提交信息"

# 场景 B:刚 commit 完,发现漏了一个文件
git add 漏的文件.txt
git commit --amend --no-edit    # --no-edit 保持原 message

# 场景 C:想改最近一次提交的作者信息
git commit --amend --author="新名字 <new@example.com>"

# 注意:amend 会改写提交哈希!
#   - 还没 push:随便 amend,安全
#   - 已经 push 到私人分支:可以,但要 force-with-lease 推
#   - 已经 push 到共享分支:绝对不要 amend(用 revert 代替)

# ---
# 修改更早的提交(用交互式 rebase,见 rebase 篇)
git rebase -i HEAD~3
# 把想改的那行改成 edit,保存,Git 会暂停在那个提交

5. git clean — 清理未追踪文件

reset 只能处理"已追踪文件的改动",对"全新的、没 add 过的文件"无能为力。这时用 git clean:

# git clean:删除"未追踪"的文件(reset 管不到的)
# 场景:工作区有一堆临时文件、构建产物,Git 没追踪,想清掉

# 预览:会删哪些(干跑一遍,不真删)
git clean -n

# 真删未追踪文件
git clean -f

# 连目录一起删(比如 dist/ 这种)
git clean -fd

# 连 .gitignore 忽略的文件也删(彻底清理)
git clean -fdx
# 慎用!这会删掉 node_modules、.env 等

# reset + clean 组合拳 = 工作区完全回到某个提交
git reset --hard HEAD
git clean -fd

误删了怎么办?reflog 救你

即使 reset --hard 把提交"删了",只要它曾经被 commit 过,本地 reflog 里就有记录,能找回:

所以养成"勤 commit"的习惯——哪怕 message 写 wip,至少进了历史,reflog 兜底;只在工作区改、从没 commit,那才是真没了。

一张图记住所有撤销命令

Git 是那种"用一年才知道自己之前用错了"的工具,但每个命令背后都是干净的逻辑。坚持写有意义的 commit message、保持分支整洁、养成 PR review 习惯,你会成为团队里最受欢迎的协作伙伴。

← 上一篇 Git 历史与差异

← 返回 Git 教程目录

✈️💬