章 ▾ 第2版

7.10 Gitツール - Gitでのデバッグ

Gitでのデバッグ

Gitは主にバージョン管理のために利用されますが、ソースコードプロジェクトのデバッグに役立つコマンドもいくつか提供しています。Gitはほぼあらゆる種類のコンテンツを扱えるように設計されているため、これらのツールは非常に汎用的ですが、問題が発生した際にバグや原因の特定に役立つことがよくあります。

ファイルの注釈付け

コード内のバグを突き止め、それがいつ、なぜ導入されたのかを知りたい場合、ファイルの注釈付けは多くの場合、最良のツールとなります。これは、どのコミットがファイルの各行を最後に変更したかを示します。そのため、コード内のメソッドにバグがあることがわかった場合、git blameを使ってファイルに注釈を付け、その行が導入された原因となったコミットを特定できます。

以下の例では、git blameを使用して、トップレベルのLinuxカーネルのMakefile内の行について、どのコミットとコミッターが担当したかを特定し、さらに-Lオプションを使用して、そのファイルの69行目から82行目までの注釈の出力を制限しています。

$ git blame -L 69,82 Makefile
b8b0618cf6fab (Cheng Renquan  2009-05-26 16:03:07 +0800 69) ifeq ("$(origin V)", "command line")
b8b0618cf6fab (Cheng Renquan  2009-05-26 16:03:07 +0800 70)   KBUILD_VERBOSE = $(V)
^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 71) endif
^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 72) ifndef KBUILD_VERBOSE
^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 73)   KBUILD_VERBOSE = 0
^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 74) endif
^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 75)
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 76) ifeq ($(KBUILD_VERBOSE),1)
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 77)   quiet =
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 78)   Q =
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 79) else
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 80)   quiet=quiet_
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 81)   Q = @
066b7ed955808 (Michal Marek   2014-07-04 14:29:30 +0200 82) endif

最初のフィールドは、その行を最後に変更したコミットのSHA-1の一部であることに注意してください。次の2つのフィールドは、そのコミットから抽出された値(作者名とそのコミットの作成日)であり、誰がいつその行を変更したかを簡単に確認できます。その後に、行番号とファイルの内容が続きます。また、^1da177e4c3f4のようなコミット行にも注目してください。ここで^プレフィックスは、リポジトリの最初のコミットで導入されて以来、変更されていない行を示します。これは少し混乱を招くかもしれません。というのも、GitがコミットのSHA-1を変更するために^を使用する方法が少なくとも3つあることをこれまでに見てきたからです。しかし、ここではそのように解釈されます。

Gitのもう一つの優れた点は、ファイルの名前変更を明示的に追跡しないことです。スナップショットを記録し、後から暗黙的に何が名前変更されたかを判断しようとします。これの興味深い機能の1つは、あらゆる種類のコード移動も判別できることです。git blame-Cオプションを渡すと、Gitは注釈を付けているファイルを分析し、コードスニペットが他の場所からコピーされた場合、それが元々どこから来たのかを特定しようとします。例えば、GITServerHandler.mというファイルを複数のファイルにリファクタリングしており、そのうちの1つがGITPackUpload.mだとします。-Cオプションを使ってGITPackUpload.mをblameすると、コードの各セクションが元々どこから来たのかを確認できます。

