12

I have a (large) commit tree that contains several merge commits that I want to rebase to another commit. Doing a normal rebase causes git to ask me to resolve merge conflicts. I didn't want to review each merge, because this would be a lot of work. After finding out about the --preserve-merges option, which is neatly explained here, I thought I found the perfect tool for this task. However, I cannot seem to get it to work properly. I have created a toy example that demonstrates the problem.

Starting from an empty folder, we first create a branch with a merge and another branch on which we will rebase.

A---B--
\      \
 ---C---D
 \
  ---E

Where master refers to B, branch refers to D and goodbye-branch refers to E.

git init
echo Hello > Hello.txt
git add Hello.txt
git commit -m "Create Hello.txt (commit A)"
git tag start

echo World! >> Hello.txt
git commit -am "Change to Hello World (commit B)"

git checkout start
git checkout -b branch
echo Dave >> Hello.txt
git commit -am "Change to Hello Dave (commit C)"

git merge master
echo Hello World, Dave! > Hello.txt
git add Hello.txt
git commit -m "Merge branch master into branch (commit D)"

git checkout start
git checkout -b goodbye-branch
echo Goodbye > Goodbye.txt
git add Goodbye.txt
git commit -m "Add Goodbye.txt (commit E)"

Up to this point, everything went ok. There was a merge conflict, but we resolved it. Now we try to rebase branch onto E to end up with the following commit tree:

A---E----B'
     \    \
      C'---D'

git checkout branch
git rebase -p goodbye-branch

This end with the following error:

Auto-merging Hello.txt
CONFLICT (content): Merge conflict in Hello.txt
Automatic merge failed; fix conflicts and then commit the result.
Error redoing merge f567809e2cc91244cc7fdac210e1771dc75e4d86

The file contains the following content:

Hello
<<<<<<< HEAD
Dave
=======
World!
>>>>>>> 0437403c97f33f229e41ec9584ce891a50052e48

What am I doing wrong? I would expect git to be able to use commit D to resolve the merge conflict it encounters while rebasing.

I am using Git 1.9.4.msysgit.1, which is the most recent version right now.

Community
  • 1
  • 1
