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