Abstract

「git bisect」を使用すると、ソフトウェアユーザーと開発者はデグレ(regression)を引き起こしたコミットを簡単に見つけることができます。 デグレと戦うための優れたツールが重要である理由を示します。 「git bisect」がどのように機能するか、および内部で使用するアルゴリズムについて説明します。 それから、「git bisect」を利用して現在の慣行を改善する方法を説明します。 また、「git bisect」が将来どのように改善されるかについて議論します。

Introduction to "git bisect"

Gitは、Linus Torvaldsによって作成され、濱野 純(Junio Hamano)によって維持されている分散バージョン管理システム(DVCS;Distributed Version Control system)です。

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

実際には、人々はバグやデグレ(regression)と呼ばれる「悪い」(bad)振る舞いをもたらすコミットに特に興味を持っています。 コミットには(うまくすれば)ソースコードの変更の非常に小さな組が含まれているため、彼らはこれらコミットに興味を持っています。そして、そもそもどこを見ているのかわからないときより、非常に小さな変更セットをチェックするだけでよいならば、問題を理解して適切に修正する方がはるかに簡単です。

そこで、人々が「悪い」(bad)振る舞いをもたらすコミットを見つけるのを助けるために、「git bisect」コマンドセットが発明されました。 そしてもちろん、「git bisect」の用語では、「興味深い動作」が存在するコミットは「bad」コミットと呼ばれ、他のコミットは「good」コミットと呼ばれます。 そして、私たちが興味を持っている振る舞いを紹介するコミットは「最初のbadコミット」と呼ばれます。 注意: 私達が探索しているコミット空間に複数の「最初のbadコミット」が存在する可能性があることに注意してください。

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

Fighting regressions overview

Regressions: a big problem

デグレ(regressions)は、ソフトウェア業界では大きな問題です。 しかし、その実態を知るのは困難です。

2002年のNIST(訳注:アメリカ連邦標準・技術局)の調査 [1] のように、一般的なバグについてはいくつかの数字があります:

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

そして

ソフトウェア開発者はすでに開発コストの約80%を欠陥の特定と修正に費やしているものの、ソフトウェア以外のタイプの製品には、このような高レベルのエラーが含まれているものはほとんどありません。

つまり結論としては:

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

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

しかしながら、ウィキペディア([3])によると:

メンテナンスに対する一般的な認識は、単にバグを修正しているだけ、というものです。しかし、長年にわたる調査と調査によると、保守作業の80%以上は、非修正アクションに使用されています(Pigosky1997)。 このような認識は、実際にはシステムの機能拡張である問題レポートをユーザーが提出することによって広まっています。

しかし、デグレに注意する必要があるため、既存のソフトウェアの改善には非常にコストがかかると推測できます。 少なくとも、これは上記の研究結果と整合しています。

もちろん、ある種のソフトウェアが開発され、それからしばらくの間、あまり改善されることなく使用され、そして最終的に捨てられます。 もちろん、この場合、デグレは大きな問題ではないかもしれません。 しかしその一方で、多くの人々によって何年も、あるいは何十年もの間継続的に開発され維持されている大きなソフトウェアがたくさんあります。 そして、そのようなソフトウェアに(時には深刻に)依存する人がしばしばいるので、デグレは本当に大きな問題です。

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

最初のrcリリースから最終リリースまでの時間は、rcバージョンをテストし、バグ、特にデグレと戦うために使用されることになっています。 そして、この時間はリリースサイクル時間の80%以上です。 しかし、これはまだ戦いの終わりではなく、もちろん、リリース後も続くのです。

そして、Ingo Molnar(有名なLinuxカーネル開発者)は git bisect の使用についてこう言っています:

私はマージウィンドウで最も積極的に使用します(多くのツリーが上流でマージされ、バグの流入が最も多い場合)。ええ、1日に複数回使用する場合があります。 私の平均はおおよそ1日1回です。

つまり、開発者は常にデグレと戦っています。実際、バグが見つかったらすぐに修正する必要があることはよく知られています。 そのため、この目的のための優れたツールがあるのは興味深いことです。

Other tools to fight regressions

では、デグレと戦うために使用されるツールは何ですか? それらは、通常のバグと戦うために使用されるものとほぼ同じです。 唯一の特定のツールは、テストスイートと「git bisect」に似たツールです。

