Chapters ▾ 2nd Edition

7.9 Ferramentas do Git - Rerere

Rerere

A funcionalidade git rerere é um pouco uma funcionalidade oculta. O nome significa “reutilizar a resolução gravada” (reuse recorded resolution) e, como o nome indica, ele permite que você peça ao Git para lembrar como você resolveu um conflito de hunk para que da próxima vez que ele vir o mesmo conflito, o Git possa resolvê-lo para você automaticamente.

Há uma série de cenários em que esta funcionalidade pode ser muito útil. Um dos exemplos mencionados na documentação é quando você deseja garantir que uma branch de tópico de longa duração, no final das contas, faça o merge perfeitamente, mas você não quer ter um monte de commits de merge intermediários sobrecarregando seu histórico de commits. Com o rerere habilitado, você pode tentar o merge ocasional, resolver os conflitos, depois recuar o merge. Se você fizer isso de forma contínua, o merge final deve ser fácil porque o rerere pode simplesmente fazer tudo para você automaticamente.

Esta mesma tática pode ser usada se você quiser manter uma branch rebased, assim não terá que lidar com os mesmos conflitos de rebase a cada vez que o fizer. Ou se você quiser pegar uma branch que você fez o merge e consertou um monte de conflitos e então decidir fazer rebase em vez disso — você provavelmente não terá que fazer todos os mesmos conflitos novamente.

Outra aplicação de rerere é quando você faz o merge ocasional de um bando de branches de tópicos em evolução, para formar uma head de testes, como o próprio projeto Git faz frequentemente. Se os testes falharem, você pode rebobinar os merges e refazê-los sem a branch de tópico que fez os testes falharem, sem precisar re-resolver os conflitos novamente.

Para habilitar a funcionalidade rerere, você simplesmente tem que rodar essa configuração:

$ git config --global rerere.enabled true

Você também pode ativá-lo criando o diretório .git/rr-cache em um repositório específico, mas a configuração (config) é mais clara e permite ativar este recurso globalmente para você.

Agora, vamos ver um simples exemplo, semelhante ao nosso anterior. Digamos que temos um arquivo chamado hello.rb parecido com isso:

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

Em uma branch alteramos a palavra “hello” para “hola”, e na outra alteramos “world” para “mundo”, como antes.

Two branches changing the same part of the same file differently
Figure 160. Duas branches mudando diferentemente a mesma parte do mesmo arquivo

Quando nós fizermos o merge em ambas branches, obteremos um conflito de merge:

$ 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.

Você deve ter notado a nova linha Recorded preimage for FILE aí. Sem isso, ela se pareceria exatamente com um normal conflito de merge. Neste ponto, o rerere pode nos dizer algumas coisas. Normalmente, você pode executar git status neste momento para ver quais deram conflito:

$ 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
#

Entretanto, git rerere vai te dizer para qual estado o pre-merge foi gravado, por meio de git rerere status:

$ git rerere status
hello.rb

E o git rerere diff irá exibir a situação atual da resolução — o que você começou para resolver e o que você solucionou.

$ 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

Também (e isto não está realmente relacionado com o rerere), você pode usar o git ls-files -u para ver os arquivos em conflito e as versões anterior, esquerda e direita:

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

Agora você pode resolve-lo para ser simplesmente puts 'hola mundo' e executar o comando git rerere diff novamente, para ver o que o rerere vai se lembrar:

$ 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

Sendo assim, bascicamente ele diz, quando o Git enxergar um conflito de hunk em um arquivo hello.rb que contenha “hello mundo” de um lado e “hola world” do outro, ele o resolverá como “hola mundo”.

Agora podemos marcá-lo como resolvido e realizar o commit dele:

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

Você pode ver que ele gravou a resolução para o ARQUIVO ("Recorded resolution for FILE").

Recorded resolution for FILE
Figure 161. Resolução gravada para o ARQUIVO (Recorded resolution for FILE)

Agora, vamos desfazer aquele merge e em seguida dar rebase em nossa branch master. Para isso, devemos recuar nossa branch por meio de git reset conforme vimos em Reset Desmistificado.

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

Nosso merge está desfeito. Agora vamos fazer o rebase da branch de tópico.

$ 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

Pronto, nós tivemos o mesmo conflito de merge conforme esperávamos, mas dê uma olhada na linha Resolved FILE using previous resolution. Ao olhar o arquivo, nós veremos que ele já foi resolvido, pois não existem marcadores de conflitos nele.

#! /usr/bin/env ruby

def hello
  puts 'hola mundo'
end

Além disso, git diff mostrará a você como ele foi automaticamente resolvido de novo (re-resolved):

$ 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
Figure 162. Conflito de merge resolvido automaticamente usando a resolução anterior

Nós podemos recriar o arquivo num estado de conflito com o 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

Nós já vimos um exemplo disto em Merging Avançado. Mas agora, vamos re-resolvê-lo executando novamente o git rerere:

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

def hello
  puts 'hola mundo'
end

Nós re-resolvemos o arquivo automaticamente com a ajuda do armazenamento cache de soluções do rerere. Agora você pode adicionar e continuar o rebase para completá-lo.

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

Portanto, se você fizer vários re-merges, ou quer manter as suas branch de tópicos atualizadas com o seu branch principal sem um monte de merges, ou você faz rebase com frequência, você pode ligar o rerere para fazer com que a sua vida fique um pouco mais fácil.