チャプター ▾ 第2版

7.9 Git ツール - Rerere

Rerere

git rerere 機能は、少し隠れた機能です。この名前は「reuse recorded resolution (記録された解決策の再利用)」の略であり、その名の通り、Git に対してチャンクのコンフリクトをどのように解決したかを記憶させ、次回同じコンフリクトが発生した際に Git が自動的に解決できるようにする機能です。

この機能は、様々なシナリオで非常に便利です。ドキュメントで言及されている例の1つは、長期にわたるトピックブランチが最終的にクリーンにマージされることを確認したいが、大量の中間マージコミットでコミット履歴を汚したくない場合です。rerere を有効にすると、時折マージを試み、コンフリクトを解決し、その後マージを取り消すことができます。これを継続的に行うと、rerere がすべて自動的に処理してくれるため、最終的なマージは簡単になるはずです。

この同じ戦術は、ブランチをリベースし続けることで、リベースするたびに同じリベースのコンフリクトに対処する必要がなくなる場合にも使用できます。または、マージして多数のコンフリクトを修正したブランチを、代わりにリベースすることにした場合でも、おそらく同じコンフリクトをすべて再度行う必要はありません。

rerere のもう一つの応用例は、Git プロジェクト自身がよく行うように、多数の進化するトピックブランチをたまにテスト可能なヘッドにマージする場合です。テストが失敗した場合、コンフリクトを再解決することなく、テストを失敗させたトピックブランチなしでマージを巻き戻してやり直すことができます。

rerere 機能を有効にするには、以下の設定を実行するだけです。

$ git config --global rerere.enabled true

特定のレポジトリに .git/rr-cache ディレクトリを作成することでも有効にできますが、設定の方がより明確で、その機能をグローバルに有効にします。

では、以前の例と同様の簡単な例を見てみましょう。以下のような hello.rb というファイルがあるとします。

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

片方のブランチで「hello」という単語を「hola」に変更し、もう一方のブランチで「world」を「mundo」に変更します。これは以前と同じです。

Two branches changing the same part of the same file differently
図 160. 同じファイルの同じ部分を異なる方法で変更する2つのブランチ

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 とはあまり関係ありませんが)、git 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'」と解決し、再度 git 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

つまり、Git が hello.rb ファイルで、片側に「hello mundo」、もう片側に「hola world」があるチャンクのコンフリクトを見た場合、それを「hola mundo」に解決するという意味です。

これで、解決済みとしてマークしてコミットできます。

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

「Recorded resolution for FILE」と表示されます。

Recorded resolution for FILE
図 161. FILE の解決が記録されました

さて、そのマージを取り消し、代わりに master ブランチの上にリベースしてみましょう。リセットの解明で見たように、git 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
Automatically resolved merge conflict using previous resolution
図 162. 以前の解決策を使用して自動的に解決されたマージコンフリクト

git 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

高度なマージでこの例を見ました。ただし、今回は git 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 を有効にして作業を少し楽にすることができます。

scroll-to-top