Git bisect 命令解析 #2 : 案例 2 之 "含合并提交" 的分析和处理

468 阅读7分钟

系列文章

  • #1 : 基础介绍 & 案例 1 之 "线性提交"
  • #2 : 案例 2 之 "含合并提交" <== 本文章
  • #3 : 案例 3 之 "含回退型合并提交"
  • #4 : 扩展命令 : skip, run 命令
  • #5 : 算法解析 : 中位 Commit 的选取
  • #6 : 算法解析 : 关于 Skip

引言

上一篇文章, 我们了解了 git bisect 的基础介绍, 并且也在单一线性提交中, 演示了一遍实际的排查过程. 可参见 : Git bisect 命令解析系列之一 : 基础介绍 & 线性提交例子

但是有一点很明确, 我们的代码仓库一般不可能是一个漂亮的线性提交, 除非一个代码仓库是及其严格地执行 rebase 进行提交管控.

注意: 我们不会在本文档讨论 rebasing vs. merging 的优劣 🤪. 我们仅从客观可能出现的 git 提交形态, 来继续探讨 git bisect 的应用案例 (及其原理)

存在 merge commit 的提交模型

按照如上说明, 仅包含线性提交的提交模型, 仅仅是存在 merge commit 的提交模型的一种特例. 所以, 我们接下来开始讨论在存在 merge commit 时, git bisect 是如何运作的.

处理过程简介

在更加通用的过程中 (存在 merge commit 的情况中), 我们可以把 bisect 过程简化为 :

  • 确定 "待检查的提交范围" ( commits_to_check )
  • 选取 "中位提交", 得到 NEXT_COMMIT_TO_CHECK
    • 通过检验, 我们确定该提交是 Bad 还是 Good
  • 利用新得到的这个信息, 再次确定新的 "待检查的提交范围", 并持续进行到没有带检查的提交 (此时得出结果)

一个合理的 "中位提交" 的选取, 要保证, 无论该轮检查得到的结果是 Bad 还是 Good, 剩下的 "待检查的提交范围" 都基本能减少到原来的一半. 由于我们会在后续文章中解析 "取中位提交的具体算法" 所以此处我们暂定有这么个效率不错的方法 :

  • B(commits_to_check) => NEXT_COMMIT_TO_CHECK

利用它, 我们可以在 "待检查的提交范围" ( commits_to_check ) 中选定一个可供检验的 "中位提交" ( NEXT_COMMIT_TO_CHECK ).

本章接下来的内容, 重点分享 "如何确定带检查的提交范围", 并演示一个 demo case.

如何确定 "待检查的提交范围" ?

先让我们回忆一下上一篇文章里的说明, 在两种不同形态的提交组合中, 我们分析了在某个 Commit 被标识为 BadGood 时, 如何推导出其他关联 Commit 的状态

在线性 Commit 中

  • 当一个 Commit 被识别为 Bad 时, 后续的 Commit 都为 Bad
  • 当一个 Commit 被识别为 Good 时, 前置的 Commit 都为 Good

img-linear-v2.png

在涉及一次合并的相关 Commit 中

  • 当合并 Commit 被识别为 Bad 时, 和线性 Commit 一样, 后续的 Commit 都为 Bad
  • 当合并 Commit 被识别为 Good 时, 则其 Parent Commits 皆为 Good
  • 当其中一个 Parent Commit 被识别为 Bad 时, 含合并 Commit 及其后续 Commit 都为 Bad
  • 当其中一个 Parent Commit 被识别为 Good 时, 和线性 Commit 一样, 只有该 Parent Commit 的前置 Commit 为 Good

img-merged-v2.png

一个更抽象的模型

在纯线性的提交模型中, 我们很容易可以理解, 在一次 git bisect 的过程里, 二分查找要处理的提交, 就是所有夹杂在 BadGood 之间的所有 Commits.

首先, 我们再进一步, 将上述的这几种提交组合, 抽象成如下的图示

  • 当某个 commit 被明确为 Bad 时,
    • 它的后续提交都为 Bad,
    • 它的前置提交都为 Unknown
  • 相反的, 当某个 commit 被标识为 Good 时,
    • 它的后续提交都为 Unknown,
    • 它的前置提交都为 Good

img-0.png

以上的模型中, 我们仅来关心两种 Commit :

  • Bad Commit 的前置提交 : 记为 Unknown 集合
  • Good Commit 的前置提交 : 记为 Good 集合

首先, 很容易地, 我们先 排除掉 一种场景 (如下图), 那就是 Bad Commit 恰好处于 Good 集合, 这种是有问题的初始化场景, 因为在这个场景中, 我们已经打破了 git bisect 能处理的前提假设.

img_1.png

用如下的图示更加直观的感受一下 (图中剪头所指的 灰色部分 即为二分查找过程感兴趣的提交集合) :

img_2.png

如果用提交的流程图示来看, 可能长的如下形态 :

img_3.png

具体的推导过程

