Skip to content

撤销、回滚与修复

本章要点

  • 撤销的三层目标:工作区 / 暂存区 / 提交历史
  • 现代命令 git restore 与老命令 git checkout 的对照
  • git reset 的三种模式:soft / mixed / hard
  • git 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 指针/分支指针移动到别的位置,或者丢弃某区域的内容

text
三个撤销目标层级(从浅到深):
工作区  →  restore(丢弃未暂存修改)
暂存区  →  restore --staged / reset(把暂存内容退回)
提交历史 → reset / revert(移动指针或生成反向提交)

丢弃工作区修改:git restore

bash
$ git restore file.txt           # 丢弃 file.txt 未暂存的修改(回到暂存区/HEAD 的状态)
$ git restore .                  # 丢弃当前目录所有未暂存修改
$ git restore --source=HEAD file.txt   # 明确从 HEAD 恢复

⚠️ 这是不可恢复的操作(除非用了 reflog 或编辑器缓存):工作区未暂存的改动被丢弃后,Git 没有留档。

老命令等价写法

bash
$ git checkout -- file.txt       # 等价于 git restore file.txt

撤销暂存:git restore --staged

bash
$ git add file.txt               # 哎呀,不该 add 的
$ git restore --staged file.txt  # 撤销暂存(文件回到 modified/untracked 状态,内容不变)
$ git reset HEAD file.txt        # 老写法,等价

💡 语义很好记:git restore --staged = "把暂存区里的这个文件拿掉,但别动工作区"。

git reset:移动分支指针

git reset当前分支的指针移动到指定提交,并可选地同步工作区/暂存区。

三种模式对比

bash
$ git reset --soft <>     # 只移动指针:所有"被撤销提交"的改动都留在暂存区
$ git reset --mixed <>    # 移动指针 + 清空暂存区:改动回到工作区(默认模式)
$ git reset --hard <>     # 移动指针 + 清空暂存区 + 丢弃工作区改动(⚠️ 危险)

A → B → C(HEAD 在 C)为例,git reset B

模式HEAD暂存区工作区C 的改动去哪了
--softB保留 C 的改动(已暂存)不变暂存区
--mixed(默认)B清空不变工作区(未暂存)
--hardB清空恢复成 B 的样子丢弃

典型用法

bash
# 撤销最近一次提交,但保留改动重新整理(最常用)
$ 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 不移动指针、不重写历史,而是生成一个反向提交把某个提交的效果抵消:

bash
$ git revert 8f4a2c9             # 生成一个新提交,内容为"撤销 8f4a2c9 的改动"
$ git revert HEAD                # 撤销最近一次提交
$ git revert --no-edit HEAD      # 不打开编辑器,直接用默认提交信息
text
撤销前:  A ─ B ─ C(HEAD)
撤销 C:  A ─ B ─ C ─ C'(新提交 C' 反向了 C 的内容)
  • ✅ 历史完整保留,适合已推送的提交、团队共享分支。
  • ✅ 可能产生冲突(如果后续提交改了同一处),解决后 git revert --continue

💡 判断口诀:没 push 用 reset(历史还没人看到,直接改);已 push 用 revert(历史公开了,追加反向提交)。

修改最近一次提交:git commit --amend

bash
$ git commit --amend -m "修正后的提交信息"   # 修改提交信息
$ git commit --amend                         # 补充遗漏的文件(先 add 再 amend)
$ git commit --amend --no-edit               # 只补文件不改信息
  • 本质是用新提交替换旧提交(哈希会变)。
  • ⚠️ 同样不要对已推送的提交使用(除非配合 force push 且确认无他人基于此提交开发)。

detached HEAD(游离头指针)与分支恢复

什么是 detached HEAD

当 HEAD 不指向任何分支、而直接指向某个提交时,就处于 detached HEAD 状态:

bash
$ git checkout 8f4a2c9          # 直接检出某次提交(不在任何分支上)
# You are in 'detached HEAD' state...

此时新提交不会属于任何分支,切走就会"丢失"(其实还在 reflog 里)。

在 detached HEAD 上意外提交了,如何保住

bash
$ git switch -c rescue-branch   # ① 立即为当前提交创建分支
$ git switch main               # ② 再切回主分支
$ git merge rescue-branch       # ③ 合并进来

切回上一个分支

bash
$ git switch -                  # 回到上一个分支(类似 cd -,超实用)
$ git checkout -                # 老写法

git reflog:Git 的后悔药

reflog(reference log)记录本地所有 HEAD 移动历史,包括 reset、rebase、amend、checkout 等一切操作。只要操作发生在本地,就一定能找回。

bash
$ 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 后反悔

bash
$ 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 refloggit reset --hard <哈希>