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 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/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/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 < 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 < [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 +