Git bisect 命令解析 #1 : 基础介绍 & 案例 1 之 "线性提交"

·  阅读 477
Git bisect 命令解析 #1 : 基础介绍 & 案例 1 之 "线性提交"

系列文章

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

引言

bisect : 中文释义为 "把 ... 一分为二",在算法中常说的 "二分查找法" 即为 bisection method (亦有翻译为 binary search method 等)。

本系列文章的主角,git 中的一个实用命令,就使用了该词作为命令名称 ( git bisect )

关于 bisect 的意义

  • 网络有一个有意思的说法 : "工程师一生的活动可以总结成两大类 : 写 bug 和 debug"。
    • 这虽然是一句调侃的话,但我们确实可以感受到,日常工作中来自众多不同形态的软件臭虫 (bug) 的深深的恶意和伤害,无论它是产生于别人的还是自己的。
    • debug,确实也是我们工作中,神圣而规律的一种日常活动.

img_bug.png

debug 工作,简单来说,无外乎就是 :

  • 明确 bug 现象和复现规律
  • 梳理并把控产生 bug 的范围
  • 排查和进行验证,逐步找到根源
  • 提出解决方案,并修复之
  • 验证,并按需补一些自动测试

其中,梳理并把控范围是尤为重要的一个环节,因为每个研发同学的生命和精力都是有限的,debug 时不考虑先梳理就 "不经思索的随便改东西看看能不能解决问题",这么做就像一只无头苍蝇般扎入任意的代码片段,那么很可能燃烧了宝贵的青春但全无收获。

举个一听就懂的例子,"如果 bug 出现在 A 模块,你肯定不会优先去 B 模块看代码"。

所以,如何快速地缩小范围并最终定位到相关的问题代码呢?

我们首先要坚定一个信念 :

"特定的 bug 肯定是在特定的某个时刻,特定的某个位置开始出现的。" ( 当然,这里说的 "位置" 不仅是指代码,可能是上下游或环境等 )。

之所以要坚定这个信念,是为了避免在 debug 中由于暂时的挫折而轻易转向各类妥协的解决方案。

有一类很常见的 bug,我们称之为 "回归型缺陷" ( regression bugs ),尤为符合上述的 "特定的某个时刻,在特定的某个位置出现" 这种说法。

  • 我们可以利用以下的两个特征,来大致识别一个 bug 是否为 "回归型缺陷" :
    • 以前确定是没问题的
    • 在某个时候突然被发现有问题
  • 对于这种 bug,我们不仅可以利用各种方式来进行位置上的定位,也可以考虑在引入时间上的定位,来辅助快速地缩小出问题代码的范围。
    • 因为,如果我们能找到一个特定的 Commit,它本身存在我们关注的问题,而它的 Parent Commit 没有问题的话,
    • 那么这个 Commit 所产生的代码更改 ( Changeset ) 极大概率是引入 bug 的范围.

本系列的文章介绍的 git bisect 命令,正是有效的方法之一,可以 帮助我们快速定位到 "第一次引入 bug 的 Commit"

关于本系列的内容计划

关于 bisect 命令的系列,预计将以七篇文章来进行分享,以下为大致的内容提纲 ( 免责声明 : 在创作过程中,有一定概率会根据实际情况进行内容的调整,亦可能会涉及本提纲的变动 ) :

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

基础介绍

  • git bisect 是一个 git 命令,利用二分法查找方法,定位出首次引入 bug 的 Commit.
  • 但更广义来说,git bisect 可用于找到任意的符合一定要求的 代码变更 的首次引入的 Commit.
    • 不一定要求必须在 debug 的使用场景才能运用该命令
    • 例如,你可以用它来找到某个特定 feature 的首次完成开发的 Commit
    • 甚至反过来,你可以用它来定位某个 bug 首次被 fix 的 Commit

相关概念

根据 git 中定位 Commit 的上下文,我们明确一些相关的基础概念 :

  • 代码变更 : 指的是一个特定的代码修改,由于该修改,会产生特定的代码或非代码的特征
  • old commit : 包含这种代码变更的 commit
  • new commit : 包含这种代码变更的 commit

以上的概念在 debug 的范畴中,可进一步理解如下 :

  • 代码变更可理解为直接引起目标 bug 的代码变更
  • good commit : 对应 old commit,不包含产生 bug 的代码变更
  • bad commit : 对应 new commit,包含产生 bug 的代码变更

Commit 状态的单调性

img_bisect.png

其次,二分查找法git bisect 的基本思路,我们需要做一个设定,那就是 :

"commit 的 old (good) | new (bad) 状态,应在 commit 的提交顺序上,呈现单调性。"

用如下的图示所述 : ( 其中红色代表 new (bad), 绿色代表 old (good), 灰色代表 未确定 )

