日本語 ▾ トピック ▾ 最新バージョン ▾ git-bisect-lk2009 は 2.40.0 で最終更新されました

要旨

"git bisect" を使用すると、ソフトウェアのユーザーや開発者は、リグレッションを導入したコミットを簡単に見つけることができます。本稿では、リグレッションに対処するための優れたツールを持つことの重要性を示します。"git bisect" が外部からどのように動作し、内部でどのようなアルゴリズムを使用しているかを説明します。次に、現在のプラクティスを改善するために"git bisect"をどのように活用できるかを説明します。そして、将来的に"git bisect"がどのように改善されるかについても議論します。

"git bisect"の紹介

Gitは、リーナス・トーバルズによって作成され、濱野 純によってメンテナンスされている分散バージョン管理システム (DVCS) です。

他の多くのバージョン管理システム (VCS) と同様に、Gitではシステムによって管理されるデータの異なる状態をコミットと呼びます。そして、VCSは主にソフトウェアのソースコードを管理するために使用されるため、ソフトウェアの「興味深い」動作変更が一部のコミットで導入されることがあります。

実際、人々はバグやリグレッションと呼ばれる「悪い」動作を導入するコミットに特に関心があります。これらのコミットに関心があるのは、コミットが(願わくば)ごく少量のソースコード変更しか含まないからです。そして、どこを見ればよいか最初からわからない場合よりも、ごく少量の変更セットを確認するだけで問題を理解し、適切に修正する方がはるかに簡単です。

そこで、人々が「悪い」動作を導入するコミットを見つけるのを助けるために、「git bisect」コマンド群が考案されました。当然のことながら、「git bisect」の用語では、「興味深い動作」が存在するコミットは「悪い」コミットと呼ばれ、その他のコミットは「良い」コミットと呼ばれます。そして、我々が関心を持つ動作を導入するコミットは「最初の悪いコミット」と呼ばれます。検索しているコミット空間には複数の「最初の悪いコミット」が存在する可能性があることに注意してください。

したがって、「git bisect」は「最初の悪いコミット」を見つけるのに役立つように設計されています。そして、可能な限り効率的にするために、二分探索を実行しようとします。

リグレッションとの戦いの概要

リグレッション:大きな問題

リグレッションはソフトウェア業界にとって大きな問題です。しかし、その主張を裏付ける具体的な数値を挙げるのは困難です。

バグ全般に関するいくつかの数値があり、例えば2002年のNISTの調査[1]では次のように述べられています。

米国商務省国立標準技術研究所 (NIST) が委託した新たに発表された調査によると、ソフトウェアのバグ、つまりエラーは非常に広く普及しており、有害であるため、米国経済に年間推定595億ドル、国内総生産の約0.6%のコストをかけています。国レベルでは、コストの半分以上がソフトウェアユーザーによって負担され、残りがソフトウェア開発者/ベンダーによって負担されています。この調査ではまた、すべてのエラーを排除することはできないものの、これらのコストの3分の1以上、推定222億ドルは、ソフトウェアの欠陥をより早く、より効果的に特定し、除去することを可能にするテストインフラの改善によって排除できることが判明しました。これらは、エラーが導入された開発段階に近い場所で、より高い割合 (ただし100%ではない) のエラーを発見することに関連する節約です。現在、すべてのエラーの半分以上は、開発プロセスの「下流」または販売後のソフトウェア使用中にしか発見されていません。

そして、

ソフトウェア開発者は、すでに開発コストの約80パーセントを欠陥の特定と修正に費やしていますが、ソフトウェア以外の製品でこれほど高いレベルのエラーを抱えたまま出荷されるものはほとんどありません。

最終的に、結論は次のように始まりました。

より高いソフトウェア品質への道は、ソフトウェアテストの大幅な改善です。

ソフトウェア関連コストの80%がメンテナンスに関するものであるという他の見積もりもあります[2]

しかし、Wikipediaによると[3]

メンテナンスは単にバグを修正することであるという一般的な認識があります。しかし、長年にわたる研究や調査では、メンテナンス作業の大部分、80%以上が非修正的な活動(Pigosky 1997)に費やされていることが示されています。この認識は、実際にはシステムの機能強化である問題報告をユーザーが提出することによって永続化しています。

しかし、既存のソフトウェアを改善することは、リグレッションに注意しなければならないため、非常にコストがかかると推測できます。少なくとも、これにより上記の研究は互いに整合性が取れるでしょう。

もちろん、ある種のソフトウェアは開発され、ほとんど改善されることなく一定期間使用され、最終的に破棄されます。この場合、もちろんリグレッションは大きな問題にならないかもしれません。しかし一方で、多くの人々によって何年、あるいは何十年にもわたって継続的に開発・保守されている大規模なソフトウェアも多数存在します。そして、このようなソフトウェアに(時には致命的に)依存している人々が多いため、リグレッションは本当に大きな問題です。

そのようなソフトウェアの1つがLinuxカーネルです。Linuxカーネルを見ると、リグレッションとの戦いに多くの時間と労力が費やされていることがわかります。リリースサイクルは2週間のマージウィンドウから始まります。次に、最初のリリース候補 (rc) バージョンがタグ付けされます。そしてその後、最終リリースまでに、約1週間ごとにさらに7〜8個のrcバージョンが登場します。

最初のrcリリースと最終リリースまでの期間は、rcバージョンのテストや、バグ、特にリグレッションとの戦いに使われることになっています。そしてこの期間はリリースサイクル全体の80%以上を占めます。しかし、もちろんリリース後も戦いは続くため、これはまだ戦いの終わりではありません。

そして、インゴ・モルナー(有名なLinuxカーネル開発者)は、git bisectの使用について次のように述べています。

私はマージウィンドウ中(多くのツリーが上流にマージされ、バグの流入が最も多い時期)に最も積極的に使用しています。そして、ええ、1日に何度も使用したケースもあります。私の平均はだいたい1日に1回です。

したがって、開発者は常にリグレッションと戦っており、バグは発見され次第、できるだけ早く修正されるべきであることはよく知られています。だからこそ、この目的のために優れたツールを持つことが興味深いのです。

リグレッションと戦うための他のツール

では、リグレッションと戦うためにどのようなツールが使われているのでしょうか? それらは、通常のバグと戦うために使われるツールとほとんど同じです。唯一具体的なツールは、テストスイートと「git bisect」のようなツールです。

