Search code examples
gitrebasemerge-base

Unable to understand merge common ancestor output and reason of conflict


I have a branch feature based on my develop branch. My feature was needed earlier than anticipated in production so I decided to rebase my feature branch on top of my master branch like this (my feature branch has only one commit):

git checkout feature
git rebase --onto origin/master HEAD~

During the rebase, I encountered a conflict, here displayed with diff3 output format:

<<<<<<< origin/master
||||||| merged common ancestors
some code I didn't touch
=======
same code I didn't touch
code I added in my commit on feature
>>>>>>>

I did not expect to have a conflict, since I only added some code and did not modify the existing one. Moreover, the "code I didn't touch" was on my feature branch based on develop but not in my commit and not in origin/master either, so I do not understand how it can appear in the merged common ancestor part.

When I run git merge-base --all HEAD origin/master it displays the last commit on origin/master, that does not have the "code I didn't touch".

To me the output of the rebase should have been something like this, yielding no conflicts (of course there would be no conflict output then, but just to show what I was expecting):

<<<<<<< origin/master
||||||| merged common ancestors
=======
code I added in feature
>>>>>>>

The code I didn't touch should not appear anywhere since it is not on origin/master nor in my commit. I thought the rebase onto command I did would have the same result as cherry-picking my feature's only commit on top of origin/master (which will be the case once I resolve the conflict).

What am I missing ?


Solution

  • What a commit is

    The code I didn't touch should not appear anywhere since it is not on origin/master nor in my commit.

    It is in your commit. You may be thinking incorrectly about what a commit is. It is not a diff. Commits are not changes. A commit is a snapshot of your entire project — all the files, in their current state. So if the parent of feature had this code, and you didn't touch that code when you created the commit that is now feature, then yes, that code is most certainly in that commit, because for it not to be there, you would have had to delete it, and you did not.

    What you did

    Switching to cherry-pick won't change anything; rebase is cherry-pick.

    So let's run with that, and treat what you did as a cherry-pick. We will say that you cherry-picked the last commit of feature — that is, the commit pointed to by the branch name feature, itself — onto origin/main. (The only actual difference in this case between rebase and cherry-pick is what happens to the branch name pointers afterward, and we aren't going to consider that at all in what follows, so it's irrelevant.)

    How cherry-pick / rebase works

    A cherry-pick is a merge, meaning that its job is to create a brand new commit, although the commit it creates is a normal commit, not a merge commit (that is, it has just one parent). The new commit is created using merge logic, using the parent of the cherry-picked commit as the merge base. (If you don't understand what I just said, read my https://www.biteinteractive.com/understanding-git-merge/.)

    Therefore, when you cherry-pick the last commit of feature onto origin/main, you are saying to Git:

    • Think about the diff from the parent of feature to feature.

    • Think about the diff from the parent of feature to origin/main.

    • Enact both those diffs as applied to the parent of feature, and make a commit expressing that, whose parent is origin/main.

    Very well. What are those diffs?

    • From the parent of feature to feature, some code was added adjacent to oldcode.

    • From the parent of feature to origin/main, that same oldcode was deleted.

    That, I think, is the part that surprises people. You seem to imagine that origin/main has no contribution to make here. But it does. The cherry-pick requires, among other things, that we be somehow able get from the parent of feature to origin/main. That can be quite an elaborate operation — and for that very reason, merge conflicts are very common when cherry-picking (including rebasing).

    The conflict

    So let's think about what each diff does with respect to oldcode in the parent of feature. feature added to it. origin/main deleted it. Thus you are asking Git both to add to the hunk and to delete the hunk. Those are contradictory instructions, so Git asks you what to do.

    Resolving the conflict is very, very easy; this is probably the easiest and most common case of a merge conflict. You know what you want; you either want origin/main to have both oldcode and the new code, or you want it to have just the new code. But Git doesn't know what you want, so this still counts as a merge conflict, which merely means that you have to do a little manual editing yourself. Do it, don't worry, be happy, and move on.

    A corollary

    Since the part that probably surprises you the most is the contribution of origin/main as a deleter of the code, let me enact a little drama for you. We start with this:

    * 31da420 (HEAD -> mybranch) myotherfile
    * 0ec170a myfile
    | * 7dcb9af (main) emptied myfile
    | * 7e2b31f myfile
    |/  
    * 61bc628 start
    

    Here's what happened so far since start.

    • On main, we added a file myfile with contents hello, and then we edited myfile to be empty.

    • On mybranch, we also added a file myfile with contents hello, and then we added another file, myotherfile.

    Now rebase / cherry-pick just the last commit of mybranch onto main. What will the state of myfile be in the newly created commit? It will be emptied. That's because, from the point of view 0ec170a, the contribution of 7dcb9af was to empty myfile. From the point of view of 0ec170a, the other commit 31da420 did not contradict that in any way.

    So you see, even if you had not added the new code, the fate of oldcode ("code I didn't touch") would not have been what you expect. You think it would have been left alone. It would not have been. It would have been deleted.