在线性 Commit 中

  • 当一个 Commit 被识别为 new (bad) 时, 后续的 Commit 都为 new (bad)
  • 当一个 Commit 被识别为 old (good) 时, 前置的 Commit 都为 old (good)

img_linear-correct.png

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

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

img_merge-correct.png

不合法的情况

  • 相反的, 若实际存在如下的情形, 则 bisect 方法无法正确查找
    • 如果遇到这种情况, 要么需要重新定义 old (good) / new (bad) 的含义, 要么需要严格控制好二分查找的基础范围
    • NOTE : 这个其实也是为什么 bisect 在 good 和 new 之外, 还提供了 old 和 new 的另外一套术语, 甚至允许操作者自行指定两者的术语 (terms)

img_incorrect.png

简单案例

接下来, 我们会从三个例子进行分享, 以了解 bisect 的运行过程 :

  • 案例 1. 线性提交 (无合并提交)
  • 案例 2. 符合规则的包含合并提交
  • 案例 3. 包含回退型合并提交 ( 这种属于使用 bisect 易翻车的例子 )

Case 1. 线性提交 (无 Merge Commit)

关于这个场景, 以下为一些基础背景 :

  • 以下的文字描述, 可配合下面的配图进行理解
  • repo 仅有 master 分支, 共 18 个 Commit
    • repo 中, 仅有一个 hello.md 文件, 代表我们关心的代码内容
  • 已明确第 5 个提交是好的 (good), 包含 IMPORTANT CODE 这行代码
    • 注意 : 为了便于理解, 我们暂时用 good / bad 这组 terms
    • commit #5 的 hash 为 fd49...
  • 并且, 已明确最后一个提交 (第 18 个提交), 是有问题, 这行关键代码不见了
    • commit #18 的 hash 为 73b8...
  • 这个 case 的目标是, 快速定位到 "这行代码被删除的首个提交"

img_0_init.png

img_1_18.png

额外的一个小解释 :

  • 之所以要用 代码行丢失 作为 bug 依据, 是因为 : git blame 本身, 也是一个非常有价值的用于追踪问题代码的方式
  • 在某些能从关键代码特征推断问题的场景, 用 blame 可能也是一种思路 😉 (希望大家有更多的思路打开)
  • 但在 代码行丢失 的情况下, blame 相对来讲在复杂的真实提交环境下, 会略显不足

在开始之前, 我们利用线性提交这个场景, 理解一下 git bisect 是如何进行 二分查找 的, 如下图所示 :

  • 设置好初始化的 good commit 和 bad commit 之后, 查找就开始了
  • 这时, git bisect 算法会自动定位到一个 中位 Commit, 我们只需要检查这个 Commit 是好是坏
  • 如果是好的, 那么根据上面的规则推断, 前置的 Commit 都应该是好的, 那么 git bisect 会在剩余的一半 Commit 中, 再定位出一个 中位 Commit, 继续检查
  • 如果这个是坏的, 那么说明后续的 Commit 都是坏的, 以此继续执行下去
  • ... 直到 bisect 程序定位到 第一个有问题的 Commit (bad commit)

img_1_010_illustration.png

bisect start

首先, 我们要先开始一次 bisect 追查 :

# git bisect start [<bad> [<good>...]]
git bisect start 73b8 fd49

# 以上的命令, 可等价于 :
git bisect start
git checkout 73b8
git bisect bad
git checkout fd49
git bisect good
复制代码

img_2_start.png

可以看到, bisect 程序通过 二分法 的方式, 将当前的中位 Commit 设置在 Commit #18 与 Commit #5 之间的 Commit #11

img_3_middle.png

bisect good / bad

通过查看 Commit #11 所在的代码, 可以知道 Commit #11 是好的 ( 因为包含我们想要的 IMPORTANT CODE 这行代码 )

img_4_good.png

所以, 我们告诉 bisect 这个 : Commit #11 是好的 (good), 接下来让我们看 #14

img_5_11_good.png

Commit #14 代码行缺失, 是坏的 (bad)

img_6_14_bad.png

于是, 将 Commit #14 标识为 bad, 接下来要检查 Commit #13

img_7_14_bad_cli.png

审查知道 Commit #13 是好的,

img_8_13_good.png

img_9_finish.png

根据 Commit #14 的实际代码提交确认, 它确实是第一个引入问题的 Commit

img_10_14_commit.png

bisect log / replay / reset

  • ok, 定位到问题之后, 我们可以利用 git bisect log 对查找过程进行打印和存底
    • 关于 log, 我们还可以使用 git bisect replay 这个命令, 可以很方便地复现查找过程
    • 这个方式会很有用, 因为如果在中间过程若出现任何的人为失误, 可以通过导出 log, 剔除掉错误的步骤, 快速回退到之前的某个操作步骤
  • 最后, 可以使用 git bisect reset 这个命令结束查找, 退回到最初所在的 commit

img_11_log_reset.png

系列文章

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

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

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改