系列文章
- #1 : 基础介绍 & 案例 1 之 "线性提交"
- #2 : 案例 2 之 "含合并提交"
- #3 : 案例 3 之 "含回退型合并提交" <== 本文章
- #4 : 扩展命令 : skip, run 命令
- #5 : 算法解析 : 中位 Commit 的选取
- #6 : 算法解析 : 关于 Skip
引言
之前两篇文章, 都是在分析比较理想的 Git 提交案例, 换个说法, 就是提交历史和 Good & Bad
的定义符合二分法预设的要求.
但是现实总是比较骨感, 直接在实际的历史提交中使用 bisect 找问题翻车概率还不小 ( "我以为我应该能找到出现问题的提交, 结果我找到了个莫名其妙的提交" )
比如今天要讨论的场景 : "含回退型的合并提交", 就是其中一种比较常见的例子. 在这种例子中, 如果不做一些处理转换, 则无法利用上 git bisect.
好消息是, 本文章的例子, 外加前面两篇的解析, 基本可以举一反三地应对现实场景中绝大多数的问题, 愉快地使用上 git bisect 定位问题提交.
一个故事
先通过一个小故事 (主人公 : 小明同学), 来让理解一下这个例子.
某天, 小明发现, 一个重要的操作按钮, 从界面上消失了, 他记得不久前还是好的. 于是他将 "按钮消失" 作为 bad commit
的标志特征, "按钮正常显示" 作为 good commit
的标志特征, 愉快地开始了 git bisect
查找之旅.
但是他发觉, bisect
每一步都是按照规则进行检查和标识的, 可是最后找到的 commit 并非是一个 First bad commit
而是总是找到一个与 good commit
错开的另一个分支的较早的 commit
.
经过排查, 小明明确了以下几点 :
bad commit
并非由 "主动引入的有问题代码" 造成, 而是由于合并不当产生的问题- 按钮经历了 "无" (未开发) > "有" (已开发) > "无" (合并错误, 丢失相关代码)
所以, 小明提出了新的需求, "我们如何定位到有问题的那个合并提交呢?"
分析
一个简化的例子
我们将以上的例子稍作简化, 来看一下以下的图示 :
- 在上面的图示中, 问题的根源来自中间一个 "有问题的合并" (即中间那个红色的合并提交, 按我们之前的文章中所述, 这个可视为 "First bad commit")
- 最上方的红色提交, 指的是 "初始化 git bisect 时指定的 Bad commit" (
git bisect bad
) - 中间的绿色提交, 指的是 "初始化 git bisect 时指定的 Good commit" (
git bisect good
)
故事中小明的困境
在上面故事中, 小明遇到的困境是 "First bad commit" 往往会被定位到比较莫名的地方, 如下图所示 :
造成这个困境, 最大的原因在于 : 我们用有没有问题的表现, 去界定 Good
& Bad
, 因而在正确代码第一次被引入之前的提交, 在检查时, 也会被认为是 Bad commit
例如在以上例子, 如果将 "按钮不存在" 这个作为 "有问题的特征" 来识别 Bad commit
的话, 那么 git bisect
是这么理解我们的代码提交的 (如下图) :
实际上, 我们应这么理解代码提交过程 (如下图) :
- 以上蓝色的提交指代的是 "虽然按钮不存在, 但亦不应被视为 Bad commit" 的提交
- 绿色提交指的是 "按钮正常显示" 的 "Good commit"
- 红色提交指的是 "出现合并错误之后" (含该错误的合并提交) 的 "Bad commit"
问题的转换
所以, 利用以上分析, 我们可以转变一下思路, 使用以下过程来定位 "有问题的合并提交" (注意 : 以下的完整过程涉及两次完整的 git bisect
查找) :
小 Tips : 这里为了避免和 "正确, 错误" 的概念混淆, 推荐使用
Old
和New
来界定
- 首先, 利用
git bisect
找出 "按钮被首次正常加入的提交" (First good commit
)Old commit
: 代表 "按钮未被加入 (无按钮)" 的提交New commit
: 代表 "按钮正常显示" 的提交
- 确认
First good commit
之后, 我们将Good
和Bad
进行重新界定, 再进行一次git bisect
查找, 即可找到有问题的合并提交
.Good commit
的情况 (两种)- 情况 1 : 前置提交不包含 "按钮被首次正常加入的提交"
- 情况 2 : 按钮正确显示
Bad commit
的情况- 出现错误合并的合并提交及其后续提交 (错误现象, 且前置提交包含
First good commit
的提交)
- 出现错误合并的合并提交及其后续提交 (错误现象, 且前置提交包含
一个 Demo 例子
接下来, 通过一个的 Demo 例子, 让我们完整地过一下上述的过程.
温馨提示 如果你看过前面两篇文章的话, 那么请注意, 由于本例子要贴合上面小明的故事, 所以本次 Demo 代码仓库的提交内容与之前的例子略有不同 :
- 之前两篇文章中的 git repo, 都会有一个特定的提交, 主动将代码中的某行特殊代码删除, 然后通过
git bisect
找到这个特定的提交 - 本篇文章中的 git repo, 这行特殊代码不是被主动删除, 而是在某个有问题的合并过程中丢失, 我们的目标也是要利用
git bisect
来找到这个特定的合并提交
Demo 例子简介
如上所述, 在 Demo git repo 中, 我们有一行特殊的代码 SOME IMPORTANT CODE
:
这个 git repo 中, 有三个分支 :
- master
- branch 1
- branch 2
初始的现象是, 在 branch 1 分支中的 b55ba
提交处, 我们能在代码中看到特殊代码
但是, 在几次合并之后, master 分支中, 这行特殊代码却不见了
过程 1 : 一个有问题的查找过程
首先, 展示一下在上面小明的故事中所述的, 有问题的查找过程 (即最终未能定位到有问题的合并提交).
在这个过程中, 我们仅通过代码表象上的对与错, 来区分 Good commit
和 Bad commit
, 即 : 如果有特殊代码行 (SOME IMPORTANT CODE
) 则为 Good commit
, 无特殊代码行, 则为 Bad commit
, 根据上的 Demo 例子简介, 我们可运行如下命令, 启动查找 :
第 1 次审查 : 此时审查提交 21f8c
, 特殊代码行不存在 :
因此, 标记为 Bad commit
:
第 2 次审查 : 再次审查提交 373ed
, 无特殊代码行 :
再次标记为 Bad commit
:
第 3 次审查 : 再次审查提交 82e41
, 无特殊代码行 :
标记为 Bad commit
:
第 4 次审查 : 审查最后一个提交 432b8
, 仍无特殊代码行 :
我们标记提交 432b8
为 Bad commit
之后, git bisect
查找就结束了, 很明显没有达到我们的实际预期, 因为最终定位到的, 并非有问题的提交 :
过程 2 : 一个正确的查找过程
根据上面 问题的转换
一节的问题分析, 我们将会用两个独立的小步骤, 来定位到有问题的合并提交 :
- 步骤一 : 利用
git bisect
找到First good commit
, 即, 特殊代码行首次被引入的那个提交 - 步骤二 : 对
Good commit
和Bad commit
重新定义之后, 再进行一次git bisect
查找Good commit
条件 :- 情况 1 : 前置提交 不 包含
First good commit
- 情况 2 : 前置提交包含
First good commit
, 且存在特殊代码行
- 情况 1 : 前置提交 不 包含
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
.
过程 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 code
转 echo
输出.
另外, 通过步骤一, 已知 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-base
的exit code
输出,0
和1
转换成yes
(是前置提交) 和no
(不是前置提交) 的打印- 简单介绍
COMMAND1 && COMMAND2
中, 当前面的COMMAND1
exit code 为0
时, 就会执行COMMAND2
. 而COMMAND1 || COMMAND2
中, 如果前置的COMMAND1
exit code 为1
就会执行COMMAND2
.- 但是, 请记得要使用
&& xxx || yyy
的顺序, 并且确保xxx
命令返回的 exit code 必为 0,|| yyy && xxx
(大概率) 会导致yyy
和xxx
都被依次执行
以上的方法准备妥当之后, 再次类似过程 1 中的启动方式, 启动本步骤的 git bisect
查找 :
第 1 次审查 : 符合 Bad commit
条件
于是, 标记为 Bad commit
第 2 次审查 : 与过程 1 的第 2 次审查结果一样, 我们直接跳到第 3 次审查, 这时要检查 82e41
第 3 次审查 : 此时该提交不包含 First good commit
, 符合 Good commit
条件.
标记为 Good commit
第 4 次审查 : 符合 Bad commit
条件
标记为 Bad commit
第 5 次审查 : 为最后一次审查, 位于如下位置, 再来看看 😉
很明显, 这个合并提交亦为 Bad commit
通过, 提交的变更可以看出, 本应在此提交看到新增的特殊代码行被合入, 但实际并没有, 说明提交时出现了错误, 而这个错误也就一层层地往后续的其他合并提交污染过去 (从 git 的角度来看, 这个特殊代码行未被合入, 像是合并者有意为之, 故会影响后续的相关合并)
本次 Demo 演示结束
本文结语
最后让我们用一个思考题来结束本文, 为什么能找到 "有问题的合并提交" 这个过程 2, 需要额外引入一个步骤, 来定位 First good commit
呢?
附录
- NOTE: 文章封面插图来源 Debug with git @Alexey
- 文章开始插图, 来源 google (侵删)
系列文章
- #1 : 基础介绍 & 案例 1 之 "线性提交"
- #2 : 案例 2 之 "含合并提交"
- #3 : 案例 3 之 "含回退型合并提交" <== 本文章
- #4 : 扩展命令 : skip, run 命令
- #5 : 算法解析 : 中位 Commit 的选取
- #6 : 算法解析 : 关于 Skip
本文原作者 jsPop, 欢迎留言交流 🥳