I have this project where the remote repo has the main development branch, and I have a fork containing the experimental branch. I'm required to rebase
changes from the development branch to my experimental branch before I push to my fork. So it goes like:
git checkout experimentalbranch
git fetch remoterepo
git rebase remoterepo/developmentbranch
By this time, I hit conflicts. However, I'm not familiar with any of these changes (I'm rebasing weeks worth of changes, because they didn't merge my changes immediately). Also, it's my first time doing rebase
. I'm more accustomed to merge
.
In meld, it usually is like <<LOCAL||REMOTE>>
for merge
, which sounds very intuitive. But in rebase
, it's <<HEAD||COMMIT MESSAGE>>
. Who is HEAD
? Is it the HEAD
of the development branch? Is it the latest code in the development branch or somewhere else?
The whole thing is fundamentally at least a little bit confusing because Git lets its internal workings show right through to you.
Note that the cases we are concerned with here occur when you run:
git checkout somebranch; git rebase origin/their-branch
or similar. The rebase has halted temporarily to force you to resolve a merge conflict, after which you're supposed to git add
the resolved conflict and run git rebase --continue
. (If you use some merge tool with git mergetool
, or a fancy GUI interface, that interface may do some or all of this for you some other way, but underneath, it's git add
ing the resolved files and running git rebase --continue
.)
At the very beginning, the HEAD
commit is their branch, so that if you use git checkout --ours
or git checkout --theirs
, --ours
means theirs—the final commit of origin/their-branch
—while --theirs
means yours, the first commit you're rebasing. This is the normal everyday sort of Git confusion (see What is the precise meaning of "ours" and "theirs" in git?) and is not what led to the original question.
Later, however, the HEAD
commit is actually a kind of mixture. It's the result of copying some number of your commits atop their latest commit. You're now getting a conflict between your own partly-built new series of commits, and your own original commits. The source of this conflict is usually something "they" did (something that changed along the way in origin/their-branch
). You still have to resolve this conflict. When you do, you may see the very same conflict recur in later commits.
Again, HEAD
or local
or --ours
is a commit that rebase has built by combining your changes and their changes, and the other commit (remote
or >>>>>>>
or --theirs
) is your own commit, which rebase is trying to copy atop HEAD
.
When merging (including rebasing, which is a special case of repeated "merging" internally), there are two "heads" (two specific branch-tips) involved. Let's call these your-branch
and origin/their-branch
:
G - H -------- <-- HEAD=your-branch
/ \
... - E - F M <-- desired merge commit [requires manual merge]
\ /
I - J - K - L <-- origin/their-branch
This point is commonly (and unsurprisingly) confusing, although when labeled like this it's clear enough.
Making it worse, though, git uses --ours
and --theirs
to refer to the two head commits during a merge, with "ours" being the one you were on (commit H
) when you ran git merge
, and "theirs" being, well, theirs (commit L
). But when you're doing a rebase, the two heads are reversed, so that "ours" is the head you're rebasing on-to—i.e., their updated code—and "theirs" is the commit you're currently rebasing, i.e., your own code.
This is because rebase actually uses a series of cherry-pick operations. You start with much the same picture:
G - H <-- HEAD=your-branch
/
... - E - F
\
I - J - K - L <-- origin/their-branch
What git needs to do here is to copy the effect of commits G
and H
, i.e., git cherry-pick
commit G
, then do it again with commit H
. But to do that, git has to switch to commit L
first, internally (using "detached HEAD" mode):
G - H <-- your-branch
/
... - E - F
\
I - J - K - L <-- HEAD, origin/their-branch
Now it can start the rebase operation by comparing the trees for commits F
and G
(to see what you changed), then comparing F
vs L
(to see if some of your work is already in L
) and taking any changes not already in L
and add it. This is a "merge" operation, internally.
G - H <-- your-branch
/
... - E - F G' <-- HEAD
\ /
I - J - K - L <-- origin/their-branch
If the merge does not go well, HEAD
is still left at commit L
(because commit G'
does not yet exist). Thus, yes, HEAD
is the head of their development branch—at least, it is right now.
Once the copy of G
exists, though, HEAD
moves to G'
and git attempts to copy the changes from H
, in the same manner (diff G
vs H
, then diff F
vs G'
, and merge the results):
G - H <-- your-branch
/
... - E - F G' - H' <-- HEAD
\ /
I - J - K - L <-- origin/their-branch
Again, if the merge fails and needs help, you're left with HEAD
pointing to G'
instead of H'
as H'
does not yet exist.
Once the merges all succeed and commits G'
and H'
do exist, git removes the label your-branch
from commit H
, and makes it point to commit H'
instead:
G - H
/
... - E - F G' - H' <-- HEAD=your-branch
\ /
I - J - K - L <-- origin/their-branch
and you are now rebased and HEAD
is once again what you would expect. But during the rebase, HEAD
is either their branch-tip (commit L
), or one of the new commits copied and appended past their branch-tip; and --ours
means the branch being grown at the end of L
while --theirs
means the commit being copied-from (G
or H
above).
(This is basically git exposing the raw mechanism of how it does what it does, which happens rather a lot in git.)