Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rebase a branch from another branch that is rebased from master

Tags:

git

rebase

I have this scenario:

o---o---o---o---o  master
     \
      o---o---o---o---o  next
                       \
                        o---o---o  topic

As above, I did some commits on master and I want to update the other two branches and have:

o---o  master
     \
      o---o---o---o---o  next
                       \
                        o---o---o  topic

I already did the git rebase master on next branch, without thinking about that topic now hasn't the base commit of next branch.

What could I do now? Was I wrong to rebase immediately the intermediate branch? What should I do?

like image 797
Antonino Scarfì Avatar asked Sep 20 '16 12:09

Antonino Scarfì


People also ask

How do I rebase a branch from another branch to master?

To rebase, make sure you have all the commits you want in the rebase in your master branch. Check out the branch you want to rebase and type git rebase master (where master is the branch you want to rebase on).


1 Answers

There are two relatively easy solutions to this problem. One is to just try the rebase, which might just work. But if not, it's still easy. Here's the second, more powerful way, followed by why:

git checkout topic; git rebase --onto next next@{1}

What's going on: how rebase works

The view is, I think, much clearer if you draw the graph a bit differently. Here's the original, redrawn slightly:

A---B---C---D---E                   <-- master
     \
      F---G---H---I---J             <-- next
                       \
                        K---L---M   <-- topic

Here's what you have after git checkout next; git rebase master:

A---B---C---D---E                   <-- master
     \           \
     |            F'-G'-H'-I'-J'    <-- next
     \
      F---G---H---I---J             [was "next", but not anymore]
                       \
                        K---L---M   <-- topic

This is because rebase works by copying commits, with some slight (or major) change(s). New commit F' is a copy of original F, G' is a copy of G, and so on.

If you now run git checkout topic; git rebase next, it usually just works—but not always. The reason for this is that, by default, rebase chooses which commits to copy and where to copy-after from a single argument. This gets a bit complicated, so let's take this in two steps.

When you say git rebase next here, the single argument is next: the copies will go after the tip commit of next (which is now J'). This part is totally straightforward: that one identifier, next, finds the commit --onto which the copies are appended. (There's a reason I spelled this --onto instead of just "onto". :-) )

The what gets copied part is trickier, in two ways. First, next specifies what not to copy, while the current branch (topic) specifies what to copy. Git gets a list of all commits reachable from topic—which is commits A, B, F, G, H, and so on all the way through L. Then it excludes all commits reachable from next, which is commits A, B, C, D, E, F' (but not F), G' (but not G), and so on through J'. It doesn't matter that some excluded commits are not in the included list: we just exclude those that are in the included list.

Unfortunately, this leaves commits F, G, H, I, and J in the pile of commits-to-be-copied. This is where, as you noticed, things go wrong. But somehow, most of the time, this works anyway. What's going on when it works? And why doesn't it always work?

The answer is that rebase also excludes "patch-equivalent commits". The general idea here is that when you're rebasing, Git scans through the "exclude" list to see if any of those commits do the same thing as a commit in the "include" list. Since F' is a copy of F, it's likely to be "the same" as F, at least in a patch-equivalence sense. The same goes for G vs G', H vs H', and so on.

As long as the commits are patch-identical to their rebased versions, they get excluded.

When this is true—and it usually is—you can simply git checkout topic; git rebase next and it just works. It's when it's not true—when some of F through H got modified "too much" while being copied, and their copies are no longer patch-equivalent—that it fails.

As a sort of rule of thumb,1 if Git is able to do the first rebase all by itself, it will be able to do the second all by itself too. It's mainly when it stops with a conflict, and makes you fix it by hand, that the patches become "not-patch-equivalent". But it's easy enough to deal with this, because git rebase has a longer form.

Instead of:

git rebase next

we can write:

git rebase --onto next <something>

Based on the graph we drew, the <something> to use here is anything that specifies commit J. That is, we need to find where next was. What commit did next point to, before we rebased it? It now points to J' but it used to point to J. The commits we need Git to exclude are those that next used to reach: A, B, F, G, H, I, and J. If Git can't find these on its own, via patch-equivalence, we can just tell it.

That still leaves the question of how to spell a name that identifies commit J. One way is by raw hash ID, which always works, but is a bit of a pain (both to find, and to copy, though cut-and-paste works pretty well for copying hashes). But look at the redrawn graph again, with the bit that says was "next", but not anymore. If only we could tell Git to look up the previous value of next.

But wait! Git has reflogs! We can tell it to do just that! The previous value of next is spelled next@{1}. (The number increments every time we change the label next, by adding a new commit to it for instance, but all we've done just now is the one rebase, so it's still next@{1}.) So at this point we can just do this:

git checkout topic; git rebase --onto next next@{1}

(depending on your shell, you might need to use quotes around something with braces in it, e.g., 'next@{1}' or next@\{1\} or some such).


1There are some exceptions. For instance, in this case, if git diff E F' doesn't produce the same change-set as git diff B F due to diff picking up different semi-irrelevant matches, like blank lines or close-brace lines, that would cause a problem. However, the answer to dealing with these is the same as the answer to dealing with hand-applied rebases due to conflicts.

like image 198
torek Avatar answered Nov 15 '22 06:11

torek