系列文章
- #1 : 基础介绍 & 案例 1 线性提交
- #2 : 案例 2 含合并提交
- #3 : 案例 3 含 "回退型" 合并提交
- #4 : 扩展命令 : 应用过程中的一些扩展问题 ( <== 本篇文章 )
- #5 : 算法解析 : 中位 commit 的选取
- #6 : 算法解析 : 关于 skip 的处理
引言
本系列的前三篇文章, 介绍了 git bisect 的基本原理, 并分析了三种不同类型的例子, 希望可以让你在各种场景下都能游刃有余地利用上 git bisect.
但是, 在真实的应用过程中, 很可能还会遇到一些疑问 :
- 问题 1. 我就是任性, 我就不用 git bisect 自动推荐的提交, 是否可以主动切换另一个提交?
- 问题 2. 如果很不幸, git bisect 选中的某个提交, 恰好无法被确认好坏 ("good commit" 或者 "bad commit"), 怎么办?
- 问题 3. 如果更不幸的, 一大堆的提交都是无法确认好坏, 怎么办?
- 问题 4. 我们是否有方法, 让 "机械地确认过程" 变得更加的高效?
- 问题 5. 如果某个自动化的过程中, 又双叒叕遇到无法确认好坏的提交, 又该怎么办?
- 问题 6. 如果某个确认过程比较复杂, 存在异步情况, 但是步骤稳定, 还有可能实现自动化么?
本章, 就让我们来看看这些形形色色的问题.
git bisect 过程中可能遇到的问题
无法确定提交的状况
- 问题 1. 我就是任性, 我就不用 git bisect 自动推荐的提交, 是否可以主动切换另一个提交?
- 问题 2. 如果很不幸, git bisect 选中的某个提交, 恰好无法被确认好坏 ("good commit" 或者 "bad commit"), 怎么办?
这两个问题, 可以被合并起来解答, 因为他们本质上是很相似的场景, 比如问题 2 中的情况, 实际上是因为 git bisect 推荐的 commit 不合适, 导致需要切换到其他的 commit, 和问题 1 的切换是很类似的, 只不过一种被动, 一种主动而已.
下面, 我们先解释一下问题 2 出现的原因.
比如说, 某个问题和 UI 界面上出现的问题相关, 并且, 你也并不确认与其直接关联的问题代码. 这时, 如果 git bisect 给你推荐了一个, 启动不起来看不到 UI 界面的 commit, 那么这时就很尴尬了.
另外, 即便是构建等过程都正常, 但如果有另外的因素干扰了出问题的现象, 导致无法确定或无法区分, 这些也可归属于不好确定的情况 ( untesable commits ).
还有, 就算一个 commit 费点周章还是能识别出好坏, 我们还是有可能想要跳过它, 因为... 懒 😂
关于这个情况, git bisect 提供了 skip 命令, 你可以通过执行 git bisect skip 直接忽略当前的 commit, 同时, 程序会自动再为你推荐另外一个 commit 进行确认. ( 关于 skip 的一些算法处理, 计划在后续的一篇文章中提及, 此处不做详细介绍 ), 这个是问题 2 的答案.
然后, 问题 1 呢? 我们能主动无视掉程序推荐的 commit, 手动选择一个 commit 进行确认么? 答案是可以的, 在 git bisect 的官方文档中, 有提及 "你可以手动选择一个周边的 commit, 并转而确认这个 commit (you can manually select a nearby commit and test that one instead)"
关于问题 1 扩展出来的一个满足好奇的问题, 如果我无视原有 git bisect 的前提要求, 胡乱切一个 commit, 然后随意标记一个不可能的状态, 如设置 bad commit 的某个后续提交为 good, 会发生什么事情?
程序会友好地抛你一脸提示, 并且无视你的后续正确的操作, 持续抛你一脸的提示...
无法确定提交状态, 并且这类提交连成一片
- 问题 3. 如果更不幸的, 一大堆的提交都是无法确认好坏, 怎么办?
比如之前的场景里, 我们需要系统跑起来, 看到 UI 界面才能判断 commit 是好是坏, 但好死不死一个 commit 把系统构建给搞挂了, 并且还一挂挂一溜. 元芳, 你这次怎么看?
这种场景下, 有两种可能,
- 第一种 : 首次出问题的提交, 没有 在无法确认的提交区域中
- 第二种 : 首次出问题的提交, 刚刚好落在 无法确认的提交区域中
对于第一种情况其实还好, 基本上还是能正确定位到首次出问题的提交 (如下图). 另外, 虽然后面会计划再讨论 skip 命令的具体逻辑, 但这还是简单介绍一下, 在选择跳过 commit 时, skip 通过引入伪随机数调整权重, 来降低落入无法确认的提交范围的概率.
第二种情况, 就比较麻烦了, 如下图所示 :
由于无法确认 bad commit 紧接着的前置提交的状态, git bisect 无法获得所需信息, 故最后仅能给出一个可能的提交范围.
参考 git 官方文档, 对于这种类别的提交, 基本的处理思路是 :
- 1) 确认有问题的范围,
- 找到首次无法确认的提交, 记为 BBC (bisect breaking commit)
- 找到后续首次解决问题的提交, 即为 BFC (bisect fixing commit)
- 假定中间夹了 X1, X2, X3, X4 个提交.
- 2) 通过处理, 把无法确认状态的提交 X1, X2, X3, X4 单独修复出来
- 例如, 可以通过 git 的交互式 rebase, 把 BFC 先往前挪动, 并和 BBC squash 为一个可确认状态的提交 ( 这里假设, 这种改动, 会修复 squash 之后的提交, 使其状态变成可确认了 )
- 接着, X1, X2, X3, X4 将被移出范围之外 (有可能会存在一定的 rebase 的冲突需要解决), 这些与原来提交所对应的新的提交序列, 记为 X1', X2', X3', X4'
- 3) 通过上述处理, 后续在进行
git bisect时, 如果选取得到 X1 ~ X4 的任意提交, 只需要使用 X1' ~ X4' 对应的提交来进行状态确认
当然, 这么做, 在提交的状态确认的时候, 还存在有一次显式的前置处理 ( 无论是人肉执行的, 还是脚本自动对应处理 patch ), 这么做还是略显笨拙. 实际上, git 官方文档中, 推荐了一种方式 git replace, 是一个非常有意思且实用的进阶方法, 它可以在不实际更改 git 代码仓库历史的情况下, 替换提交历史的内容或结构, 很值得额外了解一下, 本处就不再展开讨论.
git bisect 提效
- 问题 4. 我们是否有方法, 让 "机械地确认过程" 变得更加的高效?
如果某个 "bug" 的特征被确定后, 基本还没明确根源, 大多数场景下, 可以找到标准化的 "确认好坏的流程". 而当一个流程足以被标准化时, 往往一个脚本就不难写了.
如果感觉对了, 脚本也被撸出来, 那么 git bisect 提供了一个非常使用的子命令 run, 用于将问题查找变成完全自动化的过程.
参考 git 文档中提供的一个例子, 假设某个代码仓库对应一个程序, 那么我们可以利用如下的命令, 来完成自动化查找 :
$ cat ~/test.sh
#!/bin/sh
make || exit 125 # 如果无法 build 成功, 返回 125
~/check_test_case.sh # 一个另外的脚本,提供对构建后程序的运行验证
$ git bisect start HEAD HEAD~10 -- # 加装 bug 出现过在前面 10 次提交中
$ git bisect run ~/test.sh
$ git bisect reset # 完成退出
注意以上的特殊的返回码 125, 这个可以回答我们的问题 5
- 问题 5. 如果某个自动化的过程中, 又双叒叕遇到无法确认好坏的提交, 又该怎么办?
按照 git 的文档, 如果 run 所执行的程序或脚本返回这个错误码 ( 125 ), 那么代表本次提交无法确认状态.
选取
125是因为这个错误码是合适的错误码中最高的一个,126和127被 POSIX shell 用来定义特定的出错状况 ( 125 was chosen as the highest sensible value to use for this purpose, because 126 and 127 are used by POSIX shells to signal specific error status. )
另外, 记得上述的例子中的 test.sh 和 check_test_case.sh 不要放置在代码仓库所属的文件夹下面, 否则运行时随着 commit 被切换, 这两个脚本是会被重置, 或出现问题的. ( 上述例子中, 是直接放置在了 Home 文件夹下 )
涉及异步检查的自动化处理
- 问题 6. 如果某个确认过程比较复杂, 存在异步情况, 但是步骤稳定, 还有可能实现自动化么?
像 "看看某些文件的特定特征", "build 命令行, 运行产物看结果" 这种, 直觉上很容易处理成顺序执行的脚本的, 好像都挺简单.
但如果某个确认过程, 我们需要启动一个比较大型的 web 服务, 然后要在网页界面上做点小操作, 才能看到的话, 就比较不好处理成脚本.
对于这个场景, 可供参考的思路是 : 考虑用 "等待机制" 把异步过程, 变成一个阻塞的同步脚本执行
下面用比较具体的代码来演示 (以 NodeJS 的项目以及处理代码为例), 涉及的代码如下 :
server.js: 假设代码的运行, 会启动一个 web 服务, 这个用一个简易server.js来代替detect.js: 用于等待 web 服务, 确认结果, 最后退出服务, 并返回对应的status code
以上涉及的演示代码, 假设都是放在 Home 文件夹下, 用 node ~/server.js 可运行 ( 但是请注意 : 真实场景下, 运行 node ~/server.js 应被类似 npm install && npm start 等命令所代替 )
server.js 代码内容
const express = require('express');
function runServer() {
const app = express();
app.get('/', (req, rsp) => {
// 模拟一个随机的好的或坏的结果
const resultStatus = Math.random() < 0.5 ? 'correct' : 'wrong';
rsp.send(`${resultStatus} result`);
});
setTimeout(() => {
const port = 8881;
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`);
});
}, 3000); // 模拟运行了一段时间才启动
}
runServer();
detect.js 代码内容
小贴士代码较长, 可以从下面的入口方法 main 开始看
const childProcess = require('child_process');
const axios = require('axios');
// 等待方法
function sleep(interval = 1000) {
return new Promise((res) => {
setTimeout(res, interval);
});
}
/**
* 正常的确认结束时的统一处理
*
* @param {'good' | 'bad' | 'skip'} bisectResult
* @param {ChildProcess} childProcess
*/
function exitWithResult(
bisectResult,
childProcess = null
) {
if (childProcess) {
childProcess.kill()
}
console.log(`git bisect ${bisectResult}`);
process.exit(
bisectResult === 'good' ? 0
: bisectResult === 'bad' ? 1
// skip 的情况
: 125
);
}
async function main() {
// step 1. 子进程中启动程序
const child = childProcess.exec(`node ${__dirname}/server.js`);
child.stdout.on('data', data => {
console.log();
console.log(data);
});
child.stderr.on('data', ex => {
console.log();
console.log(ex);
// 启动过程发生错误的话, 当做 skip 处理
exitWithResult('skip', child);
});
// 主动退出程序的情况下, 记得把子进程也清理掉
process.on('SIGINT', () => {
child.kill();
// 注意, 当主动退出时, 我们要返回 > 127 的 exit code
// 用于告知 git bisect, 这个退出是一种主动的结束行为, 而非标志为 `bad` 或 `skip`
process.exit(128);
});
// step 2. 做一些检查
let timeoutCount = 100;
let bisectResult = 'skip';
while (true) {
await sleep(800);
// 一个简易的超时机制, 发生超时时, 我们假定是 skip 的情况
timeoutCount--;
if (timeoutCount <= 0) {
exitWithResult('skip', child)
}
try {
const result = await axios.get('http://localhost:8881');
// 结果确认
if (result.data.includes('correct')) {
exitWithResult('good', child);
} else {
exitWithResult('bad', child);
}
break;
} catch (ex) {
// 继续等待
console.error('waiting for server', ex.message);
}
}
}
void main();
命令行运行
有了以上的脚本后, 我们切换到一个要做 git bisect 查找问题的代码仓库, 执行如下命令 :
$ cd path/to/my/repo
# 假设 bisect 的范围, 是当前提交以及之前的 20 个提交
$ git bisect HEAD HEAD~20
$ git bisect run node ~/detect.js
$ git bsect reset
然后, 就可以看到 git bisect 乐呵乐呵地自动跑起来了.
再进一步的思考 : 半自动化过程
有时候, 如果确认过程并不能很容易被明确为一个标准化过程 ( 实际上, 识别和设计标准的过程, 也是需要不少时间的 ), 那么全自动的 git bisect 就不合适了.
换句话说, 作为一个程序员, 我们也要适当地平衡好 "优化行为" 和 "手动执行" 的任务, 有时要避免对所有任务都要不分场合, 不计一切地进行优化 ( 比如在计划用 git bisect 找问题时, 忘记了 "找问题" 的原始目标, 而一味地沉浸在企图自动化查找过程这个衍生目标, 那就有些钻牛角尖了 ). 虽然, 追求卓越理应是流淌在我们的血液中, 但讲究时宜, 考虑影响也是必要的. 实际上, 我们完全可以在当时当刻先把问题给找了, 然后记一个后续的 todo, 后面得空时, 继续深入学习和研究. ( 这里好像扯的有点远, 请见谅 😃 )
不过, 这里既然提到, 我们不妨来设计一个相对通用, 且折衷的方案 :
即, 对于 git bisect 的过程, 假设确认问题一时不太容易处理成标准化的过程, 那么我们还是可以把大多数的机械操作给自动化了 ( 比如, 安装依赖, 设置程序, 启动服务等按部就班的操作 ), 然后一切妥当之后, 把实际确认的行为, 留给用户手动执行.
如下, 参考一个用于代替上述 detect.js 的 manual-detect.js
const childProcess = require('child_process');
// inqurier 是一个很有意思的支持交互式命令行的第三方库
const inquirer = require('inquirer');
// 等待方法
function sleep(interval = 1000) {
return new Promise((res) => {
setTimeout(res, interval);
});
}
/**
* 正常的确认结束时的统一处理
*
* @param {'good' | 'bad' | 'skip'} bisectResult
* @param {ChildProcess} childProcess
*/
function exitWithResult(
bisectResult,
childProcess = null
) {
if (childProcess) {
childProcess.kill();
}
console.log(`git bisect ${bisectResult}`);
process.exit(
bisectResult === 'good' ? 0
: bisectResult === 'bad' ? 1
// skip 的情况
: 125
);
}
async function main() {
// step 1. 子进程中启动程序
// 在很多场景中, 本程序只需替换以下的启动部分, 就可以直接复用了
const child = childProcess.exec(`node ${__dirname}/server.js`);
child.stdout.on('data', data => {
console.log();
console.log(data);
});
child.stderr.on('data', ex => {
console.log();
console.log(ex);
// 启动过程发生错误的话, 当做 skip 处理
exitWithResult('skip', child);
});
// 主动退出程序的情况下, 记得把子进程也清理掉
process.on('SIGINT', () => {
child.kill();
process.exit(128);
});
// step 2. 接受等待用户的主动选择
while (true) {
await sleep(800);
try {
const rsp = await inquirer.prompt([
{
name: 'bisectResult',
message: 'How does the commit look like?',
type: 'list',
choices: [
{name: 'good'},
{name: 'bad'},
{name: 'skip'},
]
}
]);
exitWithResult(rsp.bisectResult, child);
} catch (ex) {
// 继续等待
console.error('Something is wrong', ex.message);
}
}
}
void main();
除了中间过程, 需要手动确认一下提交的好坏, 命令执行和上面的自动化场景一致.
结语
git bisect 命令, 其应用部分, 基本就谈论至此.
虽然讲了不少, 但还是不可能把实际应用中遇到的所有情况都一一讨论. 但是, 学习本身也确实如此, 在任何时候, 我们并不会把所有可能涉及的知识或技能都练到满级, 再投入应用. 相对于具体的知识和技能而言, 学习的方法, 解决问题的思路, 开放而平衡的心态, 会更加的重要.
希望前几篇的文章, 已经能提供一些有用, 且足够可理解的样本, 帮助大家利用上一些工具, 让自己的研发生活更加轻松一些.
(本文完)
系列文章
- #1 : 基础介绍 & 案例 1 线性提交
- #2 : 案例 2 含合并提交
- #3 : 案例 3 含 "回退型" 合并提交
- #4 : 扩展命令 : 应用过程中的一些扩展问题 ( <== 本篇文章 )
- #5 : 算法解析 : 中位 commit 的选取
- #6 : 算法解析 : 关于 skip 的处理
参考
本文原作者 jsPop, 欢迎留言交流 🥳