Patchwork [2,of,2] rebase: support multiple roots for rebaseset

login
register
mail settings
Submitter Pierre-Yves David
Date Jan. 14, 2013, 8:38 p.m.
Message ID <826298a2b238eabbd7e5.1358195927@yamac.lan>
Download mbox | patch
Permalink /patch/620/
State Superseded
Headers show

Comments

Pierre-Yves David - Jan. 14, 2013, 8:38 p.m.
# HG changeset patch
# User Pierre-Yves David <pierre-yves.david@ens-lyon.org>
# Date 1358175124 -3600
# Node ID 826298a2b238eabbd7e5f011d083d24729183761
# Parent  81d93e7ab9c10f6cfdd643c694f228119d3c04db
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 you draft on lastest upstream

  hg rebase --dest @ --rev 'draft()'

- exclusion of specify 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)::'
tonfa - Jan. 15, 2013, 11:15 p.m.
On Mon, Jan 14, 2013 at 9:38 PM, Pierre-Yves David <
pierre-yves.david@ens-lyon.org> wrote:

> # HG changeset patch
> # User Pierre-Yves David <pierre-yves.david@ens-lyon.org>
> # Date 1358175124 -3600
> # Node ID 826298a2b238eabbd7e5f011d083d24729183761
> # Parent  81d93e7ab9c10f6cfdd643c694f228119d3c04db
> 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 you draft on lastest upstream
>

s/you/your/

>
>   hg rebase --dest @ --rev 'draft()'
>
> - exclusion of specify changeset during rebase
>

s/specify/specific/


>
>   hg rebase --rev '42:: - author(Babar)'
>
> -  rebase a set of revision were multiple roots are later merged
>
>   hg rebase --rev '(18+42)::'
>

I wonder if it should accept either a set (and then only rebase that, do
not imply that descendants are rebased), or a single rev (and then
descendants are implied).


>
> diff --git a/hgext/rebase.py b/hgext/rebase.py
> --- a/hgext/rebase.py
> +++ b/hgext/rebase.py
> @@ -599,24 +599,21 @@ def buildstate(repo, dest, rebaseset, co
>          raise util.Abort(_('cannot rebase onto an applied mq patch'))
>
>      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]
> +    for root in roots:
> +        commonbase = root.ancestor(dest)
> +        if commonbase == root:
> +            raise util.Abort(_('source is ancestor of destination'))
>

More useful debug would be like:

source changeset %s is ancestor ...


> +        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')
>

ditto


> +                return None
>
> -    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))
> +    repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots))

     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