テストスイートはとても素晴らしいものです。しかし、テストスイートを単独で使用する場合、コミットごとにすべてのテストをチェックするように使用することが前提となっています。つまり、テストスイートはあまり効率的ではないということです。なぜなら、多くのテストが何の面白みもない結果を得るために実行され、組み合わせの爆発に悩まされるからです。

実際問題として、大きなソフトウェアには多くの異なる構成オプションがあり、各コミット後に各テストケースが各構成に合格する必要があることです。 したがって、リリースごとに N個の構成、M個のコミット、T個のテストケース がある場合は、あなたは以下のように行うべきです:

N * M * T tests

ここで、NとMとTはすべて、あなたのソフトウェアのサイズとともに成長します。

そのため、すぐに全てを完全にテストすることはできなくなります。

また、いくつかのバグがあなたのテストスイートをすり抜けた場合は、テストスイートにテストを追加できます。 しかし、新しく改善されたテストスイートを使用してバグが発生した場所を見つけたい場合は、bisectプロセスをエミュレートするか、「bad」コミットから始めて、各コミットを逆方向に愚直ににテストする必要があります。すげー無駄です。

"git bisect" overview

Starting a bisection

使用する最初の「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

注意: 使用する例は実際には単純化モデル(toy example)であることに注意してください。「2.6.26-something」のようなバージョンを持つ最初のコミット、つまりトップレベルのMakefileに「SUBLEVEL = 26」行が含まれるコミットを探します。 「git bisect」を使用するよりも、Gitでこのコミットを見つけるためのより良い方法(たとえば、 git blamegit log -S<string> )があるため、これは単純化モデル(toy example)です。

Driving a bisection manually

この時点では、探索を実行する方法は基本的には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」を使用して、bisectを開始する前のブランチに戻ることができます:

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

Driving a bisection automatically

bisectプロセスを駆動するもう1つの方法は、「git bisect」に、各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

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

128〜255の終了コードは、「git bisect run」に固有のものです。 これらはbisectプロセスを即座に停止(stop)させます。 これは、渡されたコマンドが完了するまでに時間がかかりすぎる場合などに有効で、シグナルでkillすればbisectプロセスを停止させることができるからです。

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

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

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

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

または、より詳細な制御が必要な場合は、たとえば「git bisect visualize」を使用して現在の状態を調べることができます。 より適切な二等分点を見つけるのに役立つgitk(または DISPLAY 環境変数が設定されていない場合は「git log」)を起動します。

いずれにせよ、一連のテスト不可能なコミットがある場合、探しているデグレが、これらのテスト不可能なコミットの1つによって導入された可能性があります。 この場合、どのコミットがデグレを導入したかを確実に知ることはできません。

したがって、「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!

Saving a log and replaying it

他の人にあなたのbisectプロセスを見せたい場合は、例えば以下を使用してログを取得できます:

$ git bisect log > bisect_log.txt

そして、これを使用して以下のようにリプレイすることが可能です:

$ git bisect replay bisect_log.txt

"git bisect" details

Bisection algorithm

Gitコミットは有向非巡回グラフ(DAG)を形成するため、各ステップでテストするのに最適な二等分コミットを見つけることはそれほど簡単ではありません。 とにかく、Linusは、後に濱野 純(Junio Hamano)によって改良された「本当にばかげた」(truly stupid)アルゴリズムを見つけて実装しました。これは非常にうまく機能します。

スキップされたコミットがない場合に最適な二等分コミットを見つけるために「git bisect」によって使用されるアルゴリズムは以下のとおり:

1) 以下のコミットのみを保持します:

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

これは、DAGで興味のないコミット(uninteresting commits)を取り除くことを意味します。

たとえば、以下のようなグラフから始める場合:

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は「bad」コミットで、「G」は「good」コミットで、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 をコミットに関連付ける場合、これが最良の二等分点であることがわかります。 したがって、この場合、他のコミットの処理を停止して、現在のコミットを返すことができます。

Bisection algorithm debugging

どのコミットグラフでも、 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)

Bisection algorithm discussed

まず、「最良二等分点」を定義しましょう。あるコミットXの状態(「good」または「bad」)を与えることで、コミットの状態が「good」か「bad」かの情報が可能な限り多く得られる場合、私達はコミットXを最良二等分点または最良二等分コミットと呼ぶことにします。

