Help us understand the problem. What is going on with this article?

5分でざっくりわかる git diff の indent Heuristic オプションの仕組み

More than 3 years have passed since last update.

はじめに

git diff で hunk が自分が追加削除したとおりに出なくてイラッとしたことはありませんか?
それを解消するために git 2.9 で --compaction-heuristic というオプションが作られより適切な hunk を表示しようという試みがなされました。当時のリリースノート
そしてgit 2.11 からより僕らの意図通りの hunk を表示するための仕組みとして --indent-heuristic というオプションが追加されました。Githubからのリリース記事

追加する方法

git 2.11 ではない方はまずアップグレードしてください。
とりあえず試してみたい場合は git diff --indent-heuristic

常に有効にしたい場合は .gitconfig に以下のオプションを追加します。

[diff]
    indentHeuristic = true

git 2.9 からなにが変わったのか

git 2.9で導入された --compaction-heuristic ではいくつかのエッジケースで対応できないものがありました。

例えば以下のような変更をしたとします。

sample.go
// before
package main

import (
    "fmt"
)

func main() {
    var list []int
    for i := 0; i < 10; i++ {
        list = append(list, i)
    }
    fmt.Printf("%#v", list)
}
sample.go
// after
package main

import (
    "fmt"
)

func main() {
    var list []int
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
    for i := 0; i < 10; i++ {
        list = append(list, i)
    }
    fmt.Printf("%#v", list)
}

この差分を --compaction-heuristic オプションを使って取ると以下のようになってしまいます。

knsh14% git diff --compaction-heuristic
diff --git a/sample.go b/sample.go
index 23e185c..c7a113e 100644
--- a/sample.go
+++ b/sample.go
@@ -1,13 +1,16 @@
 package main

 import (
    "fmt"
 )

 func main() {
    var list []int
    for i := 0; i < 10; i++ {
+       fmt.Println(i)
+   }
+   for i := 0; i < 10; i++ {
        list = append(list, i)
    }
    fmt.Printf("%#v", list)
 }

追加したのは最初のforループなのでこれだとおかしな感じですね。1行下にずれています。

これを解消するために内部のアルゴリズムをごっそり入れ替えた --indent-heuristic が導入されました。

有効にした場合はこのようになります。

knsh14% git diff --indent-heuristic
diff --git a/sample.go b/sample.go
index 23e185c..c7a113e 100644
--- a/sample.go
+++ b/sample.go
@@ -1,13 +1,16 @@
 package main

 import (
    "fmt"
 )

 func main() {
    var list []int
+   for i := 0; i < 10; i++ {
+       fmt.Println(i)
+   }
    for i := 0; i < 10; i++ {
        list = append(list, i)
    }
    fmt.Printf("%#v", list)
 }

正しく表示されましたね。

では一体どのようにしてこれを実現しているのでしょうか。

どういう仕組になっているのか

先程のGithubの記事コミットログへのリンクがあるので内容を読んでみます。

そうすると以下のようなロジックで適切なものを選んでいるようです。

indent heuristic の概要

  1. 通常のdiff から普通の hunk を用意する
  2. この時の hunk score を算出する
  3. 1行ずらしてまたスコアを算出する
  4. ずらしていって最小になったときのものを採用する

1, 3, 4 はいいとしてキモは2のどうやってスコアを算出してるかです。

コードを見てみると measure_split 関数でその時点でのグループの上側と下側の情報を収集し、 score_add_split 関数でそれぞれのスコアを算出し、それを合わせたものをその時のスコアとして比較しているようです。
詳しいコードはずらしながら情報を収集している箇所がこのあたり 、スコアを算出しているロジックがこのあたり、最後に2つのスコアを合わせてその時点のスコアを求めている関数がここ になっています。
更に詳しく知りたい場合は読んでみてください。

まとめ

git diff で hunk をきれいに出すためのオプションの中身を覗いてみました。
--indent-heuristic は今後diffのデフォルトになるとリリースノートでも言われているので、早くGithub や Github Enterpriseにも来てほしいなあと思います。

mercari
フリマアプリ「メルカリ」を、グローバルで開発しています。
https://tech.mercari.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away