テストスイートは非常に優れています。しかし、単独で使用する場合、すべてのテストは各コミット後にチェックされることになっています。これは、多くのテストが興味深い結果なしに実行され、組み合わせ爆発に苦しむため、あまり効率的ではないことを意味します。

実際の問題は、大規模なソフトウェアはしばしば多くの異なる設定オプションを持ち、各テストケースは各コミット後、各設定でパスしなければならないということです。したがって、各リリースでN個の設定、M個のコミット、T個のテストケースがある場合、次を実行する必要があります。

N * M * T tests

ここで、N、M、T はすべてソフトウェアのサイズとともに増加します。

したがって、すぐにすべてを完全にテストすることは不可能になるでしょう。

そして、もし一部のバグがテストスイートをすり抜けた場合、テストスイートにテストを追加することができます。しかし、新しい改良されたテストスイートを使用してバグがどこで混入したかを見つけたい場合、バイセクションプロセスをエミュレートするか、あるいは手持ちの「悪い」コミットから遡って各コミットを率直にテストすることになり、それは非常に無駄かもしれません。

"git bisect"の概要

バイセクションの開始

最初に使う「git bisect」サブコマンドは、「git bisect start」で検索を開始することです。次に、コミット空間を制限するために境界を設定する必要があります。これは通常、1つの「悪い」コミットと少なくとも1つの「良い」コミットを指定して行います。これらは、「git bisect start」の最初の呼び出しで次のように渡すことができます。

$ git bisect start [BAD [GOOD...]]

または、以下を使用して設定できます。

$ git bisect bad [COMMIT]

$ git bisect good [COMMIT...]

ここでBAD、GOOD、COMMITはすべてコミットに解決できる名前です。

次に、「git bisect」は自身が選んだコミットをチェックアウトし、ユーザーにテストを求めます。

$ git bisect start v2.6.27 v2.6.25
Bisecting: 10928 revisions left to test after this (roughly 14 steps)
[2ec65f8b89ea003c27ff7723525a2ee335a2b393] x86: clean up using max_low_pfn on 32-bit

使用する例は実際にはおもちゃのようなもので、バージョンが「2.6.26-something」のような最初のコミット、つまりトップレベルのMakefileに「SUBLEVEL = 26」という行があるコミットを探します。これは、「git bisect」を使うよりもGitでこのコミットを見つける良い方法(例えば、「git blame」や「git log -S<string>」)があるため、おもちゃのような例です。

バイセクションの手動での実行

この時点で、検索を実行する方法は基本的に2つあります。ユーザーが手動で実行するか、スクリプトまたはコマンドによって自動的に実行するかです。

ユーザーが実行する場合、検索の各ステップで、ユーザーは現在のコミットをテストし、上記で説明した「git bisect good」または「git bisect bad」コマンドを使用して、それが「良好」か「不良」かを報告する必要があります。例えば、

$ git bisect bad
Bisecting: 5480 revisions left to test after this (roughly 13 steps)
[66c0b394f08fd89236515c1c84485ea712a157be] KVM: kill file->f_count abuse in kvm

そして、そのような手順をいくつか繰り返した後、「git bisect」は最終的に最初の悪いコミットを見つけます。

$ git bisect bad
2ddcca36c8bcfa251724fe342c8327451988be0d is the first bad commit
commit 2ddcca36c8bcfa251724fe342c8327451988be0d
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date:   Sat May 3 11:59:44 2008 -0700

    Linux 2.6.26-rc1

:100644 100644 5cf82581... 4492984e... M      Makefile

この時点で、コミットが何をするのかを確認し、チェックアウト(まだチェックアウトされていない場合)したり、いじってみたりすることができます。例えば、

$ git show HEAD
commit 2ddcca36c8bcfa251724fe342c8327451988be0d
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date:   Sat May 3 11:59:44 2008 -0700

    Linux 2.6.26-rc1

diff --git a/Makefile b/Makefile
index 5cf8258..4492984 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 VERSION = 2
 PATCHLEVEL = 6
-SUBLEVEL = 25
-EXTRAVERSION =
+SUBLEVEL = 26
+EXTRAVERSION = -rc1
 NAME = Funky Weasel is Jiggy wit it

 # *DOCUMENTATION*

そして、作業が終わったら、「git bisect reset」を使用して、バイセクトを開始する前にいたブランチに戻ることができます。

$ git bisect reset
Checking out files: 100% (21549/21549), done.
Previous HEAD position was 2ddcca3... Linux 2.6.26-rc1
Switched to branch 'master'

バイセクションの自動での実行

バイセクションプロセスを実行するもう1つの方法は、「git bisect」に各バイセクションステップでスクリプトまたはコマンドを起動させ、現在のコミットが「良好」か「不良」かを知ることです。そのためには、「git bisect run」コマンドを使用します。例えば、

$ git bisect start v2.6.27 v2.6.25
Bisecting: 10928 revisions left to test after this (roughly 14 steps)
[2ec65f8b89ea003c27ff7723525a2ee335a2b393] x86: clean up using max_low_pfn on 32-bit
$
$ git bisect run grep '^SUBLEVEL = 25' Makefile
running grep ^SUBLEVEL = 25 Makefile
Bisecting: 5480 revisions left to test after this (roughly 13 steps)
[66c0b394f08fd89236515c1c84485ea712a157be] KVM: kill file->f_count abuse in kvm
running grep ^SUBLEVEL = 25 Makefile
SUBLEVEL = 25
Bisecting: 2740 revisions left to test after this (roughly 12 steps)
[671294719628f1671faefd4882764886f8ad08cb] V4L/DVB(7879): Adding cx18 Support for mxl5005s
...
...
running grep ^SUBLEVEL = 25 Makefile
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[2ddcca36c8bcfa251724fe342c8327451988be0d] Linux 2.6.26-rc1
running grep ^SUBLEVEL = 25 Makefile
2ddcca36c8bcfa251724fe342c8327451988be0d is the first bad commit
commit 2ddcca36c8bcfa251724fe342c8327451988be0d
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date:   Sat May 3 11:59:44 2008 -0700

    Linux 2.6.26-rc1

:100644 100644 5cf82581... 4492984e... M      Makefile
bisect run success

