系列文章
- #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 被标识为 Bad 或 Good 时, 如何推导出其他关联 Commit 的状态
在线性 Commit 中
- 当一个 Commit 被识别为 Bad 时, 后续的 Commit 都为 Bad
- 当一个 Commit 被识别为 Good 时, 前置的 Commit 都为 Good
在涉及一次合并的相关 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
一个更抽象的模型
在纯线性的提交模型中, 我们很容易可以理解, 在一次 git bisect 的过程里, 二分查找要处理的提交, 就是所有夹杂在 Bad 和 Good 之间的所有 Commits.
首先, 我们再进一步, 将上述的这几种提交组合, 抽象成如下的图示
- 当某个
commit被明确为Bad时,- 它的后续提交都为
Bad, - 它的前置提交都为
Unknown
- 它的后续提交都为
- 相反的, 当某个
commit被标识为Good时,- 它的后续提交都为
Unknown, - 它的前置提交都为
Good
- 它的后续提交都为
以上的模型中, 我们仅来关心两种 Commit :
- Bad Commit 的前置提交 : 记为
Unknown 集合 - Good Commit 的前置提交 : 记为
Good 集合
首先, 很容易地, 我们先 排除掉 一种场景 (如下图), 那就是 Bad Commit 恰好处于 Good 集合, 这种是有问题的初始化场景, 因为在这个场景中, 我们已经打破了 git bisect 能处理的前提假设.
用如下的图示更加直观的感受一下 (图中剪头所指的 灰色部分 即为二分查找过程感兴趣的提交集合) :
如果用提交的流程图示来看, 可能长的如下形态 :
具体的推导过程
看了以上的一个抽象理解, 我们来推导一下 :
-
- 注意, 以下的描述比较干涩, 可以看看上下文的一些配图, 再回来消化这部分内容 (但相信我, 其实很好理解)
- 0.1. 这里备注一下,
git bisect的初始化, 接受一个Bad Commit和一个或若干个Good Commit - 0.2. 如果有
Good Commit非Bad 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" 吧 😅
- 就是说 : "哥们, 你再看看, 重选一组
- 0.2.1. 如果检查结果, 该提交为
-
- 接下来, 给定初始化的 Bad Commit
Cb和 Good CommitCg, 且已知Cb不是Cg的前置提交 (否则就如上文说的, 这个初始化并不合法)
- 1.1. 那么, 如果
Cb前置的所有提交不再出现merge commit,- 1.1.1. 要么必然能找到一个
Ca, 它是Cb和Cg的共同前置提交 (或理解为共同祖先提交common ancestor commit) - 1.1.2. 那么要么
Cg就是Cb的前置提交, (这时, 基于便利, 我们也把Cg记为Ca) - 1.1.3. 这里有个额外澄清 : 对于存在多个 initial commit 的 git 仓库, 以上的逻辑不一定适用 (举例, 如果一个仓库是由两个不相关仓库, 使用
--allow-unrelated-histories的合并, 我们后续有机会再看看这块)
- 1.1.1. 要么必然能找到一个
- 1.2. 如果,
Cb前置提交中存在merge commit, 那么取其最接近的一个前置的merge comit, 我们把它的两个parent commits记为Cb1和Cb2- 1.2.1. 如果
Cb1和Cb2前再没有merge comit, 那么我们按照1.1.的方式分别查找, 最终会得到两个 commit :Ca1和Ca2, 它们满足Cb1&Cb2与Cg的共同前置提交 (可以为Cg本身) - 1.2.2. 如果
Cb1或Cb2中某个提交 (或两个提交都是) 有merge commit, 那么我们对这个提交进行1.2.的方式进行查找 - 1.2.3. 最后, 我们会得到一系列的共同前置提交的列表, 我们记为
List<Ca>
- 1.2.1. 如果
- 接下来, 给定初始化的 Bad Commit
-
- 得到一些里的
List<Ca>之后, 根据上述git bisect的假设, 由于Ca是Cg的前置提交 (或就是Cg本身), 所以它也应是一个Good Commit
- 得到一些里的
-
- 所以, 最终, 我们能得到一个
Cb(bad commit) 和List<Ca>( 一系列good commits), 到此为止, 我们就能进一步得到用于二分法查找的目标提交集合 : "所有处于Cb和任意Ca中间的提交"
- 3.1. 换句话解释, 就是满足即是
Cb的前置提交, 同时也是Ca的后续提交这个条件
- 所以, 最终, 我们能得到一个
上述过程的一个可视化流程图示 :
可视化过程
- 首先, 我们得到初始化 "待检查的提交范围" (
commits_to_check)- 用
B(commits_to_check) => NEXT_COMMIT_TO_CHECK, 确定本轮检查的 "中位提交"
- 用
- 如果检查为
Good, 则利用这个信息, 排除掉它的前置提交
- 如果检查为
Bad, 则利用上面的1.的过程, 推导下一轮的 "待检查的提交范围"
- 如果 "待检查的提交范围" 非空, 按照上述过程继续
- 否则, 得出
First Bad Commit
- 否则, 得出
一个实际 Demo 案例
和上一篇文章一样, 我们先介绍一下本 Demo 的基础背景 :
- 温馨提示 : 以下的文字描述, 可配合下面的配图进行食用, 风味更佳 😉
- Demo 中的 git repo 有 三个分支 : master, branch1, branch2
- repo 中, 仅有一个
hello.md文件, 代表我们关心的代码内容
- repo 中, 仅有一个
- 目前, 已明确 :
- 461afb6 : 为
Good Commit - 2f585c3 : 为
Bad Commit
- 461afb6 : 为
Bad的标准 :hello.md中有一行内容IMPORTANT CODE, 如果这行内容被删除, 则视为Bad
- 为了更加明确, 我们来看看
Good Commit( 注意, 下述 "hello.md 的审阅" 这个窗口代表hello.md文件的内容 )
- 然后, 再来看看
Bad Commit( 注意 : 要配合上一张图理解 )
完整的脚本过程
首先, 我们开始一次 bisect :
git bisect start 2f585c3 461afb6
按照上述的解释, 由于 Good Commit 不是 Bad Commit 的直接祖先, 所以 git bisect 命令会先让我们检查一下他们的共同祖先提交.
经检查, 可知这个共同的祖先 ( 49bc9566 ) 提交为 Good
于是我们继续, 执行 git bisect good
一看, 哟, 还是好的, 于是我们继续标记为 Good
这次是 Bad, 执行 git bisect bad
经检查, 好的, 继续标记为 Good, 然后就得出 First Bad Commit ( 1612ec160c ) 了
经代码审查, 确实在这个 commit, 产生了 Bad 的代码更改.
以上就是整个完整的 bisect 过程.
后续
后续会将用到的几个 Demo 用的 git repo 推送到 github 上, 方便读者可以实际尝试一下. 当然, 你也可以在实际的业务项目中, 尝试以 某个代码特征 作为标记, 来进行实际演练以加深印象.
下一篇文章, 会介绍 "在 merge 翻车时", 我们该如何应对. 😈
系列文章
- #1 : 基础介绍 & 案例 1 之 "线性提交"
- #2 : 案例 2 之 "含合并提交" <== 本文章
- #3 : 案例 3 之 "含回退型合并提交"
- #4 : 扩展命令 : skip, run 命令
- #5 : 算法解析 : 中位 Commit 的选取
- #6 : 算法解析 : 关于 Skip
本文原作者 jsPop, 欢迎留言交流 🥳