# 撤销到底撤的是哪一层：restore、reset 的三种模式和 revert 各自动了什么

撤销命令的区别不在“撤多少”，而在它覆盖哪个区域、内容从哪来——restore 只改文件，reset 按 --soft/--mixed/--hard 决定退几格，已推送的提交则交给 revert 新增一个反向提交

> Git 基础操作 · 暂存区 · HEAD 与引用 · 约 6 分钟 · 10 月 21 日

## 本篇要点

1. 撤销命令的区别不在“撤多少”，而在“它覆盖哪个区域、用谁的内容去覆盖”。
2. 工作区 → 暂存区 → 提交是一条单向链；撤销就是把某一环换成上游某一环的内容，越往回拨，后面几环的差异越会失去依托。
3. git restore 只改工作区（内容取自暂存区），git restore --staged 只改暂存区（内容取自 HEAD），两者都不动提交历史，所以不会引起推送被拒。
4. git reset 一定会挪分支指针，三种模式的区别是退到哪一步停手：--soft 只挪指针，--mixed 连暂存区一起换（改动退回未暂存），--hard 三个区域全部对齐（改动消失）。
5. 只想丢掉工作区改动该用 restore；用 reset --hard HEAD~1 会连带撤掉一个提交，因为 --hard 一次拨了三环。
6. 已经推送的提交该用 revert：它生成一个反着做的新提交，指针往前走，推送是快进；reset 则让指针往回走，推上去需要强推并会打乱别人的历史。
7. revert 只在效果上抵消、不删除原提交；要“从没存在过”只能 reset 或 rebase，前提是那段历史还没推出去或只有你一个人在用。
8. git reset --hard 不删未跟踪文件、不动远端，也没有真正销毁提交——指针挪开后仍可用 reflog 找回。

---

“我刚才那步搞砸了，怎么撤？”

这是 Git 里最容易问出错误答案的一句话。因为“撤”对应着好几条不同的路：`git restore`、`git reset --soft`、`git reset --mixed`、`git reset --hard`、`git revert`。它们看起来都是“回去”，实际动的却是三个不同的地方。选错一个，轻则改动没了，重则提交历史被改掉，再往外推还会把别人的分支搞乱。

所以这篇不给一张“遇到情况就查”的对照表——那种表换个场景就失效。要讲的是能推出来的那条规则：**每条撤销命令都由两件事确定——它动哪个区域，以及它把那个区域的内容换成谁。**

## 三个区域，一条单向的链

