D469: rebase: initial support for multiple destinations
quark (Jun Wu)
phabricator at mercurial-scm.org
Wed Aug 30 01:40:35 UTC 2017
quark updated this revision to Diff 1418.
quark edited the summary of this revision.
REPOSITORY
rHG Mercurial
CHANGES SINCE LAST UPDATE
https://phab.mercurial-scm.org/D469?vs=1148&id=1418
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
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
To: quark, #hg-reviewers
Cc: martinvonz, mercurial-devel
More information about the Mercurial-devel
mailing list