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

概要

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

"git bisect" の紹介

Git は、Linus Torvalds によって作成され、Junio Hamano によって保守されている分散バージョン管理システム (DVCS) です。

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

実際、人々は、バグまたはリグレッションと呼ばれる「悪い」動作を導入するコミットに特に関心があります。彼らがこれらのコミットに関心があるのは、コミットが (うまくいけば) 非常に小さなソースコード変更のセットしか含まないためです。そして、どこを探せばよいか分からない場合に比べて、非常に小さな変更のセットだけをチェックすればよい場合に、問題を理解し、適切に修正する方がはるかに簡単です。

そこで、「悪い」動作を導入するコミットを見つけるのに役立つように、「git bisect」の一連のコマンドが考案されました。そして、「git bisect」の用語では、「興味深い動作」が存在するコミットを「bad」コミットと呼び、他のコミットを「good」コミットと呼ぶことになります。そして、私たちが関心のある動作を導入するコミットを「最初のbadコミット」と呼びます。検索対象のコミット空間には複数の「最初のbadコミット」が存在する可能性があることに注意してください。

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

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

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

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

一般的にバグに関する数字はいくつかあり、例えば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%以上を占めます。しかし、もちろんリリース後も戦いは続くので、これで終わりではありません。

そして、Ingo Molnar(有名な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つの「bad」コミットと少なくとも1つの「good」コミットを指定して行われます。これらは、「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で「git bisect」を使用するよりも良い方法でこのコミットを見つけることができるため(例えば「git blame」や「git log -S<string>」など)、簡単な例です。

二分探索を手動で実行する

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

ユーザーが操作する場合、検索の各ステップで、ユーザーは現在のコミットをテストし、上記で説明した「git bisect good」または「git bisect bad」コマンドを使用して、それが「good」か「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」は最終的に最初の「bad」コミットを見つけます。

$ 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」に、現在のコミットが「good」か「bad」かを判断するために、各バイセクションステップでスクリプトまたはコマンドを起動するように指示することです。これを行うには、「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

この例では、「grep ^SUBLEVEL = 25 Makefile」を「git bisect run」のパラメータとして渡しました。これは、各ステップで渡されたgrepコマンドが起動されることを意味します。そして、終了コードが0(成功を意味します)の場合、git bisectは現在の状態を「good」とマークします。終了コードが1(または125以外の1から127までの任意のコード)の場合、現在の状態は「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」を使用して現在の状態を調べることができます。DISPLAY 環境変数が設定されていない場合、gitk(または「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) を形成するため、各ステップでテストすべき最適な二分探索コミットを見つけるのはそれほど単純ではありません。しかし、Linusは「本当に愚かな」アルゴリズムを発見し実装し、後にJunio Hamanoによって改善され、かなりうまく機能します。

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

1) 次のコミットのみを保持します。

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

これは、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...

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

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

2) グラフの「good」の端から開始し、各コミットにその祖先の数に1を加えた数を関連付けます。

たとえば、Hが「bad」コミットで、AとDが「good」コミットの親である次のグラフでは、

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が最適な二分探索ポイントまたは最適な二分探索コミットであるのは、その状態(「good」または「bad」)を知ることで、コミットの状態が「good」であるか「bad」であるかにかかわらず、可能な限り多くの情報が得られる場合であるとします。

これは、最適な二分探索コミットとは、次の関数が最大になるコミットであるということです。

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

ここで、information_if_good(X) は X が good の場合に得られる情報であり、information_if_bad(X) は X が bad の場合に得られる情報です。

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

また、上記の二分探索アルゴリズムのステップ1)の後でクリーンアップされたグラフがあると仮定します。これは、グラフから削除できるコミット数という観点から得られる情報を測定できることを意味します。

そして、グラフ内のコミットXを取り上げてみましょう。

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

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

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

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

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

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

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

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

しかし、git bisect で使用されるアルゴリズムでは次のようになります。

            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

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

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

スキップアルゴリズム

一部のコミットがスキップされた場合(「git bisect skip」を使用)、二分探索アルゴリズムはステップ1)から3)までは同じです。しかし、その後、ほぼ以下のステップを使用します。

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

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

8) それ以外の場合は、ソートされたリストからスキップされたコミットをすべて除外する。

9) 0から1の間の乱数を生成するために擬似乱数ジェネレーター (PRNG) を使用する。

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

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

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

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

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

しかし、Ingo MolnarとH. Peter Anvin(もう一人の有名なLinuxカーネル開発者)は、最適な二分探索ポイントが、すべてのコミットがテスト不能な領域に集中してしまうことがあると不満を述べました。この場合、ユーザーはテスト不能なコミットを多数テストするよう求められ、非常に非効率になる可能性があります。

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

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