これは、最良の二等分コミットは、以下の関数の値が最大になるコミットであることを意味します:

`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コミットの近くにあると、多かれ少なかれ情報が得られないと想定します)。

また、上記のbisectアルゴリズムの ステップ 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」としてマークされると、より多くの情報が得られます。これは、新しい「bad」コミットの祖先ではない以前の「bad」コミットの祖先が最初のbadコミットではないこともわかっているためです。 それらがgoodかbadかはわかりませんが、新しい「bad」コミットの祖先ではないため、最初のbadコミットではないことはわかっています。

したがって、コミットが「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の祖先である必要があるため)。

つまり、現在のアルゴリズムは、私達が最初に想定した限りにおいて最良のように見えます。

Skip algorithm

(「git bisect skip」を使用して)一部のコミットがスキップされた場合、bisectアルゴリズムは ステップ 1)から ステップ 3)までは同じです。 ただし、それ以降は大まかに以下の手順を使用します:

6) 関連値の降順でコミットを並べ替えます

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

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

9) 疑似乱数ジェネレーター(PRNG)を使用して、0〜1の乱数を生成します

10) この乱数に平方根を乗じて0に偏らせます

11) 結果に、フィルターされたリスト内のコミット数を乗じて、このリストへのインデックスを取得します

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

Skip algorithm discussed

(skip algorithmの)ステップ 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カーネル開発者)は両方とも、全コミットがテスト不能な領域内にすべての最良の二等分点がたまたまあると不満を漏らしました。 そしてこの場合、ユーザーは多くのテスト不能なコミットをテストするように求めらますが、これは非常に非効率的である可能性があります。

実際、一度破損が発生した後、他の多くのコミットが導入された後にのみ破損が修正されたため、テスト不能なコミットはテストできないことがよくあります。

もちろん、こういう破損は、ほとんどの場合、私達がコミットグラフで見つけようとしている破損とは無関係です。 しかし、それは私たちが興味深い「bad振る舞い」が存在するかどうかを知ることを妨げます。

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

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

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

したがって、goodコミットとbadコミットから離れてコミットを優先するバイアスのあるPRNGを使用することは、良い選択のように見えました。

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

Checking merge bases

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

前の例では、「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」であり、コミット「F」によって「main」ブランチで修正された場合はどうなりますか?

このようなbisectの結果は、Hが最初のbadコミットであると探索します。しかし実際にはBです。つまり、これは間違いです!

そして、実際には、あるブランチで作業している人は、別のブランチで作業している人がバグを修正したことに気付いていないことがあります。 また、Fが複数のバグを修正した場合や、リリースに間に合わなかった大きな開発努力の戻し(revert)であることもあり得ます。

実際、開発チームは開発ブランチとメンテナンスブランチの両方を維持することが多く、メンテナンスブランチにない開発ブランチでデグレをbisectしたいときに「git bisect」が機能すれば非常に簡単です。 彼らは以下を使用してbisectを開始できるはずです:

$ git bisect start dev main

この追加機能を有効にするために、bisectを開始したときに、いくつかのgoodコミットがbadコミットの祖先でない場合、まずbadコミットとgoodコミットの間のマージベースを計算し、これらのマージベースをチェックアウトしてテストする最初のコミットとして選択するようにしています。

1つのマージベースがbadである場合、bisectプロセスは以下のようなメッセージで停止します(訳注:マージベースBBBBBBがbadです。これは、BBBBBBと[GGGGGG,…]の間でバグが修正されたことを意味します):

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

ここで、BBBBBBはbadなマージベースのsha1ハッシュであり、 [GGGGGG,…] はgoodなコミットのsha1のコンマ区切りのリストです。

一部のマージベースがスキップされた場合、bisectプロセスは続行されますが、スキップされたマージベースごとに以下のメッセージが出力されます(訳注:…最初のbadなコミットがMMMMMMとBBBBBBの間であるかどうかを確認することはできません。とにかく続けます…):

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なマージベースがない場合、このステップの後、bisectプロセスは通常どおり続行されます。

Best bisecting practices

Using test suites and git bisect together

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

すべてのテストケース(T) がすべての構成(N)に合格することを、いくつかのポイント(たとえば、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が必要です。

テストスイートのもう1つの優れた点は、テストスイートを持っている場合、badい振る舞いをテストする方法をすでに知っていることです。 したがって、この知識を使用して、デグレがあると思われる場合に「git bisect」の新しいテストケースを作成できます。 したがって、バグをbisectして修正する方が簡単です。 そしてそれから、作成したばかりのテストケースをテストスイートに追加できます。

したがって、テストケースの作成方法とbisectする方法を知っている場合は、好循環になります:

よりテストされる ⇒ テストの作成がより簡単になる ⇒ bisectするのがより簡単なる ⇒ よりテストされる

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

Bisecting build failures

以下のようなものを使用して、壊れたビルドを非常に簡単に自動的にbisectすることができます:

$ git bisect start BAD GOOD
$ git bisect run make

Passing sh -c "some commands" to "git bisect run"

例えば:

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

一方、これを頻繁に行う場合は、タイピングしまくらなくていいようにスクリプトを作成する価値があります。

Finding performance regressions

これは、濱野 純(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つの良いアイデアは、適切なコミットメッセージを用意することです。 いくつかの変更が行われた理由を理解するのに非常に役立ちます。

これらの一般的なベストプラクティスは、あなたが頻繁にbisectする場合に非常に役立ちます。

Avoiding bug prone merges

最初のマージ自体は、マージがソースコードの競合解決を必要としない場合でも、いくつかのデグレを引き起こす可能性があります。 これは、一方のブランチでセマンティックの変更が発生する可能性がある一方で、もう一方のブランチはそれを認識していないためです。

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

競合を解決するために多くのファイルを修正する必要がある場合、これはさらに悪化します。 そのため、このようなマージは「悪のマージ」(evil merges)と呼ばれます。 それらは、デグレを追跡することを非常に困難にする可能性があります。 最初のbadコミットがそのようなマージである場合、それを知ることは誤解を招く可能性さえあります。なぜなら、バグは1つのブランチのセマンティック変更に起因する場合、競合解決が悪いことに起因すると考える可能性があるからです。

とにかく、「git rebase」を使用して履歴を線形化できます。 これは、そもそもマージを回避するために使用できます。 または、非線形履歴の代わりに線形履歴をbisectするために使用できます。これにより、1つのブランチで意味が変更された場合に、より多くの情報が得られるはずです。

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

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

Adapting your work-flow

デグレを処理するための特別なワークフローは、素晴らしい結果をもたらす可能性があります。

ここで、 Andreas Ericsson が使用するワークフローの例を以下に示します:

  • テストスイートでは、デグレを明らかにするテストスクリプトを書きます

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

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

  • 修正とテストスクリプトの両方をコミットします(必要に応じてさらにテストを行います)

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

具体的な数字を挙げると、以前は報告から修正までの平均サイクルが 142.6 時間でした((壁掛け時計の時間だけを計測する、ちょっと変わったバグトラッカーによるものです)。Gitに移行してからは、16.2時間まで短縮されました。これは主に、バグフィックスを怠らないようになったことと、誰もがバグフィックスに参加するようになったからです(私たちは、バグを見つけるのをいかにGitに任せて怠慢しているかを誇りに思っています)。新しいリリースのたびに、バグの数は40%ほど減っています(ほぼ間違いなく、私たちがテストを書くことにどう感じているかによるものです)。

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

他のメッセージで Andreas は、上記の「ベストプラクティス」も使用していると述べています。小さな論理コミット、トピックブランチ、悪意のないマージ(no evil merge)などです。これらのプラクティスはすべて、bisectをより簡単でより便利にすることによって、コミットグラフの二分探索性を改善します。

したがって、上記の点を念頭に適切なワークフローを設計する必要があります。 これにより、bisectがより簡単に、より便利に、標準的になります。

品質担当者と、可能であればエンドユーザーを巻き込む

「git bisect」の良いところの1つは、それが開発者用のツールだけではないということです。 品質担当者、さらにはエンドユーザー(ソースコードにアクセスできる場合、またはすべてのビルドにアクセスできる場合)が効果的に使用できます。

一時期、linuxのカーネルメーリングリストで、エンドユーザーに常にbisectを要求して良いのかという議論があり、良いという意見を支持する非常に良い指摘がありました。

たとえば、 David Miller は以下のように書いています [6] :

人々が理解していないのは、これが「エンド・ノードの原則」(end node principle)が適用される状況であるということです。リソース(ここでは開発者)が限られている場合、開発者に負担の大部分を押し付けないようにします。その代わりに、多くのリソースを持っているエンドノード(ここではユーザー)に物事を押し付け、状況の大きさが実態の大きさに沿うようにするのです。

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

また、興味深いのは、バグを報告しているエンドユーザー(またはバグを再現した品質担当者)が、バグが発生する環境にアクセスできることです。 そのため、多くの場合、デグレをより簡単に再現できます。 そして、bisectすることができれば、バグが発生した環境からより多くの情報が抽出されます。つまり、バグを理解して修正するのが簡単になります。

オープンソースプロジェクトでは、エンドユーザーからより有益な貢献を得るための良い方法となり、品質管理や開発活動に導入することができます。

Using complex scripts

カーネル開発の場合のように、bisectを完全に自動化できるように複雑なスクリプトを開発する価値がある場合があります。

Ingo Molnar はこれについて以下のように言っています [7]:

私は完全に自動化された起動時ハングアップ対応bisectスクリプトを持っています。 これは「git-bisect run」に基づいています。 スクリプトを実行すると、カーネルが完全に自動的にビルドおよび起動され、起動が失敗すると(スクリプトは、継続的に監視するシリアルログを介して、またはタイムアウトを介して、システムが10分以内に起動しない場合は 「bad」カーネルとします)、スクリプトはビープ音を介して私の注意を喚起し、テストボックスの電源を入れ直します。 (ええもちろん、それを100%自動化するためには、制御された電源コンセントを利用する必要があります)

Combining test suites, git bisect and other systems together

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

たとえば、一部のテストスイートは、いくつかの異常な(またはランダムな)構成で夜間に自動的に実行される可能性があります。 また、テストスイートでデグレが見つかった場合は、「git bisect」を自動的に起動し、その結果を「git bisect」で最初に見つかったbadなコミットの作成者や他の人にメールで送信できます。 また、バグ追跡システムの新しいエントリも自動的に作成される可能性があります。

The future of bisecting

"git replace"

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

議論を単純化するために、テスト不能な領域はコミットの単純な文字列であり、あるコミットによって導入された破損によって作成され(bisect breaking commitの場合はBBCと呼びます)、後で別のコミットによって修正された(bisect fixing commitの場合はBFCと呼びます)と仮定します。

例えば:

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

ここで、Yはgoodで、BFCはbadであり、BBCと、X1〜X6はテストできません。

この場合、手動でbisectする場合は、BBCの直前から始まる特別なブランチを作成することができます。 このブランチの最初のコミットは、BFCをつぶしたBBCになるはずです(squash)。 そしてブランチの他のコミットは、BBC と BFC の間のコミットをブランチの最初のコミットでリベースし、BFC の後のコミットもリベースしたものでなければなりません。

例えば:

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

ここで、 ‘'` 付きの(X1’,X2',X3',X4',X5',X6',Z')がリベースされたコミットです。

