Git bisect 命令解析 #3 : 案例 3 之 "回退型合并提交" 的分析和处理

865

系列文章

引言

之前两篇文章, 都是在分析比较理想的 Git 提交案例, 换个说法, 就是提交历史和 Good & Bad 的定义符合二分法预设的要求.

但是现实总是比较骨感, 直接在实际的历史提交中使用 bisect 找问题翻车概率还不小 ( "我以为我应该能找到出现问题的提交, 结果我找到了个莫名其妙的提交" )

比如今天要讨论的场景 : "含回退型的合并提交", 就是其中一种比较常见的例子. 在这种例子中, 如果不做一些处理转换, 则无法利用上 git bisect.

image.png

好消息是, 本文章的例子, 外加前面两篇的解析, 基本可以举一反三地应对现实场景中绝大多数的问题, 愉快地使用上 git bisect 定位问题提交.

一个故事

先通过一个小故事 (主人公 : 小明同学), 来让理解一下这个例子.

image.png

image.png

某天, 小明发现, 一个重要的操作按钮, 从界面上消失了, 他记得不久前还是好的. 于是他将 "按钮消失" 作为 bad commit 的标志特征, "按钮正常显示" 作为 good commit 的标志特征, 愉快地开始了 git bisect 查找之旅.

但是他发觉, bisect 每一步都是按照规则进行检查和标识的, 可是最后找到的 commit 并非是一个 First bad commit 而是总是找到一个与 good commit 错开的另一个分支的较早的 commit.

经过排查, 小明明确了以下几点 :

  • bad commit 并非由 "主动引入的有问题代码" 造成, 而是由于合并不当产生的问题
  • 按钮经历了 "无" (未开发) > "有" (已开发) > "无" (合并错误, 丢失相关代码)

所以, 小明提出了新的需求, "我们如何定位到有问题的那个合并提交呢?"

分析

一个简化的例子

我们将以上的例子稍作简化, 来看一下以下的图示 :

image.png

  • 在上面的图示中, 问题的根源来自中间一个 "有问题的合并" (即中间那个红色的合并提交, 按我们之前的文章中所述, 这个可视为 "First bad commit")
  • 最上方的红色提交, 指的是 "初始化 git bisect 时指定的 Bad commit" (git bisect bad)
  • 中间的绿色提交, 指的是 "初始化 git bisect 时指定的 Good commit" (git bisect good)

故事中小明的困境

在上面故事中, 小明遇到的困境是 "First bad commit" 往往会被定位到比较莫名的地方, 如下图所示 :

image.png

造成这个困境, 最大的原因在于 : 我们用有没有问题的表现, 去界定 Good & Bad, 因而在正确代码第一次被引入之前的提交, 在检查时, 也会被认为是 Bad commit

例如在以上例子, 如果将 "按钮不存在" 这个作为 "有问题的特征" 来识别 Bad commit 的话, 那么 git bisect 是这么理解我们的代码提交的 (如下图) :

image.png

实际上, 我们应这么理解代码提交过程 (如下图) :

image.png

  • 以上蓝色的提交指代的是 "虽然按钮不存在, 但亦不应被视为 Bad commit" 的提交
  • 绿色提交指的是 "按钮正常显示" 的 "Good commit"
  • 红色提交指的是 "出现合并错误之后" (含该错误的合并提交) 的 "Bad commit"

问题的转换

所以, 利用以上分析, 我们可以转变一下思路, 使用以下过程来定位 "有问题的合并提交" (注意 : 以下的完整过程涉及两次完整的 git bisect 查找) :

小 Tips : 这里为了避免和 "正确, 错误" 的概念混淆, 推荐使用 OldNew 来界定

  • 首先, 利用 git bisect 找出 "按钮被首次正常加入的提交" (First good commit)
    • Old commit : 代表 "按钮未被加入 (无按钮)" 的提交
    • New commit : 代表 "按钮正常显示" 的提交

image.png

  • 确认 First good commit 之后, 我们将 GoodBad 进行重新界定, 再进行一次 git bisect 查找, 即可找到 有问题的合并提交.
    • Good commit 的情况 (两种)
      • 情况 1 : 前置提交不包含 "按钮被首次正常加入的提交"
      • 情况 2 : 按钮正确显示
    • Bad commit 的情况
      • 出现错误合并的合并提交及其后续提交 (错误现象, 且前置提交包含 First good commit 的提交)

image.png

一个 Demo 例子

接下来, 通过一个的 Demo 例子, 让我们完整地过一下上述的过程.

温馨提示 如果你看过前面两篇文章的话, 那么请注意, 由于本例子要贴合上面小明的故事, 所以本次 Demo 代码仓库的提交内容与之前的例子略有不同 :

  • 之前两篇文章中的 git repo, 都会有一个特定的提交, 主动将代码中的某行特殊代码删除, 然后通过 git bisect 找到这个特定的提交
  • 本篇文章中的 git repo, 这行特殊代码不是被主动删除, 而是在某个有问题的合并过程中丢失, 我们的目标也是要利用 git bisect 来找到这个特定的合并提交

Demo 例子简介

如上所述, 在 Demo git repo 中, 我们有一行特殊的代码 SOME IMPORTANT CODE :

image.png

这个 git repo 中, 有三个分支 :

  • master
  • branch 1
  • branch 2

初始的现象是, 在 branch 1 分支中的 b55ba 提交处, 我们能在代码中看到特殊代码

image.png

但是, 在几次合并之后, master 分支中, 这行特殊代码却不见了

image.png

过程 1 : 一个有问题的查找过程

首先, 展示一下在上面小明的故事中所述的, 有问题的查找过程 (即最终未能定位到有问题的合并提交).

