バグコミットは唐突に仕込まれ、そのコミット(犯人)を特定するのには時間がかかる場合があります。
git bisectは「バグが仕込まれたコミットを特定」を補助してくれるgitの便利機能です。
誰しもみんな↓みたいな経験をしたことがあるでしょう。
- 「なんかこの機能バグってるよ、いつからバグってんのこれ」
- 「なんでこんなスタイル崩れちゃってるの、どの修正でおかしくなったんだろう」
エンジニア・デザイナーであれば、何度もこのような場面に出くわしてると思います。今後もたくさん経験するはずです。
バグが仕込まれたコミットを特定できれば、問題の解決は早いでしょう。
その「バグが仕込まれたコミットを特定」する便利なものが「git bisect」です。
普段やりがちな「バグが仕込まれたコミットを特定」する方法
bisectを紹介する前に、バグコミットを特定するために、通常はどのような探し方をしがちなのか見ておきます。
大抵みんなこんな探し方をしてることでしょう。
- めぼしいプルリクを探して、そこのコミットを一つずつ見てみる
- 予想が当たれば早いですが、マイナーな機能な場合たくさんのプルリクを見る羽目になるでしょう
- バグとなっていそうなコードを探して、その箇所のコミットを見る
- 大抵の場合これで事足りそうですが、本当によくわからないバグの場合はこの方法では困難です
- バグが仕込まれる前のコミット〜直近コミットまでの間をresetしたりしながら探す
- 2分探索的に探すと一番早そうですが、真ん中くらいのコミットを探して戻して、検証してっていうのはかなり面倒です
どれも結構体力を使ってしまう探し方です。
git bisectは「3」の考え方に基づいて、処理されます。
git bisectで「バグが仕込まれたコミットを特定」する
git bisectでは2分探索を使ってバグが仕込まれたコミットを特定していきます。
2分探索の説明も踏まえてどのようにgit bisectで「バグが仕込まれたコミット」を特定していくかを見ていきます。
以下のように、バグコミットを含むコミット一覧があるとします。
実際はハッシュ値ですが、分かりやすく数字にしてます。
- 9999(最新コミット)
- 8888
- 7777
- 6666
- 5555
- 4444
- 3333(ここでバグが仕込まれた)
- 2222
- 1111
- 0000(最初のコミット)
ここでは、3333のコミットでバグが仕込まれたとします。もちろん3333でバグが仕込まれたとは実装者は分からない状態です。
今回取り扱うコードは、色付きboxと文言を表示しているような単純なHTMLです。
正常なコードとバグっているコードを以下に示します。
正常なコード
<style>
html {
text-align: center;
}
.box {
width: 200px;
height: 200px;
background: #8cf;
margin: 0 auto;
}
</style>
<html>
<h1>git bisect 0000</h1>
<div class="box"><div>
</html>
バグっているコード
<style>
html {
text-align: center;
}
.box {
width: 200px;
height: 200px;
background: #f88; /*色を赤くしてしまっているので、バグとみなす*/
margin: 0 auto;
}
</style>
<html>
<h1>git bisect 3333(bug)</h1>
<div class="box"><div>
</html>
これらのコミットでは各コミットで以下のようなスタイル・文言の修正をしているとします。
途中で水色のboxが赤になっていますが、これをバグとしています(ミスって色の変更しちゃったとする)
コミット毎に表示がどうなっているかの図
git bisectでは以下コマンドを実行していくことで探索範囲を決めます。
git bisect start [バグってる状態のコミットハッシュ] [正常に動作してるコミットハッシュ]
今回は、HEAD(バグってる)と最初のコミット(バグってない)を指定してみることにします。
git bisect start HEAD 0000
ここからが探索開始で、以下のように2分探索で絞り込んで、コミットを特定していきます。
git bisect startを実行後、①の状態になる。
これよりバグっていたら、
git bisect bad
バグってなかったら、
git bisect good
を入力して絞り込んでいきます。
各状態での説明をする↓
- ①
- HEADが4444のコミットになるが、ここでもまだバグっているので、badと実行する
- 4444のコミット以前にバグが仕込まれているはずなので、0000 ≤ 次の探索対象 ≤ 4444となる
- ②
- HEADが2222のコミットになり、この状態であれば正常に動作することを確認できたので、goodと実行する
- 2222は正常なので、3333 ≤ 次の探索対象 ≤ 4444となる
- 2222のコミットが探索対象とならないは、それがバグじゃないから
- ③
- HEADが3333のコミットとなり、バグってるのでbadを実行
- この時点で4444はバグっていることは分かっているので、3333でバグが仕込まれたということが確定する
3ステップだけ確認すればバグコミットを特定することができました。
手動でやっても良いですが、git bisectを使うとコミットを切り替えるということが簡単に実現できます。
強力な自動探索
手動でgood/badとコマンド打っていき、バグっている箇所をみつけるのも良いですが、実は自動探索することもできます。
※プログラムに対して自動テストができる場合にのみ使えます
nodeでconsole.logを表示するコードにバグが仕込まれたとして説明します。
正しいコード
console.log("hello bisect 0000");
バグっているコード
console.log("hello bisect 3333 bug"; // 閉じ括弧を入れるの忘れている
最初の例と同様に10コミットあるうちの3333のコミットで上記バグコードをいれてしまったとします。
最初と同様以下のように探索する範囲を指定します。
git bisect start HEAD 0000
次にgit bisect runというコマンドを使ってコードを実行しながら、バグっている箇所をあぶり出します。
git bisect run node index.js
以下実行結果です。
% git bisect run node index.js
running node index.js
/path/to/project/index.js:1
console.log("hello bisect 4444";
^^^^^^^^^^^^^^^^^^^
SyntaxError: missing ) after argument list
at Object.compileFunction (node:vm:352:18)
at wrapSafe (node:internal/modules/cjs/loader:1031:15)
at Module._compile (node:internal/modules/cjs/loader:1065:27)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:17:47
Bisecting: 1 revision left to test after this (roughly 1 step)
[2222] 2222
running node index.js
hello bisect 2222
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[3333] 3333
running node index.js
/path/to/project/index.js:1
console.log("hello bisect 3333";
^^^^^^^^^^^^^^^^^^^
SyntaxError: missing ) after argument list
at Object.compileFunction (node:vm:352:18)
at wrapSafe (node:internal/modules/cjs/loader:1031:15)
at Module._compile (node:internal/modules/cjs/loader:1065:27)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:17:47
3333 is the first bad commit
commit 3333
Author: f-kawamura <f-kawamura@ceres-inc.jp>
Date: Mon Nov 21 11:50:21 2022 +0900
3333
index.html | 4 ++--
index.js | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
bisect run success
※ハッシュ値は、分かりやすくなるように数値に置き換えてます
3333 is the first bad commit
このように結果がでたので、3333のコミットがバグ仕込まれたコミットと分かりました。
実行したら分かりますが、3333のコミットをあぶり出すのは一瞬です。
すでに単体テストがあるようなプロジェクトであったり、テストコードをあとから追加できるような設計になっていたりしていれば、git bisectが活躍しやすくなります(すでにUTあるならマージされてないと思うけどね!)。
UIとかのテストもseleniumとかcypressとかをheadlessで動かしてやれば、いい感じにバグ特定できそうです。
テストが書きやすいコード重要ですね!
今回はバグコミットに焦点を当てて話しましたが、bisectは「いつ仕様が変わったのか分からないとき」などにも有効です。good/badコマンドじゃなくて、new/oldコマンドを使ったりします。
bisectは他にも便利オプションがいくつかあるので、試してみると面白いですよ。
usage: git bisect [help|start|bad|good|new|old|terms|skip|next|reset|visualize|view|replay|log|run]
bisectバグコミット発見俳句
犯
人
2 を
分
俺 探
だ 索
っ
た
あるある。