8

Let's say I do an interactive rebase with git rebase -i. If some conflict arises I might be presented with a merge conflict and asked to do a 3-way merge. Using meld, I am presented with three windows: LOCAL (left), ??? (middle), and REMOTE (right). Here by ??? I mean simply that meld doesn't provide some special name to append to the file.

During a normal merge this makes sense, since the middle is the common ancestor and you are merging the local and remote changes to that ancestor. However this does not seem to be the case during an interactive rebase - it's unclear what each file represents.

What do these files in the 3-way merge each represent during an interactive rebase? And when editing these files, what is my goal?

Update: Based on the comments and experiments I'm seeing:

  • Left (LOCAL): Your local version of the file at this point in the commit replay sequence.
  • Right (REMOTE): The state of the file just after the current commit was originally applied.
  • Middle: The parent of the right in the original commit sequence.

My task is thus to determine the delta from Middle to Right, and then apply this delta to the Left. The Middle should reflect the state of the file after the current commit delta is applied in the new commit sequence.

Note that this configuration appears to be specific to meld, at least to some degree. Git's 3-way merge behavior may differ for other editors.

Jake
  • 7,565
  • 6
  • 55
  • 68

2 Answers2

7

The middle version is the merge base, just as with a git merge.

(The name "other" might be more appropriate than "remote" since there is no requirement that the other side of a merge be a remote, and since Mercurial consistently uses the name "other" for it, not that Git needs to match Mercurial, but some consistency might be nice. Note that Git uses the names "ours" and "theirs" here as well, so we will never get 100% consistency from Git. :-) )

But wait, how is there a merge base?

There is always a merge base.

Usually we don't even have to find it as each patch applies cleanly when treated as a patch (without attempting a three-way merge). But sometimes the patch won't apply cleanly, and we do have to fall back to the three-way merge.

(Incidentally, you can disable this fallback. See --3way, --no-3way, and am.threeWay in the git-am documentation, though the page linked here is already out of date since these controls changed recently.)

$ git rebase -i
pick aaaaaaa first commit
pick bbbbbbb second commit
pick ccccccc third commit

Let's draw the commit graph, too, so we can see what we are rebasing from and to:

              A - B - C   <-- branch
            /
... - o - *
            \
              G - H       <-- origin/branch

We'll be cherry-picking commits A, B, and C (A = aaaaaaa, etc) so that we get this result, in the end:

              A - B - C   [abandoned]
            /
... - o - *           A' - B' - C'   <-- branch
            \       /
              G - H       <-- origin/branch

Let's look closely at the first cherry-pick, of A.

This compares (diffs) A against its parent, which is commit *, and attempts to apply the resulting diff to commit H.

Commit H, however, has drifted somewhat from commit *. In fact, we can find a merge base between A and H, and it is ... commit *. This is actually a pretty decent merge-base, though it's best if Git can just apply the patch as-is, without having to fall back to the three-way merge code.

So, commit * is the merge base when cherry-picking A onto H. When the merge is done we get new commit A'. (Its new SHA-1 ID might be aaaaaa1 for instance. Probably not; let's just call it A'.)

Now we'll cherry-pick B. This diffs B versus its parent, which is A, and attempts to apply the diff to A'.

Commit A', however, has drifted somewhat from commit B. In fact, we can find a merge base between B and A', and that is ... commit * again. Unfortunately, this is a wretched merge base. Fortunately, Git only falls back on it if the patch cannot be applied as-is, and usually it can. But if it can't, Git will diff * vs B and * vs A' and try to merge those two diffs. Note that * vs B incorporates all of the changes we made in A, but * vs A' also incorporates all of those same A changes, so if we are lucky, Git notices the already-incorporated changes and does not duplicate them. edit Git cheats. (This code has changed recently in version 2.6, although the overall strategy remains the same.)

Consider the actual output of git diff when used to show just the change from commit A to commit B. This includes an index line:

diff --git a/foo b/foo
index f0b98f8..0ea3286 100644

The value on the left is the (abbreviated) hash for the version of file foo in commit A. The value on the right is the hash for the version of the file in commit B.

Git simply fakes up a merge base from the left side hash. In other words, the file version in commit A becomes the faked merge-base. (Git passes --build-fake-ancestor to git apply. This requires that the particular file blob objects be in the repository, but they are since they are in commit A. For emailed patches, Git uses this same code, but the blob may or may not be present.)

Note that Git actually does this when cherry-picking commit A as well, but this time the merge base file is the version from commit *, which really is the merge base.

Finally, we cherry-pick C. This diffs B vs C, just as we diffed A vs B last time. If we can apply the patch as is, good; if not, we fall back to using commit * as the merge base again. It is once again a pretty wretched merge base. the same way as before, pretending that the version in B was the common base.

This also explains, incidentally, why you tend to see the same merge conflicts over and over again for these rebases: we're using the same merge-base each time. (Enabling git rerere can help.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • 1
    I am doing a rebase right now where the middle is not a common ancestor of the left and right - it is only an ancestor of the right (remote), and appears to be a successor of the left (local). How can this be if it's a merge base? Note that in my rebase I am deleting a commit from my history, and the middle represents the state of the file when the deleted commit had been applied. – Jake May 03 '16 at 01:03
  • Hm, how indeed? I'll note here that you are looking at one file, which is not a commit (so we cannot use the file's ID to derive the commit's ID unless that particular version of that file is unique to one particular commit). It's also possible that some merge tool(s) do something different in an attempt to be helpful. There's some code in Git to extract the common portions of two tip-of-commit files, for instance, which some tools might use along the way. In particular p4merge uses `create_virtual_base` from git-sh-setup. I know nothing of `meld` though. – torek May 03 '16 at 01:16
  • Meld doesn't create files, it just diffs them. I just read up on this a bit though and it appears that the base is generated by git but is not shown by meld. Instead, meld shows the merge file in the middle, which is at the original path. I am not sure if git specifically updates this file so that it is set to the parent of the original commit. – Jake May 03 '16 at 01:24
  • Another great post which teaches me new things every day. Voted as always for the detailed answer. – CodeWizard May 03 '16 at 01:27
  • 2
    Aha, I was very curious about this so I dug through the `git am` source code and found that I am wrong about the merge bases here. This would be the answer except that git cheats! – torek May 03 '16 at 01:32
  • 1
    @CodeWizard, Jake: updated answer. It is in fact the version from the parent commit (in this particular case, but not for emailed patches where you have only the blob ID and not the actual parent commit ID). – torek May 03 '16 at 01:50
  • 1
    Interesting! Thank you for writing such a thorough answer and for doing the additional research on top of that. This has really helped my understanding of the merge/rebase process :) – Jake May 03 '16 at 05:27
-2

A merge and a rebase are identical in this respect. The only difference between a merge and a rebase is, that the history looks nicer (more linear) with a rebase. But regarding the conflicts that will arise and that you have to solve, they are the same.

Vampire
  • 35,631
  • 4
  • 76
  • 102
  • Could you clarify? A merge involves two branches. An interactive rebase involves reordering or modifying the commits within one branch. So they are not the same. As a consequence it is not clear what local, remote, and the common ancestor are. – Jake May 03 '16 at 00:33