在这个过程中, 我们仅通过代码表象上的对与错, 来区分 Good commitBad commit, 即 : 如果有特殊代码行 (SOME IMPORTANT CODE) 则为 Good commit, 无特殊代码行, 则为 Bad commit, 根据上的 Demo 例子简介, 我们可运行如下命令, 启动查找 :

image.png

第 1 次审查 : 此时审查提交 21f8c, 特殊代码行不存在 :

image.png

因此, 标记为 Bad commit :

image.png

第 2 次审查 : 再次审查提交 373ed, 无特殊代码行 :

image.png

再次标记为 Bad commit :

image.png

第 3 次审查 : 再次审查提交 82e41, 无特殊代码行 :

image.png

标记为 Bad commit :

image.png

第 4 次审查 : 审查最后一个提交 432b8, 仍无特殊代码行 :

image.png

我们标记提交 432b8Bad commit 之后, git bisect 查找就结束了, 很明显没有达到我们的实际预期, 因为最终定位到的, 并非有问题的提交 :

image.png

image.png

过程 2 : 一个正确的查找过程

根据上面 问题的转换 一节的问题分析, 我们将会用两个独立的小步骤, 来定位到有问题的合并提交 :

  • 步骤一 : 利用 git bisect 找到 First good commit, 即, 特殊代码行首次被引入的那个提交
  • 步骤二 : 对 Good commitBad commit 重新定义之后, 再进行一次 git bisect 查找
    • Good commit 条件 :
      • 情况 1 : 前置提交 包含 First good commit
      • 情况 2 : 前置提交包含 First good commit, 且存在特殊代码行
    • Bad commit 条件 :
      • 前置提交包含 First good commit, 且 存在特殊代码行

过程 2 - 步骤一 : 找到 First good commit

本步骤就不再赘述, 但按上述的介绍, 推荐使用 New / Old 来代替 Bad / Good 的表述方式, 因为实际表现上, 查找过程的 Bad 其实是 "表现正确的代码", 更换一下表述, 会在整个过程中更加易于被理解. (否则, 你会在标记 Bad / Good 时, 需要自己多叨叨几句: "hmmmm, 这个提交看着是好的, 所以要标记为 Bad. 这个提交是坏的, 所以要比较为 Good. 🤪🤪🤪" ...)

更换表述之后, 查找过程实际上很简单, 只要把过程 1 中的初始化 Good commit 定义为 Initial new commit, 然后在它往前较早的某个前置提交中, 随意找到一个无特殊代码行的提交, 定义为 Intial old commit, 然后按照有无特殊代码行作为 New / Old 的条件, 定位到 First good commit 为 提交 24a4b.

image.png

过程 2 - 步骤二 : 找到错误合并提交

在步骤二之前, 先考虑这个问题 "如何判断一个提交 A 的前置提交中, 包含另一个提交 B?"

git 的子命令 merge-base 配合 --is-ancestor 选项即可做到, 参考 : git-merge-base --is-ancestor, 具体用法如下所示

# 检查 FIRST_COMMIT 是否是 SECOND_COMMIT 的前置提交
git merge-base --is-ancestor FIRST_COMMIT SECOND_COMMIT

但这个命令行是利用 exit code 提供结果 (0 代表是前置提交, 1 代表不是), 故为了能直接看到, 我们需要做个简单的 exit codeecho 输出.

另外, 通过步骤一, 已知 First good commit 提交为 24a4b, 故用于检查 "当前正在审查的提交, 其前置提交是否包含 24a4b" 的命令行即可如下所示 :

# 如果是, 打印 `yes`, 否则, 打印 `no`
git merge-base --is-ancestor 24a4b HEAD && echo yes || echo no

小 Tips :

  • 以上的 && echo yes || echo no 是一种命令行的小技巧, 由于将 git merge-baseexit code 输出, 01 转换成 yes (是前置提交) 和 no (不是前置提交) 的打印
  • 简单介绍 COMMAND1 && COMMAND2 中, 当前面的 COMMAND1 exit code 为 0 时, 就会执行 COMMAND2. 而 COMMAND1 || COMMAND2 中, 如果前置的 COMMAND1 exit code 为 1 就会执行 COMMAND2.
  • 但是, 请记得要使用 && xxx || yyy 的顺序, 并且确保 xxx 命令返回的 exit code 必为 0, || yyy && xxx (大概率) 会导致 yyyxxx 都被依次执行

以上的方法准备妥当之后, 再次类似过程 1 中的启动方式, 启动本步骤的 git bisect 查找 :

image.png

第 1 次审查 : 符合 Bad commit 条件

image.png

于是, 标记为 Bad commit

image.png

第 2 次审查 : 与过程 1 的第 2 次审查结果一样, 我们直接跳到第 3 次审查, 这时要检查 82e41

第 3 次审查 : 此时该提交不包含 First good commit, 符合 Good commit 条件.

image.png

标记为 Good commit

image.png

第 4 次审查 : 符合 Bad commit 条件

image.png

标记为 Bad commit

image.png

第 5 次审查 : 为最后一次审查, 位于如下位置, 再来看看 😉

image.png

很明显, 这个合并提交亦为 Bad commit

image.png

通过, 提交的变更可以看出, 本应在此提交看到新增的特殊代码行被合入, 但实际并没有, 说明提交时出现了错误, 而这个错误也就一层层地往后续的其他合并提交污染过去 (从 git 的角度来看, 这个特殊代码行未被合入, 像是合并者有意为之, 故会影响后续的相关合并)

image.png

本次 Demo 演示结束

本文结语

最后让我们用一个思考题来结束本文, 为什么能找到 "有问题的合并提交" 这个过程 2, 需要额外引入一个步骤, 来定位 First good commit 呢?

附录

系列文章

本文原作者 jsPop, 欢迎留言交流 🥳