Resolving conflicts with git-rerere


This is a guest post by Bitbucket user Jeremy Greer


Conflict is inevitable when working on any significant project using Git.  When it comes down to it, a conflict is two commits changing the same thing in different ways.  There’s no getting around this.

Resolving these conflicts is never fun.  They overlap with your work, sure, but understanding how and why can be tricky.  You can’t just blindly accept either side. They were both changed for a reason. Even if you have a complete understanding of both perspectives, if it’s been a while since you did the work, it can take some time to brush up and understand what to do. But they must be resolved.

With a long-lived feature branch, you may periodically want to merge in `develop` in order to verify that everything works together.  These merge commits accumulate and can be viewed as noise. To avoid this, you can toss the merge after verifying things still work. This isn’t the _real_ merge, as the feature is still in progress.  If there’s a conflict early during the life of your feature branch, each time you do a “test merge,” not to mention the final merge, you’ll have to resolve the conflict again. Or suppose you’re not doing test merges, but have a PR ready to go with a nasty conflict you worked your way through… and the maintainer asked you to rebase.  Again, you’ll be faced with resolving the same conflicts.

The best way to avoid painful conflicts is to resolve them early and often.  Early, so the code is fresh in your mind, and often to minimize the number of conflicts.

Hello, git-rerere

rerere stands for *re*use *re*corded *re*solution and it does just that.  Once enabled, it will record conflicts and the resolutions that you create.  The next time you encounter the same conflict, your resolution will be applied automatically. This is helpful in a scenario with a long lived branch that needs to be rebased multiple times. 

Why use rerere? You reap the benefits of rerere without any intervention.  After you enable it, it takes images and applies resolutions automatically.  Even garbage collection is automatic. You can interact with it explicitly, though, if you’re so inclined.

When you encounter a conflict, `git rerere status` will tell you what files you’re working on creating a resolution for.  `git rerere diff` will show you the current state of the resolution, so you can see what you’ve changed so far. This has a nice syntax which you can think of as (change a + change b) = resolution.

-<<<<<<<
-Hello, Mr. Blue.
-=======
-Hello, Mr. Yellow.
->>>>>>>
+Hello, Mr. Green.

Finally, if you’re unlucky enough to have files rerere can’t resolve for you (missing from rr-cache, submodules, etc), `git rerere remaining` will tell you what’s left over that it couldn’t fix for you.

There are a few more options you can read about with `man git-rerere`. Typically, though, you don’t need to interact with rerere as it is invoked alongside other Git commands automatically for you.

Use Cases

To get started, you need to enable it.  You can do this per project or globally.

git config rerere.enabled true
git global config rerere.enabled true

Now, let’s work through a conflict.  If you’d like to follow along, you can checkout the repo at https://bitbucket.org/jexgrizzle/rerere-basic/.

how to use git rerere
git checkout feature
git merge master

This is what you’re used to seeing when you get conflicts, but there’s something new.

> Recorded preimage for ‘file.txt’

This tells us rerere has seen the conflict and recorded the “before” state, or preimage.  Let’s resolve the conflict and continue the merge.

using rerere to resolve conflicts

Once we merge, you’ll notice another new line.

> Recorded resolution for ‘file.txt’.

This tells us that rerere has recorded our resolution, or postimage.  We’ll dig into the details in a moment. First, let’s undo the merge and try it again.

git reset --hard HEAD^
git merge master

This time we’ll see an innocuous little line that indicates the awesomeness of rerere.

> Resolved ‘file.txt’ using previous resolution.

git status shows us that the file had a conflict.

conflict resolution with rerere

But when you look at the file, you’ll see that the resolution has already been applied!

how to use rerere to resolve conflicts

All we have to do is stage it and commit.  rerere will leave it unstaged so you still get a chance to see what happened.  All that’s left for you is to stage and commit. No more working through the same painful resolutions again.

How it works

