Git
English ▾ トピック ▾ 最新バージョン ▾ 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"用語では、「興味深い動作」が存在するコミットは「悪い」コミットと呼ばれ、他のコミットは「良い」コミットと呼ばれます。そして、関心のある動作を導入するコミットは、「最初の悪いコミット」と呼ばれます。検索しているコミットスペースに複数の「最初の悪いコミット」が存在する可能性があることに注意してください。

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

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

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

リグレッションはソフトウェア業界における大きな問題です。しかし、その主張の背後にある実際の数字を出すのは困難です。

2002年のNISTの研究[1]のような、バグ全般に関するいくつかの数字があります。

ソフトウェアのバグやエラーは非常に蔓延しており、非常に有害であるため、米国経済に年間推定595億ドル、または国内総生産の約0.6%の損失をもたらしていると、商務省の国立標準技術研究所(NIST)が委託した新たに発表された調査によるとされています。国家レベルでは、コストの半分以上がソフトウェアユーザーによって負担され、残りはソフトウェア開発者/ベンダーによって負担されます。また、すべてのエラーを取り除くことはできないものの、これらのコストの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つの「悪い」コミットと少なくとも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 blame」や「git log -S<string>」など、Gitを使用してこのコミットを見つけるより良い方法があるからです。

手動で二分探索を駆動する

この時点で、基本的に検索を駆動する方法は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

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

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

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

テストできないコミットを避ける

現在の状態がテストできない場合もあります。例えば、当時それを妨げるバグがあったため、コンパイルされない場合などです。これは、特別な終了コード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) を形成するため、各ステップでテストする最適な二分探索コミットを見つけるのはそれほど簡単ではありません。いずれにせよ、Linusは「本当に愚かな」アルゴリズムを見つけて実装し、その後Junio Hamanoによって改善され、非常にうまく機能しています。

したがって、スキップされたコミットがない場合に、最適な二分探索コミットを見つけるために「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の状態 (「良い」または「悪い」) を知ることが、コミットの状態が「良い」か「悪い」かをできるだけ多くの情報を提供する場合は、コミット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を取得しましょう。

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

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

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

Xが「悪い」とわかった場合、その子孫はすべて「悪い」ことがわかっているので、次のように言いたいと思います

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

しかし、ステップ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

しかし、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が悪い場合、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 bisect skip」がGitバージョン1.5.4(2008年2月1日リリース)で開発されてから、Gitバージョン1.6.4(2009年7月29日リリース)まで使用していたアルゴリズムでした。

しかし、Ingo MolnarとH. Peter Anvin(もう一人の有名なLinuxカーネル開発者)は、最良の二分点候補がすべてテスト不可能なコミット領域に集中する場合があると不満を述べました。この場合、ユーザーは多くのテスト不可能なコミットをテストする必要があり、非常に非効率的でした。

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

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

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

そのため、最初の二分コミットがスキップされたときに、次に最適なスキップされていない二分コミットを単純に選択するのは良くありません。

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

そのため、goodコミットとbadコミットから離れたコミットを優先するように偏ったPRNG(擬似乱数生成器)を使用するのが良い選択肢のように思えました。

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

マージベースの確認

上記の「二分探索アルゴリズム」では説明されていない、二分探索アルゴリズムには別の調整があります。

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

もちろん、「bad」コミットは「good」コミットの祖先にはなれません。goodコミットの祖先は「good」であると想定されているためです。そして、すべての「good」コミットはbadコミットに関連している必要があります。「bad」コミットのブランチとリンクがないブランチには存在できません。しかし、goodコミットがbadコミットに関連していても、その祖先でも子孫でもない可能性があります。

たとえば、「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)で説明したように、goodコミットのすべての祖先はgoodであると想定されるため削除します。

そのため、残るのは

H-I-J

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

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

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

実際、開発チームは開発ブランチとメンテナンスブランチの両方を維持することが多く、「git bisect」がメンテナンスブランチにない開発ブランチでのリグレッションを二分探索したいときにうまく機能すれば、非常に簡単になります。次のコマンドを使って二分探索を開始できるはずです。

$ git bisect start dev main

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

あるマージベースがbadであることが判明した場合、二分探索プロセスは次のようなメッセージで停止します。

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

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

