7

Suppose we have the following revision graph:

A-B (master)
   \
    C (feature-a)
     \
      D (feature-b) [depends on feature a]
       \
        E (feature-c) [depends on feature b]

And master is then modified to follow with commit F. Is there any simple way to rebase E onto F (master) so that branches feature-a, feature-b and feature-c all end up as follows:

A-B-F (master)
     \
      C' (feature-a)
       \
        D' (feature-b)
         \
          E' (feature-c)

?

In real world situations there are obviously multiple patches between each feature so re-attaching branches in-between manually to rebased history is tedious and error prone job. I know that I can rebase E to E' with a simple git checkout feature-c && git rebase master but that leaves branches feature-a and feature-b pointing to commits C and D instead of C' and D'. Rebase should have all the info to move all the branches, right?

Mikko Rantalainen
  • 14,132
  • 10
  • 74
  • 112
  • 1
    Look at this question i think the problem is similar: http://stackoverflow.com/questions/5600659/rebasing-a-branch-including-all-its-children?rq=1 – Rudy Bunel Nov 08 '13 at 14:14

4 Answers4

2

There's nothing built in to git to do this, but it's certainly script-able.

What you need to do is:

  • Identify the branch(es) to be rebased (here feature-a, feature-b, and feature-c).
  • For each branch, determine which to-be-rebased branches "contain" them (let's call this "is a predecessor"). Branch X is a predecessor of Y if Y is a descendent of the X (and choose something here to handle/break-ties-with two branch names that identify the same commit). In this case, feature-a is a predecessor of both feature-b and feature-c, while feature-b is a predecessor of feature-c (only). Save the "distance back" values for predecessors (how far to chase parent chain).
  • Perform a topological sort. (Actually you can cheat and just find leaves. You only need a cycle-check if you pick branch names by some method other than observing the commit DAG.)
  • For each leaf (in this case just feature-c):
    • Rebase it.
    • For each predecessor of this leaf, move it from wherever it is now, to N-parents-back from the new leaf tip.

(That's it, all done.)

Predecessor testing is easy with git merge-base --is-ancestor (pairwise) or git branch --contains (en masse, but requires filtering away non-rebased branches). Finding the "N back" value is a little trickier, but I believe can be done with git rev-list piped to wc -l, for instance.

Edit: I see that the linked answer (in comment above) uses a similar algorithm—including topo-sort/cyclicality-checking, needed because the branch selection method is not "take from commit DAG"—but with more work, explicitly rebasing each branch as directed. If you work from the DAG, the leaf rebase has done all the work, and the predecessors can simply be relabeled, as I noted.

torek
  • 448,244
  • 59
  • 642
  • 775
  • I guess you mean answer http://stackoverflow.com/a/5600770/334451 ? That's certainly much more complex script than I was hoping for. – Mikko Rantalainen Nov 11 '13 at 06:56
  • Yes; but that script can (and presumably "should be able to") rebase originally-unrelated tree sections. E.g., it could rebase branch `experimental` on top of `feature-b` even if `experimental` is based off commit `K` that (after much backward-step-following) ultimately joins `master` at commit `A`. What you've proposed in your question is much simpler: "keep existing relationships intact" rather than "create whole new ones". – torek Nov 11 '13 at 07:02
  • @torek thanks for the beautifully precise explanation. i'd like to try to implement a script for your version of the algorithm (that doesn't do the extra work of rebasing each predecessor) but i've got one more case that might break your algorithm and i wanted your input. see: http://i.imgur.com/xU5G0le.png – Ankur Nov 26 '14 at 20:34
  • @Ankur: ah, I see. Yes, for this case there are two "forks" that need rebasing. It can be done in one pass with `git filter-branch` but that can be even more difficult (though in the case in your diagram, it should actually be easier, as you're just inserting one new parent on a single commit reachable from many branches). – torek Nov 26 '14 at 23:56
1

Try this:

git checkout featurea
git rebase
git branch --contains (sha1 of your old commit C) |xargs -n 1 git rebase --onto (sha1 of your new commit C') (sha1 of your old commit C)
Rudy Bunel
  • 784
  • 1
  • 7
  • 15
  • That's pretty close. Unfortunately, that requires locating SHA-1 of `feature-a` when the current `HEAD` usually points to `feature-c` and I know I want the whole set rebased to new `master`. Any suggestions about how I can find the branch name nearest the `master` to automatically script this? (That is, in this case I'd need `feature-a` as the answer when given `feature-c` and `master`.) – Mikko Rantalainen Nov 11 '13 at 06:41
0

The best way I can currently think of is rebasing one after the other:

git rebase --onto master B feature-a
git rebase --onto feature-a C feature-b
git rebase --onto feature-b D feature-c

But whenever you run into a problem like this, please think again, if you really want this and understand the consequences.

michas
  • 25,361
  • 15
  • 76
  • 121
0

It should be simple. Rebasing E on F would rabes the whole branch with commits C and D.

git checkout B
…
git commit -m 'F'
git checkout E
git rebase F
Hans Ginzel
  • 8,192
  • 3
  • 24
  • 22