Search code examples
gitgit-rebase

How does `git rebase master` check a reapplying commit as a conflict?


Assume we have two example with its commits (line 1, line 2... is the content of a foo file, which is the only file in working directory):

Example 1:

line 1     line 1
line 2     line 22
line 3     line 3
  C0---------C1 [master]
   \
    C2 [test]
  line 1
  line 222
  line 3

Example 2:

line 1     line 1
line 2     line 22
line 3     line 3
  C0---------C1 [master]
   \
    C2--------C3 [test]
  line 1     line 1
  line 22    line 222
  line 3     line 3

I ran git checkout test and git rebase master on each of those examples, and I saw that Example 1 has a conflict, while Example 2 doesn't have any.

In Example 1, when reapplying C2, I guess Git do a 3-way merge on C2, C1 and C0, which detect that "line 222" on C2 different with "line 22" on C1 and different with "line 2" on base (C0), so Git check that as a conflict.

In Example 2, the patch of C2 is already has in master so Git skip it. But when reapplying C3, if Git does a 3-way merge on C3, C1 and C0, which detect that "line 222" on C3 different with "line 22" on C1 and different with "line 2" on base (C0), then Git should check that as a conflict, but in real Git doesn't.

So in real, how does Git rebasing check a reapplying commit as a conflict?


Solution

  • Each commit is copied by (or as if by) git cherry-pick. A cherry-pick is a merge whose merge base is the parent of the commit being picked, with the --ours version being the HEAD commit and the --theirs version being the commit being picked. So, given:

    C0--C1   <-- master
     \
      C2--C3   <-- test
    

    and:

    git checkout test && git rebase master
    

    Git will first copy C2 to C2', then copy C3 to C3'.

    When copying C2, the merge base is C0 (parent of C2), --ours is C1 (tip of master), and --theirs is C2 (being copied). After the copy is finished we have this:

           C2'  <-- HEAD
          /
    C0--C1   <-- master
     \
      C2--C3   <-- test
    

    Next, Git cherry-picks C3. The merge base is therefore C2 (parent of C3), --ours is C2', and --theirs is C3.

    There is a merge conflict if the difference (git diff) from base to ours, vs the difference from base to theirs, touches "the same lines" of the same file. "Same" in this case includes one line after the line touched. So, compare the contents of C2 (base) to each of C2' and C3, to see which lines changed in ours (C2 vs C2') and theirs (C2 vs C3). If we've taken "their" change to line 2 when producing C2', the difference from C2 to C2' is empty, and therefore the merge is trivially resolved by taking the file from C3.