章节 ▾ 第二版

7.9 Git 工具 - Rerere

Rerere

git rerere 功能是一个有点隐藏的特性。它的名字代表“reuse recorded resolution”(重用已记录的解决方案),顾名思义,它允许你让 Git 记住你是如何解决一个冲突块的,这样下次它遇到相同的冲突时,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. 两个分支以不同方式更改同一文件的相同部分

当我们合并这两个分支时,我们会得到一个合并冲突

$ 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 也会通过 git rerere status 告诉你它记录了哪些预合并状态

$ 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. 文件的已记录解决方案

现在,让我们撤消该合并,然后将其变基到我们的 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 来让你的生活更轻松一些。