Git
Chapters ▾ 2nd Edition

7.9 Инструменты Git - Rerere

Rerere

Функциональность git rerere — частично скрытый компонент Git. Её имя является сокращением для «reuse recorded resolution» («повторно использовать сохранённое решение»). Как следует из названия, эта функциональность позволяет попросить Git запомнить то, как вы разрешили некоторую часть конфликта, так что в случае возникновения такого же конфликта, Git сможет его разрешить автоматически.

Существует несколько ситуаций, в которых данная функциональность может быть действительно удобна. Один из примеров, упомянутый в документации, состоит в том, чтобы обеспечить в будущем простоту слияния некоторой долгоживущей ветки, не создавая при этом набор промежуточных коммитов слияния. При использовании rerere вы можете время от времени выполнять слияния, разрешать конфликты, а затем откатывать слияния. Если делать это постоянно, то итоговое слияние должно пройти легко, так как rerere сможет разрешить все конфликты автоматически.

Такая же тактика может быть использована, если вы хотите сохранить ветку легко перебазируемой, то есть вы не хотите сталкиваться с одними и теми же конфликтами каждый раз при перебазировании. Или, например, вы решили ветку, которую уже сливали и разрешали при этом некоторые конфликты, вместо слияния перебазировать — навряд ли вы захотите разрешать те же конфликты ещё раз.

Другая ситуация возникает, когда вы изредка сливаете несколько веток, относящихся к ещё разрабатываемым задачам, в одну тестовую ветку, как это часто делается в проекте самого Git. Если тесты завершатся неудачей, вы можете откатить все слияния и повторить их, исключив из них ветку, которая поломала тесты, при этом не разрешая конфликты снова.

Для того, чтобы включить функциональность rerere, достаточно изменить настройки следующим образом:

$ git config --global rerere.enabled true

Также вы можете включить её, создав каталог .git/rr-cache в нужном репозитории, но включение через настройки понятнее и может быть сделано глобально.

Давайте рассмотрим простой пример, подобный используемому ранее. Предположим, у нас есть файл вида:

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

Как и ранее, в одной ветке мы изменили слово «hello» на «hola», а в другой — слово «world» на «mundo».

rerere1

Когда мы будем сливать эти две ветки в одну, мы получим конфликт:

$ 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), вы можете использовать команду 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

То есть, когда 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»).

rerere2

Теперь давайте отменим это слияние и перебазируем ветку i18n-world поверх master. Как мы видели в Раскрытие тайн reset, мы можем переместить нашу ветку назад, используя команду reset.

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

Наше слияние отменено. Теперь давайте перебазируем ветку i18n-world.

$ 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

Итак, если вы выполняете много повторных слияний или хотите сохранять тематическую ветку в состоянии, актуальном вашей основной ветке, без множества слияний в истории, или часто перебазируете ветки, то вы можете включить rerere. Это, в какой-то мере, упростит вам жизнь.

scroll-to-top