したがって、badマージベースがない場合、二分探索プロセスはこのステップの後、通常どおり続行されます。

二分探索のベストプラクティス

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

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

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

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

ここで、cはテストのラウンド数(したがって小さな定数)、bはコミットあたりのバグの比率(うまくいけば小さな定数も)です。

したがって、各コミット後にすべてをテストする場合のO(N * T * M)に対してO(N * T)であるため、明らかにずっと優れています。

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

テストスイートのもう1つの良い点は、テストスイートがある場合、悪い動作をテストする方法をすでに知っているということです。したがって、リグレッションが発生した場合は、この知識を使用して「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を使用する場合でも、各コミットに1つの小さな論理的な変更のみを含めることをお勧めします。

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

もう1つの良い考えは、良いコミットメッセージを用意することです。それらは、いくつかの変更がなぜ行われたのかを理解するのに非常に役立ちます。

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

バグが発生しやすいマージの回避

まず、マージ自体は、マージでソースコードの競合を解決する必要がない場合でも、いくつかのリグレッションを引き起こす可能性があります。これは、あるブランチで意味的な変更が行われているときに、他のブランチがそれを認識していない可能性があるためです。

たとえば、あるブランチが関数の意味を変更する一方で、別のブランチが同じ関数への呼び出しを追加する可能性があります。

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

いずれにせよ、「git rebase」を使用して履歴を線形化できます。これは、そもそもマージを回避するために使用できます。または、非線形の履歴の代わりに線形の履歴で二分探索を行うために使用できます。これは、あるブランチで意味的な変更があった場合に、より多くの情報を提供するはずです。

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

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

ワークフローの適応

リグレッションを処理するための特別なワークフローは、大きな成果をもたらす可能性があります。

以下は、Andreas Ericssonが使用するワークフローの例です。

  • テストスイートで、リグレッションを露呈するテストスクリプトを記述します。

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

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

  • 修正とテストスクリプト(および必要に応じてさらに多くのテスト)の両方をコミットします。

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

いくつかの具体的な数値を示すと、以前はレポートから修正までの平均サイクルが142.6時間でした(私たちのやや奇妙なバグトラッカーによると、これは単純に実経過時間を測定します)。Gitに移行して以来、これを16.2時間に短縮しました。主に、バグ修正を常に把握できるようになったことと、バグを修正するために誰もが競い合っているためです(Gitが代わりにバグを見つけてくれるため、私たちはどれほど怠け者であるかを非常に誇りに思っています)。新しいリリースごとにバグが約40%減少します(ほぼ確実に、私たちがテストを書くことについてどのように感じているかが原因です)。

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

他のメッセージでアンドレアスは、彼らも上記で説明した「ベストプラクティス」、つまり、小さな論理コミット、トピックブランチ、邪悪なマージの禁止などを使用していると述べています。これらのプラクティスはすべて、二分探索をより簡単かつ有用にすることで、コミットグラフの二分探索性を向上させます。

したがって、優れたワークフローは、上記の点を中心に設計されるべきです。つまり、二分探索をより簡単、より有用、かつ標準的なものにすることです。

QA担当者と可能であればエンドユーザーの関与

「git bisect」の優れた点の1つは、それが開発者ツールだけではないということです。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は正常であり、BFCは不良であり、BBCおよびX1からX6まではテストできないことがわかっています。

この場合、手動で二分探索している場合は、BBCの直前で始まる特別なブランチを作成できます。このブランチの最初のコミットは、BFCをスカッシュしたBBCである必要があります。また、ブランチ内の他のコミットは、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」のマニュアルページで説明されているように、「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/」階層に置換「refs」を格納します。これらの「refs」は、(「refs/heads/」に格納されている)ブランチや(「refs/tags/」に格納されている)タグのようなものであり、つまり、開発者間でブランチやタグのように自動的に共有できることを意味します。

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

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

「git replace」の1つの問題は、現在、すべての置換refsを「refs/replace/」に格納していることですが、二分探索にのみ役立つ置換refsが「refs/replace/bisect/」にある方がおそらく良いでしょう。このようにすると、置換refsは二分探索にのみ使用でき、「refs/replace/」に直接ある他のrefsはほぼ常に使用できます。

散発的なバグの二分探索

「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」を使用するときにどれくらいの時間を節約できると思うか尋ねたときの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