何が起きてこうなったのか

運用しているコードの調査をする際などに、 「このコードはどういう経緯を経てこのようになったのだろう?」 と疑問に抱くことは多々あるかと思います。

この時 git blame を使えば効率的に特定のコードブロックの変更を追うことができます。

この文書では、 git blame を用いて、特定にメソッド変更履歴を追う方法を解説します。

説明のために以下のようなコードがあるとしてください。

  • 1000 行くらいある ruby のファイル hoge.rb がある
  • 中に、 7 行くらいある target_method というメソッドが定義されている
  • target_method はかれこれ 5 回くらい書き換えられている

盛大なネタバレ

このドキュメント無視して tig blame を使うといいよ!

簡単な使い方

git blame はファイルの行ごとに、最後に変更されたリビジョンを表示するコマンドです。

$ git blame hoge.rb
6d305b5190c (Bob   2015-04-07 15:28:07 +0900  1) def foo
6d305b5190c (Bob   2015-04-07 15:28:07 +0900  2)   bar = 1
191b9234aea (Alice 2017-01-12 18:13:36 +0900  3)   baz = 2
dae880d329d (Ted   2015-04-15 16:08:27 +0900  4)   (bar + baz).times do
dae880d329d (Ted   2015-04-15 16:08:27 +0900  5)     puts "!"
191b9234aea (Alice 2017-01-12 18:13:36 +0900  6)   end
dae880d329d (Ted   2015-04-15 16:08:27 +0900  7) end

表示内容は左側から、その行を最後に更新したコミットの SHA-1 の一部、コミッター、コミット日時、行番号、そしてファイルの内容です。

オプション -L で表示範囲を絞り込む

デフォルトではファイル全部の中身を表示するので、実運用上は、 -L オプションを用いて表示内容を絞り込むと便利です。

-L <start>,<end>, -L :<funcname>
  与えられた範囲のみを注釈して表示します。複数回オプションが指定された場合は可能な限り範囲を重複させて表示します。

ここで <start><end> は両方共オプショナルです。ですので、 -L <start>, -L <start>,, -L ,<end> はそれぞれ 「<start> からファイル末尾まで」「<start> からファイル末尾まで」「ファイル先頭から <end> まで」と正しく解釈されます。

<start><end> は次の3つの値をとることができます。

数値を指定する

<start> または <end> には数値を指定することができます。この数値は行番号とみなされます。

$ git blame -L 10,24 app/models/user.rb

正規表現を指定する

POSIXの正規表現を指定することができます。 <start> に正規表現を指定した場合、前の -L の範囲の終わりから検索をし、マッチする行を表示します。 前に -L が設定されていないなら、ファイルの初めから検索をします。 <end> に正規表現をs指定した場合は、<start> から検索をし、マッチする行までを表示します。

$ git blame -L "/def display_name/","/end/" app/models/user.rb

オフセットを指定する

<end> にのみ指定でき、<start> から初めて何行表示するかを指定できます。

$ git blame -L "/def display_name/",+30 app/models/user.rb

関数名を指定する

<funcname> を指定すると、その関数の定義部のみが表示されます。 C言語のみかと思います。少なくとも ruby のコードでは動きませんでした。

$ git blame -L :blame_origin blame.c

この辺のオプションを駆使して、表示範囲を絞り込んでみてみましょう。 例えば、 target_method の定義部分だけをみたいのであれば、こんなオプションが良いでしょう。

$ git blame -L "/def target_method/","/^end/" foo.rb
6d305b5190c (Bob   2015-04-07 15:28:07 +0900  247) def target_method
6d305b5190c (Bob   2015-04-07 15:28:07 +0900  248)   bar = 1
191b9234aea (Alice 2017-01-12 18:13:36 +0900  249)   baz = 2
dae880d329d (Ted   2015-04-15 16:08:27 +0900  250)   (bar + baz).times do
dae880d329d (Ted   2015-04-15 16:08:27 +0900  251)     puts "!"
191b9234aea (Alice 2017-01-12 18:13:36 +0900  252)   end
dae880d329d (Ted   2015-04-15 16:08:27 +0900  253) end

引数 <rev> で blame のもとなるリビジョンを指定する

最新版のソースから blame していても、歴史を追うことはできません。

でも、心配いりません。 git blame <rev> file することで、 blame するソースコードのリビジョンを指定することができます。

先ほどの target_method で絞り込んだ blame 結果をもう一度みてみましょう。

$ git blame -L "/def target_method/","/^end/" foo.rb
6d305b5190c (Bob   2015-04-07 15:28:07 +0900  247) def target_method
6d305b5190c (Bob   2015-04-07 15:28:07 +0900  248)   bar = 1
191b9234aea (Alice 2017-01-12 18:13:36 +0900  249)   baz = 2
dae880d329d (Ted   2015-04-15 16:08:27 +0900  250)   (bar + baz).times do
dae880d329d (Ted   2015-04-15 16:08:27 +0900  251)     puts "!"
191b9234aea (Alice 2017-01-12 18:13:36 +0900  252)   end
dae880d329d (Ted   2015-04-15 16:08:27 +0900  253) end

この中で、最も新しいのは、 2017/01/12 にコミットされた 191b9234aea だとわかります。

ですので、1つ前のコミットをもとに blame すると、191b9234aea が適用される前の状態での blame 結果がわかります。

$ git blame -L "/def target_method/","/^end/" 191b9234aea^1 foo.rb
6d305b5190c (Bob   2015-04-07 15:28:07 +0900  247) def target_method
6d305b5190c (Bob   2015-04-07 15:28:07 +0900  248)   bar = 1
ibe7a4b882d (Carol 2016-12-24 18:13:36 +0900  249)   baz = @global_val
dae880d329d (Ted   2015-04-15 16:08:27 +0900  250)   (bar + baz).times do
dae880d329d (Ted   2015-04-15 16:08:27 +0900  251)     puts "!"
ibe7a4b882d (Carol 2016-12-24 18:13:36 +0900  249)   end if baz > 2
dae880d329d (Ted   2015-04-15 16:08:27 +0900  253) end

これを繰り返すことで、歴史を延々と遡れますね!

辿りやすくしよう

毎回このコマンドを手打ちすると考えるとバカバカしく感じます。

そんなわけで、使い捨てのスクリプトを書いておきましょう。

これで辿りやすくなりましたね!

車輪の再開発乙。こういうのはだいたい先人が作っているものだって。

ここまで、わざわざいろいろ調べて自作しましたが、この手のツールは大抵の場合は誰かが実装しています。

vcs-ann は git または svn に対応した blame 結果のブラウジングツールです。便利。

あと、そもそも tig を普段使いしている人であれば、 tig blame すれば十分に履歴をたどれます。

やっぱり世界は広いですね。