$ git blame -C -L 141,153 GITPackUpload.m
f344f58d GITServerHandler.m (Scott 2009-01-04 141)
f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherObjectShasFromC
f344f58d GITServerHandler.m (Scott 2009-01-04 143) {
70befddd GITServerHandler.m (Scott 2009-03-22 144)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 145)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 146)         NSString *parentSha;
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 147)         GITCommit *commit = [g
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 148)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 149)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 150)
56ef2caf GITServerHandler.m (Scott 2009-01-05 151)         if(commit) {
56ef2caf GITServerHandler.m (Scott 2009-01-05 152)                 [refDict setOb
56ef2caf GITServerHandler.m (Scott 2009-01-05 153)

これは本当に便利です。通常、元のコミットとして表示されるのは、そのコードをコピーしたコミットです。なぜなら、そのファイルでその行を初めて触ったのがその時だからです。Gitは、たとえそれが別のファイルにあったとしても、その行を最初に書いた元のコミットを教えてくれます。

ファイルの注釈付けは、問題の箇所を最初から知っている場合に役立ちます。何が壊れているのかわからず、コードが機能していた最後の状態から数十または数百のコミットがある場合、git bisectに助けを求めることになるでしょう。bisectコマンドは、コミット履歴に対してバイナリサーチを実行し、どのコミットが問題を導入したかを可能な限り迅速に特定するのに役立ちます。

あなたのコードのリリースを本番環境にプッシュしたばかりで、開発環境では発生していなかったことについてバグ報告を受けており、なぜコードがそのような動作をするのか想像もつかないとします。コードに戻ると、その問題を再現できることがわかりますが、何が問題なのかは特定できません。その問題を特定するためにコードをバイセクトできます。まずgit bisect startを実行して開始し、次にgit bisect badを使って、現在いるコミットが壊れていることをシステムに伝えます。次に、git bisect good <good_commit>を使って、最後に正常だった状態がいつだったかをbisectに伝えなければなりません。

$ git bisect start
$ git bisect bad
$ git bisect good v1.0
Bisecting: 6 revisions left to test after this
[ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] Error handling on repo

Gitは、最後に正常なコミット(v1.0)としてマークしたコミットと現在の壊れたバージョンとの間に約12のコミットがあることを特定し、その中間にあるコミットをチェックアウトしました。この時点で、テストを実行して、このコミットで問題が存在するかどうかを確認できます。もし問題が存在すれば、この中間コミットより前に導入されたことになります。存在しなければ、問題は中間コミットより後に導入されたことになります。ここでは問題がないことが判明し、git bisect goodと入力してGitにそれを伝え、作業を続けます。

$ git bisect good
Bisecting: 3 revisions left to test after this
[b047b02ea83310a70fd603dc8cd7a6cd13d15c04] Secure this thing

次に、先ほどテストしたコミットと壊れたコミットの中間にある別のコミットに移動します。もう一度テストを実行すると、このコミットが壊れていることが判明するため、git bisect badでGitにそのことを伝えます。

$ git bisect bad
Bisecting: 1 revisions left to test after this
[f71ce38690acf49c1f3c9bea38e09d82a5ce6014] Drop exceptions table

このコミットは問題なく、これでGitは問題がどこで導入されたかを特定するために必要なすべての情報を持っています。最初の壊れたコミットのSHA-1を伝え、そのコミットのコミット情報と変更されたファイルの一部を表示することで、このバグを導入した可能性のある出来事を特定できるようになります。

$ git bisect good
b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit
commit b047b02ea83310a70fd603dc8cd7a6cd13d15c04
Author: PJ Hyett <pjhyett@example.com>
Date:   Tue Jan 27 14:48:32 2009 -0800

    Secure this thing

:040000 040000 40ee3e7821b895e52c1695092db9bdc4c61d1730
f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4 M  config

完了したら、git bisect resetを実行して、開始前のHEADの状態に戻すべきです。そうしないと、奇妙な状態になる可能性があります。

$ git bisect reset

これは、数分で何百ものコミットから導入されたバグをチェックできる強力なツールです。実際、プロジェクトが正常であれば0を返し、問題があれば非0を返すスクリプトがあれば、git bisectを完全に自動化できます。まず、既知の壊れたコミットと正常なコミットを提供することで、バイセクトの範囲を再度伝えます。これを希望する場合は、bisect startコマンドでそれらをリストアップし、既知の壊れたコミットを最初に、既知の正常なコミットを2番目にリストアップすることで実行できます。

$ git bisect start HEAD v1.0
$ git bisect run test-error.sh

これにより、Gitが最初に壊れたコミットを見つけるまで、チェックアウトされた各コミットでtest-error.shが自動的に実行されます。また、makemake testsなど、自動テストを実行するものを実行することもできます。

scroll-to-top