Patchwork D469: rebase: initial support for multiple destinations

login
register
mail settings
Submitter phabricator
Date Aug. 30, 2017, 4:53 a.m.
Message ID <e22008bbc87f80380415896cafc39d88@localhost.localdomain>
Download mbox | patch
Permalink /patch/23497/
State Not Applicable
Headers show

Comments

phabricator - Aug. 30, 2017, 4:53 a.m.
This revision was automatically updated to reflect the committed changes.
Closed by commit rHG5e83a8fe6bc4: rebase: initial support for multiple destinations (authored by quark).

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D469?vs=1418&id=1426

REVISION DETAIL
  https://phab.mercurial-scm.org/D469

AFFECTED FILES
  hgext/rebase.py
  mercurial/configitems.py
  mercurial/scmutil.py
  tests/test-rebase-dest.t

CHANGE DETAILS




To: quark, #hg-reviewers
Cc: martinvonz, mercurial-devel

Patch

diff --git a/tests/test-rebase-dest.t b/tests/test-rebase-dest.t
--- a/tests/test-rebase-dest.t
+++ b/tests/test-rebase-dest.t
@@ -76,3 +76,237 @@ 
   (use hg pull followed by hg rebase -d DEST)
   [255]
 
+Setup rebase with multiple destinations
+
+  $ cd $TESTTMP
+
+  $ cat >> $TESTTMP/maprevset.py <<EOF
+  > from __future__ import absolute_import
+  > from mercurial import registrar, revset, revsetlang, smartset
+  > revsetpredicate = registrar.revsetpredicate()
+  > cache = {}
+  > @revsetpredicate('map')
+  > def map(repo, subset, x):
+  >     """(set, mapping)"""
+  >     setarg, maparg = revsetlang.getargs(x, 2, 2, '')
+  >     rset = revset.getset(repo, smartset.fullreposet(repo), setarg)
+  >     mapstr = revsetlang.getstring(maparg, '')
+  >     map = dict(a.split(':') for a in mapstr.split(','))
+  >     rev = rset.first()
+  >     desc = repo[rev].description()
+  >     newdesc = map.get(desc)
+  >     if newdesc == 'null':
+  >         revs = [-1]
+  >     else:
+  >         query = revsetlang.formatspec('desc(%s)', newdesc)
+  >         revs = repo.revs(query)
+  >     return smartset.baseset(revs)
+  > EOF
+
+  $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > allowemptycommit=1
+  > [extensions]
+  > drawdag=$TESTDIR/drawdag.py
+  > [phases]
+  > publish=False
+  > [alias]
+  > tglog = log -G --template "{rev}: {desc} {instabilities}" -r 'sort(all(), topo)'
+  > [extensions]
+  > maprevset=$TESTTMP/maprevset.py
+  > [experimental]
+  > rebase.multidest=true
+  > stabilization=all
+  > EOF
+
+  $ rebasewithdag() {
+  >   N=`$PYTHON -c "print($N+1)"`
+  >   hg init repo$N && cd repo$N
+  >   hg debugdrawdag
+  >   hg rebase "$@" > _rebasetmp
+  >   r=$?
+  >   grep -v 'saved backup bundle' _rebasetmp
+  >   [ $r -eq 0 ] && rm -f .hg/localtags && hg tglog
+  >   cd ..
+  >   return $r
+  > }
+
+Destination resolves to an empty set:
+
+  $ rebasewithdag -s B -d 'SRC - SRC' <<'EOS'
+  > C
+  > |
+  > B
+  > |
+  > A
+  > EOS
+  nothing to rebase - empty destination
+  [1]
+
+Multiple destinations and --collapse are not compatible:
+
+  $ rebasewithdag -s C+E -d 'SRC^^' --collapse <<'EOS'
+  > C F
+  > | |
+  > B E
+  > | |
+  > A D
+  > EOS
+  abort: --collapse does not work with multiple destinations
+  [255]
+
+Multiple destinations cannot be used with --base:
+
+  $ rebasewithdag -b B+E -d 'SRC^^' --collapse <<'EOS'
+  > B E
+  > | |
+  > A D
+  > EOS
+  abort: unknown revision 'SRC'!
+  [255]
+
+Rebase to null should work:
+
+  $ rebasewithdag -r A+C+D -d 'null' <<'EOS'
+  > C D
+  > | |
+  > A B
+  > EOS
+  already rebased 0:426bada5c675 "A" (A)
+  already rebased 2:dc0947a82db8 "C" (C)
+  rebasing 3:004dc1679908 "D" (D tip)
+  o  4: D
+  
+  o  2: C
+  |
+  | o  1: B
+  |
+  o  0: A
+  
+Destination resolves to multiple changesets:
+
+  $ rebasewithdag -s B -d 'ALLSRC+SRC' <<'EOS'
+  > C
+  > |
+  > B
+  > |
+  > Z
+  > EOS
+  abort: rebase destination for f0a671a46792 is not unique
+  [255]
+
+Destination is an ancestor of source:
+
+  $ rebasewithdag -s B -d 'SRC' <<'EOS'
+  > C
+  > |
+  > B
+  > |
+  > Z
+  > EOS
+  abort: source is ancestor of destination
+  [255]
+
+Switch roots:
+
+  $ rebasewithdag -s 'all() - roots(all())' -d 'roots(all()) - ::SRC' <<'EOS'
+  > C  F
+  > |  |
+  > B  E
+  > |  |
+  > A  D
+  > EOS
+  rebasing 2:112478962961 "B" (B)
+  rebasing 4:26805aba1e60 "C" (C)
+  rebasing 3:cd488e83d208 "E" (E)
+  rebasing 5:0069ba24938a "F" (F tip)
+  o  9: F
+  |
+  o  8: E
+  |
+  | o  7: C
+  | |
+  | o  6: B
+  | |
+  | o  1: D
+  |
+  o  0: A
+  
+Different destinations for merge changesets with a same root:
+
+  $ rebasewithdag -s B -d '((parents(SRC)-B-A)::) - (::ALLSRC)' <<'EOS'
+  > C G
+  > |\|
+  > | F
+  > |
+  > B E
+  > |\|
+  > A D
+  > EOS
+  rebasing 3:a4256619d830 "B" (B)
+  rebasing 6:8e139e245220 "C" (C tip)
+  o    8: C
+  |\
+  | o    7: B
+  | |\
+  o | |  5: G
+  | | |
+  | | o  4: E
+  | | |
+  o | |  2: F
+   / /
+  | o  1: D
+  |
+  o  0: A
+  
+Move to a previous parent:
+
+  $ rebasewithdag -s E+F+G -d 'SRC^^' <<'EOS'
+  >     H
+  >     |
+  >   D G
+  >   |/
+  >   C F
+  >   |/
+  >   B E  # E will be ignored, since E^^ is empty
+  >   |/
+  >   A
+  > EOS
+  rebasing 4:33441538d4aa "F" (F)
+  rebasing 6:cf43ad9da869 "G" (G)
+  rebasing 7:eef94f3b5f03 "H" (H tip)
+  o  10: H
+  |
+  | o  5: D
+  |/
+  o  3: C
+  |
+  | o  9: G
+  |/
+  o  1: B
+  |
+  | o  8: F
+  |/
+  | o  2: E
+  |/
+  o  0: A
+  
+Source overlaps with destination (not handled well currently):
+
+  $ rebasewithdag -s 'B+C+D' -d 'map(SRC, "B:C,C:D")' <<'EOS'
+  > B C D
+  >  \|/
+  >   A
+  > EOS
+  rebasing 1:112478962961 "B" (B)
+  rebasing 2:dc0947a82db8 "C" (C)
+  o  5: C
+  |
+  o  3: D
+  |
+  | o  4: B orphan
+  | |
+  | x  2: C
+  |/
+  o  0: A
+  
diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
--- a/mercurial/scmutil.py
+++ b/mercurial/scmutil.py
@@ -402,11 +402,11 @@ 
         return wdirrev
     return rev
 
