6篇文章讲清楚Git:Git 撤销与恢复:reset、revert、restore 应该怎么选?(4/6)

撤销相关的命令有一堆,reset、restore、revert、checkout、amend,最容易踩坑的地方不是不会用某个命令,而是用错了对象:明明只是想取消一下暂存,结果把工作区的改动也一起删了。

所以选命令之前先问自己一句:这个改动走到哪一步了。

text 复制代码
改完了,还没 add          → git restore <file>
已经 add,还在暂存区       → git restore --staged <file>
已经 commit,但没 push     → git reset / git commit --amend
已经 push 了              → git revert(安全)

下面按这个顺序一个个来。

一、还没 add:丢掉工作区的改动

shell 复制代码
git restore a.txt              # 把 a.txt 恢复成暂存区的样子
git restore .                  # 所有文件

⚠️ 这个操作不可逆,工作区里没 add 的改动会永久消失 。原因和第 1 篇讲的对象模型有关:这些改动连 blob 对象都还没生成,reflog 也不会起作用

老写法是 git checkout -- a.txt,效果一样,但 checkout 身兼数职容易误操作,推荐用 restore

如果只想撤销其中一部分,可以按块来选:

shell 复制代码
git restore -p a.txt           # 交互式,一块一块问要不要还原

那如果改错了想撤销,但这个改动已经进暂存区了呢?

二、已经 add:取消暂存

shell 复制代码
git restore --staged a.txt     # 从暂存区拿出来,工作区的改动保留

关键点在于工作区的内容不受影响,改的东西还在,只是"这次提交不打算带上它"了。

老写法是:

shell 复制代码
git reset HEAD a.txt           # 等价,这里 reset 的语义是"重置暂存区"

这其实就是 git reset 不带模式参数时的默认行为(--mixed)。很多教程直接说"用 git reset 取消 add",但没解释为什么,于是就和后面更危险的用法混在一起了,所以我们先记住这一层。

三、已经 commit,还没 push

这是 reset 的主场。

reset 到底重置了什么

git reset 会动三个东西,用哪个参数决定动其中几个:

模式 移动 HEAD 和分支指针 重置暂存区 重置工作区 危险程度
--soft 是 否 否 只是撤销提交
--mixed(默认) 是 是 否 改动还在,安全
--hard 是 是 是 ⚠️ 改动彻底丢失

三种模式从温和到暴力,我们实际跑一遍看差别。仓库里有三个提交,f.txt 的内容分别是 line1、line1 line2、line1 line2 line3:

shell 复制代码
git log --oneline
text 复制代码
9c67ceb commit3
b5898a9 commit2
0c06c01 commit1

--soft:只撤销提交

shell 复制代码
git reset --soft HEAD~1
git status -s
text 复制代码
M  f.txt

M 在第一列 ,说明改动在暂存区 。提交没了,但改动原封不动地待在暂存区里,随时可以重新提交。--soft 的典型用途就是"提交信息写错了,或者少提交了文件,想重新提交一次"。

--mixed:撤销提交和 add

shell 复制代码
git reset HEAD~1
git status -s
text 复制代码
 M f.txt

M 跑到第二列 了,说明改动退到了工作区,暂存区被清空了,文件内容还在。

这是不带参数时的默认模式,也是最常用的:撤销提交,把改动放回工作区,然后可以重新挑着 add。

--hard:连工作区一起重置

shell 复制代码
git reset --hard HEAD~1
git log --oneline
text 复制代码
b5898a9 commit2
0c06c01 commit1

commit3 从历史里没了,f.txt 的内容也退回到了只有 line1 line2,commit3 加的那一行是真的没了。

⚠️ --hard 会丢弃工作区里所有未提交的改动 ,不管有没有 add 过。用之前一定先 git status 看一眼,或者先 git stash 存一份(第 5 篇讲)。

HEAD~1 是什么意思

HEAD~1 是"往前一个提交",几个常见写法:

shell 复制代码
git reset HEAD~1        # 往前 1 个
git reset HEAD~3        # 往前 3 个
git reset HEAD^         # 等价于 HEAD~1
git reset <具体的哈希>   # 直接回到某个提交
git reset --hard 9c67ceb

~ 是沿着父提交往回走,^ 是选第几个父提交(合并提交有两个父),日常用 ~ 就够了。

四、已经 push 了:用 revert

分支已经推送到远程,别人可能已经拉下去了,这时候不能用 reset。

原因是 reset 把分支指针往回挪,等于宣称"这些提交从来没存在过"。但别人手里已经有它们了,我们再推上去还得强推,两边历史就对不上,别人一拉取就乱套。

revert 的做法完全不同:它不删历史,而是产生一个新的提交,用来抵消指定的那个提交。

shell 复制代码
git revert 9c67ceb
text 复制代码
[main 7d1e4f2] Revert "commit3"

历史变成了这样:

text 复制代码
9c67ceb commit3     ← 保留了,没删
b5898a9 commit2
0c06c01 commit1
7d1e4f2 Revert "commit3"    ← 新提交,内容上把 commit3 抵消掉

