撤销、回滚与修复
本章要点
- 撤销的三层目标:工作区 / 暂存区 / 提交历史
- 现代命令
git restore与老命令git checkout的对照 git reset的三种模式:soft / mixed / hardgit revert:安全的回滚(不重写历史)- detached HEAD 状态与
git switch -的妙用 git reflog:Git 的"后悔药仓库"- 修改最近一次提交:
git commit --amend
💡 本章内容先看这张决策表,遇到"我要撤销 X"直接查:
| 我想撤销的是…… | 命令 |
|---|---|
| 工作区里未 add 的修改 | git restore <文件> |
| 已 add 进暂存区的修改 | git restore --staged <文件> |
| 最近一次提交(还没 push) | git reset --soft HEAD~1 或 --amend |
| 已 push 的提交(想安全回滚) | git revert <哈希> |
| 一切都乱了 | git reflog 找回 |
撤销的本质:Git 的"移动指针"
撤销操作几乎都可以理解为:把 HEAD 指针/分支指针移动到别的位置,或者丢弃某区域的内容。
三个撤销目标层级(从浅到深):
工作区 → restore(丢弃未暂存修改)
暂存区 → restore --staged / reset(把暂存内容退回)
提交历史 → reset / revert(移动指针或生成反向提交)丢弃工作区修改:git restore
$ git restore file.txt # 丢弃 file.txt 未暂存的修改(回到暂存区/HEAD 的状态)
$ git restore . # 丢弃当前目录所有未暂存修改
$ git restore --source=HEAD file.txt # 明确从 HEAD 恢复⚠️ 这是不可恢复的操作(除非用了 reflog 或编辑器缓存):工作区未暂存的改动被丢弃后,Git 没有留档。
老命令等价写法
$ git checkout -- file.txt # 等价于 git restore file.txt撤销暂存:git restore --staged
$ git add file.txt # 哎呀,不该 add 的
$ git restore --staged file.txt # 撤销暂存(文件回到 modified/untracked 状态,内容不变)
$ git reset HEAD file.txt # 老写法,等价💡 语义很好记:
git restore --staged= "把暂存区里的这个文件拿掉,但别动工作区"。
git reset:移动分支指针
git reset 把当前分支的指针移动到指定提交,并可选地同步工作区/暂存区。
三种模式对比
$ git reset --soft <提交> # 只移动指针:所有"被撤销提交"的改动都留在暂存区
$ git reset --mixed <提交> # 移动指针 + 清空暂存区:改动回到工作区(默认模式)
$ git reset --hard <提交> # 移动指针 + 清空暂存区 + 丢弃工作区改动(⚠️ 危险)以 A → B → C(HEAD 在 C)为例,git reset B:
| 模式 | HEAD | 暂存区 | 工作区 | C 的改动去哪了 |
|---|---|---|---|---|
--soft | B | 保留 C 的改动(已暂存) | 不变 | 暂存区 |
--mixed(默认) | B | 清空 | 不变 | 工作区(未暂存) |
--hard | B | 清空 | 恢复成 B 的样子 | 丢弃 |
典型用法
# 撤销最近一次提交,但保留改动重新整理(最常用)
$ git reset --soft HEAD~1
$ git status # 改动都还在暂存区,可以重新 add -p 或改提交信息
# 撤销最近一次提交,改动回工作区
$ git reset HEAD~1 # 等价于 git reset --mixed HEAD~1
# 完全丢弃最近 N 次提交的改动(⚠️ 慎重!)
$ git reset --hard HEAD~3⚠️
reset --hard会永久丢弃工作区未提交的改动,执行前务必确认没有想保留的内容。 ⚠️ 只对未推送(未 push)的提交使用 reset。已推送的提交被 reset 会造成本地与远程不一致,且重写公共历史。
git revert:安全的回滚(推荐用于已推送提交)
git revert 不移动指针、不重写历史,而是生成一个反向提交把某个提交的效果抵消:
$ git revert 8f4a2c9 # 生成一个新提交,内容为"撤销 8f4a2c9 的改动"
$ git revert HEAD # 撤销最近一次提交
$ git revert --no-edit HEAD # 不打开编辑器,直接用默认提交信息撤销前: A ─ B ─ C(HEAD)
撤销 C: A ─ B ─ C ─ C'(新提交 C' 反向了 C 的内容)- ✅ 历史完整保留,适合已推送的提交、团队共享分支。
- ✅ 可能产生冲突(如果后续提交改了同一处),解决后
git revert --continue。
💡 判断口诀:没 push 用 reset(历史还没人看到,直接改);已 push 用 revert(历史公开了,追加反向提交)。
修改最近一次提交:git commit --amend
$ git commit --amend -m "修正后的提交信息" # 修改提交信息
$ git commit --amend # 补充遗漏的文件(先 add 再 amend)
$ git commit --amend --no-edit # 只补文件不改信息- 本质是用新提交替换旧提交(哈希会变)。
- ⚠️ 同样不要对已推送的提交使用(除非配合 force push 且确认无他人基于此提交开发)。
detached HEAD(游离头指针)与分支恢复
什么是 detached HEAD
当 HEAD 不指向任何分支、而直接指向某个提交时,就处于 detached HEAD 状态:
$ git checkout 8f4a2c9 # 直接检出某次提交(不在任何分支上)
# You are in 'detached HEAD' state...此时新提交不会属于任何分支,切走就会"丢失"(其实还在 reflog 里)。
在 detached HEAD 上意外提交了,如何保住
$ git switch -c rescue-branch # ① 立即为当前提交创建分支
$ git switch main # ② 再切回主分支
$ git merge rescue-branch # ③ 合并进来切回上一个分支
$ git switch - # 回到上一个分支(类似 cd -,超实用)
$ git checkout - # 老写法git reflog:Git 的后悔药
reflog(reference log)记录本地所有 HEAD 移动历史,包括 reset、rebase、amend、checkout 等一切操作。只要操作发生在本地,就一定能找回。
$ git reflog
# 8f4a2c9 (HEAD -> main) HEAD@{0}: commit: feat(login): ...
# 3b1e7d0 HEAD@{1}: reset: moving to HEAD~1
# c9a21f4 HEAD@{2}: commit: docs: ...经典救回场景:reset --hard 后反悔
$ git reset --hard HEAD~1 # 糟糕,误删了提交
$ git reflog # 找到被删提交的哈希(比如 8f4a2c9)
$ git reset --hard 8f4a2c9 # 回到它,一切恢复!💡 只要提交曾存在于本地(哪怕被 reset/rebase 掉),reflog 一般都能找到。 ⚠️ reflog 有保留期限(默认 90 天)且只记录本地操作,被 gc 清理后无法找回。 ⚠️ 未提交的工作区改动不在 reflog 内,无法这样找回。
撤销专题:完整对照表
| 场景 | 现代写法(2.23+) | 老写法 |
|---|---|---|
| 丢弃工作区修改 | git restore <文件> | git checkout -- <文件> |
| 撤销暂存 | git restore --staged <文件> | git reset HEAD <文件> |
| 撤销最近提交(保留改动) | git reset --soft HEAD~1 | 同 |
| 撤销最近提交(改动回工作区) | git reset HEAD~1 | 同 |
| 彻底丢弃最近提交 | git reset --hard HEAD~1 | 同 |
| 安全回滚已推送提交 | git revert <哈希> | 同 |
| 修改最近提交 | git commit --amend | 同 |
| 找回丢失的提交 | git reflog → git reset --hard <哈希> | 同 |
