10、Git后悔药系列:git reset、git revert、git restore、git stash
说实话,每个开发者都经历过那种瞬间——刚敲完git commit,突然意识到「完了,改错了」。或者更惨的,辛苦写了两小时的代码,想切分支时发现工作区一团乱麻。
Git的设计者早就料到我们会犯错。所以它给了我们一套「后悔药」。今天我就把这几种药掰开揉碎了讲清楚。你记住一个原则:只要没推送到远程,基本都能救回来。
核心逻辑图:下面这张图展示了Git后悔药体系的完整脉络,建议你先看一遍,再往下读。
一、git reset:时光倒流三兄弟
reset是我用得最多的后悔药。它本质上就是「把HEAD指针挪回过去」。但怎么挪,有三个模式。
1. --soft:温柔版
只移动HEAD指针。工作区和暂存区都不动。说白了就是「我后悔提交了,但代码改了什么我还想留着」。
# 撤销最近一次提交,但保留所有改动在暂存区
git reset --soft HEAD~1
# 撤销最近三次提交,改动全部保留在暂存区
git reset --soft HEAD~3
我的习惯:提交信息写错了,或者漏了文件,我就用--soft。改完重新commit就好,不会丢任何东西。
2. --mixed:默认模式
移动HEAD,重置暂存区,但不动工作区。这是reset的默认行为。你想想看,相当于「提交撤销了,暂存也清空了,但代码还在编辑器里」。
# 撤销最近一次提交,取消暂存,但保留工作区改动
git reset HEAD~1
# 等价于
git reset --mixed HEAD~1
我在项目中遇到过这种情况:提交后发现改得太糙了,想重新整理一下。--mixed就特别合适。代码还在,但暂存区干净了,我可以重新git add。
3. --hard:暴力版
这个要小心。HEAD移动、暂存区清空、工作区也重置。三管齐下,所有未提交的改动都会消失。
# 彻底回到某个历史版本,当前所有改动全部丢弃
git reset --hard HEAD~1
# 回到指定commit
git reset --hard a1b2c3d
⚠️ 我曾经犯过的错:有一次在分支上用了git reset --hard HEAD~3,然后发现那三个提交里有同事刚合并过来的代码。嗯,还好我提前用git reflog找到了。记住:--hard之前,一定确认没有未提交的重要改动。
二、git revert:安全的撤销
reset的问题在于——它会改写历史。如果提交已经推送到远程,别人已经拉下来了,你再用reset,别人再push就会冲突。这时候就该revert出场了。
revert的原理很简单:创建一个「反提交」。比如你提交了「增加A功能」,revert就创建一个「删除A功能」的新提交。历史不会被改写,只是多了一个反向操作。
# 撤销某一次提交(生成一个新的反向提交)
git revert a1b2c3d
# 撤销最近一次提交
git revert HEAD
# 撤销连续多个提交(会生成多个反向提交)
git revert HEAD~3..HEAD
什么时候用revert?
- 提交已经推送到远程仓库
- 多人协作的分支上
- 需要保留完整的操作记录
我个人习惯:只要代码上了master/main分支,一律用revert。reset只敢在本地分支上玩。
三、git restore:文件级别的后悔药
restore是Git 2.23之后引入的命令,专门用来处理「文件级别」的撤销。说白了,它替代了git checkout的部分功能,语义更清晰。
1. 恢复工作区的文件
如果你改了某个文件但还没git add,想放弃修改:
# 放弃工作区的修改
git restore index.html
# 恢复多个文件
git restore *.js
2. 从暂存区恢复到工作区
如果你已经git add了,想取消暂存:
# 取消暂存,但保留工作区的修改
git restore --staged index.html
嗯,这里要注意:--staged只是把文件从暂存区移出来,工作区的改动还在。相当于git reset HEAD file的替代品。
3. 恢复到某个历史版本
# 把某个文件恢复到指定commit时的状态
git restore --source a1b2c3d index.html
避坑指南:restore默认会覆盖工作区文件。如果你有没保存的改动,先stash或者备份。我曾经手快直接restore了一个改了半天的配置文件,结果...嗯,重写了一遍。
四、git stash:临时避难所
这个命令我几乎每天用。场景很典型:你正在feature分支上改代码,突然线上出了bug需要紧急修复。但当前代码改了一半,不能提交。
stash就是干这个的——把工作现场「存起来」,等忙完了再「取出来」。
基本操作
# 暂存当前工作区改动(包括暂存区)
git stash
# 查看暂存列表
git stash list
# 恢复最近一次暂存(并删除该暂存记录)
git stash pop
# 恢复最近一次暂存(但不删除记录)
git stash apply
# 删除最近一次暂存
git stash drop
带名字的暂存
如果你同时有多个任务在切换,给stash起个名字会清晰很多:
# 带消息的暂存
git stash push -m "登录模块重构中"
# 恢复指定的暂存
git stash apply stash@{0}
# 查看某个暂存的详情
git stash show stash@{1}
我的工作流:
- 发现线上bug → git stash save "hotfix-wip"
- 切到master → 修bug → 提交 → 切回feature
- git stash pop → 继续写代码
stash的进阶用法
# 只暂存未跟踪的文件
git stash -u
# 只暂存已暂存的文件
git stash --staged
# 从某个stash创建新分支(解决冲突时特别好用)
git stash branch new-feature stash@{0}
⚠️ 注意:stash默认不会暂存未跟踪的文件。如果你有新建的文件,记得加-u参数。我曾经因为这个丢过一个新写的工具类,还好有IDE的历史记录救了我一命。
五、总结对比
| 命令 | 作用范围 | 是否改写历史 | 适用场景 |
|---|---|---|---|
| git reset --soft | 提交 | 是 | 撤销提交,保留改动在暂存区 |
| git reset --mixed | 提交+暂存区 | 是 | 撤销提交和暂存,保留工作区 |
| git reset --hard | 提交+暂存区+工作区 | 是 | 彻底回退,丢弃所有改动 |
| git revert | 提交 | 否(新增反向提交) | 安全撤销已推送的提交 |
| git restore | 工作区/暂存区 | 否 | 文件级别的撤销 |
| git stash | 工作区+暂存区 | 否 | 临时保存工作现场 |
最后说一句:这些命令都不难,难的是在正确的时候用正确的那个。我的建议是——先想清楚「我要撤销到什么程度」,再选命令。别一慌就git reset --hard,那可能是真的「后悔药变毒药」。