-def revsingle(repo, revspec, default='.'):
+def revsingle(repo, revspec, default='.', localalias=None):
     if not revspec and revspec != 0:
         return repo[default]
 
-    l = revrange(repo, [revspec])
+    l = revrange(repo, [revspec], localalias=localalias)
     if not l:
         raise error.Abort(_('empty revision set'))
     return repo[l.last()]
@@ -445,7 +445,7 @@ 
 
     return repo.lookup(first), repo.lookup(second)
 
-def revrange(repo, specs):
+def revrange(repo, specs, localalias=None):
     """Execute 1 to many revsets and return the union.
 
     This is the preferred mechanism for executing revsets using user-specified
@@ -471,7 +471,7 @@ 
         if isinstance(spec, int):
             spec = revsetlang.formatspec('rev(%d)', spec)
         allspecs.append(spec)
-    return repo.anyrevs(allspecs, user=True)
+    return repo.anyrevs(allspecs, user=True, localalias=localalias)
 
 def meaningfulparents(repo, ctx):
     """Return list of meaningful (or all if debug) parentrevs for rev.
diff --git a/mercurial/configitems.py b/mercurial/configitems.py
--- a/mercurial/configitems.py
+++ b/mercurial/configitems.py
@@ -223,6 +223,9 @@ 
 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
     default=False,
 )
