撤销时光机:restore / reset / revert¶
本文你会学到:
- 三种撤销场景及对应工具
git restore:丢弃工作区/暂存区的修改git commit --amend:修改最近一次提交git reset:移动 HEAD 指针(软/混合/硬)git revert:安全撤销已发布的提交- 如何选择:什么时候用哪个?
⚠️ 先建立"反悔"的心智模型¶
在 Git 里,"撤销"并不是一个单一操作——根据你的代码**到达了哪个阶段**,对应不同的命令:
graph LR
W["🗂️ 工作区\n(还没 add)"]
S["📋 暂存区\n(已 add)"]
C["🗄️ 仓库\n(已 commit)"]
W -->|"git add"| S
S -->|"git commit"| C
W -->|"git restore <file>"| W
S -->|"git restore --staged <file>"| S
C -->|"git reset / git revert"| C
style W fill:transparent,stroke:#539bf5,color:#adbac7,stroke-width:2px
style S fill:transparent,stroke:#f57c00,color:#adbac7,stroke-width:2px
style C fill:transparent,stroke:#388e3c,color:#adbac7,stroke-width:2px
1️⃣ 撤销工作区的修改:git restore¶
场景¶
你修改了一个文件,但改错了,想恢复成上次 git add 或 git commit 时的样子。
| 丢弃工作区的修改 | |
|---|---|
注意:这是不可逆的
git restore <file> 会**直接丢弃工作区的修改**,没有回收站,确认再执行。如果有重要改动,先 git stash 保存再说。
撤销暂存区(从暂存区移出)¶
你用 git add 暂存了文件,但后悔了,想把它移出暂存区(保留工作区修改):
| 把文件从暂存区移出 | |
|---|---|
2️⃣ 修改最近一次提交:--amend¶
场景¶
刚提交完,发现提交信息写错了,或者漏加了一个文件:
| 修补最近一次提交 | |
|---|---|
amend 会改变 commit SHA
--amend 实际上是**用新的提交替换旧提交**(SHA 会变化)。如果这个提交已经 push 到远程仓库,--amend 后再 push 需要加 --force-with-lease,且会影响其他人的工作。未 push 的提交随便 amend,已 push 的提交慎用。
3️⃣ 移动 HEAD:git reset¶
git reset 是更强大的工具——它可以让当前分支指针回退到某个历史提交。有三种模式:
graph LR
C1["提交A"] --> C2["提交B"] --> C3["提交C(HEAD)"]
C2 -.->|"git reset 到B"| H["HEAD 移到这里"]
classDef commit fill:transparent,stroke:#768390,color:#adbac7,stroke-width:1px
classDef head fill:transparent,stroke:#f57c00,color:#adbac7,stroke-width:2px
class C1,C2,C3 commit
class H head
三种模式对比¶
| 模式 | 命令 | 提交记录 | 暂存区 | 工作区 |
|---|---|---|---|---|
| 软重置 | --soft |
回退 | 保留 | 保留 |
| 混合重置(默认) | --mixed |
回退 | 清空 | 保留 |
| 硬重置 | --hard |
回退 | 清空 | 清空 |
--hard 很危险
--hard 会丢失工作区和暂存区的所有修改,且**不可找回**(除非你记住了 SHA,可以用 git reflog 补救)。操作前务必确认。
引用方式速查¶
4️⃣ 安全撤销已发布提交:git revert¶
reset vs revert 的本质区别¶
git reset 是"改写历史",而 git revert 是"新增一个反向提交":
graph LR
subgraph "reset 方式(危险)"
A1["A"] --> B1["B(被删除)"] --> C1["C(HEAD)"]
A1 -.->|"reset 后 HEAD 指向A"| A1
end
subgraph "revert 方式(安全)"
A2["A"] --> B2["B"] --> C2["C"] --> R2["revert-B\n(消除B的改动)"]
end
classDef c fill:transparent,stroke:#768390,color:#adbac7,stroke-width:1px
classDef danger fill:transparent,stroke:#d32f2f,color:#adbac7,stroke-width:2px
classDef safe fill:transparent,stroke:#388e3c,color:#adbac7,stroke-width:2px
class A1,B1,C1 danger
class A2,B2,C2,R2 safe
| git revert 用法 | |
|---|---|
什么时候用 reset,什么时候用 revert?¶
| 情况 | 推荐 | 原因 |
|---|---|---|
| 提交**还没有 push** | git reset |
本地历史随便改 |
| 提交**已经 push** 到共享分支 | git revert |
不破坏他人的历史 |
| 只是想撤销一个中间提交 | git revert |
reset 会连带丢失之后的提交 |
5️⃣ 后悔药:git reflog¶
以为 reset --hard 之后数据没了?别怕,只要你没有做 GC,Git 会在 reflog 里保留最近 90 天的所有操作记录:
| 用 reflog 找回 | |
|---|---|
reflog 是你的最终保险
只要提交曾经存在过(哪怕被 reset 掉了),90天内都能通过 reflog 找回。这就是为什么 Git 几乎不会真正"丢失"数据。
🔁 撤销合并提交:revert -m 1¶
当一个错误的 PR 已经合并到 main,需要撤销整个合并时,普通的 git revert 不够——合并提交有两个父提交,Git 需要知道"以哪个父提交为主线":
revert 合并提交后,重新合并同一分支需要注意
用 revert -m 1 撤销了合并后,如果想重新合并修复后的 feature 分支,需要先 revert 那个 revert 提交(让 Git 重新认为 feature 的改动是"新的"):
🧪 实战演练:完整撤销场景¶
以下是一个典型的混合撤销场景,综合运用本篇所有工具:
小结:如何选择撤销工具?¶
graph TD
Q1{"改动到了哪一步?"}
Q1 -->|"只在工作区,还没 add"| A1["git restore <file>"]
Q1 -->|"已 add,在暂存区"| A2["git restore --staged <file>"]
Q1 -->|"已 commit(未 push)"| Q2{"要怎么处理改动?"}
Q1 -->|"已 commit 且已 push"| A5["git revert(安全)"]
Q2 -->|"改动保留在暂存区"| A3["git reset --soft"]
Q2 -->|"改动保留在工作区"| A4["git reset --mixed(默认)"]
Q2 -->|"改动全部丢弃"| A6["git reset --hard ⚠️"]
classDef action fill:transparent,stroke:#539bf5,color:#adbac7,stroke-width:2px
classDef danger fill:transparent,stroke:#d32f2f,color:#adbac7,stroke-width:2px
classDef question fill:transparent,stroke:#768390,color:#adbac7,stroke-width:1px
class A1,A2,A3,A4,A5 action
class A6 danger
class Q1,Q2 question