この例では、「git bisect run」のパラメータとして「grep *^SUBLEVEL = 25* Makefile」を渡しました。これは、各ステップで渡されたgrepコマンドが起動されることを意味します。そして、終了コード0(成功を意味します)で終了した場合、git bisectは現在の状態を「good」とマークします。終了コード1(または1から127までのコードで、特別なコード125を除く)で終了した場合、現在の状態は「bad」とマークされます。

終了コード128から255は「git bisect run」にとって特別です。これらはバイセクションプロセスを即座に停止させます。これは、例えば渡されたコマンドの完了に時間がかかりすぎる場合に便利です。シグナルでそれを強制終了することができ、バイセクションプロセスが停止するためです。

「git bisect run」に渡されるスクリプトで、非常に異常な状況が検出された場合に「exit 255」を実行することも有用です。

テスト不能なコミットの回避

現在の状態がテストできないことがあります。例えば、その時点でそれを妨げるバグがあったためにコンパイルできない場合などです。これは、特別な終了コード125の目的です。これは、「git bisect run」に、現在のコミットをテスト不能としてマークし、別のコミットを選択してチェックアウトするよう指示します。

バイセクションプロセスが手動で実行される場合、「git bisect skip」を使用して同じことを行うことができます。(実際、特別な終了コード125は「git bisect run」にバックグラウンドで「git bisect skip」を使用させます。)

あるいは、より詳細な制御が必要な場合は、例えば「git bisect visualize」を使用して現在の状態を検査することができます。これはgitk(または`DISPLAY`環境変数が設定されていない場合は「git log」)を起動し、より良いバイセクションポイントを見つけるのに役立ちます。

いずれにしても、テスト不能なコミットが連続している場合、探しているリグレッションがこれらのテスト不能なコミットのいずれかによって導入された可能性があります。この場合、どのコミットがリグレッションを導入したかを確実に判断することはできません。

したがって、「git bisect skip」を使用した場合(または実行スクリプトが特殊コード125で終了した場合)、次のような結果が得られる可能性があります。

There are only 'skip'ped commits left to test.
The first bad commit could be any of:
15722f2fa328eaba97022898a305ffc8172db6b1
78e86cf3e850bd755bb71831f42e200626fbd1e0
e15b73ad3db9b48d7d1ade32f8cd23a751fe0ace
070eab2303024706f2924822bfec8b9847e4ac1b
We cannot bisect more!

ログの保存と再生

バイセクションプロセスを他の人に見せたい場合、例えば以下を使用してログを取得できます。

$ git bisect log > bisect_log.txt

そして、以下を使用して再生することも可能です。

$ git bisect replay bisect_log.txt

"git bisect"の詳細

バイセクションアルゴリズム

Gitのコミットは有向非巡回グラフ (DAG) を形成するため、各ステップでテストする最適なバイセクションコミットを見つけるのはそれほど単純ではありません。しかし、リーナスは「本当に愚かな」アルゴリズムを発見し実装し、後に濱野 純によって改善され、かなりうまく機能しています。

したがって、スキップされたコミットがない場合に「git bisect」が最適なバイセクションコミットを見つけるために使用するアルゴリズムは次のとおりです。

1) 次のコミットのみを保持する

a) 「悪い」コミットの祖先である(「悪い」コミット自体も含む)、b) 「良い」コミットの祖先ではない(「良い」コミットは除く)。

これは、DAG内の興味のないコミットを排除することを意味します。

例えば、次のようなグラフから開始する場合、

G-Y-G-W-W-W-X-X-X-X
	   \ /
	    W-W-B
	   /
Y---G-W---W
 \ /   \
Y-Y     X-X-X-X

-> time goes this way ->

ここでBが「悪い」コミット、Gが「良い」コミット、W、X、Yが他のコミットである場合、この最初のステップの後、次のグラフが得られます。

W-W-W
     \
      W-W-B
     /
W---W

したがって、WとBのコミットのみが保持されます。コミットXとYはそれぞれ規則a)とb)によって削除され、コミットGも規則b)によって削除されるためです。

Gitユーザー向けに注意すると、これは以下のコマンドによって与えられるコミットのみを保持することと等価です。

git rev-list BAD --not GOOD1 GOOD2...

また、保持されるコミットが「良好な」コミットの子孫である必要はないことにも注意してください。したがって、次の例では、WとZのコミットが保持されます。

G-W-W-W-B
   /
Z-Z

2) グラフの「良好な」終端から開始し、各コミットにその祖先の数に1を加えた数を関連付ける

例えば、Hが「悪い」コミットであり、AとDが一部の「良い」コミットの親である次のグラフの場合、

A-B-C
     \
      F-G-H
     /
D---E

これにより、

1 2 3
A-B-C
     \6 7 8
      F-G-H
1   2/
D---E

3) 各コミットにmin(X, N - X)を関連付ける

ここで、Xはステップ2)でコミットに関連付けられた値であり、Nはグラフ内のコミットの総数です。

上記の例ではN = 8なので、これにより次のようになります。

1 2 3
A-B-C
     \2 1 0
      F-G-H
1   2/
D---E

4) 最適なバイセクションポイントは、関連付けられた数値が最も高いコミットです。

したがって、上記の例では最適なバイセクションポイントはコミットCです。

5) アルゴリズムを高速化するためにいくつかのショートカットが実装されていることに注意してください。

Nは最初からわかっているため、min(X, N - X)がN/2より大きくなることはないことがわかります。したがって、ステップ2)と3)の間に、あるコミットにN/2を関連付けることができた場合、それが最適なバイセクションポイントであることがわかります。この場合、他のコミットの処理を停止し、現在のコミットを返すことができます。

バイセクションアルゴリズムのデバッグ

任意のコミットグラフについて、「git rev-list --bisect-all」を使用して各コミットに関連付けられた番号を確認できます。

例えば、上記のグラフの場合、次のようなコマンドは

$ git rev-list --bisect-all BAD --not GOOD1 GOOD2

次のような出力を生成します。

