diff --git a/hgext/rebase.py b/hgext/rebase.py --- a/hgext/rebase.py +++ b/hgext/rebase.py @@ -574,9 +574,9 @@ def abort(repo, originalwd, target, stat merge.update(repo, repo[originalwd].rev(), False, True, False) rebased = filter(lambda x: x > -1 and x != target, state.values()) if rebased: - strippoint = min(rebased) + strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)] # no backup of rebased cset versions needed - repair.strip(repo.ui, repo, repo[strippoint].node()) + repair.strip(repo.ui, repo, strippoints) clearstatus(repo) repo.ui.warn(_('rebase aborted\n')) return 0 @@ -599,65 +599,65 @@ def buildstate(repo, dest, rebaseset, co roots = list(repo.set('roots(%ld)', rebaseset)) if not roots: raise util.Abort(_('no matching revisions')) - if len(roots) > 1: - raise util.Abort(_("can't rebase multiple roots")) - root = roots[0] - - commonbase = root.ancestor(dest) - if commonbase == root: - raise util.Abort(_('source is ancestor of destination')) - if commonbase == dest: - samebranch = root.branch() == dest.branch() - if not collapse and samebranch and root in dest.children(): - repo.ui.debug('source is a child of destination\n') - return None + roots.sort() + state = {} + detachset = set() + for root in roots: + commonbase = root.ancestor(dest) + if commonbase == root: + raise util.Abort(_('source is ancestor of destination')) + if commonbase == dest: + samebranch = root.branch() == dest.branch() + if not collapse and samebranch and root in dest.children(): + repo.ui.debug('source is a child of destination\n') + return None - repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root)) - state = dict.fromkeys(rebaseset, nullrev) - # Rebase tries to turn into a parent of while - # preserving the number of parents of rebased changesets: - # - # - A changeset with a single parent will always be rebased as a - # changeset with a single parent. - # - # - A merge will be rebased as merge unless its parents are both - # ancestors of or are themselves in the rebased set and - # pruned while rebased. - # - # If one parent of is an ancestor of , the rebased - # version of this parent will be . This is always true with - # --base option. - # - # Otherwise, we need to *replace* the original parents with - # . This "detaches" the rebased set from its former location - # and rebases it onto . Changes introduced by ancestors of - # not common with (the detachset, marked as - # nullmerge) are "removed" from the rebased changesets. - # - # - If has a single parent, set it to . - # - # - If is a merge, we cannot decide which parent to - # replace, the rebase operation is not clearly defined. - # - # The table below sums up this behavior: - # - # +--------------------+----------------------+-------------------------+ - # | | one parent | merge | - # +--------------------+----------------------+-------------------------+ - # | parent in :: | new parent is | parents in :: are | - # | | | remapped to | - # +--------------------+----------------------+-------------------------+ - # | unrelated source | new parent is | ambiguous, abort | - # +--------------------+----------------------+-------------------------+ - # - # The actual abort is handled by `defineparents` - if len(root.parents()) <= 1: - # ancestors of not ancestors of - detachset = repo.changelog.findmissingrevs([commonbase.rev()], - [root.rev()]) - state.update(dict.fromkeys(detachset, nullmerge)) - # detachset can have root, and we definitely want to rebase that - state[root.rev()] = nullrev + repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots)) + state.update(dict.fromkeys(rebaseset, nullrev)) + # Rebase tries to turn into a parent of while + # preserving the number of parents of rebased changesets: + # + # - A changeset with a single parent will always be rebased as a + # changeset with a single parent. + # + # - A merge will be rebased as merge unless its parents are both + # ancestors of or are themselves in the rebased set and + # pruned while rebased. + # + # If one parent of is an ancestor of , the rebased + # version of this parent will be . This is always true with + # --base option. + # + # Otherwise, we need to *replace* the original parents with + # . This "detaches" the rebased set from its former location + # and rebases it onto . Changes introduced by ancestors of + # not common with (the detachset, marked as + # nullmerge) are "removed" from the rebased changesets. + # + # - If has a single parent, set it to . + # + # - If is a merge, we cannot decide which parent to + # replace, the rebase operation is not clearly defined. + # + # The table below sums up this behavior: + # + # +------------------+----------------------+-------------------------+ + # | | one parent | merge | + # +------------------+----------------------+-------------------------+ + # | parent in | new parent is | parents in :: are | + # | :: | | remapped to | + # +------------------+----------------------+-------------------------+ + # | unrelated source | new parent is | ambiguous, abort | + # +------------------+----------------------+-------------------------+ + # + # The actual abort is handled by `defineparents` + if len(root.parents()) <= 1: + # ancestors of not ancestors of + detachset.update(repo.changelog.findmissingrevs([commonbase.rev()], + [root.rev()])) + for r in detachset: + if r not in state: + state[r] = nullmerge return repo['.'].rev(), dest.rev(), state def clearrebased(ui, repo, state, collapsedas=None): @@ -677,12 +677,16 @@ def clearrebased(ui, repo, state, collap else: rebased = [rev for rev in state if state[rev] != nullmerge] if rebased: - if set(repo.changelog.descendants([min(rebased)])) - set(state): - ui.warn(_("warning: new changesets detected " - "on source branch, not stripping\n")) - else: + stripped = [] + for root in repo.set('roots(%ld)', rebased): + if set(repo.changelog.descendants([root.rev()])) - set(state): + ui.warn(_("warning: new changesets detected " + "on source branch, not stripping\n")) + else: + stripped.append(root.node()) + if stripped: # backup the old csets by default - repair.strip(ui, repo, repo[min(rebased)].node(), "all") + repair.strip(ui, repo, stripped, "all") def pullrebase(orig, ui, repo, *args, **opts): diff --git a/tests/test-rebase-obsolete.t b/tests/test-rebase-obsolete.t --- a/tests/test-rebase-obsolete.t +++ b/tests/test-rebase-obsolete.t @@ -306,3 +306,26 @@ Test that rewriting leaving instability +Test multiple root handling +------------------------------------ + + $ hg rebase --dest 4 --rev '7+11+9' + $ hg log -G + @ 14:00891d85fcfc C + | + | o 13:102b4c1d889b D + |/ + | o 12:bfe264faf697 H + |/ + | o 10:7c6027df6a99 B + | | + | x 7:02de42196ebe H + | | + +---o 6:eea13746799a G + | |/ + | o 5:24b6387c8c8c F + | | + o | 4:9520eea781bc E + |/ + o 0:cd010b8cd998 A + diff --git a/tests/test-rebase-scenario-global.t b/tests/test-rebase-scenario-global.t --- a/tests/test-rebase-scenario-global.t +++ b/tests/test-rebase-scenario-global.t @@ -542,6 +542,108 @@ We would expect heads are I, F if it was $ hg clone -q -u . ah ah6 $ cd ah6 $ hg rebase -r '(4+6)::' -d 1 - abort: can't rebase multiple roots - [255] + saved backup bundle to $TESTTMP/ah6/.hg/strip-backup/3d8a618087a7-backup.hg (glob) + $ hg tglog + @ 8: 'I' + | + o 7: 'H' + | + o 6: 'G' + | + | o 5: 'F' + | | + | o 4: 'E' + |/ + | o 3: 'D' + | | + | o 2: 'C' + | | + o | 1: 'B' + |/ + o 0: 'A' + $ cd .. + +More complexe rebase with multiple roots +each root have a different common ancestor with the destination and this is a detach + +(setup) + + $ hg clone -q -u . a a8 + $ cd a8 + $ echo I > I + $ hg add I + $ hg commit -m I + $ hg up 4 + 1 files updated, 0 files merged, 3 files removed, 0 files unresolved + $ echo I > J + $ hg add J + $ hg commit -m J + created new head + $ echo I > K + $ hg add K + $ hg commit -m K + $ hg tglog + @ 10: 'K' + | + o 9: 'J' + | + | o 8: 'I' + | | + | o 7: 'H' + | | + +---o 6: 'G' + | |/ + | o 5: 'F' + | | + o | 4: 'E' + |/ + | o 3: 'D' + | | + | o 2: 'C' + | | + | o 1: 'B' + |/ + o 0: 'A' + +(actual test) + + $ hg rebase --dest 'desc(G)' --rev 'desc(K) + desc(I)' + saved backup bundle to $TESTTMP/a8/.hg/strip-backup/23a4ace37988-backup.hg (glob) + $ hg log --rev 'children(desc(G))' + changeset: 9:adb617877056 + parent: 6:eea13746799a + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: I + + changeset: 10:882431a34a0e + tag: tip + parent: 6:eea13746799a + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: K + + $ hg tglog + @ 10: 'K' + | + | o 9: 'I' + |/ + | o 8: 'J' + | | + | | o 7: 'H' + | | | + o---+ 6: 'G' + |/ / + | o 5: 'F' + | | + o | 4: 'E' + |/ + | o 3: 'D' + | | + | o 2: 'C' + | | + | o 1: 'B' + |/ + o 0: 'A' +