rebase: initial support for multiple destinations

This patch defines `SRC` (a single source revision) and `ALLSRC` (all source
revisions) to be valid names in  `--dest` revset if `--src` or `--rev` is
used. So destination could be defined differently according to source
revisions. The names are capitalized to make it clear they are "dynamically
defined", distinguishable from normal revsets (Thanks Augie for the
suggestion).

This is useful, for example, `-r 'orphan()' -d 'calc-dest(SRC)'` to solve
instability, which seems to be a highly wanted feature.

The feature is not completed, namely if `-d` overlaps with `-r`, things
could go wrong. A later patch will handle that case.

The feature is also gated by `experimental.rebase.multidest` config option
which is default off.

Differential Revision: https://phab.mercurial-scm.org/D469
This commit is contained in:
Jun Wu 2017-08-29 17:27:37 -07:00
parent 4706eacb8d
commit 973c4f76d4
4 changed files with 287 additions and 16 deletions

View File

@ -46,6 +46,7 @@ from mercurial import (
repair,
repoview,
revset,
revsetlang,
scmutil,
smartset,
util,
@ -736,8 +737,7 @@ def _definedestmap(ui, repo, destf=None, srcf=None, basef=None, revf=None,
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 @@ def _definedestmap(ui, repo, destf=None, srcf=None, basef=None, revf=None,
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 @@ def _definedestmap(ui, repo, destf=None, srcf=None, basef=None, revf=None,
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 @@ def adjustdest(repo, rev, destmap, state):
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 @@ def adjustdest(repo, rev, destmap, state):
| |
| 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

View File

@ -223,6 +223,9 @@ coreconfigitem('experimental', 'mergedriver',
coreconfigitem('experimental', 'obsmarkers-exchange-debug',
default=False,
)
coreconfigitem('experimental', 'rebase.multidest',
default=False,
)
coreconfigitem('experimental', 'revertalternateinteractivemode',
default=True,
)

View File

@ -402,11 +402,11 @@ def intrev(ctx):
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 @@ def revpair(repo, revs):
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 @@ def revrange(repo, specs):
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.

View File

@ -76,3 +76,237 @@ Check rebase.requiredest interaction with pull --rebase
(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