本节摘要:改错了是 Git 日常的常态,关键是知道该用哪个命令救。本节讲清两个主角的分工:
git restore管文件级恢复(丢弃工作区修改、取消暂存),git reset管提交级回退(移动 HEAD 指针,分 soft/mixed/hard 三档)。最后给出一张"什么场景用什么命令"的决策表,并提醒 hard 模式的危险。
阅读完本节,你应当能够:
Git 的价值一半在"记录",另一半在"反悔"。现实开发里,反悔是高频动作:
没有撤销能力,版本控制就只是"保险柜"——只能存,取不出来。Git 在"反悔"上提供了成套工具,但正因为命令太多,很多人遇到问题第一反应是"完蛋了",而不是"该用哪条"。
本节的目标很实在:把"撤销"这件事按场景拆清楚,给你一张"出错地图"——每种错误对应哪条命令、会动哪个区、有没有风险。读完之后,遇到撤销场景你不该再慌,而是像查菜单一样找到对应那一条。
先说结论性的分工,后面展开:
git restoregit reset两条线分别管"文件层的反悔"和"历史层的反悔",大部分撤销需求都落在其中一条上。
这里值得先打破一个神话:很多人以为"Git 的撤销很危险,一不小心就删库"。真实情况是,Git 的撤销绝大多数都是可逆的——文件内容在对象库里存着、提交对象在仓库里躺着,你删掉的往往只是"指向它们的指针",而指针有 reflog 可以找回。真正不可逆的场景其实很少(最典型的就是 reset --hard 丢弃未提交的工作区改动)。所以遇到反悔需求,第一反应不该是"完了",而是"这条命令会动哪个区、丢的是指针还是内容"。带着这个框架读下面的原理,你会觉得清晰很多。
git restore 是 Git 2.23 起专为"恢复文件"设计的命令,意图明确,不碰 HEAD、不碰历史,只在工作区与暂存区之间搬运内容。
用法一:丢弃工作区的修改。
git restore 文件名 git restore . # 恢复当前目录全部文件
效果:把工作区里未暂存的修改丢掉,文件回到暂存区(或最近提交)里的版本。这条命令不可逆——未暂存的修改一旦被丢弃就找不回来了(除非你之前 stash 过,第 5 章讲)。
用法二:取消暂存。
git restore --staged 文件名
效果:把文件从暂存区撤下,回到"已修改未暂存"状态,工作区的修改保留。这是"add 错了"的标准解药。
用法三:从特定提交恢复文件。
git restore --source HEAD~1 文件名
效果:把文件恢复到指定提交(这里是上一个提交)时的内容。--source 指定来源提交,默认是 HEAD 或暂存区。
restore 的三条通路:默认从暂存区/HEAD 恢复工作区,--staged 反向撤下暂存,--source 从任意提交取内容。全部不碰历史。
git reset 是更强大的命令,它的核心动作是移动 HEAD 指针,让当前分支指向另一个提交,从而"回到过去"。根据模式不同,它还会顺带重置暂存区和工作区。

