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 ?
CodePudding user response:
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
tofeature
.Think about the diff from the parent of
feature
toorigin/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
feature
tofeature
, some code was added adjacent tooldcode
.From the parent of
feature
toorigin/main
, that sameoldcode
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 filemyfile
with contentshello
, and then we editedmyfile
to be empty.On
mybranch
, we also added a filemyfile
with contentshello
, 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.