DEVELOPER DESK阅读地图
8个主要章节
5段代码示例
6组图片与图表

先定位当前任务,再对照操作前提、结果与容易混淆的边界。

章节速览8 节 · 展开选择

差异审查让我们看清一次提交改了什么,bisect帮助定位行为改变的位置。找到问题提交之后,还有一步:怎样撤销它的影响,同时保留后来已经完成的其他工作?

git revert 的思路是向前增加一份逆向修改。理解它时,要同时看内容和祖先关系:内容可以被反向修改,旧提交仍留在历史里。合并提交有多个父版本,这个区别尤其重要。

先选差异

明确目标提交相对哪个父版本引入了变化。

再看当前状态

逆向修改应用到现在,独立的后续工作可以保留。

最后看祖先

合并的内容被撤销,合并关系仍然存在。

A关闭功能并保留旧说明,B打开功能,C更新说明;在C后撤销B生成R,功能关闭而新说明保留,A到R仍是一条连续历史
R 的父提交是 C。 图中是两个教学文件的合成历史;撤销 B 的开关修改后,C 的独立说明修改仍保留。R 的内容也因此不同于 A。查看原图

先把提交和快照分开

在普通单父提交中,设 B 的父提交为 A。B 引入的变化来自 A 与 B 的比较;撤销 B 时,Git 尝试在当前内容上应用相反方向的变化,成功后通常记录一个新提交 R。这一行为由 git-revert 官方手册定义。

“相反方向”是理解差异的模型,不是把每一行当作独立加减数。当前版本可能已经移动、修改了相关内容,因此应用时可能需要合并,也可能产生冲突。即便没有文本冲突,后续代码仍可能在语义上依赖被撤销的功能。

本文只讨论已经提交的、明确选中的单个目标,练习文件都是已有普通文本文件。开始前先审查工作区与暂存区,确认干净,并检查目标涉及的全部路径与变更类型;若目标超出项目允许修改的范围,应先按项目规则处理。

两个文件的手算:后来的说明保留下来

初始提交 A 中,settings.txt 内容是 feature=off,note.txt 内容是 note=old。B 只把开关改为 on,C 只把说明改为 new。现在位于 C,选择撤销 B。

提交 父提交 settings.txt note.txt 这一步的作用
A 无 feature=off note=old 初始快照
B A feature=on note=old 打开功能
C B feature=on note=new 更新独立说明
R C feature=off note=new 撤销 B 的影响
RR R feature=on note=new 再撤销 R 的影响

R 和 A 都关闭功能,却有不同的说明。R 的父提交是 C,A、B、C 仍可沿祖先关系到达。即便某个例子的文件快照恰好与旧版本相同,新提交的身份与祖先也不因此相同。

最后一行 RR 是本例的“撤销撤销”:R 关闭了功能,撤销 R 就重新打开它。这里只因为改动彼此独立,结果可以直接手算;真实历史中间若有重叠修改,仍需处理冲突和行为依赖。

一次单提交撤销的审查顺序

下面的 $target 是 PowerShell 变量,须替换为已经核对的完整提交 ID。A、B、C、R 是本文图中的标签,并非要求仓库里存在这些分支。先用只读命令确认目标;单父场景中的 ^ 表示第一父版本,语法见 gitrevisions 官方文档。

1
2
3
4
5
$target = '替换为已核对的完整提交ID'
git status --short
git show --stat --oneline $target
git show --no-patch --format='%H %P' $target
git diff "${target}^" $target -- settings.txt

本例的目标只涉及 settings.txt;其他目标应先查看它实际涉及的全部文件。确认父版本、内容与权限范围,再针对这一个提交执行:

1
2
3
4
git revert --no-edit $target
git show --stat --oneline HEAD
git diff 'HEAD^' HEAD -- settings.txt
git status --short

--no-edit 采用默认撤销说明,不启动提交说明编辑器。它没有跳过冲突处理、内容审查或测试。命令成功之后,应确认新提交的差异只表达预期撤销,并运行与被改行为相关的测试。若需要解释撤销原因,可以使用编辑提交说明的正常流程。

冲突之后,继续与中止分别做什么

把例子改成 A 的开关为 off、B 改为 on、C 又把同一行改为 experimental。此时撤销 B 需要把 on 反向改成 off,当前却已经是 experimental,不能假定 Git 会自动保留你想要的语义。

遇到冲突后,先确认当前进行的是这一次 revert,再检查未合并路径与差异:

1
2
3
git status
git diff --name-only --diff-filter=U
git diff -- settings.txt

若决定完成撤销,通过编辑器解决 settings.txt 的冲突,明确保留哪部分当前行为。完成行为测试后,只暂存这个已审查的文件,再核对暂存差异:

1
2
3
4
git add -- settings.txt
git diff --cached --check
git diff --cached -- settings.txt
git revert --continue

--continue 继续正在进行的操作;它不会替你证明解法正确。若决定取消本次尚未完成的撤销,则使用 git revert --abort,回到本次操作开始前的状态。本次冲突处理期间的编辑也会随之撤回,需要保留的解法应事先另行记录。--quit 只是忘记进行中的操作状态,不能当作恢复开始状态的同义词。三者的区别见上述 revert 手册的 sequencer 小节。

自检:没有冲突,就能保证后续工作不受影响吗?

不能。两个文件的文本修改可以顺利合并,但后来的调用可能依赖被关闭的功能。无冲突只说明 Git 得到了可应用的文本结果;仍应检查行为、配置兼容性和相关测试。

合并提交为什么必须选择父版本

考虑一个两父合并 M:P1 更新说明,P2 打开功能,合并后两者都在。这里 P1、P2 是 M 的父列表顺序,不是分支名的排序,也不是图中位置决定的。

P1为功能关新说明,P2为功能开旧说明,M合并为功能开新说明;以第一父为基线撤销得到功能关新说明,以第二父为基线撤销得到功能开旧说明
先问相对谁发生了变化。 M 相对 P1 引入功能开关变化,相对 P2 引入说明变化。本图把撤销直接接在 M 后,因而两个结果分别等于选定父版本的内容;若还有后续提交,需要把它们也纳入分析。概念图不是项目历史截图。查看原图
比较基线 基线内容 M 相对它的变化 紧接 M 撤销后的内容
第一父 P1 关、新说明 功能关 → 开 关、新说明
第二父 P2 开、旧说明 说明旧 → 新 开、旧说明

git revert -m 1 M 选择第一父为比较基线,反向应用 P1→M;-m 2 选择第二父,反向应用 P2→M。这里的数字从 1 开始。选择的是保留哪条父基线的含义,不是“删除第几个父提交”。 结果仍有 M 作为祖先。Pro Git 的合并章节给出了同类解释。

实际操作先检查父列表与两份比较;以下仍是 PowerShell 的单个提交变量:

1
2
3
4
$merge = '替换为已核对的合并提交ID'
git show --no-patch --format='%H %P' $merge
git diff "${merge}^1" $merge
git diff "${merge}^2" $merge

通常在某分支上执行普通合并时,第一父是合并前该分支的 tip,但不应仅凭分支名猜测。确认第一父确实是要保留的比较基线后,才针对这个合并执行 git revert --no-edit -m 1 $merge。不要把 -m 1 当成所有合并都适用的固定答案。

撤销合并之后,再合并为什么没有恢复旧功能

本例在 M 后创建 R,撤销 P1→M 的功能变化。P2 仍是 R 的祖先,因此再合并同一个 P2,不会把它当作从未合并的新历史。

如果 P2 后面新增 P3,只添加独立的 fix.txt,再次合并会带来这份新改动,旧功能不一定因此恢复。本例结果是“功能关、新说明、新修补文件”:修补文件到了,旧开关仍然关闭。祖先判断与内容撤销各自都在发挥作用。

当分支是在原有提交上追加修复,而且确实要恢复 R 撤掉的内容时,可以先审查并撤销 R 这个普通提交,然后再合并后续修复。此时通常不需要对 R 传 -m:R 撤销的是合并,但 R 自己只有一个父提交。

如果分支已经基于撤销后的主线重新实现功能,恢复 R 可能重复引入旧实现并造成冲突。这是另一种历史,应重新比较合并基线与实际差异。Git 项目自身的 revert-a-faulty-merge 说明专门区分了追加修复与重做历史,不能机械套用“再撤销一次”。

用真实 Git 核验教学历史

本文的验证位于 tests/verify-git-revert-article.cjs。它在新建的独立教学仓库中,用 Git 对象命令构造上述快照与父关系,再实际运行单提交 revert、两种主线撤销、冲突继续/中止和再次合并。所有实验目录均保留,不接触博客的部署仓库,不清理文件或缓存。

核验分别比较文件内容、新提交的父 ID、祖先可达性和工作区状态。随机案例只改变合成文件中的内容,用于检查独立后续修改的保留;它们不构成对所有合并策略、重命名、二进制文件或真实业务兼容性的保证。本文所有图都依据这些教学状态绘制,不把它们称为生产项目的恢复记录。

自检:合并的第一父是 P1,撤销时写 -m 1,是删除 P1 吗?

不是。它把 P1 作为比较基线,反向应用 P1 到 M 的变化。紧接 M 的简单例子中,结果内容等于 P1,但 M、P1、P2 都仍在历史中。若以后需要恢复原合入内容,还要同时考虑这份撤销提交与后续分支的祖先关系。

接着读