e15b73ad3db9b48d7d1ade32f8cd23a751fe0ace (dist=3)
15722f2fa328eaba97022898a305ffc8172db6b1 (dist=2)
78e86cf3e850bd755bb71831f42e200626fbd1e0 (dist=2)
a1939d9a142de972094af4dde9a544e577ddef0e (dist=2)
070eab2303024706f2924822bfec8b9847e4ac1b (dist=1)
a3864d4f32a3bf5ed177ddef598490a08760b70d (dist=1)
a41baa717dd74f1180abf55e9341bc7a0bb9d556 (dist=1)
9e622a6dad403b71c40979743bb9d5be17b16bd6 (dist=0)

二分法アルゴリズムについて

バイセクションアルゴリズムの考察

まず、「最適なバイセクションポイント」を定義しましょう。コミットXが最適なバイセクションポイントまたは最適なバイセクションコミットであるとは、その状態(「良好」または「不良」)を知ることで、コミットの状態が「良好」であるか「不良」であるかにかかわらず、可能な限り多くの情報が得られる場合であるとします。

f(X) = min(information_if_good(X), information_if_bad(X))

これは、最適なバイセクションコミットは、以下の関数が最大となるコミットであることを意味します。

ここで、information_if_good(X)はXが良好な場合に得られる情報であり、information_if_bad(X)はXが不良な場合に得られる情報です。

ここで、「最初の悪いコミット」は1つだけであると仮定します。これは、そのすべての子孫は「悪い」コミットであり、他のすべてのコミットは「良い」コミットであることを意味します。そして、すべてのコミットが「良い」または「悪い」状態であるか、または最初の悪いコミットである確率が等しいと仮定します。したがって、c個のコミットの状態を知ることは、これらのc個のコミットがグラフ上のどこにあっても、cが何であっても、常に同じ量の情報を提供します。(したがって、これらのコミットが例えばブランチ上にあるか、良いコミットまたは悪いコミットの近くにあるかどうかが、情報の多寡に影響を与えないと仮定します。)

また、上記のバイセクションアルゴリズムのステップ1)の後にあるような、整理されたグラフがあると仮定しましょう。これは、グラフから削除できるコミットの数で得られる情報を測定できることを意味します。

そして、グラフ内のコミットXを考えてみましょう。

information_if_good(X) = number_of_ancestors(X)  (TRUE)

Xが「良好」と判明した場合、その祖先はすべて「良好」であることがわかるので、次のように言いたいと思います。

これは、ステップ1) b) で「良好な」コミットの祖先を削除するため、真です。

information_if_bad(X) = number_of_descendants(X)  (WRONG)

Xが「不良」と判明した場合、その子孫はすべて「不良」であることがわかるので、次のように言いたいと思います。

しかし、これは誤りです。なぜなら、ステップ1) a) で、悪いコミットの祖先のみを保持するからです。したがって、コミットが「悪い」とマークされた場合、より多くの情報が得られます。なぜなら、以前の「悪い」コミットの祖先であって、新しい「悪い」コミットの祖先ではないコミットは、最初の悪いコミットではないこともわかるからです。それらが良いか悪いかはわかりませんが、新しい「悪い」コミットの祖先ではないため、最初の悪いコミットではないことはわかります。

information_if_bad(X) = N - number_of_ancestors(X)  (TRUE)

したがって、あるコミットが「不良」とマークされた場合、新しい「不良」コミットの祖先であるものを除いて、グラフ内のすべてのコミットを削除できることがわかります。これは、次のことを意味します。

ここで、Nは(整理された)グラフ内のコミットの数です。

f(X) = min(number_of_ancestors(X), N - number_of_ancestors(X))

したがって、最終的にこれは、最適なバイセクションコミットを見つけるには、関数を最大化する必要があることを意味します。

そしてこれは素晴らしいことです。なぜなら、ステップ2)でnumber_of_ancestors(X)を計算し、ステップ3)でf(X)を計算するからです。

            G-H-I-J
           /       \
A-B-C-D-E-F         O
           \       /
            K-L-M-N

次のグラフを例にとってみましょう。

g(X) = min(number_of_ancestors(X), number_of_descendants(X))

これに対して以下の非最適な関数を計算すると、

            4 3 2 1
            G-H-I-J
1 2 3 4 5 6/       \0
A-B-C-D-E-F         O
           \       /
            K-L-M-N
            4 3 2 1

次のような結果が得られます。

            7 7 6 5
            G-H-I-J
1 2 3 4 5 6/       \0
A-B-C-D-E-F         O
           \       /
            K-L-M-N
            7 7 6 5

しかし、git bisectが使用するアルゴリズムでは、次の結果が得られます。

したがって、最適なバイセクションポイントとしてG、H、K、またはLを選択しました。これはFよりも優れています。なぜなら、例えばLが悪い場合、L、M、Nが悪いだけでなく、G、H、I、Jが最初の悪いコミットではないこともわかるからです(最初の悪いコミットは1つだけであり、Lの祖先であると仮定しているため)。

したがって、現在のアルゴリズムは、我々が最初に仮定したことを考えると、可能な限り最良であるように思われます。

スキップアルゴリズム

いくつかのコミットがスキップされた場合(「git bisect skip」を使用して)、バイセクションアルゴリズムはステップ1)から3)までは同じです。しかし、その後、おおよそ次のステップを使用します。

6) 関連付けられた値の降順でコミットをソートする

7) 最初のコミットがスキップされていない場合、それを返してここで停止できる

8) それ以外の場合、ソートされたリストからスキップされたすべてのコミットをフィルタリングする

9) 擬似乱数ジェネレーター (PRNG) を使用して、0から1の間の乱数を生成する

10) この乱数にその平方根を掛けて、0に偏らせる

11) 結果をフィルタリングされたリスト内のコミット数で掛けて、このリストへのインデックスを取得する

12) 計算されたインデックスのコミットを返す

スキップアルゴリズムの考察

ステップ7)(スキップアルゴリズム内)の後、2番目のコミットがスキップされているかどうかを確認し、そうでない場合はそれを返すことができます。そして実際、それはGitバージョン1.5.4(2008年2月1日リリース)で「git bisect skip」が開発されてから、Gitバージョン1.6.4(2009年7月29日リリース)まで使用されていたアルゴリズムでした。

しかし、インゴ・モルナーとH.ピーター・アンビン(もう一人の有名なLinuxカーネル開発者)は共に、最適なバイセクションポイントがすべてテスト不能なコミットのある領域に集中してしまうことがあると不満を述べました。そしてこの場合、ユーザーは多くのテスト不能なコミットをテストするように求められ、非常に非効率的になる可能性がありました。