模式一:--soft(最温柔)
git reset --soft 提交
只移动 HEAD 指针,暂存区和工作区原样保留。原来"被跳过"的提交的改动全部留在暂存区,等你重新整理。适合"把最近几个提交合并成一个"的场景。
模式二:--mixed(默认)
git reset 提交 # 等价于 --mixed git reset --mixed 提交
移动 HEAD,并把暂存区重置为目标提交的状态,工作区保留。被跳过的提交的改动变成未暂存状态,需要重新 add。这是默认模式,也是"想撤销提交但保留改动"的常用选择。
模式三:--hard(最暴力)
git reset --hard 提交
移动 HEAD,暂存区和工作区全部重置为目标提交的状态。被跳过的提交的改动彻底丢弃。这是最危险的模式——工作区未提交的修改也会一起消失。
三个模式用一张表总结:
| 模式 | 移动 HEAD | 重置暂存区 | 重置工作区 | 危险度 |
|---|---|---|---|---|
| --soft | 是 | 否 | 否 | 低 |
| --mixed | 是 | 是 | 否 | 中 |
| --hard | 是 | 是 | 是 | 高 |
soft/mixed/hard 的区别,就像"回拨时钟"的三种程度:soft 只把表针拨回去(其他不动);mixed 拨表针 + 把闹钟设置清零;hard 拨表针 + 清零闹钟 + 把房间也还原成当时的样子。参数越强,波及面越大,丢失风险越高。
假设你的历史是 A → B → C → D(HEAD 在 D)。分别体验三种回退:
# 回退到 B,保留 C、D 的改动在暂存区 git reset --soft B git status # C、D 的所有改动都在 "to be committed" # 回退到 B,C、D 的改动变成未暂存 git reset B # 默认 mixed git status # C、D 的改动都在 "Changes not staged" # 回退到 B,彻底丢弃 C、D(危险) git reset --hard B git log --oneline # 只剩 A、B
留意第三种的后果:C、D 两个提交从分支上"消失"了,但如果它们的哈希你还记得,理论上能通过 reflog 找回(第 5 章)。这也是 reset 的隐藏后门——它丢的是"指针指向",不是"对象本身"。
对应地,把文件级的反悔也走一遍:
echo bug > app.js git add app.js # 误暂存了 git restore --staged app.js # 撤下暂存,修改保留 git status # app.js 回到未暂存状态 git restore app.js # 丢弃工作区修改 git status # 工作区干净
注意第二条 restore 之后文件内容并没有变——restore --staged 只动暂存区。真正把内容改回去的是第三条 restore(从暂存区/HEAD 恢复)。两条命令一前一后,先把"登记"撤了,再把"内容"还原。
| 场景 | 命令 | 丢东西吗 |
|---|---|---|
| 丢弃工作区未暂存修改 | git restore 文件 | 丢 |
| 取消暂存(保留修改) | git restore --staged 文件 | 不丢 |
| 把文件恢复到历史版本 | git restore --source 提交 文件 | 可能丢 |
| 撤销最近提交但保留改动 | git reset --soft HEAD~1 | 不丢 |
| 撤销提交且重置暂存区 | git reset --mixed HEAD~1 | 不丢 |
| 彻底回退到某提交 | git reset --hard 提交 | 丢(危险) |
原则一:先想清楚"我要反悔的是文件还是提交"。 文件层的反悔用 restore,提交层的反悔用 reset。混用是最常见的错误——比如用 reset --hard 只为了丢一个文件的修改,结果把提交也回退了。
原则二:reset --hard 前先确认。 敲之前问自己:这些被丢弃的改动我真的不要了吗?有没有没提交的工作区改动会一起被清掉?如果有一丝犹豫,先 git stash 或 git commit 把现场保住,再 reset。
把 Git 的撤销工具按"破坏性"排个序,心里有数哪个绝对安全、哪个要三思:
| 工具 | 安全等级 | 说明 |
|---|---|---|
| git restore --staged | 完全安全 | 只撤暂存登记,内容都在 |
| git restore 文件 | 低风险 | 丢未暂存修改,但已提交的历史不受影响 |
| git reset --soft | 安全 | 改动全在暂存区,随时可重新提交 |
| git reset --mixed | 较安全 | 改动变未暂存,重新 add 即可 |
| git reset --hard | 高风险 | 未提交改动全部消失 |
| git push --force | 极高风险 | 改写远程历史,影响他人(第 4、5 章) |
这张表的实用价值在于:绝大多数日常反悔需求,都能落在安全区解决。真正危险的命令就那么两三个,记住它们的名字、每次用之前多问一句,你就规避了 Git 里绝大部分事故。
有经验的 Git 用户有一个共同习惯:在执行任何有风险的操作前,先让工作区处于可恢复状态。具体做法:
git stash(第 5 章细讲)。git commit 一个 WIP(工作进行中)提交,之后随便折腾。git rev-parse HEAD),作为回退坐标。这个习惯的价值在于,它把"高风险操作"降级为"可逆操作"。reset --hard 之后再也不用祈祷运气——你有明确的坐标可以回去。这比任何"小心操作"的叮嘱都实在。换个说法:Git 事故里九成不是工具坏了,而是没给自己留退路。
git reset 文件名 等价于 git restore --staged 文件名——它把指定文件从暂存区撤下,不动工作区。这是老命令的遗留用法,Git 2.23 后推荐用 restore 表达更清晰。了解它的存在即可,不必刻意使用。
⚠️ 常见坑:
git reset --hard之后才想起工作区有没保存的修改——这些修改已经没了。所以硬重置的铁律是:先 status 看有没有未提交改动,先 stash 或 commit 保存,再动手。
💡 关键直觉:restore 是"擦掉草稿纸上的字",reset 是"把整个日历撕回某一页"。擦字只影响当前这页,撕日历会连带后面所有页。想清楚你要擦字还是撕日历,再选命令。
"restore 和 checkout 到底啥区别?" checkout 是老命令,一身兼数职(切分支、恢复文件),容易让人混淆。Git 2.23 起把"恢复文件"拆给了 restore、把"切分支"拆给了 switch。现代实践:恢复文件用 restore,切分支用 switch。
"reset --hard 误操作了,能救吗?" 分情况。如果被丢的提交有哈希记得住,用 git reflog 找回来(第 5 章详细讲);如果只是未提交的工作区改动,基本没救了。所以再强调一遍:hard 前先保存现场。
"撤销提交用 reset 还是 revert?" reset 是"本地回退"(移动指针),revert 是"新增一个反向提交"(不动历史)。如果提交已推送远程,用 revert 安全;只在本地,reset 更干净。第 5 章会展开 revert。
"我想回到某个历史版本,用哪个?" 看你想"看"还是想"回"。只查看历史状态用 git checkout 提交 或 restore --source;真正把分支回退到那个提交用 reset。前者不改变分支位置,后者会。
"git restore 会不会把暂存区也清了?" 默认不会——restore 文件只动工作区;restore --staged 只动暂存区;restore --source 会同时更新工作区和暂存区。按你选的选项决定波及范围。
"撤销命令记不住怎么办?" 背一张最小速查表就够:想丢工作区改动用 restore,想撤暂存用 restore --staged,想回退提交用 reset,彻底回退且不怕丢用 reset --hard。四个场景,两条命令,其余都是变体。把这四条刻在脑门上,遇到 90% 的撤销需求都不会慌。剩下的 10% 属于 reflog 和 revert,那是第 5 章的事。
"restore 能恢复被删除的文件吗?" 能。如果文件在最近一次提交里存在、只是工作区里被删了,git restore 文件名 能把它从暂存区/提交里恢复回来。这正是"文件丢失"场景的标准救法——先别慌,Git 往往比你想象的更"记得"东西。
下一章进入 Git 的灵魂——分支管理:分支的本质、增删切换、合并与冲突,Rebase 变基。