章 ▾ 第2版

7.9 Gitツール - Rerere

Rerere

git rerere 機能は、少し隠れた機能です。この名前は「reuse recorded resolution」(記録された解決の再利用)の略で、その名の通り、一度解決したハンクの競合をGitに記憶させ、次回同じ競合が発生した際に自動的に解決させることを可能にします。

この機能が非常に便利になるシナリオは数多くあります。ドキュメントに記載されている例の一つは、長期にわたるトピックブランチが最終的にきれいにマージされることを確認したいが、中間マージコミットがコミット履歴を乱雑にするのを避けたい場合です。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つのブランチをマージすると、マージ競合が発生します。

$ 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 ブランチの上にリベースしてみましょう。Resetの解明で見たように、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