> @@ -651,17 +648,21 @@ def buildstate(repo, dest, rebaseset, co
>      #
> +--------------------+----------------------+-------------------------+
>      # | 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
> +    roots.sort(reverse=False)
>

isn't reverse=False implied?
(but you could merge it with the for, e.g. for root in sorted(roots))


> +    for root in roots:
> +        if len(root.parents()) <= 1:
> +            # ancestors of <root> not ancestors of <dest>
> +            detachset = repo.changelog.findmissingrevs([commonbase.rev()],
> +                                                       [root.rev()])
> +            for r in detachset:
> +                if r not in state:
> +                    state[r] = nullmerge
> +            # detachset can have root, and we definitely want to rebase
> that
> +            state[root.rev()] = nullrev
>      return repo['.'].rev(), dest.rev(), state
>
>  def clearrebased(ui, repo, state, collapsedas=None):
>      """dispose of rebased revision at the end of the rebase
>
> @@ -677,16 +678,20 @@ def clearrebased(ui, repo, state, collap
>          if markers:
>              obsolete.createmarkers(repo, markers)
>      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"))
>

I don't think you need the for, simply do descendants(roots) - state.

 +                else:
> +                    stripped.append(root.node())
> +            for nroot in stripped:
>                  # backup the old csets by default
> -                repair.strip(ui, repo, repo[min(rebased)].node(), "all")
> +                repair.strip(ui, repo, nroot, "all")
>

I don't think we want to call strip multiple times. It can take a list as
argument, should you use that?

Also update the rebase command doc? Or do you prefer if someone else
contributes it?


>  def pullrebase(orig, ui, repo, *args, **opts):
>      'Call rebase after pull if the latter has been invoked with --rebase'
>      if opts.get('rebase'):
> 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
> @@ -282,11 +282,11 @@ not be rebased.
>  Test unstability creation option
>  ------------------------------------
>
>    $ hg log -r 'children(8)'
>    9:cf44d2f5a9f4 D (no-eol)
> -  $ hg rebase -r 8
> +  $ hg rebase --rev 8
>

unrelated change?


>    $ hg log -G
>    @  11:0d8f238b634c C
>    |
>    o  10:7c6027df6a99 B
>    |
> @@ -304,5 +304,28 @@ Test unstability creation option
>    |/
>    o  0:cd010b8cd998 A
>
>
>
> +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
> @@ -540,8 +540,26 @@ We rebase E and G on B
>  We would expect heads are I, F if it was supported
>
>    $ hg clone -q -u . ah ah6
>    $ cd ah6
>    $ hg rebase -r '(4+6)::' -d 1
> -  abort: can't rebase multiple roots
> -  [255]
> -  $ cd ..
> +  saved backup bundle to
> $TESTTMP/ah6/.hg/strip-backup/c01897464e7f-backup.hg (glob)
> +  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'
> +
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel
>

Patch

diff --git a/hgext/rebase.py b/hgext/rebase.py
--- a/hgext/rebase.py
+++ b/hgext/rebase.py
@@ -599,24 +599,21 @@  def buildstate(repo, dest, rebaseset, co
         raise util.Abort(_('cannot rebase onto an applied mq patch'))
 
     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]
+    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
 
-    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))
+    repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots))
     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
@@ -651,17 +648,21 @@  def buildstate(repo, dest, rebaseset, co
     # +--------------------+----------------------+-------------------------+
     # | 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
+    roots.sort(reverse=False)
+    for root in roots:
+        if len(root.parents()) <= 1:
+            # ancestors of <root> not ancestors of <dest>
+            detachset = repo.changelog.findmissingrevs([commonbase.rev()],
+                                                       [root.rev()])
+            for r in detachset:
+                if r not in state:
+                    state[r] = nullmerge
+            # detachset can have root, and we definitely want to rebase that
+            state[root.rev()] = nullrev
     return repo['.'].rev(), dest.rev(), state
 
 def clearrebased(ui, repo, state, collapsedas=None):
     """dispose of rebased revision at the end of the rebase
 
@@ -677,16 +678,20 @@  def clearrebased(ui, repo, state, collap
         if markers:
             obsolete.createmarkers(repo, markers)
     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())
+            for nroot in stripped:
                 # backup the old csets by default
-                repair.strip(ui, repo, repo[min(rebased)].node(), "all")
+                repair.strip(ui, repo, nroot, "all")
 
 
 def pullrebase(orig, ui, repo, *args, **opts):
     'Call rebase after pull if the latter has been invoked with --rebase'
     if opts.get('rebase'):
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
@@ -282,11 +282,11 @@  not be rebased.
 Test unstability creation option
 ------------------------------------
 
   $ hg log -r 'children(8)'
   9:cf44d2f5a9f4 D (no-eol)
-  $ hg rebase -r 8
+  $ hg rebase --rev 8
   $ hg log -G
   @  11:0d8f238b634c C
   |
   o  10:7c6027df6a99 B
   |
@@ -304,5 +304,28 @@  Test unstability creation option
   |/
   o  0:cd010b8cd998 A
   
 
 
+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
@@ -540,8 +540,26 @@  We rebase E and G on B
 We would expect heads are I, F if it was supported
 
   $ hg clone -q -u . ah ah6
   $ cd ah6
   $ hg rebase -r '(4+6)::' -d 1
-  abort: can't rebase multiple roots
-  [255]
-  $ cd ..
+  saved backup bundle to $TESTTMP/ah6/.hg/strip-backup/c01897464e7f-backup.hg (glob)
+  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'
+