対話的リベースを使用して、Gitでこのようなブランチを簡単に作成できます。

例えば以下のように使用します:

$ git rebase -i Y Z

そしてBBCの後にBFCを動かしてつぶし(squash)ます。

その後、あなたは新しいブランチで通常どおりbisectを開始でき、最終的に最初のbadコミットを見つけるでしょう。

例えば:

$ git bisect start Z' Y

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

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

しかし、テスト不能な領域を回避する上記の方法は少々ぎこちないです。 特別なブランチを使用することは、これらのブランチを通常のブランチのように開発者が共有できるので便利ですが、リスクは人々がそのようなブランチをたくさん取得することです。 そして、それは通常の「git bisect」ワークフローを混乱させます。 したがって、「git bisect run」を完全に自動的に使用する場合は、スクリプトに特別なコードを追加して、特別なブランチでbisectを再開する必要があります。

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

したがって、bisectするときに 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/ 階層に格納します。 これらの「ref」は、ブランチ(refs/heads/ に格納)またはタグ(refs/tags に格納)のようなものであり、開発者間でブランチまたはタグのように自動的に共有できることを意味します。

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

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

「git replace」の問題の1つは、現在すべての置換refを refs/replace/ に格納していることですが、bisectにのみ役立つ置換refが refs/replace/bisect/ にあるとよいでしょう。 このように、置換参照は二等分にのみ使用できますが、「refs /replace/」に直接含まれる他の参照はほぼ常に使用されます。 git replace の問題点として、現在 `refs/replace/ に全ての置換用refを格納していますが、bisectするためだけに有用な置換用refを refs/replace/bisect/ に格納した方が良いのではないかと思います。こうすることで、 refs/replace/ に直接ある他のrefsがほぼ常に使用される一方で、置換refsはbisectするときだけ使用することができます。

まばらなバグのbisect

「git bisect」のもう1つの可能な改善点は、実行されるテストにオプションで冗長性を追加して、まばらなバグ(sporadic bugs)を追跡する際の信頼性を高めることです。

散発的なバグ(sporadic bugs)と呼ばれるいくつかのバグは、コンパイラの出力に大きく依存していて、すべてのカーネルビルドでは現れないため、この改善は一部のカーネル開発者から要求されています。

このアイデアは、たとえば「git bisect」を実行するたびに、(その子孫あるいは祖先がそれぞれ「good」または「bad」と判定されたために、)すでに「good」または「bad」と判定されたコミットをテストするようユーザーに求めることができます。もしあるコミットが以前に間違って分類されていた場合、できれば多くの間違いが起こる前に、bisectを早期に中止することができます。その場合、ユーザーは何が起こったのかを調べ、修正された bisect ログを使用して bisect を再開しなければなりません。

ベイジアン検索理論(Bayesian Search Theory)を使用してそのようなことを行う、Github上に Ealdwulf Wuffinga によって作成された BBChop というプロジェクトがすでにあります [9] :

BBChopは「gitbisect」(または同等のもの)に似ていますが、バグが断続的に発生する場合に機能します。 つまり、検知漏れ(false negatives)(あるバージョンにバグが含まれていても、今回はそのバージョンが機能する場合)が存在する場合に機能します。 誤検知(false positives)がないことを前提としています(原則として、同一プローチで機能しますが、追加するのは簡単ではない場合があります)。

ただし、BBChopはVCSから独立しているため、GitユーザーとしてはGitに何かを統合する方が簡単です。

結論

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

「git bisect」内のアルゴリズムにいくつかの改善が可能であり、いくつかの新機能が役立つ場合もありますが、全体的な「git bisect」はすでに非常にうまく機能し、多く使用されており、すでに非常に便利です。 最後の主張を裏付けるために、Ingo Molnarが著者に「git bisect を使うとどのくらい時間が節約できると思いますか」と聞かれたときの答えを引用しましょう:

たくさん節約できたよ。

約10年前、Linuxパッチキューの最初の「二等分」を実行しました。 それはGitの前(そしてBitKeeperよりも前)でした。 私は文字通り、パッチを整理し、本質的にそのバグに関連していると推測したスタンドアロンのコミットを作成するのに何日も費やしました。

それはツールとして絶対的に最後の手段でした。 手動の「パッチ二等分」よりも、printkの出力を確認するのに何日も費やしたと思います。

Git bisect を使用すると、これは簡単です。最良の場合、自動化された方法で、20〜30分で最大15ステップのカーネルbisectを実行できます。 手動のヘルプを使用したり、複数の重複するバグをbisectしたりしても、1時間以上かかることはめったにありません。

実際、git bisectがなければ、「デバッグしようと試みさえしない」しないバグがあるので、非常に貴重です。 過去には、デバッグするのに直ぐに絶望的と分かるバグパターンがありました。それは、せいぜい クラッシュ/バグシグネチャ をlkmlに送信して、他の誰かが何かを考えてくれることを期待するのがせいぜいでした。

そして今や、bisectが失敗したとしても、それはバグについて何か価値のあることを教えてくれます。それは非決定論的であり、タイミングやカーネルイメージのレイアウトに依存します。

つまり、git bisect は無条件に良いものなのです — このフレーズは自由に引用していいです ;-)

謝辞

この論文のレビュー、Gitメーリングリストに送信したパッチのレビュー、いくつかのアイデアの議論と改善の支援、「git bisect」の大幅な改善、Gitの開発および維持における素晴らしい仕事に協力してくれた濱野 純(Junio Hamano)に感謝します。

この論文に登場する非常に有用な情報を提供してくれた Ingo Molnar、この論文へのコメント、「git bisect」を改善するための提案、およびLinuxカーネルメーリングリストでの「git bisect」の伝道に感謝します。

「gitbisect」、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 プログラム委員会に感謝します。

References