実際、テスト不能なコミットは、ある時点で破損が導入され、その破損が他の多くのコミットが導入された後にようやく修正されたためにテスト不能となることがよくあります。

この破損は、もちろんほとんどの場合、コミットグラフ内で特定しようとしている破損とは無関係です。しかし、それが興味深い「悪い動作」が存在するかどうかを知ることを妨げます。

したがって、テスト不能なコミットの近くにあるコミットは、それ自体がテスト不能である可能性が高いのは事実です。そして、最適なバイセクションコミットも、しばしばまとめて見つかることがあります(バイセクションアルゴリズムのため)。

これが、最初のバイセクションコミットがスキップされたときに、単に次に最適なスキップされていないバイセクションコミットを選択するのが悪い考えである理由です。

グラフ上のほとんどのコミットは、テストされたときにかなりの量の情報を提供する可能性があることがわかりました。そして、平均して多くの情報を提供しないコミットは、良いコミットと悪いコミットの近くにあるものです。

したがって、良いコミットと悪いコミットから離れたコミットを優先するように偏りを持たせたPRNGを使用することは、良い選択のように思われました。

このアルゴリズムの明らかな改善点は、PRNGを使用する前に、最適なバイセクションコミットのそれに近い関連値を持つ、別のブランチにあるコミットを探すことです。なぜなら、そのようなコミットが存在する場合、それもテスト不能である可能性は低く、したがって、ほぼランダムに選択されたものよりも多くの情報を提供する可能性が高いからです。

マージベースの確認

上記の「バイセクションアルゴリズム」では説明されていない、バイセクションアルゴリズムの別の調整点があります。

以前の例では、「良い」コミットが「悪い」コミットの祖先であると仮定しました。しかし、これは「git bisect」の要件ではありません。

もちろん、「悪い」コミットが「良い」コミットの祖先であることはできません。なぜなら、「良い」コミットの祖先は「良い」とされているからです。そして、すべての「良い」コミットは「悪い」コミットに関連していなければなりません。「悪い」コミットのブランチとリンクのないブランチにあることはできません。しかし、良いコミットが悪いコミットに関連しており、かつその祖先でも子孫でもないことは可能です。

A-B-C-D-E-F-G  <--main
       \
        H-I-J  <--dev

例えば、「main」ブランチと、コミット「D」でmainブランチからフォークされた「dev」ブランチが次のように存在する場合が考えられます。

コミット「D」は、ブランチ「main」と「dev」のマージにおける最適な共通祖先であるため、「マージベース」と呼ばれます。

ここで、コミットJが不良でコミットGが良好であると仮定し、これまで説明してきたようにバイセクションアルゴリズムを適用してみましょう。

バイセクションアルゴリズムのステップ1) b) で説明されているように、良いコミットの祖先も良いはずであるため、それらの祖先はすべて削除します。

H-I-J

したがって、残るのはこれだけになります。

しかし、最初の悪いコミットが「B」であり、それが「main」ブランチでコミット「F」によって修正された場合はどうなるでしょうか?

そのようなバイセクションの結果は、実際にはBであるにもかかわらず、Hが最初の悪いコミットであると判明することになります。それは間違っています!

そして、実際には、あるブランチで作業している人々が、別のブランチで作業している人々がバグを修正したことを認識していないということが起こり得ます!また、Fが複数のバグを修正したり、リリース準備ができていなかった大規模な開発作業のリバートである可能性もあります。

$ git bisect start dev main

実際、開発チームは開発ブランチとメンテナンスブランチの両方を維持していることが多く、メンテナンスブランチにない開発ブランチでのリグレッションをバイセクトしたい場合に「git bisect」が機能するだけで、彼らにとっては非常に簡単になるでしょう。彼らは以下を使用してバイセクトを開始できるはずです。

この追加の便利な機能を有効にするために、バイセクションが開始され、一部の良好なコミットが不良なコミットの祖先ではない場合、まず不良なコミットと良好なコミット間のマージベースを計算し、これらのマージベースを最初にチェックアウトしてテストするコミットとして選択します。

The merge base BBBBBB is bad.
This means the bug has been fixed between BBBBBB and [GGGGGG,...].

あるマージベースが不良と判明した場合、バイセクションプロセスは次のようなメッセージと共に停止します。

ここでBBBBBBは不良マージベースのSHA-1ハッシュであり、[GGGGGG,…​]は良好なコミットのSHA-1ハッシュをコンマで区切ったリストです。

Warning: the merge base between BBBBBB and [GGGGGG,...] must be skipped.
So we cannot be sure the first bad commit is between MMMMMM and BBBBBB.
We continue anyway.

マージベースの一部がスキップされた場合、バイセクションプロセスは継続されますが、スキップされた各マージベースについて以下のメッセージが表示されます。

ここでBBBBBBは不良コミットのSHA-1ハッシュ、MMMMMMはスキップされたマージベースのSHA-1ハッシュ、[GGGGGG,…​]は良好なコミットのSHA-1ハッシュをコンマで区切ったリストです。

したがって、不良なマージベースがない場合、このステップの後もバイセクションプロセスは通常通り続行されます。

バイセクトのベストプラクティス

テストスイートとgit bisectの併用

テストスイートとgit bisectの両方を使用している場合、各コミット後にすべてのテストが合格することを確認することの重要性は低くなります。もちろん、他のバグのバイセクトがより困難になる可能性があるため、多くのものを壊さないようにいくつかのチェックを行うことはおそらく良い考えです。

c * N * T + b * M * log2(M) tests

いくつかのポイント(例えばrcおよびベータリリース)で、すべてのN個の設定に対してすべてのT個のテストケースが合格することに集中することができます。そして、一部のテストが合格しない場合は、「git bisect」(またはより良い「git bisect run」)を使用できます。したがって、おおよそ次のことを実行する必要があります。

ここで、cはテストのラウンド数(小さな定数)、bはコミットあたりのバグの比率(これも小さな定数であることを願います)です。

したがって、各コミット後にすべてをテストするとO(N * T * M)になるのに対し、O(N * T)であるため、もちろんはるかに優れています。

