Search code examples
gitrebaseancestor

Does git rebase ever require a common commit ancestor?


While experimenting with git, I created two branches without a common commit ancestor. Let's call them "master" and "other". The current branch is "master".

As expected, trying to merge "other" via:

git merge other

produced: fatal: refusing to merge unrelated histories

This is precisely what I expected to happen. Surprisingly to me, running rebase via:

git rebase other

succeeded.

This was a surprise to me as I assumed that rebase requires a common commit ancestor just like git merge. Does git rebase ever require a common ancestor?


Solution

  • I think the way to understand this, as with so much about rebase, is to understand two things:

    • rebase is just cherry-pick: it creates new commits based on successive diffs, leaving the old ones in place, and appends them to a target. The difference is merely that, afterwards, cherry picking advances the destination branch name, whereas rebasing transfers the source branch name.

    • git rebase xxx is a shorthand. The results can therefore be surprising.

    The full form of git rebase is git rebase --onto x y z, which means: "Starting at (but not including) y, cherry-pick each successive commit onto x until you have cherry-picked z."

    When you use the shorthand form, x is usually the commit that you specify, z is the current branch, and y is the common ancestor of the two.

    But there are circumstances where the shorthand doesn't work that way. In this case, there is no common ancestor. So for y, Git chooses the "root", i.e. nothingness — just as if you had used the full form with the --root option.


    To illustrate, let us suppose branch one consists of commits a and then b, and branch two consists of commits c and then d:

    a <-- b (one)
    
    c <-- d (two)
    

    Then if you are on two and you say git rebase one, one is b, so Git walks backwards from two (d) to c and says to itself: can I cherry-pick the diff "nothingness-to-c" onto b? If so (because there's no conflict), it does. Then it says: can I cherry-pick the diff "c-to-d" onto the commit I just created? If so, it does. And that's the end — we've reached the current branch commit — so it stops, and shifts the current branch pointer (HEAD) to the last new commit that it created:

    a <-- b <-- c' <-- d' (two)
          ^
        (one)
    

    Note that c' and d' are copies (ie new commits created by Git). The original c and d still exist, but no branch name now points at them, and eventually they will be deleted through garbage collection.