したがって、テスト不能なコミットの近くのコミットは、それ自体がテスト不能である可能性が高いというのは事実です。そして、最適な二分探索コミットも(二分探索アルゴリズムのため)一緒によく見つかります。

これが、最初のものがスキップされたときに、次の最適な未スキップの二分探索コミットを単純に選択するのが悪い考えである理由です。

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

そのため、PRNGを使用して、良いコミットや悪いコミットから離れたコミットを優先するバイアスをかけることが良い選択肢であるように思われました。

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

マージベースの確認

上記の「二分探索アルゴリズム」で説明されていない、二分探索アルゴリズムのもう1つの工夫があります。

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

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

例えば、「main」ブランチと、「D」という名前のコミットでmainブランチからフォークした「dev」ブランチが存在する場合があります。

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

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

さて、コミットJがbadで、コミットGがgoodであると仮定し、以前に説明したように二分探索アルゴリズムを適用します。

二分探索アルゴリズムのステップ1) b) で説明されているように、良いコミットの祖先はすべて良いとされているため、それらをすべて削除します。

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

H-I-J

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

このような二分探索の結果は、Hが最初のbadコミットであると判明することですが、実際にはBです。したがって、それは間違っているでしょう!

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

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

$ git bisect start dev main

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

いずれかのマージベースが不良である場合、二分探索プロセスは次のようなメッセージで停止します。

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

ここでBBBが不良なマージベースのsha1ハッシュであり、[GGGGGG,...]は良好なコミットのsha1のコンマ区切りのリストです。

一部のマージベースがスキップされた場合、二分探索プロセスは続行されますが、スキップされた各マージベースに対して次のメッセージが表示されます。

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はbadコミットのsha1ハッシュ、MMMMMMはスキップされたマージベースのsha1ハッシュ、[GGGGGG,...]はgoodコミットのsha1のコンマ区切りのリストです。

したがって、不良なマージベースがなければ、このステップの後も二分探索プロセスは通常通り継続されます。

最適な二分探索の実践

テストスイートとgit bisectを一緒に使う

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

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

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

ここで、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'"

一方で、これを頻繁に行う場合は、入力の手間を省くためにスクリプトを用意する価値があります。

パフォーマンスリグレッションの特定

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

このスクリプトは、「git bisect run」に渡して、パフォーマンスの低下を引き起こしたコミットを見つけることができます。

#!/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

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

後のコミットで修正されるとしても、意図的に何かを壊すような変更を含むコミットを持たないことは、明らかに良い考えです。

また、どのVCSを使用する場合でも、各コミットに小さな論理的な変更のみを含めることも良い考えです。

コミットの変更が小さければ小さいほど、「git bisect」は効果的になります。そして、そもそも「git bisect」の必要性は減るでしょう。なぜなら、小さな変更は、コミッター自身によるレビューだけであっても、レビューが簡単だからです。

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

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

バグの原因となるマージを避ける

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

例えば、一方のブランチが関数のセマンティクスを変更し、もう一方のブランチが同じ関数への呼び出しをさらに追加する場合があります。

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

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

マージは、より小さなブランチを使用したり、長いバージョン関連ブランチの代わりに多くのトピックブランチを使用したりすることで、より単純にすることもできます。

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

ワークフローを調整する

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

Andreas Ericsson が使用しているワークフローの例を以下に示します。

  • テストスイートで、リグレッションを明らかにするテストスクリプトを作成する

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

  • 前のステップで明らかになったバグを修正する

  • 修正とテストスクリプトの両方をコミットする(必要であれば追加のテストも)

そして、Andreas がこのワークフローについて述べたことは次のとおりです。[5]

具体的な数字を挙げると、以前はレポートから修正までのサイクルが平均142.6時間でした(多少変なバグトラッカーがウォールクロック時間だけを測定していたため)。Gitに移行して以来、これを16.2時間に短縮しました。これは主に、バグ修正に今すぐ取り組めるようになったことと、誰もがバグ修正に取り組みたがっているからです(Gitにバグを見つけさせるという、私たちの怠惰さを非常に誇りに思っています)。新しいリリースごとにバグが約40%減少しています(これは、テストを書くことに対する私たちの考え方が変わったことがほぼ確実に原因です)。

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

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

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

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

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

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

例えば、David Miller は次のように書いています[6]

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

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

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

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

複雑なスクリプトの使用

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

Ingo Molnar はそれについて次のように述べています[7]

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

テストスイート、git bisect、およびその他のシステムの組み合わせ

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

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

二分探索の未来

"git replace"

以前、「git bisect skip」がコミットグラフ内でコミットがテスト不能な領域を回避するためにPRNGを使用していることを確認しました。問題は、最初の悪いコミットがテスト不能な領域にある場合があることです。

