One of the things I didn’t touch on at all in the book is the git rerere
functionality. This also came up recently
during one of my trainings, and I realize that a lot of people probably could
use this, so I wanted to let you all now about it.
The git rerere
functionality is a bit of a hidden feature (Git actually has
a lot of cool hidden features, if you haven’t figured that out yet). The name
stands for “reuse recorded resolution” and as the name implies, it allows you
to ask Git to remember how you’ve resolved a hunk conflict so that the next time
it sees the same conflict, Git can automatically resolve it for you.
There are a number of scenarios in which this functionality might be really
handy. One of the examples that is mentioned in the documentation is if you
want to make sure a long lived topic branch will merge cleanly but don’t want
to have a bunch of intermediate merge commits. With rerere
turned on you
can merge occasionally, resolve the conflicts, then back out the merge. If
you do this continuously, then the final merge should be easy because rerere
can just do everything for you automatically.
This same tactic can be used if you want to keep a branch rebased so you don’t have to deal with the same rebasing conflicts each time you do it. Or if you want to take a branch that you merged and fixed a bunch of conflicts and then decide to rebase it instead - you likely won’t have to do all the same conflicts again.
The other situation I can think of is where you merge a bunch of evolving topic branches together into a testable head occasionally. If the tests fail, you can rewind the merges and re-do them without the topic branch that made the tests fail without having to re-resolve the conflicts again.
To enable the rerere functionality, you simply have to run this config setting:
$ git config --global rerere.enabled true
You can also turn it on by creating the .git/rr-cache
directory in a specific
repository, but I think the config setting is clearer, and it can be done globally.
Now let’s see a simple example. If we have a file that looks like this:
#! /usr/bin/env ruby
def hello
puts 'hello world'
end
and in one branch we change the word ‘hello’ to ‘hola’, then in another branch we change the ‘world’ to ‘mundo’.
When we merge the two branches together, we’ll get a merge conflict:
$ 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.
You should notice the new line Recorded preimage for FILE
in there. Otherwise
it should look exactly like a normal merge conflict. At this point, rerere
can tell us some stuff. Normally, you might run git status
at this point to
see what all conflicted:
$ 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
#
However, git rerere
will also tell you what it has recorded the pre-merge
state for with git rerere status
:
$ git rerere status
hello.rb
And git rerere diff
will show the current state of the resolution - what you
started with to resolve and what you’ve resolved it to.
$ 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
Also (and this isn’t really related to rerere
), you can use ls-files -u
to
see the conflicted files and the before, left and right versions:
$ git ls-files -u
100644 39804c942a9c1f2c03dc7c5ebcd7f3e3a6b97519 1 hello.rb
100644 a440db6e8d1fd76ad438a49025a9ad9ce746f581 2 hello.rb
100644 54336ba847c3758ab604876419607e9443848474 3 hello.rb
Anyhow, so now you resolve it to just be “puts ‘hola mundo’” and you can run
the rerere diff
command again to see what rerere will remember:
$ 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
So that basically says, when I see a hunk conflict that has ‘hello mundo’ on one side and ‘hola world’ on the other, resolve it to ‘hola mundo’.
Now we can mark it as resolved and commit it:
$ git add hello.rb
$ git commit
Recorded resolution for 'hello.rb'.
[master 68e16e5] Merge branch 'i18n'
You can see that it “Recorded resolution for FILE”.
Now, let’s undo that merge and then rebase it on top of our master branch instead.
$ git reset --hard HEAD^
HEAD is now at ad63f15 i18n the hello
Our merge is undone. Now let’s rebase the topic branch.
$ 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
Now, we got the same merge conflict like we expected, but check out the Resolved
FILE using previous resolution
line. If we look at the file, we’ll see that it’s
already been resolved:
$ cat hello.rb
#! /usr/bin/env ruby
def hello
puts 'hola mundo'
end
Also, git diff
will show you how it was automatically 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
You can also recreate the conflicted file state with the checkout
command:
$ git checkout --conflict=merge hello.rb
$ cat hello.rb
#! /usr/bin/env ruby
def hello
<<<<<<< ours
puts 'hola world'
=======
puts 'hello mundo'
>>>>>>> theirs
end
That might be a new command to you as well, the --conflict
option to
git checkout
. You can actually have checkout
do a couple of things in
this situation to help you resolve conflicts. Another interesting value for
that option is ‘diff3’, which will give you left, right and common to help you
resolve the conflict manually:
$ git checkout --conflict=diff3 hello.rb
$ cat hello.rb
#! /usr/bin/env ruby
def hello
<<<<<<< ours
puts 'hola world'
|||||||
puts 'hello world'
=======
puts 'hello mundo'
>>>>>>> theirs
end
Anyhow, then you can re-resolve it by just running rerere
again:
$ git rerere
Resolved 'hello.rb' using previous resolution.
$ cat hello.rb
#! /usr/bin/env ruby
def hello
puts 'hola mundo'
end
Magical re-resolving! Then you can add and continue the rebase to complete it.
$ git add hello.rb
$ git rebase --continue
Applying: i18n one word
So, if you do a lot of re-merges, or want to keep a topic branch up to date
with your master branch without a ton of merges, or you rebase often or any
of the above, turn on rerere
to help your life out a bit.