これは、テストスイートが一部のバグがコミットされるのを防ぐのに役立ち、またバグがあることを知らせるのにも非常に優れていることを意味します。しかし、バグがどこで導入されたかを効率的に伝えるのにはあまり優れていません。それを効率的に伝えるには、git bisectが必要です。

テストスイートのもう一つの良い点は、テストスイートがあれば、悪い動作をテストする方法をすでに知っていることです。したがって、リグレッションが発生した際に、この知識を使って「git bisect」用の新しいテストケースを作成できます。これにより、バグをバイセクトして修正するのが容易になります。そして、作成したばかりのテストケースをテストスイートに追加できます。

したがって、テストケースの作成方法とバイセクト方法を知っていれば、好循環のサイクルに入ることができます。

より多くのテスト ⇒ テストの作成が容易に ⇒ バイセクトが容易に ⇒ より多くのテスト

したがって、テストスイートと「git bisect」は、一緒に使用すると非常に強力で効率的な補完的なツールです。

ビルド失敗のバイセクト

$ git bisect start BAD GOOD
$ git bisect run make

次のようなものを使用して、壊れたビルドを非常に簡単に自動的にバイセクトできます。

sh -c "some commands"を"git bisect run"に渡す

$ git bisect run sh -c "make || exit 125; ./my_app | grep 'good output'"

例えば、

一方で、これを頻繁に行うのであれば、タイプ量を減らすためにスクリプトを用意する価値があります。

パフォーマンスリグレッションの発見

ここに、濱野 純[4]が使用した実際のスクリプトから少し修正した例のスクリプトがあります。

#!/bin/sh

# Build errors are not what I am interested in.
make my_app || exit 255

# We are checking if it stops in a reasonable amount of time, so
# let it run in the background...

./my_app >log 2>&1 &

# ... and grab its process ID.
pid=$!

# ... and then wait for sufficiently long.
sleep $NORMAL_TIME

# ... and then see if the process is still there.
if kill -0 $pid
then
	# It is still running -- that is bad.
	kill $pid; sleep 1; kill $pid;
	exit 1
else
	# It has already finished (the $pid process was no more),
	# and we are happy.
	exit 0
fi

このスクリプトは「git bisect run」に渡すことができ、パフォーマンスリグレッションを導入したコミットを見つけるために使用されます。

一般的なベストプラクティスに従う

故意に何かを壊す変更を含むコミットを持たないことは、明らかに良いアイデアです。たとえ後で他のコミットがその破損を修正したとしてもです。

また、どのVCSを使用する場合でも、各コミットには1つの小さな論理的な変更のみを含めるのが良いアイデアです。

コミットの変更が小さければ小さいほど、「git bisect」はより効果的になります。そして、小さな変更はコミッターだけがレビューする場合でもレビューしやすいため、そもそも「git bisect」の必要性は減るでしょう。

もう一つの良いアイデアは、適切なコミットメッセージを持つことです。これにより、なぜ変更が行われたのかを理解するのに非常に役立ちます。

これらの一般的なベストプラクティスは、頻繁にバイセクトを行う場合に非常に役立ちます。

バグの原因となりやすいマージの回避

まず、マージ自体は、ソースコードの競合解決が不要な場合でも、一部のリグレッションを導入する可能性があります。これは、一方のブランチで意味的な変更が発生し、もう一方のブランチがそれを認識していないために起こります。

例えば、一方のブランチが関数の意味を変更し、もう一方のブランチが同じ関数への呼び出しをさらに追加する、といった場合です。

多くのファイルを修正して競合を解決しなければならない場合、これははるかに悪化します。そのため、そのようなマージは「邪悪なマージ(evil merges)」と呼ばれます。これらはリグレッションの追跡を非常に困難にする可能性があります。もし最初の悪いコミットがそのようなマージであった場合、バグが一方のブランチでの意味的変更から来ているにもかかわらず、人々は悪い競合解決から来ていると考えるかもしれないため、その事実を知ることが誤解を招くことさえあります。

いずれにしても、「git rebase」は履歴を線形化するために使用できます。これは、そもそもマージを避けるために使用できます。または、非線形履歴の代わりに線形履歴でバイセクトするために使用できます。これは、一方のブランチでの意味的変更の場合により多くの情報を提供するはずだからです。

マージは、より小さなブランチを使用するか、長いバージョン関連ブランチのみを使用するのではなく、多くのトピックブランチを使用することで簡素化することもできます。

そして、Linuxカーネルのlinux-nextのような特別な統合ブランチで、より頻繁にテストを行うことができます。

ワークフローの適応

リグレッションを処理するための特別なワークフローは、素晴らしい結果をもたらすことができます。

  • 以下は、アンドレアス・エリクソンが使用したワークフローの例です。

  • テストスイート内に、リグレッションを明らかにするテストスクリプトを記述する

  • 「git bisect run」を使用して、それを導入したコミットを見つける

  • 前のステップで明らかになることが多いバグを修正する

修正とテストスクリプトの両方をコミットする(必要に応じてさらにテストを追加する)

そして、アンドレアスはこのワークフローについて次のように述べています[5]

具体的な数字を挙げると、以前は(壁時計時間だけを計測するやや奇妙なバグトラッカーによると)平均してレポートから修正までのサイクルが142.6時間でした。Gitに移行してからは、それを16.2時間に短縮しました。主な理由は、今ではバグ修正に常に対応できるようになり、誰もがバグ修正に競い合っているからです(Gitにバグを見つけさせるという、いかに怠惰であるかを誇りに思っています)。新しいリリースごとにバグが約40%減少しています(これは、現在テストを書くことについてどう考えているかによるものとほぼ確実です)。

明らかに、このワークフローはテストスイートと「git bisect」間の好循環を利用しています。実際、これがリグレッションに対処するための標準手順となっています。

他のメッセージでアンドレアスは、上記の「ベストプラクティス」、すなわち小さな論理的なコミット、トピックブランチ、邪悪なマージの回避などを彼らが使用していると述べています。これらのプラクティスはすべて、バイセクトをより簡単かつ有用にすることで、コミットグラフのバイセクト可能性を向上させます。

したがって、良いワークフローは上記の点、つまりバイセクトをより簡単に、より有用に、そして標準的にするように設計されるべきです。

QA担当者および可能であればエンドユーザーの巻き込み