議論を簡単にするために、テスト不能な領域は単純な一連のコミットであり、1つのコミット(二分探索を壊すコミットとしてBBCと呼ぶ)によって導入された破壊によって作成され、後で別のコミット(二分探索を修正するコミットとしてBFCと呼ぶ)によって修正されたと仮定します。

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

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

この場合、手動で二分探索を行っている場合、BBCの直前で始まる特殊なブランチを作成できます。このブランチの最初のコミットは、BBCにBFCをスカッシュしたものになります。そして、ブランチ内の他のコミットは、BBCとBFCの間にあるコミットをブランチの最初のコミットにリベースし、その後、BFCの後のコミットもリベースしたものになります。

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

引用符で囲まれたコミットはリベースされたものです。

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

例えば、次のコマンドを使用します。

$ git rebase -i Y Z

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

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

$ git bisect start Z' Y

「git bisect run」を使用している場合は、上記と同じ手動修正を行い、特殊なブランチで別の「git bisect run」を開始できます。または、「git bisect」のmanページにあるように、「git bisect run」に渡されるスクリプトは、ソフトウェアをコンパイルしてテストする前にパッチを適用できます[8]。このパッチは、現在のテスト不能なコミットをテスト可能なコミットに変えるべきです。これにより、テストの結果は「good」または「bad」となり、「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」を「refs/replace/」階層に保存します。これらの「refs」は、ブランチ(「refs/heads/」に保存される)やタグ(「refs/tags」に保存される)のようなものであり、開発者間でブランチやタグのように自動的に共有できることを意味します。

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

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

「git replace」の1つの問題は、現在、すべての置換参照を「refs/replace/」に保存していることですが、二分探索にのみ有用な置換参照が「refs/replace/bisect/」にある方が良いかもしれません。こうすることで、置換参照は二分探索にのみ使用され、直接「refs/replace/」にある他の参照はほぼ常に使用されることになります。

散発的なバグの二分探索

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

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

アイデアは、例えば3回のテストごとに「git bisect」が、すでに「good」または「bad」と判明しているコミットをテストするようにユーザーに依頼することです(子孫または祖先のいずれかがそれぞれ「good」または「bad」と判明しているため)。もしコミットが以前に誤って分類されていた場合、二分探索は早期に中止され、うまくいけばあまり多くの間違いが犯される前に停止します。その後、ユーザーは何が起こったかを調べ、修正された二分探索ログを使用して二分探索を再開する必要があります。

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

BBChop は Git bisect (またはそれに相当するもの) に似ていますが、バグが断続的に発生する場合に機能します。つまり、誤検出 (バグが含まれていても今回たまたま機能する場合) がある場合でも機能します。誤陽性はないと仮定しています (原則として、同じアプローチが機能しますが、追加は自明ではないかもしれません)。

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

結論

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

「git bisect」内部のアルゴリズムにはいくつかの改善の余地があり、いくつかの新機能が特定のケースで役立つ可能性がありますが、全体として「git bisect」はすでに非常にうまく機能し、多くの場所で利用され、非常に有用です。この最後の主張を裏付けるために、著者が「git bisect」を使用することでどれくらいの時間を節約できると考えるか尋ねたときのIngo Molnarの最後の言葉を引用しましょう。

大量に

約10年前、私は初めてLinuxパッチキューの二分探索を行いました。それはGit(そしてBitKeeper以前でさえ)以前の時代でした。文字通り何日もかけてパッチを分類し、そのバグに関連していると推測されるスタンドアロンのコミットを実質的に作成しました。

それは絶対的な最終手段でした。手動でパッチ二分探索を行うよりも、printk出力を見るのに何日も費やすことを選びました。

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

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

そして、たとえ今日の二分探索が失敗したとしても、それはバグについて貴重な情報、つまり非決定的であること、タイミングまたはカーネルイメージのレイアウトに依存することを示してくれます。

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

謝辞

本論文のレビュー、Gitメーリングリストに送ったパッチのレビュー、いくつかのアイデアの議論と改善への協力、「git bisect」の大幅な改善、そしてGitの維持と開発における素晴らしい仕事をしてくださったJunio Hamano氏に深く感謝いたします。

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

「git bisect」、Git、そしてLinuxを発明、開発、普及してくださったLinus Torvalds氏に深く感謝いたします。

私がGitに取り組んでいたときに、何らかの形で助けてくれた他の多くの素晴らしい人々、特にAndreas Ericsson、Johannes Schindelin、H. Peter Anvin、Daniel Barkalow、Bill Lear、John Hawley、Shawn O. Pierce、Jeff King、Sam Vilain、Jon Seymourに深く感謝いたします。

著者に講演と本論文の発表を選んでくださったLinux-Kongressプログラム委員会に深く感謝いたします。

scroll-to-top