Git
Chapters ▾ 2nd Edition

7.9 Git のさまざまなツール - Rerere

Rerere

git rerere コマンドはベールに包まれた機能といってもいいでしょう。これは “reuse recorded resolution” の略です。その名が示すとおり、このコマンドは、コンフリクトがどのように解消されたかを記録してくれます。そして、同じコンフリクトに次に出くわしたときに、自動で解消してくれるのです。

いくつもの場面で、この機能がとても役立つと思います。Git のドキュメントで挙げられている例は、長期にわたって開発が続いているトピックブランチを問題なくマージされるようにしておきたいけれど、そのためのマージコミットがいくつも生まれるような状況は避けたい、というものです。rerere を有効にした状態で、マージをときおり実行し、コンフリクトをそのたびに解消したうえで、マージを取り消してみてください。この手順を継続的に行っておけば、最終的なマージは容易なものになるはずです。rerere がすべてを自動で処理してくれるからです。

リベースする度に同じコンフリクトを処理することなく、ブランチをリベースされた状態に保っておくときにもこの方法が使えます。あるいは、コンフリクトをすべて解消して、ようやっとマージし終えた後に、リベースを使うことに方針を変更したとしましょう。rerere を使えば、同じコンフリクトを再度処理せずに済みます。

その他にも、開発中のトピックブランチをいくつもまとめてマージして、テスト可能な HEAD を生成するとき(Git 本体のプロジェクトでよく行われています)にもこのコマンドが使えます。テストが失敗したら、マージを取り消したうえで失敗の原因となったブランチを除外してからテストを再実行するわけですが、rerere を使えばその際にコンフリクトを解消する必要がなくなるのです。

rerere を有効にするには、以下の設定コマンドを実行しましょう。

$ git config --global rerere.enabled true

該当のリポジトリに .git/rr-cache というディレクトリを作成しても rerere は有効になりますが、設定するほうがわかりやすいでしょう。設定であれば、全リポジトリに適用することもできます。

では実際の例を見てみましょう。以前使ったような単純な例です。 hello.rb というファイル名の、以下のようなファイルがあったとします。

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

今いるブランチではこのファイルの “hello” という単語を “hola” に変更し、別のブランチでは “world” を “mundo” に変更したとします。前回と同様ですね。

rerere1

これら2つのブランチをマージしようとすると、コンフリクトが発生します。

$ git merge i18n-world
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Recorded preimage for 'hello.rb'
Automatic merge failed; fix conflicts and then commit the result.

コマンド出力に Recorded preimage for FILE という見慣れない行があるのに気づかれたでしょう。他の部分は、よくあるコンフリクトのメッセージと変わりありません。この時点で、rerere からわかることがいくつかあります。こういった場合、いつもであれば以下のように git status を実行し、何がコンフリクトしているのかを確認するものです。

$ git status
# On branch master
# Unmerged paths:
#   (use "git reset HEAD <file>..." to unstage)
#   (use "git add <file>..." to mark resolution)
#
#	both modified:      hello.rb
#

ですが、ここで git rerere status を実行すると、どのファイルのマージ前の状態が git rerere によって保存されたかがわかります。

$ git rerere status
hello.rb

更に、git rerere diff を実行すると、コンフリクト解消の状況がわかります。具体的には、着手前がどういう状態であったか、どういう風に解消したのか、がわかります。

$ git rerere diff
--- a/hello.rb
+++ b/hello.rb
@@ -1,11 +1,11 @@
 #! /usr/bin/env ruby

 def hello
-<<<<<<<
-  puts 'hello mundo'
-=======
+<<<<<<< HEAD
   puts 'hola world'
->>>>>>>
+=======
+  puts 'hello mundo'
+>>>>>>> i18n-world
 end

また(rerere 特有の話ではありませんが)、コンフリクトしているファイルと、そのファイルの3バージョン(マージ前・コンフリクトマーカー左向き・コンフリクトマーカー右向き)が ls-files -u を使うとわかります。

$ git ls-files -u
100644 39804c942a9c1f2c03dc7c5ebcd7f3e3a6b97519 1	hello.rb
100644 a440db6e8d1fd76ad438a49025a9ad9ce746f581 2	hello.rb
100644 54336ba847c3758ab604876419607e9443848474 3	hello.rb

さて、このコンフリクトは puts 'hola mundo' と修正しておきます。そして、 もう一度 rerere diff コマンドを実行すると、rerere が記録する内容を確認できます。

$ git rerere diff
--- a/hello.rb
+++ b/hello.rb
@@ -1,11 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-<<<<<<<
-  puts 'hello mundo'
-=======
-  puts 'hola world'
->>>>>>>
+  puts 'hola mundo'
 end

これを記録したということは、hello.rb に同じコンフリクト(一方は “hello mundo” でもう一方が “hola world”)が見つかった場合、自動的に “hola mundo” に修正されるということになります。

では、この変更内容をコミットしましょう。

$ git add hello.rb
$ git commit
Recorded resolution for 'hello.rb'.
[master 68e16e5] Merge branch 'i18n'

コマンド出力から、Git がコンフリクト解消方法を記録した("Recorded resolution for FILE")ことがわかります。

rerere2

ではここで、このマージを取り消して master ブランチにリベースしてみましょう。リセットコマンド詳説 で紹介したとおり、ブランチを巻き戻すには reset を使います。

$ git reset --hard HEAD^
HEAD is now at ad63f15 i18n the hello

マージが取り消されました。続いてトピックブランチをリベースしてみます。

$ git checkout i18n-world
Switched to branch 'i18n-world'

$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: i18n one word
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Failed to merge in the changes.
Patch failed at 0001 i18n one word

予想どおり、マージコンフリクトが発生しました。一方、Resolved FILE using previous resolution というメッセージも出力されています。該当のファイルを確認してみてください。コンフリクトはすでに解消されていて、コンフリクトを示すマーカーは挿入されていないはずです。

#! /usr/bin/env ruby

def hello
  puts 'hola mundo'
end

また、ここで git diff を実行すると、コンフリクトの再解消がどのように自動処理されたかがわかります。

$ git diff
diff --cc hello.rb
index a440db6,54336ba..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end
rerere3

なお、checkout コマンドを使うと、ファイルがコンフリクトした状態を再現できます。

$ git checkout --conflict=merge hello.rb
$ cat hello.rb
#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

これは 高度なマージ手法 で使用した例と同じ内容ですが、ここでは rerere を使ってコンフリクトをもう一度解消してみましょう。

$ git rerere
Resolved 'hello.rb' using previous resolution.
$ cat hello.rb
#! /usr/bin/env ruby

def hello
  puts 'hola mundo'
end

rerere がキャッシュした解消方法で、再処理が自動的に行われたようです。結果をインデックスに追加して、リベースを先に進めましょう。

$ git add hello.rb
$ git rebase --continue
Applying: i18n one word

マージの再実行を何度も行うことがある、頻繁に master ブランチをマージせずにトピックブランチを最新の状態に保ちたい、リベースをよく行う……いずれかに当てはまる場合は rerere を有効にしておきましょう。日々の生活がちょっとだけ楽になると思います。