changeset 18424:100fdc84670f

rebase: support multiple roots for rebaseset We have all the necessary mechanism to rebase a set with multiple roots, we only needed a proper handling of this case we preparing and concluding the rebase. This changeset des that. Rebase set with multiple root allows some awesome usage of rebase like: - rebase all your draft on lastest upstream hg rebase --dest @ --rev 'draft()' - exclusion of specific changeset during rebase hg rebase --rev '42:: - author(Babar)' - rebase a set of revision were multiple roots are later merged hg rebase --rev '(18+42)::'
author Pierre-Yves David <pierre-yves.david@ens-lyon.org>
date Thu, 17 Jan 2013 00:35:01 +0100
parents 5d6ee2494f63
children 6da1e979340a
files hgext/rebase.py tests/test-rebase-obsolete.t tests/test-rebase-scenario-global.t
diffstat 3 files changed, 196 insertions(+), 67 deletions(-) [+]
line wrap: on
line diff
--- a/hgext/rebase.py	Wed Jan 16 05:21:11 2013 +0100
+++ b/hgext/rebase.py	Thu Jan 17 00:35:01 2013 +0100
@@ -574,9 +574,9 @@
         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 @@
     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 <dest> into a parent of <root> 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 <dest> or are themselves in the rebased set and
-    #   pruned while rebased.
-    #
-    # If one parent of <root> is an ancestor of <dest>, the rebased
-    # version of this parent will be <dest>. This is always true with
-    # --base option.
-    #
-    # Otherwise, we need to *replace* the original parents with
-    # <dest>. This "detaches" the rebased set from its former location
-    # and rebases it onto <dest>. Changes introduced by ancestors of
-    # <root> not common with <dest> (the detachset, marked as
-    # nullmerge) are "removed" from the rebased changesets.
-    #
-    # - If <root> has a single parent, set it to <dest>.
-    #
-    # - If <root> 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 ::<dest> | new parent is <dest> | parents in ::<dest> are |
-    # |                    |                      | remapped to <dest>      |
-    # +--------------------+----------------------+-------------------------+
-    # | unrelated source   | new parent is <dest> | ambiguous, abort        |
-    # +--------------------+----------------------+-------------------------+
-    #
-    # The actual abort is handled by `defineparents`
-    if len(root.parents()) <= 1:
-        # ancestors of <root> not ancestors of <dest>
-        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 <dest> into a parent of <root> 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 <dest> or are themselves in the rebased set and
+        #   pruned while rebased.
+        #
+        # If one parent of <root> is an ancestor of <dest>, the rebased
+        # version of this parent will be <dest>. This is always true with
+        # --base option.
+        #
+        # Otherwise, we need to *replace* the original parents with
+        # <dest>. This "detaches" the rebased set from its former location
+        # and rebases it onto <dest>. Changes introduced by ancestors of
+        # <root> not common with <dest> (the detachset, marked as
+        # nullmerge) are "removed" from the rebased changesets.
+        #
+        # - If <root> has a single parent, set it to <dest>.
+        #
+        # - If <root> 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 <dest> | parents in ::<dest> are |
+        # | ::<dest>         |                      | remapped to <dest>      |
+        # +------------------+----------------------+-------------------------+
+        # | unrelated source | new parent is <dest> | ambiguous, abort        |
+        # +------------------+----------------------+-------------------------+
+        #
+        # The actual abort is handled by `defineparents`
+        if len(root.parents()) <= 1:
+            # ancestors of <root> not ancestors of <dest>
+            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 @@
     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):
--- a/tests/test-rebase-obsolete.t	Wed Jan 16 05:21:11 2013 +0100
+++ b/tests/test-rebase-obsolete.t	Thu Jan 17 00:35:01 2013 +0100
@@ -306,3 +306,26 @@
   
 
 
+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
+  
--- a/tests/test-rebase-scenario-global.t	Wed Jan 16 05:21:11 2013 +0100
+++ b/tests/test-rebase-scenario-global.t	Thu Jan 17 00:35:01 2013 +0100
@@ -542,6 +542,108 @@
   $ 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'
+