LabHub
学习 学习路径 课程

Git 实战

撤销由两个问题决定

在 LabHub 中继续学习

一句话总结

选择回退方法取决于两个问题:要回退的内容位于哪里(工作树 / 索引 / 提交),以及该提交是否已经传给其他人。

概念图: 未提交的修改会消失 · 表中真正危险的只有两行: · 不确定是否应该丢弃时,先提交,或用 git stash 暂存起来。

为什么需要它

Git 回退命令之所以令人觉得困难,不是因为命令太多,而是因为从未学过选择命令的标准。第一个问题决定命令类型,第二个问题决定是否可以改写历史。

还可以记住一个令人安心的原则:曾经提交过的内容几乎总能恢复,从未提交过的内容则几乎无法恢复。因此,真正危险的时刻不是“删除提交”,而是“覆盖未提交的修改”。

它如何运作

reset 的三种模式具有不同的回退范围。

模式 回退范围 会丢失什么 使用场景
--soft 只移动分支引用 把多个提交重新合并为一个提交
--mixed(默认) 回退到索引 暂存状态 希望重新选择暂存内容时
--hard 同时覆盖索引和工作树 所有未提交的修改 完全丢弃本地实验时
--keep 回退到索引,但存在冲突的本地修改时中止 希望在覆盖前被拒绝时

风险等级不取决于命令名称,而取决于“该命令是否会覆盖未提交的修改”。

如果提交已经存在于其他人的仓库中,任何删除该提交的方法都会破坏他人的历史。正确答案不是修改历史,而是追加历史,因此要使用 revert。如果通过强制推送删除提交,已经拉取过它的人仍会保留本地历史;他们下一次推送时,被删除的提交还可能重新出现。

对合并执行 revert 还有一个陷阱。revert 合并后,即使修复分支再重新合并,先前撤销的修改也不会回来,因为 Git 只根据历史可达性判断是否已经合并。正确做法是再次 revert 那次 revert。

生产现场中的表现

使用 git reset --hard 删除两个提交后脸色发白,是很常见的场景。通常仍然可以恢复:打开 git reflog,回到 HEAD@{1} 即可。但需要知道两点。删除分支时,该分支的引用日志也会一并删除(HEAD 的 reflog 中仍会保留);默认过期时间则是可达对象 90 天、不可达对象 30 天。

还有会立即移除安全网的命令:git reflog expire --expire=now --allgit gc --prune=now。如果团队把它们称为“清理”,并习惯性执行,事故发生时就不会留下恢复手段。

真正的最后手段,是通过 git fsck --full --unreachable --no-reflogs 查找游离对象,再使用 git cat-file -p <blob> > 파일 把内容取出。即使只做过暂存,内容也已经成为对象,因此可以用这种方法恢复。

不同场景应该使用什么

把前面两个问题应用到实际场景,就得到下面的表。紧急情况下,只需查看此表,不必再纠结命令选择。

场景 应使用的命令 注意事项
刚刚写错提交消息 commit --amend 已经推送时不要使用
刚刚的提交漏掉一个文件 添加后执行 commit --amend --no-edit 与上一项相同
希望把三个提交合并为一个 执行 reset --soft HEAD~3 后重新提交 修改内容会完整保留
只想取消暂存 restore --staged <파일> 不会改动文件内容
把一个文件恢复到最后一次提交状态 restore <파일> 未提交的修改会消失
取消已经推送的提交 revert <커밋> 会产生新提交并保留历史
完全丢弃本地实验 reset --hard 唯一真正无法回退的位置
误删后恢复 通过 reflog 找到记录,再执行 reset --hard <해시> 通常在 30~90 天内仍可恢复

表中真正危险的只有两行:restore <파일>reset --hard。二者都会覆盖从未提交过的修改,而这些修改没有记录在任何地方,reflog 也无法恢复。因此建议养成一个习惯:**不确定是否应该丢弃时,先提交,或用 git stash 暂存起来。**二者都会创建对象,从那一刻起就有恢复方法。

关于 stash 还要补充一点。默认行为不会暂存未跟踪的新文件,因此新建文件会继续留在工作树中,并混入下一项工作。要一起清理这些文件,需要加上 -u。不了解这一点,就会长时间困惑于“明明 stash 了,为什么工作树仍不干净”。

下一项实验要做什么

由于需要创建同一仓库的多个副本,首先会编写一个批量生成仓库的脚本。随后在不同仓库中分别执行 amend、reset 的三种模式、revert 和 reflog 恢复,直观比较结果差异,最后整理一份按场景分类的速查表。