「git bisect」の素晴らしい点の1つは、開発者ツールであるだけでなく、QA担当者やエンドユーザーでさえも効果的に使用できることです(ソースコードにアクセスできる場合、またはすべてのビルドにアクセスできる場合)。

ある時点で、Linuxカーネルのメーリングリストで、常にエンドユーザーにバイセクトを依頼するのが適切かどうかについて議論があり、それが適切であるという見解を支持する非常に良い点が挙げられました。

例えば、デビッド・ミラーは次のように書いています[6]

人々が理解していないのは、これは「エンドノード原則」が適用される状況だということです。リソース(ここでは開発者)が限られている場合、彼らに負担の大部分を押し付けるべきではありません。そうではなく、多くのリソース、つまりエンドノード(ここではユーザー)に物事を押し出すことで、状況が実際にスケールするのです。

これは、QA担当者やエンドユーザーがそれを行うことができれば、多くの場合「安価」であることを意味します。

興味深いのは、バグを報告しているエンドユーザー(またはバグを再現したQA担当者)が、バグが発生した環境にアクセスできることです。そのため、彼らはリグレッションをより簡単に再現できることがよくあります。そして、彼らがバイセクトできれば、バグが発生した環境からより多くの情報が抽出されることになり、それはバグを理解し、修正するのをより容易にするということです。

オープンソースプロジェクトにとって、エンドユーザーからより有用な貢献を得たり、QAおよび開発活動に彼らを紹介したりする良い方法となり得ます。

複雑なスクリプトの使用

カーネル開発のような一部のケースでは、バイセクトを完全に自動化できるように複雑なスクリプトを開発する価値があります。

これについてインゴ・モルナーは次のように述べています[7]

私は完全に自動化された起動ハングバイセクションスクリプトを持っています。「git-bisect run」に基づいています。スクリプトを実行すると、カーネルが完全に自動でビルドされ、起動します。そして起動が失敗すると(スクリプトはシリアルログを継続的に監視することでそれを検知します。または、システムが10分以内に起動しない場合はタイムアウトによって「悪い」カーネルと判断します)、スクリプトはビープ音で私の注意を引き、私はテストボックスの電源を入れ直します。(ええ、100%自動化するためには、管理された電源コンセントを利用するべきですね)

テストスイート、git bisect、および他のシステムの併用

テストスイートとgit bisectを併用すると非常に強力であることがわかりました。他のシステムと組み合わせることができれば、さらに強力になります。

例えば、いくつかのテストスイートは、通常とは異なる(あるいはランダムな)設定で夜間に自動実行されることがあります。そして、テストスイートによってリグレッションが発見された場合、「git bisect」が自動的に起動され、その結果は「git bisect」によって発見された最初の悪いコミットの作者、そしておそらく他の人々にもメールで送信されます。また、バグ追跡システムに新しいエントリが自動的に作成されることもあります。

バイセクトの未来

"git replace"

先に述べたように、「git bisect skip」は現在、コミットがテスト不能なコミットグラフの領域を避けるためにPRNGを使用しています。問題は、最初の悪いコミットがテスト不能な領域に存在する場合があることです。

sh -c "some commands"を"git bisect run"に渡す

...-Y-BBC-X1-X2-X3-X4-X5-X6-BFC-Z-...

議論を単純化するために、テスト不能な領域は単純なコミットの連鎖であり、あるコミット(バイセクトを壊すコミットとしてBBCと呼びます)によって導入された破損によって作成され、後で別のコミット(バイセクトを修正するコミットとしてBFCと呼びます)によって修正されたと仮定します。

ここでYは良好でBFCは不良であり、BBCとX1からX6はテスト不能であることがわかっています。

sh -c "some commands"を"git bisect run"に渡す

      (BBC+BFC)-X1'-X2'-X3'-X4'-X5'-X6'-Z'
     /
...-Y-BBC-X1-X2-X3-X4-X5-X6-BFC-Z-...

この場合、手動でバイセクトを行っている場合、BBCの直前から始まる特別なブランチを作成することができます。このブランチの最初のコミットは、BFCがその中にsquashされたBBCであるべきです。そして、そのブランチの他のコミットは、BBCとBFCの間にあるコミットをブランチの最初のコミットにリベースし、その後BFCの後のコミットもリベースしたものであるべきです。

ここで「'」で引用されたコミットはリベースされています。

このようなブランチは、Gitでインタラクティブリベースを使用して簡単に作成できます。

$ git rebase -i Y Z

例えば、以下を使用する。

そして、BFCをBBCの後に移動してスクアッシュします。

sh -c "some commands"を"git bisect run"に渡す

$ git bisect start Z' Y

その後、新しいブランチで通常通りバイセクトを開始でき、最終的には最初の悪いコミットを見つけるはずです。

「git bisect run」を使用している場合、上記と同じ手動修正を適用し、特別なブランチで別の「git bisect run」を開始することができます。あるいは、「git bisect」のmanページにあるように、「git bisect run」に渡されるスクリプトは、ソフトウェアをコンパイルしてテストする前にパッチを適用することができます[8]。このパッチは、現在のテスト不能なコミットをテスト可能なものに変えるべきです。そうすれば、テストは「良好」または「不良」という結果になり、「git bisect」は最初の悪いコミットを見つけることができるでしょう。そしてスクリプトは、テストが完了したら、スクリプトを終了する前にパッチを削除することを忘れてはなりません。

(パッチの代わりに「git cherry-pick BFC」を使用して修正を適用できることに注意してください。この場合、テスト後およびスクリプトから戻る前に、「git reset --hard HEAD^」を使用してチェリーピックを元に戻す必要があります。)

しかし、テスト不能な領域を回避するための上記の方法は少し手間がかかります。特別なブランチを使用することは、通常のブランチと同様に開発者間で共有できるため良いのですが、そのようなブランチが多数できてしまうリスクがあります。そして、それは通常の「git bisect」ワークフローを妨げます。したがって、「git bisect run」を完全に自動的に使用したい場合、特別なブランチでバイセクションを再開するための特別なコードをスクリプトに追加する必要があります。

いずれにしても、上記の特別なブランチの例では、Z'とZのコミットが同じソースコードの状態(Gitの用語で「ツリー」)を指すはずであることに気づくでしょう。これは、Z'がZと同じ変更をわずかに異なる順序で適用した結果であるためです。