+coreconfigitem('experimental', 'rebase.multidest',
+    default=False,
+)
 coreconfigitem('experimental', 'revertalternateinteractivemode',
     default=True,
 )
diff --git a/hgext/rebase.py b/hgext/rebase.py
--- a/hgext/rebase.py
+++ b/hgext/rebase.py
@@ -46,6 +46,7 @@ 
     repair,
     repoview,
     revset,
+    revsetlang,
     scmutil,
     smartset,
     util,
@@ -736,8 +737,7 @@ 
         raise error.Abort(_('you must specify a destination'),
                           hint=_('use: hg rebase -d REV'))
 
-    if destf:
-        dest = scmutil.revsingle(repo, destf)
+    dest = None
 
     if revf:
         rebaseset = scmutil.revrange(repo, revf)
@@ -757,7 +757,10 @@ 
             ui.status(_('empty "base" revision set - '
                         "can't compute rebase set\n"))
             return None
-        if not destf:
+        if destf:
+            # --base does not support multiple destinations
+            dest = scmutil.revsingle(repo, destf)
+        else:
             dest = repo[_destrebase(repo, base, destspace=destspace)]
             destf = str(dest)
 
@@ -806,9 +809,40 @@ 
         dest = repo[_destrebase(repo, rebaseset, destspace=destspace)]
         destf = str(dest)
 
-    # assign dest to each rev in rebaseset
-    destrev = dest.rev()
-    destmap = {r: destrev for r in rebaseset} # {srcrev: destrev}
+    allsrc = revsetlang.formatspec('%ld', rebaseset)
+    alias = {'ALLSRC': allsrc}
+
+    if dest is None:
+        try:
+            # fast path: try to resolve dest without SRC alias
+            dest = scmutil.revsingle(repo, destf, localalias=alias)
+        except error.RepoLookupError:
+            if not ui.configbool('experimental', 'rebase.multidest'):
+                raise
+            # multi-dest path: resolve dest for each SRC separately
+            destmap = {}
+            for r in rebaseset:
+                alias['SRC'] = revsetlang.formatspec('%d', r)
+                # use repo.anyrevs instead of scmutil.revsingle because we
+                # don't want to abort if destset is empty.
+                destset = repo.anyrevs([destf], user=True, localalias=alias)
+                size = len(destset)
+                if size == 1:
+                    destmap[r] = destset.first()
+                elif size == 0:
+                    ui.note(_('skipping %s - empty destination\n') % repo[r])
+                else:
+                    raise error.Abort(_('rebase destination for %s is not '
+                                        'unique') % repo[r])
+
+    if dest is not None:
+        # single-dest case: assign dest to each rev in rebaseset
+        destrev = dest.rev()
+        destmap = {r: destrev for r in rebaseset} # {srcrev: destrev}
+
+    if not destmap:
+        ui.status(_('nothing to rebase - empty destination\n'))
+        return None
 
     return destmap
 
@@ -903,8 +937,8 @@ 
     adjusted destinations for rev's p1 and p2, respectively. If a parent is
     nullrev, return dest without adjustment for it.
 
-    For example, when doing rebase -r B+E -d F, rebase will first move B to B1,
-    and E's destination will be adjusted from F to B1.
+    For example, when doing rebasing B+E to F, C to G, rebase will first move B
+    to B1, and E's destination will be adjusted from F to B1.
 
         B1 <- written during rebasing B
         |
@@ -916,11 +950,11 @@ 
         | |
         | x <- skipped, ex. no successor or successor in (::dest)
         | |
-        | C
+        | C <- rebased as C', different destination
         | |
-        | B <- rebased as B1
-        |/
-        A
+        | B <- rebased as B1     C'
+        |/                       |
+        A                        G <- destination of C, different
 
     Another example about merge changeset, rebase -r C+G+H -d K, rebase will
     first move C to C1, G to G1, and when it's checking H, the adjusted