DieterDP
  • 4,039
  • 2
  • 29
  • 38
  • 1
    You've helped a great deal Jubobs, and I've upvoted your answer. But strictly speaking, the question is still unanswered. The solution to this question would be the windows-version of the [rerere-teach.sh](http://git.661346.n2.nabble.com/Teaching-rerere-about-existing-merges-td7582116.html) script. – DieterDP Sep 11 '14 at 13:38

1 Answers1

31

TL; DR

The --preserve-merges flag simply tells git-rebase to try to recreate merge commits instead of ignoring them. It does not give git rebase the ability to remember how merge conflicts were resolved, i.e. it does not record conflict resolutions for future use. What you want to use for that is rerere.

In your toy example, the conflict arising during the rebase is exactly the same as the one you resolved during the preceding merge. If you had activated rerere before the merge, you wouldn't have had to resolve that conflict again during the rebase.

If you anticipate that you will merge, then rebase a branch, you should activate rerere so that, in the future, you only need to resolve a given merge conflict once, not multiple times.

Detailed explanation

Let's break down your toy example.

git init
echo Hello > Hello.txt
git add Hello.txt
git commit -m "Create Hello.txt (commit A)"
git tag start

echo World! >> Hello.txt
git commit -am "Change to Hello World (commit B)"

git checkout start
git checkout -b branch
echo Dave >> Hello.txt
git commit -am "Change to Hello Dave (commit C)"

So far, so good. Right before your first git merge command, your repo looks like this:

enter image description here

In commit A, Hello.text contains

Hello

In commit B, Hello.text contains

Hello
World!

And in commit C, Hello.text contains

Hello
Dave

Now, when you try to merge master into branch by running

git merge master

Git reports a merge conflict because it has no way of figuring out, on its own, whether the contents of Hello.txt after the merge should be

Hello
World!
Dave

or

Hello
Dave
World!

or something else...

You resolve that conflict by overwriting the contents of Hello.txt with Hello World, Dave!, staging your changes, and completing the merge commit.

echo "Hello World, Dave!" > Hello.txt
git add Hello.txt
git commit -m "Merge branch master into branch (commit D)"

Your repo now looks like this:

enter image description here

Then you run

git checkout start
git checkout -b goodbye-branch
echo Goodbye > Goodbye.txt
git add Goodbye.txt
git commit -m "Add Goodbye.txt (commit E)"

At that stage, your repo looks as follows:

enter image description here

Now you run

git checkout branch
git rebase -p goodbye-branch

but experience a conflict. Before explaining why this conflict arises, let's look at what your repo would look like if that git-rebase operation were successful (i.e. conflict free):

enter image description here

Now let's see why you run into the same conflict in Hello.txt as during your first merge; Goodbye.txt is not problematic in any way, here. A rebase can actually be decomposed in a sequence of more elementary operations (checkouts and cherry-picks); more on this at http://think-like-a-git.net/sections/rebase-from-the-ground-up.html. Long story short... In the middle of your git rebase operation, your repo will look as follows:

enter image description here

The situation is very similar to that right before your first merge: in commit B', Hello.text contains

Hello
World!

And in commit C', Hello.text contains

Hello
Dave

Then Git attempts to create merge B' and C', but a merge conflict arises for the exact same reason as the first merge conflict you experienced: Git has no way of figuring out whether the Dave line should go before or after the World! line. Therefore, the rebase operation grinds to a halt, and Git asks you to resolve that merge conflict before it can complete the rebase.

What you can do about it: use rerere

Git's rerere is your friend, here.

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.

[...] 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.

If rerere had been enabled,

git config --global rerere.enabled true

before the merge, then Git would have recorded how you resolved the merge conflict when creating commit D, and would have applied the same resolution when it encountered the same conflict during the subsequent rebase. The conflict would still have interrupted the rebase operation, but it would have been resolved automatically. All you would have had to do is git rebase --continue.

However, it looks like rerere wasn't already activated before the merge, which means Git must have kept no record of how you resolved the conflict the first time. At this stage, you can either activate rerere now and resolve all those same conflicts manually again, or use the rerere-train.sh script (see also this blog post) to use the existing history to pre-seed the rerere cache.

Community
  • 1
  • 1
jub0bs
  • 60,866
  • 25
  • 183
  • 186
  • 3
    I understand that this is the situation when making the merge. I don't get why this occurs again while rebasing. After all, git already knows how the conflict is to be resolved, because the merging commit is also being rebased... Assuming that this is simply how rebasing works, is there any other way to avoid having to re-resolve all conflicts while moving my commits around? – DieterDP Sep 05 '14 at 08:48
  • All I can tell you, based on the information given in your question, is that the conflict arise during the merge (not the rebase), and that Git won't allow you to proceed further before you resolve it. That said, conflicts can arise during `rebase`, `cherry-pick`, `revert` operations, etc. Perhaps you should edit your question to add a toy example in which a conflict arises solely due to a `rebase`, and I'll try my best to explain the cause of that conflict. – jub0bs Sep 05 '14 at 10:25
  • Regarding avoiding the need to resolve the same conflicts again and again, you may want to look up [`git-rerere`](http://git-scm.com/blog/2010/03/08/rerere.html). – jub0bs Sep 05 '14 at 10:54
  • I'm not 100% sure we are on the same page here. I've slightly edited my question to be more clear. The error I listed shows up after the rebase command, not the merge command. Since commit *D* contains the merge conflict resolution, I would expect git to use it while rebasing. The git-rerere command does look promising, unfortunately, there is no built-in tool for loading already existing merges. There is a [linux script](http://git.661346.n2.nabble.com/Teaching-rerere-about-existing-merges-td7582116.html) available in the git source repo, but I'm having trouble running it on windows. – DieterDP Sep 05 '14 at 12:02
  • 4
    The rerere is the only solution that I'm aware of, but I agree with @DieterDP in that git is capable of solving the conflict during the rebase even without rerere activated, because the conflict arise when git is cherry-picking D to produce D'. If you inspect the commit D, it actually contains the conflict resolution and applying the resolution over the conflict does not "re-conflict", i.e., as the conflict is not even slightly different because of the rebase, it could apply the same solution. – André Sassi Sep 07 '14 at 19:21
  • @AndréSassi I'm not sure I understand what you wrote. The conflict that occurs in the rebase (commit D') is the same as the one in the original merge (commit D), but it won't automatically resolve itself unless `rerere` was activated before the merge. Follow the OP's toy example with `rerere` off, then again from scratch with `rerere` on, and you will see that you do indeed need `rerere`. – jub0bs Sep 07 '14 at 19:25
  • 1
    I think what @AndréSassi means is that git *could*, if it were smarter, "look ahead" to see that there was a commit after the one it is currently cherry-picking, and therefore the tree associated with that commit defines (in a sense) the correct conflict-resolution. Git is not that smart, though. – torek Dec 29 '14 at 22:05
  • @torek Aaah thanks. André's comment makes more sense to me now. – jub0bs Dec 29 '14 at 22:29
  • I can confirm that `rerere` is required to preserve merges during rebase where one of the commits in merged branch has changed (was fixed up for example). This applies even for merges that does not have any conflicts at all (merges that could be fast forwarded for example). – Vlastimil Ovčáčík Jun 17 '16 at 13:15
  • 1
    That's a very clear and interesting explanation with graphics and all. To the point it'd could be a good blog-post – Juh_ Dec 14 '17 at 10:01
  • The only problem I see now after enabling rerere is when I had screwed up the conflict resolution and I want the merge to skip using recorded resolutions just this one time. (Edit: In which case we can use `--[no-]rerere-autoupdate Allow the rerere mechanism to update the index with the result of auto-conflict resolution if possible.`) – FUD Apr 16 '18 at 09:53