したがって、バイセクト時にZをZ'に「置き換える」ことができれば、スクリプトに何も追加する必要はありません。特別なブランチと置換を共有するプロジェクトの誰もがそのまま機能するでしょう。

      (BBC+BFC)-X1'-X2'-X3'-X4'-X5'-X6'-Z'-...
     /
...-Y-BBC-X1-X2-X3-X4-X5-X6-BFC-Z

上記の例では、次のような結果になります。

それが「git replace」コマンドが作成された理由です。技術的には、「refs/replace/」階層に置換「ref」を格納します。これらの「ref」は、ブランチ(「refs/heads/」に格納される)やタグ(「refs/tags/」に格納される)のようなものであり、開発者間でブランチやタグと同様に自動的に共有できることを意味します。

「git replace」は非常に強力なメカニズムです。例えば、コミットメッセージや作者を変更するなど、すでにリリースされた履歴のコミットを修正するために使用できます。また、あるリポジトリを別の古いリポジトリとリンクするために、Gitの「grafts」の代わりに使用することもできます。

実際、この最後の機能がGitコミュニティに「売れた」機能であり、現在GitのGitリポジトリの「master」ブランチにあり、2009年10月または11月にGit 1.6.5でリリースされる予定です。

「git replace」の1つの問題は、現在すべての置換refを「refs/replace/」に格納していることですが、バイセクトにのみ有用な置換refが「refs/replace/bisect/」にあった方が良いかもしれません。こうすれば、置換refはバイセクトのみに使用され、一方「refs/replace/」に直接ある他のrefはほとんど常に使用されることになります。

散発的なバグのバイセクト

「git bisect」のもう1つの改善点は、オプションで実行されるテストに冗長性を追加し、散発的なバグを追跡する際により信頼性を高めることでしょう。

これは、一部のカーネル開発者から要望がありました。なぜなら、散発的なバグと呼ばれる一部のバグは、コンパイラの出力に非常に依存するため、すべてのカーネルビルドに現れるわけではないからです。

そのアイデアは、例えば3回のテストごとに「git bisect」が、すでに「良好」または「不良」と判明したコミット(その子孫または祖先のいずれかがそれぞれ「良好」または「不良」と判明しているため)をユーザーにテストさせるというものです。もしコミットが以前に誤って分類されていた場合、バイセクションを早期に中止することができ、うまくいけば多くの間違いが犯される前に済みます。その後、ユーザーは何が起こったかを確認し、修正されたバイセクトログを使用してバイセクションを再開する必要があります。

すでにGitHubでEaldwulf Wuffingaによって作成されたBBChopというプロジェクトがあり、ベイジアン探索理論を使用して同様のことを行っています[9]

BBChopは_git bisect_(または同等のもの)に似ていますが、バグが断続的に発生する場合に機能します。つまり、偽陰性(バグを含んでいても今回たまたま機能する場合)が存在する場合に機能します。偽陽性はないと仮定しています(原則として、同じアプローチで機能するはずですが、それを追加するのは些細なことではないかもしれません)。

しかし、BBChopはどのVCSにも依存しておらず、GitユーザーにとってはGitに統合されたものがある方が簡単でしょう。

結論

リグレッションが重要な問題であり、「git bisect」には、リグレッションに対処するために一般的に使用されるプラクティスや他のツール、特にテストスイートを非常によく補完する優れた機能があることがわかりました。しかし、それを最大限に活用するためには、一部のワークフローや(悪い)習慣を変更する必要があるかもしれません。

「git bisect」内部のアルゴリズムにはいくつかの改善が可能であり、いくつかの新しい機能は場合によっては役立つかもしれませんが、全体として「git bisect」はすでに非常にうまく機能しており、頻繁に使用されており、すでに非常に有用です。この最後の主張を裏付けるために、インゴ・モルナーが著者に「git bisect」を使うことでどれくらいの時間を節約できると思うかと尋ねられたときの最後の言葉を引用しましょう。

「*非常に*」です。

約10年前、私は初めてLinuxパッチキューの*バイセクション*を行いました。それはGit以前(BitKeeper以前ですら)の時代でした。文字通り何日も費やしてパッチを整理し、そのバグに関連していると推測される本質的にスタンドアロンのコミットを作成しました。

それは最後の手段のツールでした。手動で*パッチのバイセクション*を行うよりも、printkの出力を何日も見る方がましでした。

Git bisectを使えば簡単です。最良の場合、約15ステップのカーネルバイセクションを20〜30分で自動的に完了できます。手動での補助がある場合や、複数の重複するバグをバイセクトする場合でも、1時間を超えることはめったにありません。

実際、git bisectがなければ、デバッグすら*試みる*ことがなかったバグもあるため、それは非常に貴重です。以前は、すぐにデバッグが絶望的だと感じるバグパターンがあり、せいぜいクラッシュ/バグのシグネチャをlkmlに送って、誰かが何かを思いついてくれることを願うしかありませんでした。

そして、たとえ今日のバイセクションが失敗したとしても、それはバグについて貴重なことを教えてくれます。つまり、それが非決定論的であること、タイミングやカーネルイメージのレイアウトに依存しているということです。

したがって、git bisectは無条件に素晴らしいものです。ぜひ引用してください ;-)

謝辞

本稿のレビューにご協力いただいた濱野 純氏、Gitメーリングリストに送ったパッチのレビュー、いくつかのアイデアの議論とその改善へのご協力、「git bisect」の大幅な改善、そしてGitの保守と開発における素晴らしい仕事に心から感謝いたします。

本稿に掲載されている非常に有用な情報を提供してくださり、本稿にコメントをくださり、「git bisect」を改善するための提案をしてくださり、Linuxカーネルメーリングリストで「git bisect」を普及してくださったインゴ・モルナー氏に深く感謝いたします。

「git bisect」、Git、そしてLinuxを発明、開発、普及してくださったリーナス・トーバルズ氏に深く感謝いたします。

Gitの作業中に何らかの形で助けてくださった多くの素晴らしい方々、特にアンドレアス・エリクソン、ヨハネス・シンデリン、H.ピーター・アンビン、ダニエル・バーカロウ、ビル・リアー、ジョン・ホーレイ、ショーン・O・ピアース、ジェフ・キング、サム・ヴィレーン、ジョン・シーモアに深く感謝いたします。

scroll-to-top