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 ?
What a commit is
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
featurehad this code, and you didn't touch that code when you created the commit that is nowfeature, 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;
rebaseischerry-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 namefeature, itself — ontoorigin/main. (The only actual difference in this case betweenrebaseandcherry-pickis 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
featureontoorigin/main, you are saying to Git:Think about the diff from the parent of
featuretofeature.Think about the diff from the parent of
featuretoorigin/main.Enact both those diffs as applied to the parent of
feature, and make a commit expressing that, whose parent isorigin/main.Very well. What are those diffs?
From the parent of
featuretofeature, some code was added adjacent tooldcode.From the parent of
featuretoorigin/main, that sameoldcodewas deleted.That, I think, is the part that surprises people. You seem to imagine that
origin/mainhas no contribution to make here. But it does. The cherry-pick requires, among other things, that we be somehow able get from the parent offeaturetoorigin/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
oldcodein the parent offeature.featureadded to it.origin/maindeleted 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/mainto have botholdcodeand 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/mainas a deleter of the code, let me enact a little drama for you. We start with this:Here's what happened so far since
start.On
main, we added a filemyfilewith contentshello, and then we editedmyfileto be empty.On
mybranch, we also added a filemyfilewith contentshello, and then we added another file,myotherfile.Now rebase / cherry-pick just the last commit of
mybranchontomain. What will the state ofmyfilebe in the newly created commit? It will be emptied. That's because, from the point of view0ec170a, the contribution of7dcb9afwas to emptymyfile. From the point of view of0ec170a, the other commit31da420did 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.