Gitはファイルの移動を追跡できない

Gitのリポジトリ内のオブジェクトについて知っている人ならば知っていると思うが、Gitはリポジトリをコミット間の差分ではなくスナップショットの連続として扱っている。つまり、コミットする前と後で「何をどうした」を記録しているわけではなく、 Before-After のみを記録している。 log -p などはその場で diff を生成しているに過ぎない。
では、ファイルの移動やリネームはどのように表現されているのか。

恐らく*1、ファイルの移動やリネームは一切記録されていない。

$ git --version
git version 1.6.1.3

$ git init -q
$ echo "aaa" > a.txt
$ git add a.txt
$ git commit -m 'Add a.txt.'
[master (root-commit)]: created 4b3596b: "Add a.txt."
 1 files changed, 1 insertions(+), 0 deletions(-)
  create mode 100644 a.txt

$ echo "bbb" >> a.txt
$ git commit -a -m 'Append line "bbb" to a.txt.'
[master]: created 82a7bd8: "Append line "bbb" to a.txt."
 1 files changed, 1 insertions(+), 0 deletions(-)

$ mkdir dir
$ git mv a.txt dir
$ git commit -m 'Move a.txt to dir.'
[master]: created 9b6b6f4: "Move a.txt to dir."
 1 files changed, 0 insertions(+), 0 deletions(-)
 rename a.txt => dir/a.txt (100%)

$ echo "ccc" >> dir/a.txt
$ git commit -a -m 'Append line "ccc" to dir/a.txt.'
[master]: created fcea3d7: "Append line "ccc" to dir/a.txt."
 1 files changed, 1 insertions(+), 0 deletions(-)

$ git log --pretty=oneline dir/a.txt
fcea3d7ddd252874d9a9e535d8b65f9f4e302399 Append line "ccc" to dir/a.txt.
9b6b6f46095f83f31d449cc98c00c150d5b9f0bd Move a.txt to dir.

一応、git log に -C オプションを指定するとリネームを検出してくれる。

$ git log --stat --pretty=oneline
fcea3d7ddd252874d9a9e535d8b65f9f4e302399 Append line "ccc" to dir/a.txt.
 dir/a.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
9b6b6f46095f83f31d449cc98c00c150d5b9f0bd Move a.txt to dir.
 a.txt     |    2 --
 dir/a.txt |    2 ++
 2 files changed, 2 insertions(+), 2 deletions(-)
82a7bd82cf0ca38829d4d2f4c6776a62fd981714 Append line "bbb" to a.txt.
 a.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
4b3596b99f10773fa0f132056248defafd29b9d4 Add a.txt.
 a.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

$ git log -C --stat --pretty=oneline
fcea3d7ddd252874d9a9e535d8b65f9f4e302399 Append line "ccc" to dir/a.txt.
 dir/a.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
9b6b6f46095f83f31d449cc98c00c150d5b9f0bd Move a.txt to dir.
 a.txt => dir/a.txt |    0
 1 files changed, 0 insertions(+), 0 deletions(-)
82a7bd82cf0ca38829d4d2f4c6776a62fd981714 Append line "bbb" to a.txt.
 a.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
4b3596b99f10773fa0f132056248defafd29b9d4 Add a.txt.
 a.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

が、追跡はしてくれない。

$ git log -C --stat --pretty=oneline dir/a.txt
fcea3d7ddd252874d9a9e535d8b65f9f4e302399 Append line "ccc" to dir/a.txt.
 dir/a.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
9b6b6f46095f83f31d449cc98c00c150d5b9f0bd Move a.txt to dir.
 dir/a.txt |    2 ++
 1 files changed, 2 insertions(+), 0 deletions(-)

昔あったファイルと同名のファイルを作ってしまうと面倒なことになる。パスしか見ていない為だと思われる。

$ echo "new file" > a.txt
$ git add a.txt
$ git commit -m 'Add new file "a.txt".'
[master]: created 7a6c6c4: "Add new file "a.txt"."
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 a.txt

$ git log -C --stat --pretty=oneline a.txt
7a6c6c4bbb326ccddbdfa013dcaa3b8ca6759fdf Add new file "a.txt".
 a.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
9b6b6f46095f83f31d449cc98c00c150d5b9f0bd Move a.txt to dir.
 a.txt |    2 --
 1 files changed, 0 insertions(+), 2 deletions(-)
82a7bd82cf0ca38829d4d2f4c6776a62fd981714 Append line "bbb" to a.txt.
 a.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)
4b3596b99f10773fa0f132056248defafd29b9d4 Add a.txt.
 a.txt |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

リネーム検出に関する雑多な話。

  • -C オプションによるリネームの検出はある程度ファイルに変更があっても検出してくれて結構偉いのだが、あくまで推測なので、あまりに変更点が多すぎると削除+追加になる。
    • 逆に言えば削除+似たようなファイルを追加すると移動と誤検出される可能性もある。
    • どの程度同じだと移動になるのかは調べてないのでよくわからない。ソース見た方が早そう?
  • -C オプションではコピーの検出も行われるが、パフォーマンスの関係でコピー前のファイルが変更されていないと検出されない。
    • 検出されるのは変更前のファイルのコピーで、変更後のファイルをコピーしても検出されない。
    • パフォーマンスを考えずに一生懸命コピーを探すには、 --find-copies-harder を使う。

こんな感じなので、特定の履歴を辿ろうとした場合、移動やコピーが含まれると非常に困難、もしくは不可能になってしまう。リファクタリングを頻繁に行うようなプロジェクトでは困ったことになりそうだ。
せめてリネームを検出したらそこを辿って欲しいところ。
まあ個人的には「推測」の時点であまり信用できないなぁと思ってしまうわけだけれども。
「移動」か「新規」かは意味的に全然違うので、そこが表現できないのは結構痛いなぁ。

2009/04/07 追記

git log --follow でリネーム検出したファイルのログも追えるようです。ここでkanaさんに教えて頂きました。ありがとうございます。

*1:「恐らく」と書いたのははっきりそうであると書かれた資料を見つけたわけではないからです。もし「そんなこたーない」って方がいらっしゃいましたら是非教えて下さい。