看了以上的一个抽象理解, 我们来推导一下 :

    1. 注意, 以下的描述比较干涩, 可以看看上下文的一些配图, 再回来消化这部分内容 (但相信我, 其实很好理解)
    • 0.1. 这里备注一下, git bisect 的初始化, 接受一个 Bad Commit 和一个或若干个 Good Commit
    • 0.2. 如果有 Good CommitBad Commit 的直接祖先, 那么 git bisect 会先让你检查他们最近一个公共前置提交, 是 Good 还是 Bad (如上述图示的右图)
      • 0.2.1. 如果检查结果, 该提交为 Good Commit, 那么就将其记为 Cg, 然后往下走下走
      • 0.2.2. 如果检查结果为 Bad Commit, git bisect 会告诉你 This means the bug has been fixed between Cb and [Cg1, Cg2, ...]
        • 就是说 : "哥们, 你再看看, 重选一组 Bad Commit / Good Commit" 吧 😅
    1. 接下来, 给定初始化的 Bad Commit Cb 和 Good Commit Cg, 且已知 Cb 不是 Cg 的前置提交 (否则就如上文说的, 这个初始化并不合法)
    • 1.1. 那么, 如果 Cb 前置的所有提交不再出现 merge commit,
      • 1.1.1. 要么必然能找到一个 Ca, 它是 CbCg 的共同前置提交 (或理解为共同祖先提交 common ancestor commit)
      • 1.1.2. 那么要么 Cg 就是 Cb 的前置提交, (这时, 基于便利, 我们也把 Cg 记为 Ca)
      • 1.1.3. 这里有个额外澄清 : 对于存在多个 initial commit 的 git 仓库, 以上的逻辑不一定适用 (举例, 如果一个仓库是由两个不相关仓库, 使用 --allow-unrelated-histories 的合并, 我们后续有机会再看看这块)
    • 1.2. 如果, Cb 前置提交中存在 merge commit, 那么取其最接近的一个前置的 merge comit, 我们把它的两个 parent commits 记为 Cb1Cb2
      • 1.2.1. 如果 Cb1Cb2 前再没有 merge comit, 那么我们按照 1.1. 的方式分别查找, 最终会得到两个 commit : Ca1Ca2, 它们满足 Cb1 & Cb2Cg 的共同前置提交 (可以为 Cg 本身)
      • 1.2.2. 如果 Cb1Cb2 中某个提交 (或两个提交都是) 有 merge commit, 那么我们对这个提交进行 1.2. 的方式进行查找
      • 1.2.3. 最后, 我们会得到一系列的共同前置提交的列表, 我们记为 List<Ca>
    1. 得到一些里的 List<Ca> 之后, 根据上述 git bisect 的假设, 由于 CaCg 的前置提交 (或就是 Cg 本身), 所以它也应是一个 Good Commit
    1. 所以, 最终, 我们能得到一个 Cb ( bad commit ) 和 List<Ca> ( 一系列 good commits ), 到此为止, 我们就能进一步得到用于二分法查找的目标提交集合 : "所有处于 Cb 和任意 Ca 中间的提交"
    • 3.1. 换句话解释, 就是满足即是 Cb 的前置提交, 同时也是 Ca 的后续提交这个条件

上述过程的一个可视化流程图示 :

git-bisect--20220228.png

可视化过程

  • 首先, 我们得到初始化 "待检查的提交范围" ( commits_to_check )
    • B(commits_to_check) => NEXT_COMMIT_TO_CHECK, 确定本轮检查的 "中位提交"

img_4.png

  • 如果检查为 Good, 则利用这个信息, 排除掉它的前置提交

img_5.png

  • 如果检查为 Bad, 则利用上面的 1. 的过程, 推导下一轮的 "待检查的提交范围"

img_6.png

  • 如果 "待检查的提交范围" 非空, 按照上述过程继续
    • 否则, 得出 First Bad Commit

一个实际 Demo 案例

和上一篇文章一样, 我们先介绍一下本 Demo 的基础背景 :

  • 温馨提示 : 以下的文字描述, 可配合下面的配图进行食用, 风味更佳 😉
  • Demo 中的 git repo 有 三个分支 : master, branch1, branch2
    • repo 中, 仅有一个 hello.md 文件, 代表我们关心的代码内容
  • 目前, 已明确 :
    • 461afb6 : 为 Good Commit
    • 2f585c3 : 为 Bad Commit
  • Bad 的标准 : hello.md 中有一行内容 IMPORTANT CODE, 如果这行内容被删除, 则视为 Bad

img_19.png

  • 为了更加明确, 我们来看看 Good Commit ( 注意, 下述 "hello.md 的审阅" 这个窗口代表 hello.md 文件的内容 )

img_8.png

  • 然后, 再来看看 Bad Commit ( 注意 : 要配合上一张图理解 )

img_9.png

完整的脚本过程

首先, 我们开始一次 bisect :

git bisect start 2f585c3 461afb6

img_10.png

按照上述的解释, 由于 Good Commit 不是 Bad Commit 的直接祖先, 所以 git bisect 命令会先让我们检查一下他们的共同祖先提交.

img_11.png

经检查, 可知这个共同的祖先 ( 49bc9566 ) 提交为 Good

于是我们继续, 执行 git bisect good

img_12.png

img_13.png

一看, 哟, 还是好的, 于是我们继续标记为 Good

img_14.png

这次是 Bad, 执行 git bisect bad

img_15.png

img_16.png

经检查, 好的, 继续标记为 Good, 然后就得出 First Bad Commit ( 1612ec160c ) 了

img_17.png

经代码审查, 确实在这个 commit, 产生了 Bad 的代码更改.

img_18.png

以上就是整个完整的 bisect 过程.

后续

后续会将用到的几个 Demo 用的 git repo 推送到 github 上, 方便读者可以实际尝试一下. 当然, 你也可以在实际的业务项目中, 尝试以 某个代码特征 作为标记, 来进行实际演练以加深印象.

下一篇文章, 会介绍 "在 merge 翻车时", 我们该如何应对. 😈

系列文章

  • #1 : 基础介绍 & 案例 1 之 "线性提交"
  • #2 : 案例 2 之 "含合并提交" <== 本文章
  • #3 : 案例 3 之 "含回退型合并提交"
  • #4 : 扩展命令 : skip, run 命令
  • #5 : 算法解析 : 中位 Commit 的选取
  • #6 : 算法解析 : 关于 Skip

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