Look in .git/rr-cache/ and you’ll see how this works.  For each file containing a conflict, a new directory with a hash name is created.  In our case, we’ll see c96db5732e5a8fb4bb3f610d8ce4b167e3fe2580, which corresponds to file.txt.  Within the directory, we see “preimage”, “postimage”, and “thisimage.” When rerere sees a conflict, it creates “preimage.”

> Recorded preimage for ‘file.txt’

If you look at it, you’ll see it’s file.txt with the conflict markers (minus the sources for each you usually see).

Unsurprisingly, “postimage” is what the file should look like after the conflict is resolved.

> Recorded resolution for ‘file.txt’.

“thisimage” is a bookkeeping mechanism rerere uses during a resolution.  It’s possible an unlucky file can have many conflicts, which will all be within this directory.  “thisimage” is the one most recently being worked through.

You’ll notice there is no magic here.  These are just plain text files. Let’s demonstrate by changing the “postimage” and doing the merge again.

echo "This is the new resolution." > .git/rr-cache/c96db5732e5a8fb4bb3f610d8ce4b167e3fe2580/postimage
git reset --hard HEAD^
git merge master
git diff

- Hello, Mr. Yellow.
 -Hello, Mr. Blue.
++This is the new resolution.

Now, when rerere sees a conflict for file.txt, it looks in the corresponding rr-cache dir, find the “preimage” that matches “thisimage” and changes the file in the working directory to match “postimage.”

Some use cases

Above, we laid out two real-life scenarios where you may have to resolve the same conflicts repeatedly:  a final merge after lots of “test” merges and changing a merged branch to a rebase. Let’s go through those again with rerere enabled.

First, the long-lived branch.  If you’d like to follow along, checkout https://bitbucket.org/jexgrizzle/rerere-long-branch/.

resolving merge conflicts using git rerere

feature has been going along master for a while and there’s one conflict at the beginning.  Say we’ve had this branch for a week and done test merges every day. The conflict was introduced on the first day and the rest of the history is separate.  Each test merge we do, we have to address that same conflict.

The first day, we do a test merge and get our conflict.

git merge develop

We test things, reassure ourselves that it works, and throw away the merge.

git reset --hard HEAD^

The next day, we make more commits and do another test merge.  Before we learned about rerere, this means we’d have to resolve the conflict from yesterday again. But now, the preimage is picked up, the postimage is applied, and all we have to do is stage and commit. This really lends itself to the “resolve conflicts early and often” concept.

rebase on master

The second scenario, where we’ve merged a branch, worked through some gnarly commits, and now the maintainer wants us to rebase, is just as easy.  If you

Want to try it out? checkout https://bitbucket.org/jexgrizzle/rerere-rebase/src/master/.

We’ve got a feature branch and we’ve already worked through some nasty conflicts.  You can drop TODO-rr-cache-for-rebase-example into your .git/rr-cache/ to simulate the pain you went through here.  Without rerere, rebase would ask you to resolve that commit again. However, since we’ve got it enabled, it recorded the pre/postimages for us.  git rebase develop just does it again, no sweat.

Recap

To summarize, git rerere is all win.  It works automatically, capturing conflict and resolution pairs, and applies them automatically for you.  You can enable it locally or globally with git config rerere.enabled. It is not a silver bullet to make resolving conflicts easier, but it _will_ keep you from having to do it repeatedly.  Hopefully, this encourages you to tackle them sooner and in so doing, keep those conflicts under control. There’s no avoiding them after all, so you may as well use rerere to make fixing them easier, and even a bit fun.


Author bio: Jeremy Greer is a digital nomad with a heap of JavaScript experience and a dash of everything else. He is a TDD junky, likes to ask stupid questions, and is deeply offended by side-effects. When he’s not pushing code, he can be found writing or talking about it somewhere.

Love sharing your technical expertise? Learn more about the Bitbucket writing program.

Scaling your Bitbucket team? Upgrade your plan here