commit3 还在历史里,但它的改动被后面的 Revert 提交撤销了。这样别人拉取的时候只是多了一个普通提交,不会有任何冲突。

两者的对比

reset revert
对历史做了什么 删掉提交,指针回退 不删,新增一个抵消提交
哈希会变吗 被撤销的提交从分支上消失 老提交原样不动
能对已推送的提交用吗 不能(需要强推,会搞乱别人) 能,安全
适用场景 本地还没推的提交 已经推送到公共分支的提交

一句话:没推过用 reset,推过了用 revert。

五、改最后一次提交:commit --amend

如果只是想修补最近一次 提交,比如补个文件或者改个提交信息,不用 reset:

shell 复制代码
git add forgotten.txt
git commit --amend              # 把新文件并进上一次提交,提交信息不变
git commit --amend -m "新信息"   # 顺便改提交信息
git commit --amend --no-edit    # 保持提交信息不变,只合并文件

--amend 的本质是"用当前暂存区的内容重新生成一个提交,替换掉 HEAD"。所以有几点要注意:它也是改写历史 ,哈希会变,已经推送过的提交不要 amend;如果暂存区是空的,--amend 就只是改一下提交信息;想撤销 --amend 同样可以用 reflog 找回之前的提交。

六、reflog:最后的救命稻草

前面说的那些"危险操作",其实没有真的一段代码就凭空消失了,因为 Git 有个引用日志,记录着 HEAD 和分支指针的每一次移动:

shell 复制代码
git reflog
text 复制代码
b5898a9 HEAD@{0}: reset: moving to HEAD~1
9c67ceb HEAD@{1}: commit: commit3
b5898a9 HEAD@{2}: commit: commit2
0c06c01 HEAD@{3}: commit: commit1

左边是当时的提交哈希,中间是"第几次移动"(HEAD@{0} 是最近一次),右边是那次移动的原因。

所以 reset --hard 丢掉的提交是可以找回来的。上面这个例子里 commit3(9c67ceb)刚被 reset --hard 干掉,但它还在 reflog 里:

shell 复制代码
git reset --hard 9c67ceb
git log --oneline
text 复制代码
9c67ceb commit3     ← 回来了
b5898a9 commit2
0c06c01 commit1

文件内容也跟着回来了。

关于 reflog 有几个边界要说清楚:

  • 它只记录本地 的指针移动,所以能救回 reset --hard、rebase、commit --amend、误删分支丢掉的提交
  • 它救不回从来没 commit 过的东西,因为那连对象都没有
  • 它默认保留 90 天(不可达的对象 30 天),git gc 之后可能被回收,所以出事了要尽快找

想看某个分支的移动记录用 git reflog show <branch>。删除的分支也能用 git reflog 找到它最后的哈希,然后 git branch <名字> <哈希> 就恢复了。

强推的使用边界

有一种情况确实需要强推:我们 rebase 了自己的分支,或者 amend 了提交,本地和远程的历史对不上,Git 不允许普通推送。

shell 复制代码
git push --force              # 有风险
git push --force-with-lease   # 推荐

两者的区别在于:--force 是无条件覆盖远程,如果在我们 rebase 期间同事也往这个分支推了新提交,那些提交会被无声地抹掉 ;而 --force-with-lease 在推之前会先检查远程是不是还是我们上次看到的那个状态,如果被别人更新过就直接拒绝。

所以规则是这几条:

  1. 能用 revert 就不要强推,这是对别人最友好的做法
  2. 确实要强推时,永远用 --force-with-lease 而不是 --force
  3. 只对自己独占的分支强推 (自己的功能分支、自己的 PR 分支)。公共分支(main、develop)任何情况下都不该强推,很多平台可以直接配置保护规则禁掉
相关推荐
用户73309646123091 小时前
再也不怕手滑丢代码!给 git 高危操作加一层安全校验
git
广白2 小时前
Git Tag 实战:从出包追溯到版本发布
前端·git·面试
Winlifes5 天前
我给 Codex 做了一个 Git 面板:分支树、提交历史和工作区操作
git
郑州光合科技余经理6 天前
同城外卖小程序开发:下单成功后,后台导出能不能对上用户端状态
开发语言·前端·git·后端·uni-app·php·ai编程
我命由我123456 天前
Git 推送报错:error: src refspec main does not match any
运维·git·gitee·github·运维开发·学习方法·版本控制
Lcr3s7 天前
Git基础之(0):如何在Ubuntu(Linux)上安装git
git
玄芯散人7 天前
【筑基·058】Git协作工作流:冲突解决和PR流程
git·版本控制·团队协作
寺中人8 天前
Git 版本控制完全入门指南:从安装到实战,提交 + 分支 + 协作 + 冲突解决全拆解
git·开发工具·版本控制·团队协作·代码管理
荔枝梅梅8 天前
新手程序员 SSH 第一课:从 Git 仓库连接失败理解 SSH 协议与密钥配置
git