前面讲[暂存区那篇](https://pangzhengboyin.com/articles/git-staging-area-working-tree-commit-387503a0)时摆过这三个区域，这里直接用：

- **工作区**：你编辑器里看到的文件。
- **暂存区**（index）：下次提交要装进去的内容，`git add` 写入这里。
- **提交历史**：`git commit` 把暂存区的内容固化成提交，并让当前分支指针往前走一格。

信息单向流过去：工作区 →`add`→ 暂存区 →`commit`→ 提交。三个区域各存着一份“项目此刻的样子”；你改了文件，工作区先变；add 了，暂存区跟上；commit 了，历史才跟上。

看懂这条链，“撤销”就有准确定义了：**把某一环的内容，换成它上游某一环的内容。** 往回拨几环，就意味着后面几环的差异会失去依托——这就是各条命令危险程度不同的原因。

## restore：只动文件，历史一个字都不碰

`git restore <文件>` 的两端是固定的：**改工作区，内容从暂存区取。** 它把文件恢复成“你上次 add 时的样子”。你 add 之后又乱改一通，用它退回去；add 之前的改动找不回来，因为暂存区里从来没有过它。

`git restore --staged <文件>` 换了一端：**改暂存区，内容从当前提交（HEAD）取。** 效果是“取消 add”：文件内容原封不动还在工作区，只是不再算作已暂存，`git status` 里从“要提交的改动”挪到了“未暂存的改动”。

这两条命令都**不碰提交历史**，只在你和暂存区之间搬内容。所以它们永远不会让推送被拒——服务器那边看到的东西一点没变。

## reset 的三种模式：同一件事，往回退几格

`git reset <某个提交>` 的核心动作只有一个：**把当前分支指针挪到那个提交上**（`HEAD` 通过分支间接指过去，指针一动，`HEAD` 就跟着动）。三种模式的区别不在指针——指针一定会动——而在**退到哪一步停手**：

- `--soft`：只挪指针，暂存区和工作区一概不动。于是“目标提交之后的全部改动”原封不动躺在暂存区里。
- `--mixed`（默认，不写就是它）：挪指针，再把暂存区换成目标提交的内容。工作区依然不动。于是那些改动从“已暂存”退回成“未暂存”——文件还是你改过的样子，只是提交的登记被撤销了。
- `--hard`：挪指针，暂存区和工作区都换成目标提交的内容，三个区域重新对齐，那段时间的改动从工作区里真的消失了。

回到那条链上看，三种模式就是“往回拨几环”：

| 命令 | 分支指针 | 暂存区 | 工作区 | 差异去哪了 |
| --- | --- | --- | --- | --- |
| `restore <文件>` | 不动 | 不动 | 换成暂存区的内容 | 丢弃（工作区那一份） |
| `restore --staged <文件>` | 不动 | 换成 HEAD 的内容 | 不动 | 保留在工作区 |
| `reset --soft <提交>` | 挪 | 不动 | 不动 | 全部留在暂存区 |
| `reset --mixed <提交>` | 挪 | 换成目标提交的内容 | 不动 | 全部留在工作区 |
| `reset --hard <提交>` | 挪 | 换成目标提交的内容 | 换成目标提交的内容 | 丢弃 |
| `revert <提交>` | 往前走一格（新提交） | 不动 | 不动 | 被新提交抵消 |

有了“退几格”这个依据，几个常见用法就不用背：刚 commit 完发现漏了文件，`git reset --soft HEAD~1` 把那次提交拆回“改动都在暂存区”，改好再 commit 一次（`--amend` 做的就是这件事）；三个小提交想合成一个，`git reset --soft HEAD~3`，三条改动全回到暂存区，一次 commit 交代清楚。

还有一条最常被误用：**只想丢掉工作区的改动，却打了 `git reset --hard HEAD~1`。** 前者只动工作区（`git restore .`），后者连分支指针一起往回挪了一格，等于顺手把一个提交也撤了。`--hard` 的危险不在于它覆盖文件，而在于它一次拨了三环。

## 已经推送出去的提交：为什么该用 revert

前面已经建立过一个事实：推送这道闸只看祖先关系，服务端只接受快进（[推送那篇](https://pangzhengboyin.com/articles/why-git-push-rejected-force-with-lease-f816f580)）。现在把它和 `reset` 接上。

`reset` 做的是**让分支指针往回走**。假设远端 `main` 停在 C，你本地 `reset --hard` 回到 B，然后想推上去——C 不是你新历史的祖先，`non-fast-forward`，被拒。想推只能强推，而强推等于要求服务端**丢掉 C**。可 C 已经被同事拉到本地了，他们下次推送时又会把它带回来，或者你们的仓库从此分成两条不同的历史。这就是“不要对共享分支改写历史”这条规矩的来历。

`revert` 走的是相反方向：它读一个已有提交的改动，**把这份改动反着做一遍，生成一个新提交**。C 里加了一行 `timeout = 30`，`git revert C` 得到的提交就是把这一行删掉，而它的父提交是 C。指针没有往回走，是往前走了一格。于是：

- 推送是快进，不需要任何 `--force`；
- 同事 `git pull` 就能拿到这次撤销，谁的历史都没被打乱；
- 历史里同时留着“当初为什么加”和“后来为什么撤”，排查问题时通常比干净的假历史更有用。

代价也要说清：`revert` 只是**在效果上抵消**，并不删除那个提交。项目里确实出现过那段错误代码，`git log` 里也还看得见。要的是“这个提交从没存在过”，就只能 `reset` 或 `rebase`，前提是它还没推出去，或者那条分支只有你一个人在用——判断标准和[rebase 那篇](https://pangzhengboyin.com/articles/why-git-rebase-rewrites-commit-hashes-d376eabc)结尾那条一样。

操作上有两个细节：`revert` 要求工作区干净，它会先算出反向补丁，改不动的地方和合并一样会停下来等你处理；它对合并提交要多给一个参数说明按哪边撤，这条知道有边界就行，等碰到再查。

## 一个容易搞反的地方

`git reset --hard` 常被当成“恢复出厂设置”，其实作用范围很窄：它**不删未跟踪文件**（从没 add 过的新文件还在磁盘上，清掉要用 `git clean`）；它**不动远端**，你本地回到 B，服务器上还是 C；它**也没有真的销毁提交**，指针挪开后那些提交只是不再被任何分支指着，`git reflog` 还记得指针挪动前的样子——这也是后面要单独讲的一篇。

## 一句话的选择依据

- 想改**文件**、不碰历史 → `restore`；带 `--staged` 就是取消 add。
- 想改**历史**，而且这条分支只有你在用 → `reset`，按“改动要留在暂存区 / 留在工作区 / 彻底不要”选 `--soft`、`--mixed`、`--hard`。
- 想撤掉**已经推出去**的东西 → `revert`，新增一个反向提交，让它能快进推上去。

遇到没见过的场景，先问自己：我打算动的到底是工作区、暂存区，还是那条指针？答案出来，命令基本就定了。

## 术语表

- 撤销的“两件事”判断法：每条撤销命令都由“动哪个区域”和“内容从哪来”确定，记住这条就能在新场景里推出该用哪条命令，而不用查对照表。
- 回退深度（soft/mixed/hard）：reset 的三种模式本质是同一次往回退走了几格，每多退一格就多一个区域的差异被丢弃。
- 反向提交：revert 不删除原提交，而是把它的改动反着做一遍记成新提交，让历史只增不改，因此能快进推送。

## 来源

1. [Git 官方文档 git-reset — --soft/--mixed/--hard 各自对 HEAD、index、工作区的处理；指定路径时不移动 HEAD](https://git-scm.com/docs/git-reset)
2. [Git 官方文档 git-restore — 默认从 index 恢复工作区，--staged 时从 HEAD 恢复索引](https://git-scm.com/docs/git-restore)
3. [Git 官方文档 git-revert — 生成记录反向改动的新提交；要求工作区干净](https://git-scm.com/docs/git-revert)

---

原文：https://pangzhengboyin.com/articles/git-restore-reset-revert-which-layer-c1b207e7

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
