Git Rebase Conflict: Who is HEAD?

Joseph picture Joseph · Apr 22, 2014 · Viewed 29.3k times · Source

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?

Answer

torek picture torek · Apr 22, 2014

TL;DR (added May 2018)

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 adding 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.

Longer

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.)