merge with stable
authorPierre-Yves David <pierre-yves.david@octobus.net>
Tue, 06 Feb 2018 13:00:28 +0100
changeset 3488 1baf32675ec6
parent 3487 14b9cded3c81 (diff)
parent 3486 bee9fee8f36b (current diff)
child 3489 ee72cdc2387b
merge with stable
tests/test-prev-next.t
--- a/CHANGELOG	Sun Jan 21 16:55:02 2018 -0500
+++ b/CHANGELOG	Tue Feb 06 13:00:28 2018 +0100
@@ -1,6 +1,13 @@
 Changelog
 =========
 
+7.3.0 --(in-progress)
+---------------------
+
+  * grab: new command to grab a changeset, put in on wdir parent
+          and update to it
+  * resolve: shows how to continue evolve after resolving all conflicts
+
 7.2.2 -- (in-progress)
 ----------------------
 
--- a/hgext3rd/evolve/__init__.py	Sun Jan 21 16:55:02 2018 -0500
+++ b/hgext3rd/evolve/__init__.py	Tue Feb 06 13:00:28 2018 +0100
@@ -252,11 +252,7 @@
     evolution=all
 """.strip()
 
-
-import os
 import sys
-import re
-import collections
 import struct
 
 try:
@@ -287,23 +283,19 @@
     cmdutil,
     commands,
     context,
-    copies,
     dirstate,
     error,
     extensions,
     help,
     hg,
     lock as lockmod,
-    merge,
     node,
     obsolete,
     patch,
-    phases,
     revset,
     scmutil,
 )
 
-from mercurial.commands import mergetoolopts
 from mercurial.i18n import _
 from mercurial.node import nullid
 
@@ -312,7 +304,8 @@
     compat,
     debugcmd,
     cmdrewrite,
-    evolvestate,
+    state,
+    evolvecmd,
     exthelper,
     metadata,
     obscache,
@@ -330,8 +323,6 @@
 minimumhgversion = metadata.minimumhgversion
 buglink = metadata.buglink
 
-sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
-
 # Flags for enabling optional parts of evolve
 commandopt = 'allnewcommands'
 
@@ -353,7 +344,6 @@
 aliases, entry = cmdutil.findcmd('commit', commands.table)
 commitopts3 = cmdrewrite.commitopts3
 interactiveopt = cmdrewrite.interactiveopt
-_bookmarksupdater = rewriteutil.bookmarksupdater
 rewrite = rewriteutil.rewrite
 
 # This extension contains the following code
@@ -365,6 +355,7 @@
 
 eh = exthelper.exthelper()
 eh.merge(debugcmd.eh)
+eh.merge(evolvecmd.eh)
 eh.merge(obsexchange.eh)
 eh.merge(checkheads.eh)
 eh.merge(safeguard.eh)
@@ -520,18 +511,6 @@
         ui.setconfig('alias', 'odiff',
                      "diff --hidden --rev 'limit(precursors(.),1)' --rev .",
                      'evolve')
-    if ui.config('alias', 'grab') is None:
-        if os.name == 'nt':
-            hgexe = ('"%s"' % util.hgexecutable())
-            ui.setconfig('alias', 'grab', "! " + hgexe
-                         + " rebase --dest . --rev $@ && "
-                         + hgexe + " up tip",
-                         'evolve')
-        else:
-            ui.setconfig('alias', 'grab',
-                         "! $HG rebase --dest . --rev $@ && $HG up tip",
-                         'evolve')
-
 
 ### Troubled revset symbol
 
@@ -854,8 +833,8 @@
         raise
 
 def summaryhook(ui, repo):
-    state = evolvestate.evolvestate(repo)
-    if state:
+    evolvestate = state.cmdstate(repo)
+    if evolvestate:
         # i18n: column positioning for "hg summary"
         ui.status(_('evolve: (evolve --continue)\n'))
 
@@ -889,79 +868,6 @@
 ### Old Evolve extension content                                  ###
 #####################################################################
 
-# XXX need clean up and proper sorting in other section
-
-### changeset rewriting logic
-#############################
-
-class MergeFailure(error.Abort):
-    pass
-
-def relocate(repo, orig, dest, pctx=None, keepbranch=False):
-    """rewrite <rev> on dest"""
-    if orig.rev() == dest.rev():
-        raise error.Abort(_('tried to relocate a node on top of itself'),
-                          hint=_("This shouldn't happen. If you still "
-                                 "need to move changesets, please do so "
-                                 "manually with nothing to rebase - working "
-                                 "directory parent is also destination"))
-
-    if pctx is None:
-        if len(orig.parents()) == 2:
-            raise error.Abort(_("tried to relocate a merge commit without "
-                                "specifying which parent should be moved"),
-                              hint=_("Specify the parent by passing in pctx"))
-        pctx = orig.p1()
-
-    commitmsg = orig.description()
-
-    cache = {}
-    sha1s = re.findall(sha1re, commitmsg)
-    unfi = repo.unfiltered()
-    for sha1 in sha1s:
-        ctx = None
-        try:
-            ctx = unfi[sha1]
-        except error.RepoLookupError:
-            continue
-
-        if not ctx.obsolete():
-            continue
-
-        successors = compat.successorssets(repo, ctx.node(), cache)
-
-        # We can't make any assumptions about how to update the hash if the
-        # cset in question was split or diverged.
-        if len(successors) == 1 and len(successors[0]) == 1:
-            newsha1 = node.hex(successors[0][0])
-            commitmsg = commitmsg.replace(sha1, newsha1[:len(sha1)])
-        else:
-            repo.ui.note(_('The stale commit message reference to %s could '
-                           'not be updated\n') % sha1)
-
-    tr = repo.currenttransaction()
-    assert tr is not None
-    try:
-        r = _evolvemerge(repo, orig, dest, pctx, keepbranch)
-        if r[-1]: # some conflict
-            raise error.Abort(_('unresolved merge conflicts '
-                                '(see hg help resolve)'))
-        nodenew = _relocatecommit(repo, orig, commitmsg)
-    except error.Abort as exc:
-        with repo.dirstate.parentchange():
-            repo.setparents(repo['.'].node(), nullid)
-            repo.dirstate.write(tr)
-            # fix up dirstate for copies and renames
-            compat.duplicatecopies(repo, repo[None], dest.rev(), orig.p1().rev())
-
-        class LocalMergeFailure(MergeFailure, exc.__class__):
-            pass
-        exc.__class__ = LocalMergeFailure
-        tr.close() # to keep changes in this transaction (e.g. dirstate)
-        raise
-    _finalizerelocate(repo, orig, dest, nodenew, tr)
-    return nodenew
-
 ### new command
 #############################
 
@@ -1039,1009 +945,6 @@
     _deprecatealias('gup', 'next')
     _deprecatealias('gdown', 'previous')
 
-def _solveone(ui, repo, ctx, dryrun, confirm, progresscb, category):
-    """Resolve the troubles affecting one revision"""
-    wlock = lock = tr = None
-    try:
-        wlock = repo.wlock()
-        lock = repo.lock()
-        tr = repo.transaction("evolve")
-        if 'orphan' == category:
-            result = _solveunstable(ui, repo, ctx, dryrun, confirm, progresscb)
-        elif 'phasedivergent' == category:
-            result = _solvebumped(ui, repo, ctx, dryrun, confirm, progresscb)
-        elif 'contentdivergent' == category:
-            result = _solvedivergent(ui, repo, ctx, dryrun, confirm,
-                                     progresscb)
-        else:
-            assert False, "unknown trouble category: %s" % (category)
-        tr.close()
-        return result
-    finally:
-        lockmod.release(tr, lock, wlock)
-
-def _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat):
-    """Used by the evolve function to display an error message when
-    no troubles can be resolved"""
-    troublecategories = ['phasedivergent', 'contentdivergent', 'orphan']
-    unselectedcategories = [c for c in troublecategories if c != targetcat]
-    msg = None
-    hint = None
-
-    troubled = {
-        "orphan": repo.revs("orphan()"),
-        "contentdivergent": repo.revs("contentdivergent()"),
-        "phasedivergent": repo.revs("phasedivergent()"),
-        "all": repo.revs("troubled()"),
-    }
-
-    hintmap = {
-        'phasedivergent': _("do you want to use --phase-divergent"),
-        'phasedivergent+contentdivergent': _("do you want to use "
-                                             "--phase-divergent or"
-                                             " --content-divergent"),
-        'phasedivergent+orphan': _("do you want to use --phase-divergent"
-                                   " or --orphan"),
-        'contentdivergent': _("do you want to use --content-divergent"),
-        'contentdivergent+orphan': _("do you want to use --content-divergent"
-                                     " or --orphan"),
-        'orphan': _("do you want to use --orphan"),
-        'any+phasedivergent': _("do you want to use --any (or --rev) and"
-                                " --phase-divergent"),
-        'any+phasedivergent+contentdivergent': _("do you want to use --any"
-                                                 " (or --rev) and"
-                                                 " --phase-divergent or"
-                                                 " --content-divergent"),
-        'any+phasedivergent+orphan': _("do you want to use --any (or --rev)"
-                                       " and --phase-divergent or --orphan"),
-        'any+contentdivergent': _("do you want to use --any (or --rev) and"
-                                  " --content-divergent"),
-        'any+contentdivergent+orphan': _("do you want to use --any (or --rev)"
-                                         " and --content-divergent or "
-                                         "--orphan"),
-        'any+orphan': _("do you want to use --any (or --rev)"
-                        "and --orphan"),
-    }
-
-    if revopt:
-        revs = scmutil.revrange(repo, revopt)
-        if not revs:
-            msg = _("set of specified revisions is empty")
-        else:
-            msg = _("no %s changesets in specified revisions") % targetcat
-            othertroubles = []
-            for cat in unselectedcategories:
-                if revs & troubled[cat]:
-                    othertroubles.append(cat)
-            if othertroubles:
-                hint = hintmap['+'.join(othertroubles)]
-
-    elif anyopt:
-        msg = _("no %s changesets to evolve") % targetcat
-        othertroubles = []
-        for cat in unselectedcategories:
-            if troubled[cat]:
-                othertroubles.append(cat)
-        if othertroubles:
-            hint = hintmap['+'.join(othertroubles)]
-
-    else:
-        # evolve without any option = relative to the current wdir
-        if targetcat == 'orphan':
-            msg = _("nothing to evolve on current working copy parent")
-        else:
-            msg = _("current working copy parent is not %s") % targetcat
-
-        p1 = repo['.'].rev()
-        othertroubles = []
-        for cat in unselectedcategories:
-            if p1 in troubled[cat]:
-                othertroubles.append(cat)
-        if othertroubles:
-            hint = hintmap['+'.join(othertroubles)]
-        else:
-            length = len(troubled[targetcat])
-            if length:
-                hint = _("%d other %s in the repository, do you want --any "
-                         "or --rev") % (length, targetcat)
-            else:
-                othertroubles = []
-                for cat in unselectedcategories:
-                    if troubled[cat]:
-                        othertroubles.append(cat)
-                if othertroubles:
-                    hint = hintmap['any+' + ('+'.join(othertroubles))]
-                else:
-                    msg = _("no troubled changesets")
-
-    assert msg is not None
-    ui.write_err("%s\n" % msg)
-    if hint:
-        ui.write_err("(%s)\n" % hint)
-        return 2
-    else:
-        return 1
-
-def _cleanup(ui, repo, startnode, showprogress):
-    if showprogress:
-        ui.progress(_('evolve'), None)
-    if repo['.'] != startnode:
-        ui.status(_('working directory is now at %s\n') % repo['.'])
-
-class MultipleSuccessorsError(RuntimeError):
-    """Exception raised by _singlesuccessor when multiple successor sets exists
-
-    The object contains the list of successorssets in its 'successorssets'
-    attribute to call to easily recover.
-    """
-
-    def __init__(self, successorssets):
-        self.successorssets = successorssets
-
-def _singlesuccessor(repo, p):
-    """returns p (as rev) if not obsolete or its unique latest successors
-
-    fail if there are no such successor"""
-
-    if not p.obsolete():
-        return p.rev()
-    obs = repo[p]
-    ui = repo.ui
-    newer = compat.successorssets(repo, obs.node())
-    # search of a parent which is not killed
-    while not newer:
-        ui.debug("stabilize target %s is plain dead,"
-                 " trying to stabilize on its parent\n" %
-                 obs)
-        obs = obs.parents()[0]
-        newer = compat.successorssets(repo, obs.node())
-    if len(newer) > 1 or len(newer[0]) > 1:
-        raise MultipleSuccessorsError(newer)
-
-    return repo[newer[0][0]].rev()
-
-def builddependencies(repo, revs):
-    """returns dependency graphs giving an order to solve instability of revs
-    (see _orderrevs for more information on usage)"""
-
-    # For each troubled revision we keep track of what instability if any should
-    # be resolved in order to resolve it. Example:
-    # dependencies = {3: [6], 6:[]}
-    # Means that: 6 has no dependency, 3 depends on 6 to be solved
-    dependencies = {}
-    # rdependencies is the inverted dict of dependencies
-    rdependencies = collections.defaultdict(set)
-
-    for r in revs:
-        dependencies[r] = set()
-        for p in repo[r].parents():
-            try:
-                succ = _singlesuccessor(repo, p)
-            except MultipleSuccessorsError as exc:
-                dependencies[r] = exc.successorssets
-                continue
-            if succ in revs:
-                dependencies[r].add(succ)
-                rdependencies[succ].add(r)
-    return dependencies, rdependencies
-
-def _dedupedivergents(repo, revs):
-    """Dedupe the divergents revs in revs to get one from each group with the
-    lowest revision numbers
-    """
-    repo = repo.unfiltered()
-    res = set()
-    # To not reevaluate divergents of the same group once one is encountered
-    discarded = set()
-    for rev in revs:
-        if rev in discarded:
-            continue
-        divergent = repo[rev]
-        base, others = divergentdata(divergent)
-        othersrevs = [o.rev() for o in others]
-        res.add(min([divergent.rev()] + othersrevs))
-        discarded.update(othersrevs)
-    return res
-
-instabilities_map = {
-    'contentdivergent': "content-divergent",
-    'phasedivergent': "phase-divergent"
-}
-
-def _selectrevs(repo, allopt, revopt, anyopt, targetcat):
-    """select troubles in repo matching according to given options"""
-    revs = set()
-    if allopt or revopt:
-        revs = repo.revs("%s()" % targetcat)
-        if revopt:
-            revs = scmutil.revrange(repo, revopt) & revs
-        elif not anyopt:
-            topic = getattr(repo, 'currenttopic', '')
-            if topic:
-                revs = repo.revs('topic(%s)', topic) & revs
-            elif targetcat == 'orphan':
-                revs = _aspiringdescendant(repo,
-                                           repo.revs('(.::) - obsolete()::'))
-                revs = set(revs)
-        if targetcat == 'contentdivergent':
-            # Pick one divergent per group of divergents
-            revs = _dedupedivergents(repo, revs)
-    elif anyopt:
-        revs = repo.revs('first(%s())' % (targetcat))
-    elif targetcat == 'orphan':
-        revs = set(_aspiringchildren(repo, repo.revs('(.::) - obsolete()::')))
-        if 1 < len(revs):
-            msg = "multiple evolve candidates"
-            hint = (_("select one of %s with --rev")
-                    % ', '.join([str(repo[r]) for r in sorted(revs)]))
-            raise error.Abort(msg, hint=hint)
-    elif instabilities_map.get(targetcat, targetcat) in repo['.'].instabilities():
-        revs = set([repo['.'].rev()])
-    return revs
-
-
-def _orderrevs(repo, revs):
-    """Compute an ordering to solve instability for the given revs
-
-    revs is a list of unstable revisions.
-
-    Returns the same revisions ordered to solve their instability from the
-    bottom to the top of the stack that the stabilization process will produce
-    eventually.
-
-    This ensures the minimal number of stabilizations, as we can stabilize each
-    revision on its final stabilized destination.
-    """
-    # Step 1: Build the dependency graph
-    dependencies, rdependencies = builddependencies(repo, revs)
-    # Step 2: Build the ordering
-    # Remove the revisions with no dependency(A) and add them to the ordering.
-    # Removing these revisions leads to new revisions with no dependency (the
-    # one depending on A) that we can remove from the dependency graph and add
-    # to the ordering. We progress in a similar fashion until the ordering is
-    # built
-    solvablerevs = collections.deque([r for r in sorted(dependencies.keys())
-                                      if not dependencies[r]])
-    ordering = []
-    while solvablerevs:
-        rev = solvablerevs.popleft()
-        for dependent in rdependencies[rev]:
-            dependencies[dependent].remove(rev)
-            if not dependencies[dependent]:
-                solvablerevs.append(dependent)
-        del dependencies[rev]
-        ordering.append(rev)
-
-    ordering.extend(sorted(dependencies))
-    return ordering
-
-def divergentsets(repo, ctx):
-    """Compute sets of commits divergent with a given one"""
-    cache = {}
-    base = {}
-    for n in compat.allprecursors(repo.obsstore, [ctx.node()]):
-        if n == ctx.node():
-            # a node can't be a base for divergence with itself
-            continue
-        nsuccsets = compat.successorssets(repo, n, cache)
-        for nsuccset in nsuccsets:
-            if ctx.node() in nsuccset:
-                # we are only interested in *other* successor sets
-                continue
-            if tuple(nsuccset) in base:
-                # we already know the latest base for this divergency
-                continue
-            base[tuple(nsuccset)] = n
-    divergence = []
-    for divset, b in base.iteritems():
-        divergence.append({
-            'divergentnodes': divset,
-            'commonprecursor': b
-        })
-
-    return divergence
-
-def _preparelistctxs(items, condition):
-    return [item.hex() for item in items if condition(item)]
-
-def _formatctx(fm, ctx):
-    fm.data(node=ctx.hex())
-    fm.data(desc=ctx.description())
-    fm.data(date=ctx.date())
-    fm.data(user=ctx.user())
-
-def listtroubles(ui, repo, troublecategories, **opts):
-    """Print all the troubles for the repo (or given revset)"""
-    troublecategories = troublecategories or ['contentdivergent', 'orphan', 'phasedivergent']
-    showunstable = 'orphan' in troublecategories
-    showbumped = 'phasedivergent' in troublecategories
-    showdivergent = 'contentdivergent' in troublecategories
-
-    revs = repo.revs('+'.join("%s()" % t for t in troublecategories))
-    if opts.get('rev'):
-        revs = scmutil.revrange(repo, opts.get('rev'))
-
-    fm = ui.formatter('evolvelist', opts)
-    for rev in revs:
-        ctx = repo[rev]
-        unpars = _preparelistctxs(ctx.parents(), lambda p: p.orphan())
-        obspars = _preparelistctxs(ctx.parents(), lambda p: p.obsolete())
-        imprecs = _preparelistctxs(repo.set("allprecursors(%n)", ctx.node()),
-                                   lambda p: not p.mutable())
-        dsets = divergentsets(repo, ctx)
-
-        fm.startitem()
-        # plain formatter section
-        hashlen, desclen = 12, 60
-        desc = ctx.description()
-        if desc:
-            desc = desc.splitlines()[0]
-        desc = (desc[:desclen] + '...') if len(desc) > desclen else desc
-        fm.plain('%s: ' % ctx.hex()[:hashlen])
-        fm.plain('%s\n' % desc)
-        fm.data(node=ctx.hex(), rev=ctx.rev(), desc=desc, phase=ctx.phasestr())
-
-        for unpar in unpars if showunstable else []:
-            fm.plain('  %s: %s (%s parent)\n' % (TROUBLES['ORPHAN'],
-                                                 unpar[:hashlen],
-                                                 TROUBLES['ORPHAN']))
-        for obspar in obspars if showunstable else []:
-            fm.plain('  %s: %s (obsolete parent)\n' % (TROUBLES['ORPHAN'],
-                                                       obspar[:hashlen]))
-        for imprec in imprecs if showbumped else []:
-            fm.plain('  %s: %s (immutable precursor)\n' %
-                     (TROUBLES['PHASEDIVERGENT'], imprec[:hashlen]))
-
-        if dsets and showdivergent:
-            for dset in dsets:
-                fm.plain('  %s: ' % TROUBLES['CONTENTDIVERGENT'])
-                first = True
-                for n in dset['divergentnodes']:
-                    t = "%s (%s)" if first else " %s (%s)"
-                    first = False
-                    fm.plain(t % (node.hex(n)[:hashlen], repo[n].phasestr()))
-                comprec = node.hex(dset['commonprecursor'])[:hashlen]
-                fm.plain(" (precursor %s)\n" % comprec)
-        fm.plain("\n")
-
-        # templater-friendly section
-        _formatctx(fm, ctx)
-        troubles = []
-        for unpar in unpars:
-            troubles.append({'troubletype': TROUBLES['ORPHAN'],
-                             'sourcenode': unpar, 'sourcetype': 'orphanparent'})
-        for obspar in obspars:
-            troubles.append({'troubletype': TROUBLES['ORPHAN'],
-                             'sourcenode': obspar,
-                             'sourcetype': 'obsoleteparent'})
-        for imprec in imprecs:
-            troubles.append({'troubletype': TROUBLES['PHASEDIVERGENT'],
-                             'sourcenode': imprec,
-                             'sourcetype': 'immutableprecursor'})
-        for dset in dsets:
-            divnodes = [{'node': node.hex(n),
-                         'phase': repo[n].phasestr(),
-                        } for n in dset['divergentnodes']]
-            troubles.append({'troubletype': TROUBLES['CONTENTDIVERGENT'],
-                             'commonprecursor': node.hex(dset['commonprecursor']),
-                             'divergentnodes': divnodes})
-        fm.data(troubles=troubles)
-
-    fm.end()
-
-@eh.command(
-    '^evolve|stabilize|solve',
-    [('n', 'dry-run', False,
-      _('do not perform actions, just print what would be done')),
-     ('', 'confirm', False,
-      _('ask for confirmation before performing the action')),
-     ('A', 'any', False,
-      _('also consider troubled changesets unrelated to current working '
-        'directory')),
-     ('r', 'rev', [], _('solves troubles of these revisions')),
-     ('', 'bumped', False, _('solves only bumped changesets')),
-     ('', 'phase-divergent', False, _('solves only phase-divergent changesets')),
-     ('', 'divergent', False, _('solves only divergent changesets')),
-     ('', 'content-divergent', False, _('solves only content-divergent changesets')),
-     ('', 'unstable', False, _('solves only unstable changesets')),
-     ('', 'orphan', False, _('solves only orphan changesets (default)')),
-     ('a', 'all', False, _('evolve all troubled changesets related to the '
-                           'current  working directory and its descendants')),
-     ('c', 'continue', False, _('continue an interrupted evolution')),
-     ('l', 'list', False, 'provide details on troubled changesets in the repo'),
-    ] + mergetoolopts,
-    _('[OPTIONS]...')
-)
-def evolve(ui, repo, **opts):
-    """solve troubled changesets in your repository
-
-    Modifying history can lead to various types of troubled changesets:
-    unstable, bumped, or divergent. The evolve command resolves your troubles
-    by executing one of the following actions:
-
-    - update working copy to a successor
-    - rebase an unstable changeset
-    - extract the desired changes from a bumped changeset
-    - fuse divergent changesets back together
-
-    If you pass no arguments, evolve works in automatic mode: it will execute a
-    single action to reduce instability related to your working copy. There are
-    two cases for this action. First, if the parent of your working copy is
-    obsolete, evolve updates to the parent's successor. Second, if the working
-    copy parent is not obsolete but has obsolete predecessors, then evolve
-    determines if there is an unstable changeset that can be rebased onto the
-    working copy parent in order to reduce instability.
-    If so, evolve rebases that changeset. If not, evolve refuses to guess your
-    intention, and gives a hint about what you might want to do next.
-
-    Any time evolve creates a changeset, it updates the working copy to the new
-    changeset. (Currently, every successful evolve operation involves an update
-    as well; this may change in future.)
-
-    Automatic mode only handles common use cases. For example, it avoids taking
-    action in the case of ambiguity, and it ignores unstable changesets that
-    are not related to your working copy.
-    It also refuses to solve bumped or divergent changesets unless you
-    explicitly request such behavior (see below).
-
-    Eliminating all instability around your working copy may require multiple
-    invocations of :hg:`evolve`. Alternately, use ``--all`` to recursively
-    select and evolve all unstable changesets that can be rebased onto the
-    working copy parent.
-    This is more powerful than successive invocations, since ``--all`` handles
-    ambiguous cases (e.g. unstable changesets with multiple children) by
-    evolving all branches.
-
-    When your repository cannot be handled by automatic mode, you might need to
-    use ``--rev`` to specify a changeset to evolve. For example, if you have
-    an unstable changeset that is not related to the working copy parent,
-    you could use ``--rev`` to evolve it. Or, if some changeset has multiple
-    unstable children, evolve in automatic mode refuses to guess which one to
-    evolve; you have to use ``--rev`` in that case.
-
-    Alternately, ``--any`` makes evolve search for the next evolvable changeset
-    regardless of whether it is related to the working copy parent.
-
-    You can supply multiple revisions to evolve multiple troubled changesets
-    in a single invocation. In revset terms, ``--any`` is equivalent to ``--rev
-    first(unstable())``. ``--rev`` and ``--all`` are mutually exclusive, as are
-    ``--rev`` and ``--any``.
-
-    ``hg evolve --any --all`` is useful for cleaning up instability across all
-    branches, letting evolve figure out the appropriate order and destination.
-
-    When you have troubled changesets that are not unstable, :hg:`evolve`
-    refuses to consider them unless you specify the category of trouble you
-    wish to resolve, with ``--bumped`` or ``--divergent``. These options are
-    currently mutually exclusive with each other and with ``--unstable``
-    (the default). You can combine ``--bumped`` or ``--divergent`` with
-    ``--rev``, ``--all``, or ``--any``.
-
-    You can also use the evolve command to list the troubles affecting your
-    repository by using the --list flag. You can choose to display only some
-    categories of troubles with the --unstable, --divergent or --bumped flags.
-    """
-
-    opts = _checkevolveopts(repo, opts)
-    # Options
-    contopt = opts['continue']
-    anyopt = opts['any']
-    allopt = opts['all']
-    startnode = repo['.']
-    dryrunopt = opts['dry_run']
-    confirmopt = opts['confirm']
-    revopt = opts['rev']
-
-    troublecategories = ['phase_divergent', 'content_divergent', 'orphan']
-    specifiedcategories = [t.replace('_', '')
-                           for t in troublecategories
-                           if opts[t]]
-    if opts['list']:
-        compat.startpager(ui, 'evolve')
-        listtroubles(ui, repo, specifiedcategories, **opts)
-        return
-
-    targetcat = 'orphan'
-    if 1 < len(specifiedcategories):
-        msg = _('cannot specify more than one trouble category to solve (yet)')
-        raise error.Abort(msg)
-    elif len(specifiedcategories) == 1:
-        targetcat = specifiedcategories[0]
-    elif repo['.'].obsolete():
-        displayer = compat.changesetdisplayer(ui, repo,
-                                              {'template': shorttemplate})
-        # no args and parent is obsolete, update to successors
-        try:
-            ctx = repo[_singlesuccessor(repo, repo['.'])]
-        except MultipleSuccessorsError as exc:
-            repo.ui.write_err('parent is obsolete with multiple successors:\n')
-            for ln in exc.successorssets:
-                for n in ln:
-                    displayer.show(repo[n])
-            return 2
-
-        ui.status(_('update:'))
-        if not ui.quiet:
-            displayer.show(ctx)
-
-        if dryrunopt:
-            return 0
-        res = hg.update(repo, ctx.rev())
-        if ctx != startnode:
-            ui.status(_('working directory is now at %s\n') % ctx)
-        return res
-
-    ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'evolve')
-    troubled = set(repo.revs('troubled()'))
-
-    # Progress handling
-    seen = 1
-    count = allopt and len(troubled) or 1
-    showprogress = allopt
-
-    def progresscb():
-        if revopt or allopt:
-            ui.progress(_('evolve'), seen, unit=_('changesets'), total=count)
-
-    # Continuation handling
-    if contopt:
-        state = evolvestate.evolvestate(repo)
-        if not state:
-            raise error.Abort('no evolve to continue')
-        state.load()
-        orig = repo[state['current']]
-        with repo.wlock(), repo.lock():
-            ctx = orig
-            source = ctx.extra().get('source')
-            extra = {}
-            if source:
-                extra['source'] = source
-                extra['intermediate-source'] = ctx.hex()
-            else:
-                extra['source'] = ctx.hex()
-            user = ctx.user()
-            date = ctx.date()
-            message = ctx.description()
-            ui.status(_('evolving %d:%s "%s"\n') % (ctx.rev(), ctx,
-                                                    message.split('\n', 1)[0]))
-            targetphase = max(ctx.phase(), phases.draft)
-            overrides = {('phases', 'new-commit'): targetphase}
-
-            with repo.ui.configoverride(overrides, 'evolve-continue'):
-                node = repo.commit(text=message, user=user,
-                                   date=date, extra=extra)
-
-            obsolete.createmarkers(repo, [(ctx, (repo[node],))])
-            state.delete()
-            return
-
-    cmdutil.bailifchanged(repo)
-
-    revs = _selectrevs(repo, allopt, revopt, anyopt, targetcat)
-
-    if not revs:
-        return _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat)
-
-    # For the progress bar to show
-    count = len(revs)
-    # Order the revisions
-    if targetcat == 'orphan':
-        revs = _orderrevs(repo, revs)
-    for rev in revs:
-        progresscb()
-        _solveone(ui, repo, repo[rev], dryrunopt, confirmopt,
-                  progresscb, targetcat)
-        seen += 1
-    progresscb()
-    _cleanup(ui, repo, startnode, showprogress)
-
-def _checkevolveopts(repo, opts):
-    """ check the options passed to `hg evolve` and warn for deprecation warning
-    if any """
-
-    if opts['continue']:
-        if opts['any']:
-            raise error.Abort('cannot specify both "--any" and "--continue"')
-        if opts['all']:
-            raise error.Abort('cannot specify both "--all" and "--continue"')
-
-    if opts['rev']:
-        if opts['any']:
-            raise error.Abort('cannot specify both "--rev" and "--any"')
-        if opts['all']:
-            raise error.Abort('cannot specify both "--rev" and "--all"')
-
-    # Backward compatibility
-    if opts['unstable']:
-        msg = ("'evolve --unstable' is deprecated, "
-               "use 'evolve --orphan'")
-        repo.ui.deprecwarn(msg, '4.4')
-
-        opts['orphan'] = opts['divergent']
-
-    if opts['divergent']:
-        msg = ("'evolve --divergent' is deprecated, "
-               "use 'evolve --content-divergent'")
-        repo.ui.deprecwarn(msg, '4.4')
-
-        opts['content_divergent'] = opts['divergent']
-
-    if opts['bumped']:
-        msg = ("'evolve --bumped' is deprecated, "
-               "use 'evolve --phase-divergent'")
-        repo.ui.deprecwarn(msg, '4.4')
-
-        opts['phase_divergent'] = opts['bumped']
-
-    return opts
-
-def _possibledestination(repo, rev):
-    """return all changesets that may be a new parent for REV"""
-    tonode = repo.changelog.node
-    parents = repo.changelog.parentrevs
-    torev = repo.changelog.rev
-    dest = set()
-    tovisit = list(parents(rev))
-    while tovisit:
-        r = tovisit.pop()
-        succsets = compat.successorssets(repo, tonode(r))
-        if not succsets:
-            tovisit.extend(parents(r))
-        else:
-            # We should probably pick only one destination from split
-            # (case where '1 < len(ss)'), This could be the currently tipmost
-            # but logic is less clear when result of the split are now on
-            # multiple branches.
-            for ss in succsets:
-                for n in ss:
-                    dest.add(torev(n))
-    return dest
-
-def _aspiringchildren(repo, revs):
-    """Return a list of changectx which can be stabilized on top of pctx or
-    one of its descendants. Empty list if none can be found."""
-    target = set(revs)
-    result = []
-    for r in repo.revs('orphan() - %ld', revs):
-        dest = _possibledestination(repo, r)
-        if target & dest:
-            result.append(r)
-    return result
-
-def _aspiringdescendant(repo, revs):
-    """Return a list of changectx which can be stabilized on top of pctx or
-    one of its descendants recursively. Empty list if none can be found."""
-    target = set(revs)
-    result = set(target)
-    paths = collections.defaultdict(set)
-    for r in repo.revs('orphan() - %ld', revs):
-        for d in _possibledestination(repo, r):
-            paths[d].add(r)
-
-    result = set(target)
-    tovisit = list(revs)
-    while tovisit:
-        base = tovisit.pop()
-        for unstable in paths[base]:
-            if unstable not in result:
-                tovisit.append(unstable)
-                result.add(unstable)
-    return sorted(result - target)
-
-def _solveunstable(ui, repo, orig, dryrun=False, confirm=False,
-                   progresscb=None):
-    """Stabilize an unstable changeset"""
-    pctx = orig.p1()
-    keepbranch = orig.p1().branch() != orig.branch()
-    if len(orig.parents()) == 2:
-        if not pctx.obsolete():
-            pctx = orig.p2()  # second parent is obsolete ?
-            keepbranch = orig.p2().branch() != orig.branch()
-        elif orig.p2().obsolete():
-            hint = _("Redo the merge (%s) and use `hg prune <old> "
-                     "--succ <new>` to obsolete the old one") % orig.hex()[:12]
-            ui.warn(_("warning: no support for evolving merge changesets "
-                      "with two obsolete parents yet\n") +
-                    _("(%s)\n") % hint)
-            return False
-
-    if not pctx.obsolete():
-        ui.warn(_("cannot solve instability of %s, skipping\n") % orig)
-        return False
-    obs = pctx
-    newer = compat.successorssets(repo, obs.node())
-    # search of a parent which is not killed
-    while not newer or newer == [()]:
-        ui.debug("stabilize target %s is plain dead,"
-                 " trying to stabilize on its parent\n" %
-                 obs)
-        obs = obs.parents()[0]
-        newer = compat.successorssets(repo, obs.node())
-    if len(newer) > 1:
-        msg = _("skipping %s: divergent rewriting. can't choose "
-                "destination\n") % obs
-        ui.write_err(msg)
-        return 2
-    targets = newer[0]
-    assert targets
-    if len(targets) > 1:
-        # split target, figure out which one to pick, are they all in line?
-        targetrevs = [repo[r].rev() for r in targets]
-        roots = repo.revs('roots(%ld)', targetrevs)
-        heads = repo.revs('heads(%ld)', targetrevs)
-        if len(roots) > 1 or len(heads) > 1:
-            msg = "cannot solve split across two branches\n"
-            ui.write_err(msg)
-            return 2
-        target = repo[heads.first()]
-    else:
-        target = targets[0]
-    displayer = compat.changesetdisplayer(ui, repo, {'template': shorttemplate})
-    target = repo[target]
-    if not ui.quiet or confirm:
-        repo.ui.write(_('move:'))
-        displayer.show(orig)
-        repo.ui.write(_('atop:'))
-        displayer.show(target)
-    if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y':
-            raise error.Abort(_('evolve aborted by user'))
-    if progresscb:
-        progresscb()
-    todo = 'hg rebase -r %s -d %s\n' % (orig, target)
-    if dryrun:
-        repo.ui.write(todo)
-    else:
-        repo.ui.note(todo)
-        if progresscb:
-            progresscb()
-        try:
-            relocate(repo, orig, target, pctx, keepbranch)
-        except MergeFailure:
-            ops = {'current': orig.node()}
-            state = evolvestate.evolvestate(repo, opts=ops)
-            state.save()
-            repo.ui.write_err(_('evolve failed!\n'))
-            repo.ui.write_err(
-                _("fix conflict and run 'hg evolve --continue'"
-                  " or use 'hg update -C .' to abort\n"))
-            raise
-
-def _solvebumped(ui, repo, bumped, dryrun=False, confirm=False,
-                 progresscb=None):
-    """Stabilize a bumped changeset"""
-    repo = repo.unfiltered()
-    bumped = repo[bumped.rev()]
-    # For now we deny bumped merge
-    if len(bumped.parents()) > 1:
-        msg = _('skipping %s : we do not handle merge yet\n') % bumped
-        ui.write_err(msg)
-        return 2
-    prec = repo.set('last(allprecursors(%d) and public())', bumped).next()
-    # For now we deny target merge
-    if len(prec.parents()) > 1:
-        msg = _('skipping: %s: public version is a merge, '
-                'this is not handled yet\n') % prec
-        ui.write_err(msg)
-        return 2
-
-    displayer = compat.changesetdisplayer(ui, repo, {'template': shorttemplate})
-    if not ui.quiet or confirm:
-        repo.ui.write(_('recreate:'))
-        displayer.show(bumped)
-        repo.ui.write(_('atop:'))
-        displayer.show(prec)
-    if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y':
-        raise error.Abort(_('evolve aborted by user'))
-    if dryrun:
-        todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1())
-        repo.ui.write(todo)
-        repo.ui.write(('hg update %s;\n' % prec))
-        repo.ui.write(('hg revert --all --rev %s;\n' % bumped))
-        repo.ui.write(('hg commit --msg "%s update to %s"\n' %
-                       (TROUBLES['PHASEDIVERGENT'], bumped)))
-        return 0
-    if progresscb:
-        progresscb()
-    newid = tmpctx = None
-    tmpctx = bumped
-    # Basic check for common parent. Far too complicated and fragile
-    tr = repo.currenttransaction()
-    assert tr is not None
-    bmupdate = _bookmarksupdater(repo, bumped.node(), tr)
-    if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)):
-        # Need to rebase the changeset at the right place
-        repo.ui.status(
-            _('rebasing to destination parent: %s\n') % prec.p1())
-        try:
-            tmpid = relocate(repo, bumped, prec.p1())
-            if tmpid is not None:
-                tmpctx = repo[tmpid]
-                obsolete.createmarkers(repo, [(bumped, (tmpctx,))])
-        except MergeFailure:
-            repo.vfs.write('graftstate', bumped.hex() + '\n')
-            repo.ui.write_err(_('evolution failed!\n'))
-            msg = _("fix conflict and run 'hg evolve --continue'\n")
-            repo.ui.write_err(msg)
-            raise
-    # Create the new commit context
-    repo.ui.status(_('computing new diff\n'))
-    files = set()
-    copied = copies.pathcopies(prec, bumped)
-    precmanifest = prec.manifest().copy()
-    # 3.3.2 needs a list.
-    # future 3.4 don't detect the size change during iteration
-    # this is fishy
-    for key, val in list(bumped.manifest().iteritems()):
-        precvalue = precmanifest.get(key, None)
-        if precvalue is not None:
-            del precmanifest[key]
-        if precvalue != val:
-            files.add(key)
-    files.update(precmanifest)  # add missing files
-    # commit it
-    if files: # something to commit!
-        def filectxfn(repo, ctx, path):
-            if path in bumped:
-                fctx = bumped[path]
-                flags = fctx.flags()
-                mctx = compat.memfilectx(repo, ctx, fctx, flags, copied, path)
-                return mctx
-            return None
-        text = '%s update to %s:\n\n' % (TROUBLES['PHASEDIVERGENT'], prec)
-        text += bumped.description()
-
-        new = context.memctx(repo,
-                             parents=[prec.node(), node.nullid],
-                             text=text,
-                             files=files,
-                             filectxfn=filectxfn,
-                             user=bumped.user(),
-                             date=bumped.date(),
-                             extra=bumped.extra())
-
-        newid = repo.commitctx(new)
-    if newid is None:
-        obsolete.createmarkers(repo, [(tmpctx, ())])
-        newid = prec.node()
-    else:
-        phases.retractboundary(repo, tr, bumped.phase(), [newid])
-        obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))],
-                               flag=obsolete.bumpedfix)
-    bmupdate(newid)
-    repo.ui.status(_('committed as %s\n') % node.short(newid))
-    # reroute the working copy parent to the new changeset
-    with repo.dirstate.parentchange():
-        repo.dirstate.setparents(newid, node.nullid)
-
-def _solvedivergent(ui, repo, divergent, dryrun=False, confirm=False,
-                    progresscb=None):
-    repo = repo.unfiltered()
-    divergent = repo[divergent.rev()]
-    base, others = divergentdata(divergent)
-    if len(others) > 1:
-        othersstr = "[%s]" % (','.join([str(i) for i in others]))
-        msg = _("skipping %d:%s with a changeset that got split"
-                " into multiple ones:\n"
-                "|[%s]\n"
-                "| This is not handled by automatic evolution yet\n"
-                "| You have to fallback to manual handling with commands "
-                "such as:\n"
-                "| - hg touch -D\n"
-                "| - hg prune\n"
-                "| \n"
-                "| You should contact your local evolution Guru for help.\n"
-                ) % (divergent, TROUBLES['CONTENTDIVERGENT'], othersstr)
-        ui.write_err(msg)
-        return 2
-    other = others[0]
-    if len(other.parents()) > 1:
-        msg = _("skipping %s: %s changeset can't be "
-                "a merge (yet)\n") % (divergent, TROUBLES['CONTENTDIVERGENT'])
-        ui.write_err(msg)
-        hint = _("You have to fallback to solving this by hand...\n"
-                 "| This probably means redoing the merge and using \n"
-                 "| `hg prune` to kill older version.\n")
-        ui.write_err(hint)
-        return 2
-    if other.p1() not in divergent.parents():
-        msg = _("skipping %s: have a different parent than %s "
-                "(not handled yet)\n") % (divergent, other)
-        hint = _("| %(d)s, %(o)s are not based on the same changeset.\n"
-                 "| With the current state of its implementation, \n"
-                 "| evolve does not work in that case.\n"
-                 "| rebase one of them next to the other and run \n"
-                 "| this command again.\n"
-                 "| - either: hg rebase --dest 'p1(%(d)s)' -r %(o)s\n"
-                 "| - or:     hg rebase --dest 'p1(%(o)s)' -r %(d)s\n"
-                 ) % {'d': divergent, 'o': other}
-        ui.write_err(msg)
-        ui.write_err(hint)
-        return 2
-
-    displayer = compat.changesetdisplayer(ui, repo, {'template': shorttemplate})
-    if not ui.quiet or confirm:
-        ui.write(_('merge:'))
-        displayer.show(divergent)
-        ui.write(_('with: '))
-        displayer.show(other)
-        ui.write(_('base: '))
-        displayer.show(base)
-    if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y':
-        raise error.Abort(_('evolve aborted by user'))
-    if dryrun:
-        ui.write(('hg update -c %s &&\n' % divergent))
-        ui.write(('hg merge %s &&\n' % other))
-        ui.write(('hg commit -m "auto merge resolving conflict between '
-                 '%s and %s"&&\n' % (divergent, other)))
-        ui.write(('hg up -C %s &&\n' % base))
-        ui.write(('hg revert --all --rev tip &&\n'))
-        ui.write(('hg commit -m "`hg log -r %s --template={desc}`";\n'
-                 % divergent))
-        return
-    if divergent not in repo[None].parents():
-        repo.ui.status(_('updating to "local" conflict\n'))
-        hg.update(repo, divergent.rev())
-    repo.ui.note(_('merging %s changeset\n') % TROUBLES['CONTENTDIVERGENT'])
-    if progresscb:
-        progresscb()
-    stats = merge.update(repo,
-                         other.node(),
-                         branchmerge=True,
-                         force=False,
-                         ancestor=base.node(),
-                         mergeancestor=True)
-    hg._showstats(repo, stats)
-    if stats[3]:
-        repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
-                         "or 'hg update -C .' to abort\n"))
-    if stats[3] > 0:
-        raise error.Abort('merge conflict between several amendments '
-                          '(this is not automated yet)',
-                          hint="""/!\ You can try:
-/!\ * manual merge + resolve => new cset X
-/!\ * hg up to the parent of the amended changeset (which are named W and Z)
-/!\ * hg revert --all -r X
-/!\ * hg ci -m "same message as the amended changeset" => new cset Y
-/!\ * hg prune -n Y W Z
-""")
-    if progresscb:
-        progresscb()
-    emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit')
-    tr = repo.currenttransaction()
-    assert tr is not None
-    try:
-        repo.ui.setconfig('ui', 'allowemptycommit', True, 'evolve')
-        with repo.dirstate.parentchange():
-            repo.dirstate.setparents(divergent.node(), node.nullid)
-        oldlen = len(repo)
-        cmdrewrite.amend(ui, repo, message='', logfile='')
-        if oldlen == len(repo):
-            new = divergent
-            # no changes
-        else:
-            new = repo['.']
-        obsolete.createmarkers(repo, [(other, (new,))])
-        phases.retractboundary(repo, tr, other.phase(), [new.node()])
-    finally:
-        repo.ui.restoreconfig(emtpycommitallowed)
-
-def divergentdata(ctx):
-    """return base, other part of a conflict
-
-    This only return the first one.
-
-    XXX this woobly function won't survive XXX
-    """
-    repo = ctx._repo.unfiltered()
-    for base in repo.set('reverse(allprecursors(%d))', ctx):
-        newer = compat.successorssets(ctx._repo, base.node())
-        # drop filter and solution including the original ctx
-        newer = [n for n in newer if n and ctx.node() not in n]
-        if newer:
-            return base, tuple(ctx._repo[o] for o in newer[0])
-    raise error.Abort("base of divergent changeset %s not found" % ctx,
-                      hint='this case is not yet handled')
-
 def _gettopic(ctx):
     """handle topic fetching with or without the extension"""
     return getattr(ctx, 'topic', lambda: '')()
@@ -2087,7 +990,7 @@
 
     # we do not filter in the 1 case to allow prev to t0
     if currenttopic and topic and _gettopicidx(p1) != 1:
-        parents = [repo[_singlesuccessor(repo, ctx)] if ctx.mutable() else ctx
+        parents = [repo[utility._singlesuccessor(repo, ctx)] if ctx.mutable() else ctx
                    for ctx in parents]
         parents = [ctx for ctx in parents if ctx.topic() == currenttopic]
 
@@ -2233,7 +1136,7 @@
             ui.warn(_('explicitly update to one of them\n'))
             result = 1
         else:
-            aspchildren = _aspiringchildren(repo, [repo['.'].rev()])
+            aspchildren = evolvecmd._aspiringchildren(repo, [repo['.'].rev()])
             if topic:
                 filtered.extend(repo[c] for c in children
                                 if repo[c].topic() != topic)
@@ -2258,12 +1161,15 @@
                 return 1
             else:
                 cmdutil.bailifchanged(repo)
-                result = _solveone(ui, repo, repo[aspchildren[0]], dryrunopt,
-                                   False, lambda: None, category='orphan')
-                if not result:
+                evolvestate = state.cmdstate(repo)
+                result = evolvecmd._solveone(ui, repo, repo[aspchildren[0]],
+                                             evolvestate, dryrunopt, False,
+                                             lambda: None, category='orphan')
+                # making sure a next commit is formed
+                if result[0] and result[1]:
                     ui.status(_('working directory now at %s\n')
                               % ui.label(str(repo['.']), 'evolve.node'))
-                return result
+                return 0
             return 1
         return result
     finally:
@@ -2435,47 +1341,6 @@
                               _helploader))
         help.helptable.sort()
 
-def _relocatecommit(repo, orig, commitmsg):
-    if commitmsg is None:
-        commitmsg = orig.description()
-    extra = dict(orig.extra())
-    if 'branch' in extra:
-        del extra['branch']
-    extra['rebase_source'] = orig.hex()
-
-    backup = repo.ui.backupconfig('phases', 'new-commit')
-    try:
-        targetphase = max(orig.phase(), phases.draft)
-        repo.ui.setconfig('phases', 'new-commit', targetphase, 'evolve')
-        # Commit might fail if unresolved files exist
-        nodenew = repo.commit(text=commitmsg, user=orig.user(),
-                              date=orig.date(), extra=extra)
-    finally:
-        repo.ui.restoreconfig(backup)
-    return nodenew
-
-def _finalizerelocate(repo, orig, dest, nodenew, tr):
-    destbookmarks = repo.nodebookmarks(dest.node())
-    nodesrc = orig.node()
-    destphase = repo[nodesrc].phase()
-    oldbookmarks = repo.nodebookmarks(nodesrc)
-    bmchanges = []
-
-    if nodenew is not None:
-        phases.retractboundary(repo, tr, destphase, [nodenew])
-        obsolete.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))])
-        for book in oldbookmarks:
-            bmchanges.append((book, nodenew))
-    else:
-        obsolete.createmarkers(repo, [(repo[nodesrc], ())])
-        # Behave like rebase, move bookmarks to dest
-        for book in oldbookmarks:
-            bmchanges.append((book, dest.node()))
-    for book in destbookmarks: # restore bookmark that rebase move
-        bmchanges.append((book, dest.node()))
-    if bmchanges:
-        compat.bookmarkapplychanges(repo, tr, bmchanges)
-
 evolvestateversion = 0
 
 @eh.uisetup
@@ -2484,32 +1349,13 @@
             _("use 'hg evolve --continue' or 'hg update -C .' to abort"))
     cmdutil.unfinishedstates.append(data)
 
+    afterresolved = ('evolvestate', _('hg evolve --continue'))
+    grabresolved = ('grabstate', _('hg grab --continue'))
+    cmdutil.afterresolvedstates.append(afterresolved)
+    cmdutil.afterresolvedstates.append(grabresolved)
+
 @eh.wrapfunction(hg, 'clean')
 def clean(orig, repo, *args, **kwargs):
     ret = orig(repo, *args, **kwargs)
     util.unlinkpath(repo.vfs.join('evolvestate'), ignoremissing=True)
     return ret
-
-def _evolvemerge(repo, orig, dest, pctx, keepbranch):
-    """Used by the evolve function to merge dest on top of pctx.
-    return the same tuple as merge.graft"""
-    if repo['.'].rev() != dest.rev():
-        merge.update(repo,
-                     dest,
-                     branchmerge=False,
-                     force=True)
-    if repo._activebookmark:
-        repo.ui.status(_("(leaving bookmark %s)\n") % repo._activebookmark)
-    bookmarksmod.deactivate(repo)
-    if keepbranch:
-        repo.dirstate.setbranch(orig.branch())
-    if util.safehasattr(repo, 'currenttopic'):
-        # uurrgs
-        # there no other topic setter yet
-        if not orig.topic() and repo.vfs.exists('topic'):
-                repo.vfs.unlink('topic')
-        else:
-            with repo.vfs.open('topic', 'w') as f:
-                f.write(orig.topic())
-
-    return merge.graft(repo, orig, pctx, ['destination', 'evolving'], True)
--- a/hgext3rd/evolve/cmdrewrite.py	Sun Jan 21 16:55:02 2018 -0500
+++ b/hgext3rd/evolve/cmdrewrite.py	Tue Feb 06 13:00:28 2018 +0100
@@ -22,6 +22,7 @@
     error,
     hg,
     lock as lockmod,
+    merge,
     node,
     obsolete,
     patch,
@@ -34,6 +35,7 @@
 
 from . import (
     compat,
+    state,
     exthelper,
     rewriteutil,
     utility,
@@ -1148,3 +1150,96 @@
         tr.close()
     finally:
         lockmod.release(tr, lock, wlock)
+
+@eh.command(
+    'grab',
+    [('r', 'rev', '', 'revision to grab'),
+     ('', 'continue', False, 'continue interrupted grab'),
+     ('', 'abort', False, 'abort interrupted grab'),
+    ],
+    _('[-r] rev'))
+def grab(ui, repo, *revs, **opts):
+    """grabs a commit, move it on the top of working directory parent and
+    updates to it."""
+
+    cont = opts.get('continue')
+    abort = opts.get('abort')
+
+    if cont and abort:
+        raise error.Abort(_("cannot specify both --continue and --abort"))
+
+    revs = list(revs)
+    if opts.get('rev'):
+        revs.append(opts['rev'])
+
+    with repo.wlock(), repo.lock(), repo.transaction('grab'):
+        grabstate = state.cmdstate(repo, path='grabstate')
+
+        if not cont and not abort:
+            cmdutil.bailifchanged(repo)
+            revs = scmutil.revrange(repo, revs)
+            if len(revs) > 1:
+                raise error.Abort(_("specify just one revision"))
+            elif not revs:
+                raise error.Abort(_("empty revision set"))
+
+            origctx = repo[revs.first()]
+            pctx = repo['.']
+
+            if origctx in pctx.ancestors():
+                raise error.Abort(_("cannot grab an ancestor revision"))
+
+            rewriteutil.precheck(repo, [origctx.rev()], 'grab')
+
+            ui.status(_('grabbing %d:%s "%s"\n') %
+                      (origctx.rev(), origctx,
+                       origctx.description().split("\n", 1)[0]))
+            stats = merge.graft(repo, origctx, origctx.p1(), ['local',
+                                                              'destination'])
+            if stats[3]:
+                grabstate.addopts({'orignode': origctx.node(),
+                                   'oldpctx': pctx.node()})
+                grabstate.save()
+                raise error.InterventionRequired(_("unresolved merge conflicts"
+                                                   " (see hg help resolve)"))
+
+        elif abort:
+            if not grabstate:
+                raise error.Abort(_("no interrupted grab state exists"))
+            grabstate.load()
+            pctxnode = grabstate['oldpctx']
+            ui.status(_("aborting grab, updating to %s\n") %
+                      node.hex(pctxnode)[:12])
+            hg.updaterepo(repo, pctxnode, True)
+            return 0
+
+        else:
+            if revs:
+                raise error.Abort(_("cannot specify both --continue and "
+                                    "revision"))
+            if not grabstate:
+                raise error.Abort(_("no interrupted grab state exists"))
+
+            grabstate.load()
+            orignode = grabstate['orignode']
+            origctx = repo[orignode]
+
+        overrides = {('phases', 'new-commit'): origctx.phase()}
+        with repo.ui.configoverride(overrides, 'grab'):
+            newnode = repo.commit(text=origctx.description(),
+                                  user=origctx.user(),
+                                  date=origctx.date(), extra=origctx.extra())
+
+        if grabstate:
+            grabstate.delete()
+        if newnode:
+            obsolete.createmarkers(repo, [(origctx, (repo[newnode],))])
+        else:
+            obsolete.createmarkers(repo, [(origctx, (pctx,))])
+
+        if newnode is None:
+            ui.warn(_("note: grab of %d:%s created no changes to commit\n") %
+                    (origctx.rev(), origctx))
+            return 0
+
+        return 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/evolvecmd.py	Tue Feb 06 13:00:28 2018 +0100
@@ -0,0 +1,1169 @@
+# Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
+#                Logilab SA        <contact@logilab.fr>
+#                Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+#                Patrick Mezard <patrick@mezard.eu>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""logic related to hg evolve command"""
+
+import collections
+import re
+
+from mercurial import (
+    bookmarks as bookmarksmod,
+    cmdutil,
+    context,
+    copies,
+    error,
+    hg,
+    lock as lockmod,
+    merge,
+    node,
+    obsolete,
+    phases,
+    scmutil,
+    util,
+)
+
+from mercurial.i18n import _
+
+from . import (
+    cmdrewrite,
+    compat,
+    exthelper,
+    rewriteutil,
+    state,
+    utility,
+)
+
+TROUBLES = compat.TROUBLES
+shorttemplate = utility.shorttemplate
+_bookmarksupdater = rewriteutil.bookmarksupdater
+sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
+
+eh = exthelper.exthelper()
+_bookmarksupdater = rewriteutil.bookmarksupdater
+mergetoolopts = cmdutil.mergetoolopts
+
+def _solveone(ui, repo, ctx, evolvestate, dryrun, confirm,
+              progresscb, category):
+    """Resolve the troubles affecting one revision
+
+    returns a tuple (bool, newnode) where,
+        bool: a boolean value indicating whether the instability was solved
+        newnode: if bool is True, then the newnode of the resultant commit
+                 formed. newnode can be node, when resolution led to no new
+                 commit. If bool is False, this is ''.
+    """
+    wlock = lock = tr = None
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+        tr = repo.transaction("evolve")
+        if 'orphan' == category:
+            result = _solveunstable(ui, repo, ctx, evolvestate,
+                                    dryrun, confirm, progresscb)
+        elif 'phasedivergent' == category:
+            result = _solvebumped(ui, repo, ctx, evolvestate,
+                                  dryrun, confirm, progresscb)
+        elif 'contentdivergent' == category:
+            result = _solvedivergent(ui, repo, ctx, evolvestate,
+                                     dryrun, confirm, progresscb)
+        else:
+            assert False, "unknown trouble category: %s" % (category)
+        tr.close()
+        return result
+    finally:
+        lockmod.release(tr, lock, wlock)
+
+def _solveunstable(ui, repo, orig, evolvestate, dryrun=False, confirm=False,
+                   progresscb=None):
+    """ Tries to stabilize the changeset orig which is orphan.
+
+    returns a tuple (bool, newnode) where,
+        bool: a boolean value indicating whether the instability was solved
+        newnode: if bool is True, then the newnode of the resultant commit
+                 formed. newnode can be node, when resolution led to no new
+                 commit. If bool is False, this is ''.
+    """
+    pctx = orig.p1()
+    keepbranch = orig.p1().branch() != orig.branch()
+    if len(orig.parents()) == 2:
+        if not pctx.obsolete():
+            pctx = orig.p2()  # second parent is obsolete ?
+            keepbranch = orig.p2().branch() != orig.branch()
+        elif orig.p2().obsolete():
+            hint = _("Redo the merge (%s) and use `hg prune <old> "
+                     "--succ <new>` to obsolete the old one") % orig.hex()[:12]
+            ui.warn(_("warning: no support for evolving merge changesets "
+                      "with two obsolete parents yet\n") +
+                    _("(%s)\n") % hint)
+            return (False, '')
+
+    if not pctx.obsolete():
+        ui.warn(_("cannot solve instability of %s, skipping\n") % orig)
+        return (False, '')
+    obs = pctx
+    newer = compat.successorssets(repo, obs.node())
+    # search of a parent which is not killed
+    while not newer or newer == [()]:
+        ui.debug("stabilize target %s is plain dead,"
+                 " trying to stabilize on its parent\n" %
+                 obs)
+        obs = obs.parents()[0]
+        newer = compat.successorssets(repo, obs.node())
+    if len(newer) > 1:
+        msg = _("skipping %s: divergent rewriting. can't choose "
+                "destination\n") % obs
+        ui.write_err(msg)
+        return (False, '')
+    targets = newer[0]
+    assert targets
+    if len(targets) > 1:
+        # split target, figure out which one to pick, are they all in line?
+        targetrevs = [repo[r].rev() for r in targets]
+        roots = repo.revs('roots(%ld)', targetrevs)
+        heads = repo.revs('heads(%ld)', targetrevs)
+        if len(roots) > 1 or len(heads) > 1:
+            msg = "cannot solve split across two branches\n"
+            ui.write_err(msg)
+            return (False, '')
+        target = repo[heads.first()]
+    else:
+        target = targets[0]
+    displayer = compat.changesetdisplayer(ui, repo, {'template': shorttemplate})
+    target = repo[target]
+    if not ui.quiet or confirm:
+        repo.ui.write(_('move:'))
+        displayer.show(orig)
+        repo.ui.write(_('atop:'))
+        displayer.show(target)
+    if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y':
+            raise error.Abort(_('evolve aborted by user'))
+    if progresscb:
+        progresscb()
+    todo = 'hg rebase -r %s -d %s\n' % (orig, target)
+    if dryrun:
+        repo.ui.write(todo)
+        return (False, '')
+    else:
+        repo.ui.note(todo)
+        if progresscb:
+            progresscb()
+        try:
+            newid = relocate(repo, orig, target, pctx, keepbranch)
+            return (True, newid)
+        except MergeFailure:
+            ops = {'current': orig.node()}
+            evolvestate.addopts(ops)
+            evolvestate.save()
+            repo.ui.write_err(_('evolve failed!\n'))
+            repo.ui.write_err(
+                _("fix conflict and run 'hg evolve --continue'"
+                  " or use 'hg update -C .' to abort\n"))
+            raise
+
+def _solvebumped(ui, repo, bumped, evolvestate, dryrun=False, confirm=False,
+                 progresscb=None):
+    """Stabilize a bumped changeset
+
+    returns a tuple (bool, newnode) where,
+        bool: a boolean value indicating whether the instability was solved
+        newnode: if bool is True, then the newnode of the resultant commit
+                 formed. newnode can be node, when resolution led to no new
+                 commit. If bool is False, this is ''.
+    """
+    repo = repo.unfiltered()
+    bumped = repo[bumped.rev()]
+    # For now we deny bumped merge
+    if len(bumped.parents()) > 1:
+        msg = _('skipping %s : we do not handle merge yet\n') % bumped
+        ui.write_err(msg)
+        return (False, '')
+    prec = repo.set('last(allprecursors(%d) and public())', bumped).next()
+    # For now we deny target merge
+    if len(prec.parents()) > 1:
+        msg = _('skipping: %s: public version is a merge, '
+                'this is not handled yet\n') % prec
+        ui.write_err(msg)
+        return (False, '')
+
+    displayer = compat.changesetdisplayer(ui, repo, {'template': shorttemplate})
+    if not ui.quiet or confirm:
+        repo.ui.write(_('recreate:'))
+        displayer.show(bumped)
+        repo.ui.write(_('atop:'))
+        displayer.show(prec)
+    if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y':
+        raise error.Abort(_('evolve aborted by user'))
+    if dryrun:
+        todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1())
+        repo.ui.write(todo)
+        repo.ui.write(('hg update %s;\n' % prec))
+        repo.ui.write(('hg revert --all --rev %s;\n' % bumped))
+        repo.ui.write(('hg commit --msg "%s update to %s"\n' %
+                       (TROUBLES['PHASEDIVERGENT'], bumped)))
+        return (False, '')
+    if progresscb:
+        progresscb()
+    newid = tmpctx = None
+    tmpctx = bumped
+    # Basic check for common parent. Far too complicated and fragile
+    tr = repo.currenttransaction()
+    assert tr is not None
+    bmupdate = _bookmarksupdater(repo, bumped.node(), tr)
+    if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)):
+        # Need to rebase the changeset at the right place
+        repo.ui.status(
+            _('rebasing to destination parent: %s\n') % prec.p1())
+        try:
+            tmpid = relocate(repo, bumped, prec.p1())
+            if tmpid is not None:
+                tmpctx = repo[tmpid]
+                obsolete.createmarkers(repo, [(bumped, (tmpctx,))])
+        except MergeFailure:
+            repo.vfs.write('graftstate', bumped.hex() + '\n')
+            repo.ui.write_err(_('evolution failed!\n'))
+            msg = _("fix conflict and run 'hg evolve --continue'\n")
+            repo.ui.write_err(msg)
+            raise
+    # Create the new commit context
+    repo.ui.status(_('computing new diff\n'))
+    files = set()
+    copied = copies.pathcopies(prec, bumped)
+    precmanifest = prec.manifest().copy()
+    # 3.3.2 needs a list.
+    # future 3.4 don't detect the size change during iteration
+    # this is fishy
+    for key, val in list(bumped.manifest().iteritems()):
+        precvalue = precmanifest.get(key, None)
+        if precvalue is not None:
+            del precmanifest[key]
+        if precvalue != val:
+            files.add(key)
+    files.update(precmanifest)  # add missing files
+    # commit it
+    if files: # something to commit!
+        def filectxfn(repo, ctx, path):
+            if path in bumped:
+                fctx = bumped[path]
+                flags = fctx.flags()
+                mctx = compat.memfilectx(repo, ctx, fctx, flags, copied, path)
+                return mctx
+            return None
+        text = '%s update to %s:\n\n' % (TROUBLES['PHASEDIVERGENT'], prec)
+        text += bumped.description()
+
+        new = context.memctx(repo,
+                             parents=[prec.node(), node.nullid],
+                             text=text,
+                             files=files,
+                             filectxfn=filectxfn,
+                             user=bumped.user(),
+                             date=bumped.date(),
+                             extra=bumped.extra())
+
+        newid = repo.commitctx(new)
+    if newid is None:
+        obsolete.createmarkers(repo, [(tmpctx, ())])
+        newid = prec.node()
+    else:
+        phases.retractboundary(repo, tr, bumped.phase(), [newid])
+        obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))],
+                               flag=obsolete.bumpedfix)
+    bmupdate(newid)
+    repo.ui.status(_('committed as %s\n') % node.short(newid))
+    # reroute the working copy parent to the new changeset
+    with repo.dirstate.parentchange():
+        repo.dirstate.setparents(newid, node.nullid)
+    return (True, newid)
+
+def _solvedivergent(ui, repo, divergent, evolvestate, dryrun=False,
+                    confirm=False, progresscb=None):
+    """tries to solve content-divergence of a changeset
+
+    returns a tuple (bool, newnode) where,
+        bool: a boolean value indicating whether the instability was solved
+        newnode: if bool is True, then the newnode of the resultant commit
+                 formed. newnode can be node, when resolution led to no new
+                 commit. If bool is False, this is ''.
+    """
+    repo = repo.unfiltered()
+    divergent = repo[divergent.rev()]
+    base, others = divergentdata(divergent)
+    if len(others) > 1:
+        othersstr = "[%s]" % (','.join([str(i) for i in others]))
+        msg = _("skipping %d:%s with a changeset that got split"
+                " into multiple ones:\n"
+                "|[%s]\n"
+                "| This is not handled by automatic evolution yet\n"
+                "| You have to fallback to manual handling with commands "
+                "such as:\n"
+                "| - hg touch -D\n"
+                "| - hg prune\n"
+                "| \n"
+                "| You should contact your local evolution Guru for help.\n"
+                ) % (divergent, TROUBLES['CONTENTDIVERGENT'], othersstr)
+        ui.write_err(msg)
+        return (False, '')
+    other = others[0]
+    if len(other.parents()) > 1:
+        msg = _("skipping %s: %s changeset can't be "
+                "a merge (yet)\n") % (divergent, TROUBLES['CONTENTDIVERGENT'])
+        ui.write_err(msg)
+        hint = _("You have to fallback to solving this by hand...\n"
+                 "| This probably means redoing the merge and using \n"
+                 "| `hg prune` to kill older version.\n")
+        ui.write_err(hint)
+        return (False, '')
+    if other.p1() not in divergent.parents():
+        msg = _("skipping %s: have a different parent than %s "
+                "(not handled yet)\n") % (divergent, other)
+        hint = _("| %(d)s, %(o)s are not based on the same changeset.\n"
+                 "| With the current state of its implementation, \n"
+                 "| evolve does not work in that case.\n"
+                 "| rebase one of them next to the other and run \n"
+                 "| this command again.\n"
+                 "| - either: hg rebase --dest 'p1(%(d)s)' -r %(o)s\n"
+                 "| - or:     hg rebase --dest 'p1(%(o)s)' -r %(d)s\n"
+                 ) % {'d': divergent, 'o': other}
+        ui.write_err(msg)
+        ui.write_err(hint)
+        return (False, '')
+
+    displayer = compat.changesetdisplayer(ui, repo, {'template': shorttemplate})
+    if not ui.quiet or confirm:
+        ui.write(_('merge:'))
+        displayer.show(divergent)
+        ui.write(_('with: '))
+        displayer.show(other)
+        ui.write(_('base: '))
+        displayer.show(base)
+    if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y':
+        raise error.Abort(_('evolve aborted by user'))
+    if dryrun:
+        ui.write(('hg update -c %s &&\n' % divergent))
+        ui.write(('hg merge %s &&\n' % other))
+        ui.write(('hg commit -m "auto merge resolving conflict between '
+                 '%s and %s"&&\n' % (divergent, other)))
+        ui.write(('hg up -C %s &&\n' % base))
+        ui.write(('hg revert --all --rev tip &&\n'))
+        ui.write(('hg commit -m "`hg log -r %s --template={desc}`";\n'
+                 % divergent))
+        return (False, '')
+    if divergent not in repo[None].parents():
+        repo.ui.status(_('updating to "local" conflict\n'))
+        hg.update(repo, divergent.rev())
+    repo.ui.note(_('merging %s changeset\n') % TROUBLES['CONTENTDIVERGENT'])
+    if progresscb:
+        progresscb()
+    stats = merge.update(repo,
+                         other.node(),
+                         branchmerge=True,
+                         force=False,
+                         ancestor=base.node(),
+                         mergeancestor=True)
+    hg._showstats(repo, stats)
+    if stats[3]:
+        repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
+                         "or 'hg update -C .' to abort\n"))
+    if stats[3] > 0:
+        raise error.Abort('merge conflict between several amendments '
+                          '(this is not automated yet)',
+                          hint="""/!\ You can try:
+/!\ * manual merge + resolve => new cset X
+/!\ * hg up to the parent of the amended changeset (which are named W and Z)
+/!\ * hg revert --all -r X
+/!\ * hg ci -m "same message as the amended changeset" => new cset Y
+/!\ * hg prune -n Y W Z
+""")
+    if progresscb:
+        progresscb()
+    emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit')
+    tr = repo.currenttransaction()
+    assert tr is not None
+    try:
+        repo.ui.setconfig('ui', 'allowemptycommit', True, 'evolve')
+        with repo.dirstate.parentchange():
+            repo.dirstate.setparents(divergent.node(), node.nullid)
+        oldlen = len(repo)
+        cmdrewrite.amend(ui, repo, message='', logfile='')
+        if oldlen == len(repo):
+            new = divergent
+            # no changes
+        else:
+            new = repo['.']
+        obsolete.createmarkers(repo, [(other, (new,))])
+        phases.retractboundary(repo, tr, other.phase(), [new.node()])
+        return (True, new.node())
+    finally:
+        repo.ui.restoreconfig(emtpycommitallowed)
+
+class MergeFailure(error.Abort):
+    pass
+
+def _orderrevs(repo, revs):
+    """Compute an ordering to solve instability for the given revs
+
+    revs is a list of unstable revisions.
+
+    Returns the same revisions ordered to solve their instability from the
+    bottom to the top of the stack that the stabilization process will produce
+    eventually.
+
+    This ensures the minimal number of stabilizations, as we can stabilize each
+    revision on its final stabilized destination.
+    """
+    # Step 1: Build the dependency graph
+    dependencies, rdependencies = utility.builddependencies(repo, revs)
+    # Step 2: Build the ordering
+    # Remove the revisions with no dependency(A) and add them to the ordering.
+    # Removing these revisions leads to new revisions with no dependency (the
+    # one depending on A) that we can remove from the dependency graph and add
+    # to the ordering. We progress in a similar fashion until the ordering is
+    # built
+    solvablerevs = collections.deque([r for r in sorted(dependencies.keys())
+                                      if not dependencies[r]])
+    ordering = []
+    while solvablerevs:
+        rev = solvablerevs.popleft()
+        for dependent in rdependencies[rev]:
+            dependencies[dependent].remove(rev)
+            if not dependencies[dependent]:
+                solvablerevs.append(dependent)
+        del dependencies[rev]
+        ordering.append(rev)
+
+    ordering.extend(sorted(dependencies))
+    return ordering
+
+def relocate(repo, orig, dest, pctx=None, keepbranch=False):
+    """rewrites the orig rev on dest rev
+
+    returns the node of new commit which is formed
+    """
+    if orig.rev() == dest.rev():
+        raise error.Abort(_('tried to relocate a node on top of itself'),
+                          hint=_("This shouldn't happen. If you still "
+                                 "need to move changesets, please do so "
+                                 "manually with nothing to rebase - working "
+                                 "directory parent is also destination"))
+
+    if pctx is None:
+        if len(orig.parents()) == 2:
+            raise error.Abort(_("tried to relocate a merge commit without "
+                                "specifying which parent should be moved"),
+                              hint=_("Specify the parent by passing in pctx"))
+        pctx = orig.p1()
+
+    commitmsg = orig.description()
+
+    cache = {}
+    sha1s = re.findall(sha1re, commitmsg)
+    unfi = repo.unfiltered()
+    for sha1 in sha1s:
+        ctx = None
+        try:
+            ctx = unfi[sha1]
+        except error.RepoLookupError:
+            continue
+
+        if not ctx.obsolete():
+            continue
+
+        successors = compat.successorssets(repo, ctx.node(), cache)
+
+        # We can't make any assumptions about how to update the hash if the
+        # cset in question was split or diverged.
+        if len(successors) == 1 and len(successors[0]) == 1:
+            newsha1 = node.hex(successors[0][0])
+            commitmsg = commitmsg.replace(sha1, newsha1[:len(sha1)])
+        else:
+            repo.ui.note(_('The stale commit message reference to %s could '
+                           'not be updated\n') % sha1)
+
+    tr = repo.currenttransaction()
+    assert tr is not None
+    try:
+        r = _evolvemerge(repo, orig, dest, pctx, keepbranch)
+        if r[-1]: # some conflict
+            raise error.Abort(_('unresolved merge conflicts '
+                                '(see hg help resolve)'))
+        nodenew = _relocatecommit(repo, orig, commitmsg)
+    except error.Abort as exc:
+        with repo.dirstate.parentchange():
+            repo.setparents(repo['.'].node(), node.nullid)
+            repo.dirstate.write(tr)
+            # fix up dirstate for copies and renames
+            compat.duplicatecopies(repo, repo[None], dest.rev(), orig.p1().rev())
+
+        class LocalMergeFailure(MergeFailure, exc.__class__):
+            pass
+        exc.__class__ = LocalMergeFailure
+        tr.close() # to keep changes in this transaction (e.g. dirstate)
+        raise
+    _finalizerelocate(repo, orig, dest, nodenew, tr)
+    return nodenew
+
+def _relocatecommit(repo, orig, commitmsg):
+    if commitmsg is None:
+        commitmsg = orig.description()
+    extra = dict(orig.extra())
+    if 'branch' in extra:
+        del extra['branch']
+    extra['rebase_source'] = orig.hex()
+
+    backup = repo.ui.backupconfig('phases', 'new-commit')
+    try:
+        targetphase = max(orig.phase(), phases.draft)
+        repo.ui.setconfig('phases', 'new-commit', targetphase, 'evolve')
+        # Commit might fail if unresolved files exist
+        nodenew = repo.commit(text=commitmsg, user=orig.user(),
+                              date=orig.date(), extra=extra)
+    finally:
+        repo.ui.restoreconfig(backup)
+    return nodenew
+
+def _finalizerelocate(repo, orig, dest, nodenew, tr):
+    destbookmarks = repo.nodebookmarks(dest.node())
+    nodesrc = orig.node()
+    oldbookmarks = repo.nodebookmarks(nodesrc)
+    bmchanges = []
+
+    if nodenew is not None:
+        obsolete.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))])
+        for book in oldbookmarks:
+            bmchanges.append((book, nodenew))
+    else:
+        obsolete.createmarkers(repo, [(repo[nodesrc], ())])
+        # Behave like rebase, move bookmarks to dest
+        for book in oldbookmarks:
+            bmchanges.append((book, dest.node()))
+    for book in destbookmarks: # restore bookmark that rebase move
+        bmchanges.append((book, dest.node()))
+    if bmchanges:
+        compat.bookmarkapplychanges(repo, tr, bmchanges)
+
+def _evolvemerge(repo, orig, dest, pctx, keepbranch):
+    """Used by the evolve function to merge dest on top of pctx.
+    return the same tuple as merge.graft"""
+    if repo['.'].rev() != dest.rev():
+        merge.update(repo,
+                     dest,
+                     branchmerge=False,
+                     force=True)
+    if repo._activebookmark:
+        repo.ui.status(_("(leaving bookmark %s)\n") % repo._activebookmark)
+    bookmarksmod.deactivate(repo)
+    if keepbranch:
+        repo.dirstate.setbranch(orig.branch())
+    if util.safehasattr(repo, 'currenttopic'):
+        # uurrgs
+        # there no other topic setter yet
+        if not orig.topic() and repo.vfs.exists('topic'):
+                repo.vfs.unlink('topic')
+        else:
+            with repo.vfs.open('topic', 'w') as f:
+                f.write(orig.topic())
+
+    return merge.graft(repo, orig, pctx, ['destination', 'evolving'], True)
+
+instabilities_map = {
+    'contentdivergent': "content-divergent",
+    'phasedivergent': "phase-divergent"
+}
+
+def _selectrevs(repo, allopt, revopt, anyopt, targetcat):
+    """select troubles in repo matching according to given options"""
+    revs = set()
+    if allopt or revopt:
+        revs = repo.revs("%s()" % targetcat)
+        if revopt:
+            revs = scmutil.revrange(repo, revopt) & revs
+        elif not anyopt:
+            topic = getattr(repo, 'currenttopic', '')
+            if topic:
+                revs = repo.revs('topic(%s)', topic) & revs
+            elif targetcat == 'orphan':
+                revs = _aspiringdescendant(repo,
+                                           repo.revs('(.::) - obsolete()::'))
+                revs = set(revs)
+        if targetcat == 'contentdivergent':
+            # Pick one divergent per group of divergents
+            revs = _dedupedivergents(repo, revs)
+    elif anyopt:
+        revs = repo.revs('first(%s())' % (targetcat))
+    elif targetcat == 'orphan':
+        revs = set(_aspiringchildren(repo, repo.revs('(.::) - obsolete()::')))
+        if 1 < len(revs):
+            msg = "multiple evolve candidates"
+            hint = (_("select one of %s with --rev")
+                    % ', '.join([str(repo[r]) for r in sorted(revs)]))
+            raise error.Abort(msg, hint=hint)
+    elif instabilities_map.get(targetcat, targetcat) in repo['.'].instabilities():
+        revs = set([repo['.'].rev()])
+    return revs
+
+def _dedupedivergents(repo, revs):
+    """Dedupe the divergents revs in revs to get one from each group with the
+    lowest revision numbers
+    """
+    repo = repo.unfiltered()
+    res = set()
+    # To not reevaluate divergents of the same group once one is encountered
+    discarded = set()
+    for rev in revs:
+        if rev in discarded:
+            continue
+        divergent = repo[rev]
+        base, others = divergentdata(divergent)
+        othersrevs = [o.rev() for o in others]
+        res.add(min([divergent.rev()] + othersrevs))
+        discarded.update(othersrevs)
+    return res
+
+def divergentdata(ctx):
+    """return base, other part of a conflict
+
+    This only return the first one.
+
+    XXX this woobly function won't survive XXX
+    """
+    repo = ctx._repo.unfiltered()
+    for base in repo.set('reverse(allprecursors(%d))', ctx):
+        newer = compat.successorssets(ctx._repo, base.node())
+        # drop filter and solution including the original ctx
+        newer = [n for n in newer if n and ctx.node() not in n]
+        if newer:
+            return base, tuple(ctx._repo[o] for o in newer[0])
+    raise error.Abort("base of divergent changeset %s not found" % ctx,
+                      hint='this case is not yet handled')
+
+def _aspiringdescendant(repo, revs):
+    """Return a list of changectx which can be stabilized on top of pctx or
+    one of its descendants recursively. Empty list if none can be found."""
+    target = set(revs)
+    result = set(target)
+    paths = collections.defaultdict(set)
+    for r in repo.revs('orphan() - %ld', revs):
+        for d in _possibledestination(repo, r):
+            paths[d].add(r)
+
+    result = set(target)
+    tovisit = list(revs)
+    while tovisit:
+        base = tovisit.pop()
+        for unstable in paths[base]:
+            if unstable not in result:
+                tovisit.append(unstable)
+                result.add(unstable)
+    return sorted(result - target)
+
+def _aspiringchildren(repo, revs):
+    """Return a list of changectx which can be stabilized on top of pctx or
+    one of its descendants. Empty list if none can be found."""
+    target = set(revs)
+    result = []
+    for r in repo.revs('orphan() - %ld', revs):
+        dest = _possibledestination(repo, r)
+        if target & dest:
+            result.append(r)
+    return result
+
+def _possibledestination(repo, rev):
+    """return all changesets that may be a new parent for REV"""
+    tonode = repo.changelog.node
+    parents = repo.changelog.parentrevs
+    torev = repo.changelog.rev
+    dest = set()
+    tovisit = list(parents(rev))
+    while tovisit:
+        r = tovisit.pop()
+        succsets = compat.successorssets(repo, tonode(r))
+        if not succsets:
+            tovisit.extend(parents(r))
+        else:
+            # We should probably pick only one destination from split
+            # (case where '1 < len(ss)'), This could be the currently tipmost
+            # but logic is less clear when result of the split are now on
+            # multiple branches.
+            for ss in succsets:
+                for n in ss:
+                    dest.add(torev(n))
+    return dest
+
+def _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat):
+    """Used by the evolve function to display an error message when
+    no troubles can be resolved"""
+    troublecategories = ['phasedivergent', 'contentdivergent', 'orphan']
+    unselectedcategories = [c for c in troublecategories if c != targetcat]
+    msg = None
+    hint = None
+
+    troubled = {
+        "orphan": repo.revs("orphan()"),
+        "contentdivergent": repo.revs("contentdivergent()"),
+        "phasedivergent": repo.revs("phasedivergent()"),
+        "all": repo.revs("troubled()"),
+    }
+
+    hintmap = {
+        'phasedivergent': _("do you want to use --phase-divergent"),
+        'phasedivergent+contentdivergent': _("do you want to use "
+                                             "--phase-divergent or"
+                                             " --content-divergent"),
+        'phasedivergent+orphan': _("do you want to use --phase-divergent"
+                                   " or --orphan"),
+        'contentdivergent': _("do you want to use --content-divergent"),
+        'contentdivergent+orphan': _("do you want to use --content-divergent"
+                                     " or --orphan"),
+        'orphan': _("do you want to use --orphan"),
+        'any+phasedivergent': _("do you want to use --any (or --rev) and"
+                                " --phase-divergent"),
+        'any+phasedivergent+contentdivergent': _("do you want to use --any"
+                                                 " (or --rev) and"
+                                                 " --phase-divergent or"
+                                                 " --content-divergent"),
+        'any+phasedivergent+orphan': _("do you want to use --any (or --rev)"
+                                       " and --phase-divergent or --orphan"),
+        'any+contentdivergent': _("do you want to use --any (or --rev) and"
+                                  " --content-divergent"),
+        'any+contentdivergent+orphan': _("do you want to use --any (or --rev)"
+                                         " and --content-divergent or "
+                                         "--orphan"),
+        'any+orphan': _("do you want to use --any (or --rev)"
+                        "and --orphan"),
+    }
+
+    if revopt:
+        revs = scmutil.revrange(repo, revopt)
+        if not revs:
+            msg = _("set of specified revisions is empty")
+        else:
+            msg = _("no %s changesets in specified revisions") % targetcat
+            othertroubles = []
+            for cat in unselectedcategories:
+                if revs & troubled[cat]:
+                    othertroubles.append(cat)
+            if othertroubles:
+                hint = hintmap['+'.join(othertroubles)]
+
+    elif anyopt:
+        msg = _("no %s changesets to evolve") % targetcat
+        othertroubles = []
+        for cat in unselectedcategories:
+            if troubled[cat]:
+                othertroubles.append(cat)
+        if othertroubles:
+            hint = hintmap['+'.join(othertroubles)]
+
+    else:
+        # evolve without any option = relative to the current wdir
+        if targetcat == 'orphan':
+            msg = _("nothing to evolve on current working copy parent")
+        else:
+            msg = _("current working copy parent is not %s") % targetcat
+
+        p1 = repo['.'].rev()
+        othertroubles = []
+        for cat in unselectedcategories:
+            if p1 in troubled[cat]:
+                othertroubles.append(cat)
+        if othertroubles:
+            hint = hintmap['+'.join(othertroubles)]
+        else:
+            length = len(troubled[targetcat])
+            if length:
+                hint = _("%d other %s in the repository, do you want --any "
+                         "or --rev") % (length, targetcat)
+            else:
+                othertroubles = []
+                for cat in unselectedcategories:
+                    if troubled[cat]:
+                        othertroubles.append(cat)
+                if othertroubles:
+                    hint = hintmap['any+' + ('+'.join(othertroubles))]
+                else:
+                    msg = _("no troubled changesets")
+
+    assert msg is not None
+    ui.write_err("%s\n" % msg)
+    if hint:
+        ui.write_err("(%s)\n" % hint)
+        return 2
+    else:
+        return 1
+
+def _preparelistctxs(items, condition):
+    return [item.hex() for item in items if condition(item)]
+
+def _formatctx(fm, ctx):
+    fm.data(node=ctx.hex())
+    fm.data(desc=ctx.description())
+    fm.data(date=ctx.date())
+    fm.data(user=ctx.user())
+
+def listtroubles(ui, repo, troublecategories, **opts):
+    """Print all the troubles for the repo (or given revset)"""
+    troublecategories = troublecategories or ['contentdivergent', 'orphan', 'phasedivergent']
+    showunstable = 'orphan' in troublecategories
+    showbumped = 'phasedivergent' in troublecategories
+    showdivergent = 'contentdivergent' in troublecategories
+
+    revs = repo.revs('+'.join("%s()" % t for t in troublecategories))
+    if opts.get('rev'):
+        revs = scmutil.revrange(repo, opts.get('rev'))
+
+    fm = ui.formatter('evolvelist', opts)
+    for rev in revs:
+        ctx = repo[rev]
+        unpars = _preparelistctxs(ctx.parents(), lambda p: p.orphan())
+        obspars = _preparelistctxs(ctx.parents(), lambda p: p.obsolete())
+        imprecs = _preparelistctxs(repo.set("allprecursors(%n)", ctx.node()),
+                                   lambda p: not p.mutable())
+        dsets = divergentsets(repo, ctx)
+
+        fm.startitem()
+        # plain formatter section
+        hashlen, desclen = 12, 60
+        desc = ctx.description()
+        if desc:
+            desc = desc.splitlines()[0]
+        desc = (desc[:desclen] + '...') if len(desc) > desclen else desc
+        fm.plain('%s: ' % ctx.hex()[:hashlen])
+        fm.plain('%s\n' % desc)
+        fm.data(node=ctx.hex(), rev=ctx.rev(), desc=desc, phase=ctx.phasestr())
+
+        for unpar in unpars if showunstable else []:
+            fm.plain('  %s: %s (%s parent)\n' % (TROUBLES['ORPHAN'],
+                                                 unpar[:hashlen],
+                                                 TROUBLES['ORPHAN']))
+        for obspar in obspars if showunstable else []:
+            fm.plain('  %s: %s (obsolete parent)\n' % (TROUBLES['ORPHAN'],
+                                                       obspar[:hashlen]))
+        for imprec in imprecs if showbumped else []:
+            fm.plain('  %s: %s (immutable precursor)\n' %
+                     (TROUBLES['PHASEDIVERGENT'], imprec[:hashlen]))
+
+        if dsets and showdivergent:
+            for dset in dsets:
+                fm.plain('  %s: ' % TROUBLES['CONTENTDIVERGENT'])
+                first = True
+                for n in dset['divergentnodes']:
+                    t = "%s (%s)" if first else " %s (%s)"
+                    first = False
+                    fm.plain(t % (node.hex(n)[:hashlen], repo[n].phasestr()))
+                comprec = node.hex(dset['commonprecursor'])[:hashlen]
+                fm.plain(" (precursor %s)\n" % comprec)
+        fm.plain("\n")
+
+        # templater-friendly section
+        _formatctx(fm, ctx)
+        troubles = []
+        for unpar in unpars:
+            troubles.append({'troubletype': TROUBLES['ORPHAN'],
+                             'sourcenode': unpar, 'sourcetype': 'orphanparent'})
+        for obspar in obspars:
+            troubles.append({'troubletype': TROUBLES['ORPHAN'],
+                             'sourcenode': obspar,
+                             'sourcetype': 'obsoleteparent'})
+        for imprec in imprecs:
+            troubles.append({'troubletype': TROUBLES['PHASEDIVERGENT'],
+                             'sourcenode': imprec,
+                             'sourcetype': 'immutableprecursor'})
+        for dset in dsets:
+            divnodes = [{'node': node.hex(n),
+                         'phase': repo[n].phasestr(),
+                        } for n in dset['divergentnodes']]
+            troubles.append({'troubletype': TROUBLES['CONTENTDIVERGENT'],
+                             'commonprecursor': node.hex(dset['commonprecursor']),
+                             'divergentnodes': divnodes})
+        fm.data(troubles=troubles)
+
+    fm.end()
+
+def _checkevolveopts(repo, opts):
+    """ check the options passed to `hg evolve` and warn for deprecation warning
+    if any """
+
+    if opts['continue']:
+        if opts['any']:
+            raise error.Abort('cannot specify both "--any" and "--continue"')
+        if opts['all']:
+            raise error.Abort('cannot specify both "--all" and "--continue"')
+
+    if opts['rev']:
+        if opts['any']:
+            raise error.Abort('cannot specify both "--rev" and "--any"')
+        if opts['all']:
+            raise error.Abort('cannot specify both "--rev" and "--all"')
+
+    # Backward compatibility
+    if opts['unstable']:
+        msg = ("'evolve --unstable' is deprecated, "
+               "use 'evolve --orphan'")
+        repo.ui.deprecwarn(msg, '4.4')
+
+        opts['orphan'] = opts['divergent']
+
+    if opts['divergent']:
+        msg = ("'evolve --divergent' is deprecated, "
+               "use 'evolve --content-divergent'")
+        repo.ui.deprecwarn(msg, '4.4')
+
+        opts['content_divergent'] = opts['divergent']
+
+    if opts['bumped']:
+        msg = ("'evolve --bumped' is deprecated, "
+               "use 'evolve --phase-divergent'")
+        repo.ui.deprecwarn(msg, '4.4')
+
+        opts['phase_divergent'] = opts['bumped']
+
+    return opts
+
+def _cleanup(ui, repo, startnode, showprogress):
+    if showprogress:
+        ui.progress(_('evolve'), None)
+    if repo['.'] != startnode:
+        ui.status(_('working directory is now at %s\n') % repo['.'])
+
+def divergentsets(repo, ctx):
+    """Compute sets of commits divergent with a given one"""
+    cache = {}
+    base = {}
+    for n in compat.allprecursors(repo.obsstore, [ctx.node()]):
+        if n == ctx.node():
+            # a node can't be a base for divergence with itself
+            continue
+        nsuccsets = compat.successorssets(repo, n, cache)
+        for nsuccset in nsuccsets:
+            if ctx.node() in nsuccset:
+                # we are only interested in *other* successor sets
+                continue
+            if tuple(nsuccset) in base:
+                # we already know the latest base for this divergency
+                continue
+            base[tuple(nsuccset)] = n
+    divergence = []
+    for divset, b in base.iteritems():
+        divergence.append({
+            'divergentnodes': divset,
+            'commonprecursor': b
+        })
+
+    return divergence
+
+@eh.command(
+    '^evolve|stabilize|solve',
+    [('n', 'dry-run', False,
+      _('do not perform actions, just print what would be done')),
+     ('', 'confirm', False,
+      _('ask for confirmation before performing the action')),
+     ('A', 'any', False,
+      _('also consider troubled changesets unrelated to current working '
+        'directory')),
+     ('r', 'rev', [], _('solves troubles of these revisions')),
+     ('', 'bumped', False, _('solves only bumped changesets')),
+     ('', 'phase-divergent', False, _('solves only phase-divergent changesets')),
+     ('', 'divergent', False, _('solves only divergent changesets')),
+     ('', 'content-divergent', False, _('solves only content-divergent changesets')),
+     ('', 'unstable', False, _('solves only unstable changesets')),
+     ('', 'orphan', False, _('solves only orphan changesets (default)')),
+     ('a', 'all', False, _('evolve all troubled changesets related to the '
+                           'current  working directory and its descendants')),
+     ('c', 'continue', False, _('continue an interrupted evolution')),
+     ('l', 'list', False, 'provide details on troubled changesets in the repo'),
+    ] + mergetoolopts,
+    _('[OPTIONS]...')
+)
+def evolve(ui, repo, **opts):
+    """solve troubled changesets in your repository
+
+    Modifying history can lead to various types of troubled changesets:
+    unstable, bumped, or divergent. The evolve command resolves your troubles
+    by executing one of the following actions:
+
+    - update working copy to a successor
+    - rebase an unstable changeset
+    - extract the desired changes from a bumped changeset
+    - fuse divergent changesets back together
+
+    If you pass no arguments, evolve works in automatic mode: it will execute a
+    single action to reduce instability related to your working copy. There are
+    two cases for this action. First, if the parent of your working copy is
+    obsolete, evolve updates to the parent's successor. Second, if the working
+    copy parent is not obsolete but has obsolete predecessors, then evolve
+    determines if there is an unstable changeset that can be rebased onto the
+    working copy parent in order to reduce instability.
+    If so, evolve rebases that changeset. If not, evolve refuses to guess your
+    intention, and gives a hint about what you might want to do next.
+
+    Any time evolve creates a changeset, it updates the working copy to the new
+    changeset. (Currently, every successful evolve operation involves an update
+    as well; this may change in future.)
+
+    Automatic mode only handles common use cases. For example, it avoids taking
+    action in the case of ambiguity, and it ignores unstable changesets that
+    are not related to your working copy.
+    It also refuses to solve bumped or divergent changesets unless you
+    explicitly request such behavior (see below).
+
+    Eliminating all instability around your working copy may require multiple
+    invocations of :hg:`evolve`. Alternately, use ``--all`` to recursively
+    select and evolve all unstable changesets that can be rebased onto the
+    working copy parent.
+    This is more powerful than successive invocations, since ``--all`` handles
+    ambiguous cases (e.g. unstable changesets with multiple children) by
+    evolving all branches.
+
+    When your repository cannot be handled by automatic mode, you might need to
+    use ``--rev`` to specify a changeset to evolve. For example, if you have
+    an unstable changeset that is not related to the working copy parent,
+    you could use ``--rev`` to evolve it. Or, if some changeset has multiple
+    unstable children, evolve in automatic mode refuses to guess which one to
+    evolve; you have to use ``--rev`` in that case.
+
+    Alternately, ``--any`` makes evolve search for the next evolvable changeset
+    regardless of whether it is related to the working copy parent.
+
+    You can supply multiple revisions to evolve multiple troubled changesets
+    in a single invocation. In revset terms, ``--any`` is equivalent to ``--rev
+    first(unstable())``. ``--rev`` and ``--all`` are mutually exclusive, as are
+    ``--rev`` and ``--any``.
+
+    ``hg evolve --any --all`` is useful for cleaning up instability across all
+    branches, letting evolve figure out the appropriate order and destination.
+
+    When you have troubled changesets that are not unstable, :hg:`evolve`
+    refuses to consider them unless you specify the category of trouble you
+    wish to resolve, with ``--bumped`` or ``--divergent``. These options are
+    currently mutually exclusive with each other and with ``--unstable``
+    (the default). You can combine ``--bumped`` or ``--divergent`` with
+    ``--rev``, ``--all``, or ``--any``.
+
+    You can also use the evolve command to list the troubles affecting your
+    repository by using the --list flag. You can choose to display only some
+    categories of troubles with the --unstable, --divergent or --bumped flags.
+    """
+
+    opts = _checkevolveopts(repo, opts)
+    # Options
+    contopt = opts['continue']
+    anyopt = opts['any']
+    allopt = opts['all']
+    startnode = repo['.']
+    dryrunopt = opts['dry_run']
+    confirmopt = opts['confirm']
+    revopt = opts['rev']
+
+    troublecategories = ['phase_divergent', 'content_divergent', 'orphan']
+    specifiedcategories = [t.replace('_', '')
+                           for t in troublecategories
+                           if opts[t]]
+    if opts['list']:
+        compat.startpager(ui, 'evolve')
+        listtroubles(ui, repo, specifiedcategories, **opts)
+        return
+
+    targetcat = 'orphan'
+    if 1 < len(specifiedcategories):
+        msg = _('cannot specify more than one trouble category to solve (yet)')
+        raise error.Abort(msg)
+    elif len(specifiedcategories) == 1:
+        targetcat = specifiedcategories[0]
+    elif repo['.'].obsolete():
+        displayer = compat.changesetdisplayer(ui, repo,
+                                              {'template': shorttemplate})
+        # no args and parent is obsolete, update to successors
+        try:
+            ctx = repo[utility._singlesuccessor(repo, repo['.'])]
+        except utility.MultipleSuccessorsError as exc:
+            repo.ui.write_err('parent is obsolete with multiple successors:\n')
+            for ln in exc.successorssets:
+                for n in ln:
+                    displayer.show(repo[n])
+            return 2
+
+        ui.status(_('update:'))
+        if not ui.quiet:
+            displayer.show(ctx)
+
+        if dryrunopt:
+            return 0
+        res = hg.update(repo, ctx.rev())
+        if ctx != startnode:
+            ui.status(_('working directory is now at %s\n') % ctx)
+        return res
+
+    ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'evolve')
+    troubled = set(repo.revs('troubled()'))
+
+    # Progress handling
+    seen = 1
+    count = allopt and len(troubled) or 1
+    showprogress = allopt
+
+    def progresscb():
+        if revopt or allopt:
+            ui.progress(_('evolve'), seen, unit=_('changesets'), total=count)
+
+    evolvestate = state.cmdstate(repo)
+    # Continuation handling
+    if contopt:
+        if not evolvestate:
+            raise error.Abort('no evolve to continue')
+        evolvestate.load()
+        orig = repo[evolvestate['current']]
+        with repo.wlock(), repo.lock():
+            ctx = orig
+            source = ctx.extra().get('source')
+            extra = {}
+            if source:
+                extra['source'] = source
+                extra['intermediate-source'] = ctx.hex()
+            else:
+                extra['source'] = ctx.hex()
+            user = ctx.user()
+            date = ctx.date()
+            message = ctx.description()
+            ui.status(_('evolving %d:%s "%s"\n') % (ctx.rev(), ctx,
+                                                    message.split('\n', 1)[0]))
+            targetphase = max(ctx.phase(), phases.draft)
+            overrides = {('phases', 'new-commit'): targetphase}
+
+            with repo.ui.configoverride(overrides, 'evolve-continue'):
+                node = repo.commit(text=message, user=user,
+                                   date=date, extra=extra)
+
+            obsolete.createmarkers(repo, [(ctx, (repo[node],))])
+            evolvestate.delete()
+            return
+
+    cmdutil.bailifchanged(repo)
+
+    revs = _selectrevs(repo, allopt, revopt, anyopt, targetcat)
+
+    if not revs:
+        return _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat)
+
+    # For the progress bar to show
+    count = len(revs)
+    # Order the revisions
+    if targetcat == 'orphan':
+        revs = _orderrevs(repo, revs)
+
+    stateopts = {'category': targetcat, 'replacements': {}, 'revs': revs}
+    evolvestate.addopts(stateopts)
+    for rev in revs:
+        curctx = repo[rev]
+        progresscb()
+        ret = _solveone(ui, repo, curctx, evolvestate, dryrunopt, confirmopt,
+                        progresscb, targetcat)
+        seen += 1
+        if ret[0]:
+            evolvestate['replacements'][curctx.node()] = [ret[1]]
+    progresscb()
+    _cleanup(ui, repo, startnode, showprogress)
--- a/hgext3rd/evolve/evolvestate.py	Sun Jan 21 16:55:02 2018 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-
-"""
-This file contains class to wrap the state for hg evolve command and other
-related logic.
-
-All the data related to the command state is stored as dictionary in the object.
-The class has methods using which the data can be stored to disk in
-.hg/evolvestate file.
-
-We store the data on disk in cbor, for which we use cbor library to serialize
-and deserialize data.
-"""
-
-from __future__ import absolute_import
-
-from .thirdparty import cbor
-
-from mercurial import (
-    util,
-)
-
-class evolvestate():
-    """a wrapper class to store the state of `hg evolve` command
-
-    All the data for the state is stored in the form of key-value pairs in a
-    dictionary.
-
-    The class object can write all the data to .hg/evolvestate file and also can
-    populate the object data reading that file
-    """
-
-    def __init__(self, repo, path='evolvestate', opts={}):
-        self._repo = repo
-        self.path = path
-        self.opts = opts
-
-    def __nonzero__(self):
-        return self.exists()
-
-    def __getitem__(self, key):
-        return self.opts[key]
-
-    def load(self):
-        """load the existing evolvestate file into the class object"""
-        op = self._read()
-        self.opts.update(op)
-
-    def addopts(self, opts):
-        """add more key-value pairs to the data stored by the object"""
-        self.opts.update(opts)
-
-    def save(self):
-        """write all the evolvestate data stored in .hg/evolvestate file
-
-        we use third-party library cbor to serialize data to write in the file.
-        """
-        with self._repo.vfs(self.path, 'wb', atomictemp=True) as fp:
-            cbor.dump(self.opts, fp)
-
-    def _read(self):
-        """reads the evolvestate file and returns a dictionary which contain
-        data in the same format as it was before storing"""
-        with self._repo.vfs(self.path, 'rb') as fp:
-            return cbor.load(fp)
-
-    def delete(self):
-        """drop the evolvestate file if exists"""
-        util.unlinkpath(self._repo.vfs.join(self.path), ignoremissing=True)
-
-    def exists(self):
-        """check whether the evolvestate file exists or not"""
-        return self._repo.vfs.exists(self.path)
--- a/hgext3rd/evolve/metadata.py	Sun Jan 21 16:55:02 2018 -0500
+++ b/hgext3rd/evolve/metadata.py	Tue Feb 06 13:00:28 2018 +0100
@@ -5,7 +5,7 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-__version__ = '7.2.2.dev'
+__version__ = '7.3.0.dev'
 testedwith = '4.1.3 4.2.3 4.3.2 4.4.2'
 minimumhgversion = '4.1'
 buglink = 'https://bz.mercurial-scm.org/'
--- a/hgext3rd/evolve/obshistory.py	Sun Jan 21 16:55:02 2018 -0500
+++ b/hgext3rd/evolve/obshistory.py	Tue Feb 06 13:00:28 2018 +0100
@@ -57,7 +57,7 @@
     ] + commands.formatteropts,
     _('hg olog [OPTION]... [REV]'))
 def cmdobshistory(ui, repo, *revs, **opts):
-    """show the obsolescence history of the specified revisions.
+    """show the obsolescence history of the specified revisions
 
     If no revision range is specified, we display the log for the current
     working copy parent.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/state.py	Tue Feb 06 13:00:28 2018 +0100
@@ -0,0 +1,74 @@
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""
+This file contains class to wrap the state for commands and other
+related logic.
+
+All the data related to the command state is stored as dictionary in the object.
+The class has methods using which the data can be stored to disk in a file under
+.hg/ directory.
+
+We store the data on disk in cbor, for which we use cbor library to serialize
+and deserialize data.
+"""
+
+from __future__ import absolute_import
+
+from .thirdparty import cbor
+
+from mercurial import (
+    util,
+)
+
+class cmdstate():
+    """a wrapper class to store the state of commands like `evolve`, `grab`
+
+    All the data for the state is stored in the form of key-value pairs in a
+    dictionary.
+
+    The class object can write all the data to a file in .hg/ directory and also
+    can populate the object data reading that file
+    """
+
+    def __init__(self, repo, path='evolvestate', opts={}):
+        self._repo = repo
+        self.path = path
+        self.opts = opts
+
+    def __nonzero__(self):
+        return self.exists()
+
+    def __getitem__(self, key):
+        return self.opts[key]
+
+    def load(self):
+        """load the existing evolvestate file into the class object"""
+        op = self._read()
+        self.opts.update(op)
+
+    def addopts(self, opts):
+        """add more key-value pairs to the data stored by the object"""
+        self.opts.update(opts)
+
+    def save(self):
+        """write all the evolvestate data stored in .hg/evolvestate file
+
+        we use third-party library cbor to serialize data to write in the file.
+        """
+        with self._repo.vfs(self.path, 'wb', atomictemp=True) as fp:
+            cbor.dump(self.opts, fp)
+
+    def _read(self):
+        """reads the evolvestate file and returns a dictionary which contain
+        data in the same format as it was before storing"""
+        with self._repo.vfs(self.path, 'rb') as fp:
+            return cbor.load(fp)
+
+    def delete(self):
+        """drop the evolvestate file if exists"""
+        util.unlinkpath(self._repo.vfs.join(self.path), ignoremissing=True)
+
+    def exists(self):
+        """check whether the evolvestate file exists or not"""
+        return self._repo.vfs.exists(self.path)
--- a/hgext3rd/evolve/utility.py	Sun Jan 21 16:55:02 2018 -0500
+++ b/hgext3rd/evolve/utility.py	Tue Feb 06 13:00:28 2018 +0100
@@ -5,8 +5,14 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
+import collections
+
 from mercurial.node import nullrev
 
+from . import (
+    compat,
+)
+
 shorttemplate = "[{label('evolve.rev', rev)}] {desc|firstline}\n"
 
 def obsexcmsg(ui, message, important=False):
@@ -66,3 +72,60 @@
     if maxrevs is not None and maxrevs < len(repo.unfiltered()):
         return False
     return True
+
+class MultipleSuccessorsError(RuntimeError):
+    """Exception raised by _singlesuccessor when multiple successor sets exists
+
+    The object contains the list of successorssets in its 'successorssets'
+    attribute to call to easily recover.
+    """
+
+    def __init__(self, successorssets):
+        self.successorssets = successorssets
+
+def builddependencies(repo, revs):
+    """returns dependency graphs giving an order to solve instability of revs
+    (see _orderrevs for more information on usage)"""
+
+    # For each troubled revision we keep track of what instability if any should
+    # be resolved in order to resolve it. Example:
+    # dependencies = {3: [6], 6:[]}
+    # Means that: 6 has no dependency, 3 depends on 6 to be solved
+    dependencies = {}
+    # rdependencies is the inverted dict of dependencies
+    rdependencies = collections.defaultdict(set)
+
+    for r in revs:
+        dependencies[r] = set()
+        for p in repo[r].parents():
+            try:
+                succ = _singlesuccessor(repo, p)
+            except MultipleSuccessorsError as exc:
+                dependencies[r] = exc.successorssets
+                continue
+            if succ in revs:
+                dependencies[r].add(succ)
+                rdependencies[succ].add(r)
+    return dependencies, rdependencies
+
+def _singlesuccessor(repo, p):
+    """returns p (as rev) if not obsolete or its unique latest successors
+
+    fail if there are no such successor"""
+
+    if not p.obsolete():
+        return p.rev()
+    obs = repo[p]
+    ui = repo.ui
+    newer = compat.successorssets(repo, obs.node())
+    # search of a parent which is not killed
+    while not newer:
+        ui.debug("stabilize target %s is plain dead,"
+                 " trying to stabilize on its parent\n" %
+                 obs)
+        obs = obs.parents()[0]
+        newer = compat.successorssets(repo, obs.node())
+    if len(newer) > 1 or len(newer[0]) > 1:
+        raise MultipleSuccessorsError(newer)
+
+    return repo[newer[0][0]].rev()
--- a/hgext3rd/topic/__init__.py	Sun Jan 21 16:55:02 2018 -0500
+++ b/hgext3rd/topic/__init__.py	Tue Feb 06 13:00:28 2018 +0100
@@ -175,7 +175,7 @@
               'topic.active': 'green',
              }
 
-__version__ = '0.7.1.dev'
+__version__ = '0.8.0.dev'
 
 testedwith = '4.1.3 4.2.3 4.3.3 4.4.2'
 minimumhgversion = '4.1'
--- a/tests/test-evolve-phase.t	Sun Jan 21 16:55:02 2018 -0500
+++ b/tests/test-evolve-phase.t	Tue Feb 06 13:00:28 2018 +0100
@@ -115,6 +115,7 @@
   $ echo c2 > a
   $ hg resolve -m
   (no more unresolved files)
+  continue: hg evolve --continue
   $ hg evolve -c
   evolving 2:13833940840c "c"
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-grab.t	Tue Feb 06 13:00:28 2018 +0100
@@ -0,0 +1,302 @@
+Test for the grab command
+
+  $ cat >> $HGRCPATH <<EOF
+  > [alias]
+  > glog = log -G -T "{rev}:{node|short} {desc}\n"
+  > [extensions]
+  > EOF
+  $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
+
+  $ mkcommit() {
+  >    echo "$1" > "$1"
+  >    hg add "$1"
+  >    hg ci -m "add $1"
+  > }
+
+  $ hg init repo
+  $ cd repo
+  $ hg help grab
+  hg grab [-r] rev
+  
+  grabs a commit, move it on the top of working directory parent and
+      updates to it.
+  
+  options:
+  
+   -r --rev VALUE revision to grab
+      --continue  continue interrupted grab
+      --abort     abort interrupted grab
+  
+  (some details hidden, use --verbose to show complete help)
+
+  $ mkcommit a
+  $ mkcommit b
+  $ mkcommit c
+
+  $ hg glog
+  @  2:4538525df7e2 add c
+  |
+  o  1:7c3bad9141dc add b
+  |
+  o  0:1f0dee641bb7 add a
+  
+
+Grabbing an ancestor
+
+  $ hg grab -r 7c3bad9141dc
+  abort: cannot grab an ancestor revision
+  [255]
+
+Specifying multiple revisions to grab
+
+  $ hg grab 1f0dee641bb7 -r 7c3bad9141dc
+  abort: specify just one revision
+  [255]
+
+Specifying no revisions to grab
+
+  $ hg grab
+  abort: empty revision set
+  [255]
+
+Continuing without interrupted grab
+
+  $ hg grab --continue
+  abort: no interrupted grab state exists
+  [255]
+
+Aborting without interrupted grab
+
+  $ hg grab --abort
+  abort: no interrupted grab state exists
+  [255]
+
+Specifying both continue and revs
+
+  $ hg up 1f0dee641bb7
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg grab -r 4538525df7e2 --continue
+  abort: cannot specify both --continue and revision
+  [255]
+
+Making new branch heads
+
+  $ mkcommit x
+  created new head
+  $ mkcommit y
+
+  $ hg glog
+  @  4:d46dc301d92f add y
+  |
+  o  3:8e224524cd09 add x
+  |
+  | o  2:4538525df7e2 add c
+  | |
+  | o  1:7c3bad9141dc add b
+  |/
+  o  0:1f0dee641bb7 add a
+  
+Grabbing a revision
+
+  $ hg grab 7c3bad9141dc
+  grabbing 1:7c3bad9141dc "add b"
+  1 new orphan changesets
+  $ hg glog
+  @  5:7c15c05db6fa add b
+  |
+  o  4:d46dc301d92f add y
+  |
+  o  3:8e224524cd09 add x
+  |
+  | *  2:4538525df7e2 add c
+  | |
+  | x  1:7c3bad9141dc add b
+  |/
+  o  0:1f0dee641bb7 add a
+  
+
+When grab does not create any changes
+
+  $ hg graft -r 4538525df7e2
+  grafting 2:4538525df7e2 "add c"
+
+  $ hg glog
+  @  6:c4636a81ebeb add c
+  |
+  o  5:7c15c05db6fa add b
+  |
+  o  4:d46dc301d92f add y
+  |
+  o  3:8e224524cd09 add x
+  |
+  | *  2:4538525df7e2 add c
+  | |
+  | x  1:7c3bad9141dc add b
+  |/
+  o  0:1f0dee641bb7 add a
+  
+  $ hg grab -r 4538525df7e2
+  grabbing 2:4538525df7e2 "add c"
+  note: grab of 2:4538525df7e2 created no changes to commit
+
+  $ hg glog
+  @  6:c4636a81ebeb add c
+  |
+  o  5:7c15c05db6fa add b
+  |
+  o  4:d46dc301d92f add y
+  |
+  o  3:8e224524cd09 add x
+  |
+  o  0:1f0dee641bb7 add a
+  
+interrupted grab
+
+  $ hg up d46dc301d92f
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ echo foo > c
+  $ hg ci -Aqm "foo to c"
+  $ hg grab -r c4636a81ebeb
+  grabbing 6:c4636a81ebeb "add c"
+  merging c
+  warning: conflicts while merging c! (edit, then use 'hg resolve --mark')
+  unresolved merge conflicts (see hg help resolve)
+  [1]
+
+  $ echo foobar > c
+  $ hg resolve --all --mark
+  (no more unresolved files)
+  continue: hg grab --continue
+  $ hg grab --continue
+  $ hg glog
+  @  8:44e155eb95c7 add c
+  |
+  o  7:2ccc03d1d096 foo to c
+  |
+  | o  5:7c15c05db6fa add b
+  |/
+  o  4:d46dc301d92f add y
+  |
+  o  3:8e224524cd09 add x
+  |
+  o  0:1f0dee641bb7 add a
+  
+Testing the abort functionality of hg grab
+
+  $ echo foo > b
+  $ hg ci -Aqm "foo to b"
+  $ hg glog -r .^::
+  @  9:902d4f4602bb foo to b
+  |
+  o  8:44e155eb95c7 add c
+  |
+  ~
+
+  $ hg grab -r 7c15c05db6fa
+  grabbing 5:7c15c05db6fa "add b"
+  merging b
+  warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
+  unresolved merge conflicts (see hg help resolve)
+  [1]
+
+  $ hg grab --abort
+  aborting grab, updating to 902d4f4602bb
+
+  $ hg glog
+  @  9:902d4f4602bb foo to b
+  |
+  o  8:44e155eb95c7 add c
+  |
+  o  7:2ccc03d1d096 foo to c
+  |
+  | o  5:7c15c05db6fa add b
+  |/
+  o  4:d46dc301d92f add y
+  |
+  o  3:8e224524cd09 add x
+  |
+  o  0:1f0dee641bb7 add a
+  
+
+Trying to grab a public changeset
+
+  $ hg phase -r 7c15c05db6fa -p
+
+  $ hg grab -r 7c15c05db6fa
+  abort: cannot grab public changesets: 7c15c05db6fa
+  (see 'hg help phases' for details)
+  [255]
+
+  $ hg glog
+  @  9:902d4f4602bb foo to b
+  |
+  o  8:44e155eb95c7 add c
+  |
+  o  7:2ccc03d1d096 foo to c
+  |
+  | o  5:7c15c05db6fa add b
+  |/
+  o  4:d46dc301d92f add y
+  |
+  o  3:8e224524cd09 add x
+  |
+  o  0:1f0dee641bb7 add a
+  
+Checking phase preservation while grabbing secret changeset
+
+In case of merge conflicts
+
+  $ hg phase -r 7c15c05db6fa -s -f
+
+  $ hg grab -r 7c15c05db6fa
+  grabbing 5:7c15c05db6fa "add b"
+  merging b
+  warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
+  unresolved merge conflicts (see hg help resolve)
+  [1]
+
+  $ echo bar > b
+  $ hg resolve -m
+  (no more unresolved files)
+  continue: hg grab --continue
+
+  $ hg grab --continue
+  $ hg phase -r .
+  10: secret
+
+No merge conflicts
+
+  $ hg up d46dc301d92f
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ echo foo > l
+  $ hg add l
+  $ hg ci -qm "added l" --secret
+
+  $ hg phase -r .
+  11: secret
+
+  $ hg glog
+  @  11:508d572e7053 added l
+  |
+  | o  10:cd90ed194449 add b
+  | |
+  | o  9:902d4f4602bb foo to b
+  | |
+  | o  8:44e155eb95c7 add c
+  | |
+  | o  7:2ccc03d1d096 foo to c
+  |/
+  o  4:d46dc301d92f add y
+  |
+  o  3:8e224524cd09 add x
+  |
+  o  0:1f0dee641bb7 add a
+  
+  $ hg up cd90ed194449
+  3 files updated, 0 files merged, 1 files removed, 0 files unresolved
+
+  $ hg grab -r 508d572e7053
+  grabbing 11:508d572e7053 "added l"
+
+  $ hg phase -r .
+  12: secret
--- a/tests/test-issue-5720.t	Sun Jan 21 16:55:02 2018 -0500
+++ b/tests/test-issue-5720.t	Tue Feb 06 13:00:28 2018 +0100
@@ -70,6 +70,7 @@
   $ echo c2 > a
   $ hg resolve -m
   (no more unresolved files)
+  continue: hg evolve --continue
 
 Continue the evolution
   $ hg evolve --continue
--- a/tests/test-prev-next.t	Sun Jan 21 16:55:02 2018 -0500
+++ b/tests/test-prev-next.t	Tue Feb 06 13:00:28 2018 +0100
@@ -190,7 +190,6 @@
   move:[2] added c
   atop:[3] added b (2)
   hg rebase -r 4e26ef31f919 -d 9ad178109a19
-  working directory now at 9ad178109a19
 
 (add color output for smoke testing)
 
--- a/tests/test-stabilize-conflict.t	Sun Jan 21 16:55:02 2018 -0500
+++ b/tests/test-stabilize-conflict.t	Tue Feb 06 13:00:28 2018 +0100
@@ -167,6 +167,7 @@
   $ safesed 's/dix/ten/' babar
   $ hg resolve --all -m
   (no more unresolved files)
+  continue: hg evolve --continue
   $ hg evolve --continue
   evolving 4:71c18f70c34f "babar count up to fifteen"
   $ hg resolve -l
--- a/tests/test-stabilize-result.t	Sun Jan 21 16:55:02 2018 -0500
+++ b/tests/test-stabilize-result.t	Tue Feb 06 13:00:28 2018 +0100
@@ -98,6 +98,7 @@
   [255]
   $ hg resolve -m a
   (no more unresolved files)
+  continue: hg evolve --continue
   $ hg evolve --continue
   evolving 4:3655f0f50885 "newer a"
 
@@ -127,8 +128,7 @@
 Get a successors of 8 on it
 
   $ hg grab 1cf0aacfd363
-  rebasing 6:1cf0aacfd363 "newer a"
-  ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob)
+  grabbing 6:1cf0aacfd363 "newer a"
 
 Add real change to the successors
 
@@ -140,7 +140,7 @@
   $ hg phase --hidden --public 1cf0aacfd363
   1 new phase-divergent changesets
   $ glog
-  @  9:(73b15c7566e9|d5c7ef82d003)@default\(draft\) bk:\[\] newer a (re)
+  @  9:99c21c89bcef@default(draft) bk:[] newer a
   |
   o  7:7bc2f5967f5e@default(draft) bk:[] add c
   |
@@ -156,10 +156,10 @@
   $ hg evolve --any --dry-run --phase-divergent
   recreate:[9] newer a
   atop:[6] newer a
-  hg rebase --rev d5c7ef82d003 --dest 66719795a494;
+  hg rebase --rev 99c21c89bcef --dest 66719795a494;
   hg update 1cf0aacfd363;
-  hg revert --all --rev d5c7ef82d003;
-  hg commit --msg "phase-divergent update to d5c7ef82d003"
+  hg revert --all --rev 99c21c89bcef;
+  hg commit --msg "phase-divergent update to 99c21c89bcef"
   $ hg evolve --any --confirm --phase-divergent
   recreate:[9] newer a
   atop:[6] newer a
@@ -172,10 +172,10 @@
   perform evolve? [Ny] y
   rebasing to destination parent: 66719795a494
   computing new diff
-  committed as 8c986e77913c
-  working directory is now at 8c986e77913c
+  committed as 3d968e0b3097
+  working directory is now at 3d968e0b3097
   $ glog
-  @  11:8c986e77913c@default(draft) bk:[] phase-divergent update to 1cf0aacfd363:
+  @  11:3d968e0b3097@default(draft) bk:[] phase-divergent update to 1cf0aacfd363:
   |
   | o  7:7bc2f5967f5e@default(draft) bk:[] add c
   | |
@@ -204,7 +204,7 @@
   $ glog
   @  12:3932c176bbaa@default(draft) bk:[] More addition
   |
-  | o  11:8c986e77913c@default(draft) bk:[] phase-divergent update to 1cf0aacfd363:
+  | o  11:3d968e0b3097@default(draft) bk:[] phase-divergent update to 1cf0aacfd363:
   | |
   o |  7:7bc2f5967f5e@default(draft) bk:[] add c
   | |
@@ -233,7 +233,7 @@
   |
   | *  13:d2f173e25686@default(draft) bk:[] More addition
   |/
-  | o  11:8c986e77913c@default(draft) bk:[] phase-divergent update to 1cf0aacfd363:
+  | o  11:3d968e0b3097@default(draft) bk:[] phase-divergent update to 1cf0aacfd363:
   | |
   o |  7:7bc2f5967f5e@default(draft) bk:[] add c
   | |
@@ -283,7 +283,7 @@
   $ glog
   @  15:f344982e63c4@default(draft) bk:[] More addition
   |
-  | o  11:8c986e77913c@default(draft) bk:[] phase-divergent update to 1cf0aacfd363:
+  | o  11:3d968e0b3097@default(draft) bk:[] phase-divergent update to 1cf0aacfd363:
   | |
   o |  7:7bc2f5967f5e@default(draft) bk:[] add c
   | |
--- a/tests/test-tutorial.t	Sun Jan 21 16:55:02 2018 -0500
+++ b/tests/test-tutorial.t	Tue Feb 06 13:00:28 2018 +0100
@@ -665,11 +665,10 @@
   $ hg up 'p1(10b8aeaa8cc8)' # going on "bathroom stuff" parent
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg grab fac207dec9f5 # moving "SPAM SPAM" to the working directory parent
-  rebasing 9:fac207dec9f5 "SPAM SPAM" (tip)
+  grabbing 9:fac207dec9f5 "SPAM SPAM"
   merging shopping
-  ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob)
   $ hg log -G
-  @  a224f2a4fb9f (draft): SPAM SPAM
+  @  57e9caedbcb8 (draft): SPAM SPAM
   |
   | o  10b8aeaa8cc8 (draft): bathroom stuff
   |/
@@ -775,7 +774,7 @@
   # User test
   # Date 0 0
   #      Thu Jan 01 00:00:00 1970 +0000
-  # Node ID a224f2a4fb9f9f828f608959912229d7b38b26de
+  # Node ID 57e9caedbcb8575a01c128db9d1bcbd624ef2115
   # Parent  41aff6a42b7578ec7ec3cb2041633f1ca43cca96
   SPAM SPAM
   
@@ -808,14 +807,13 @@
 for simplicity sake we get the bathroom change in line again
 
   $ hg grab 10b8aeaa8cc8
-  rebasing 8:10b8aeaa8cc8 "bathroom stuff"
+  grabbing 8:10b8aeaa8cc8 "bathroom stuff"
   merging shopping
-  ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob)
   $ hg phase --draft .
   $ hg log -G
-  @  75954b8cd933 (draft): bathroom stuff
+  @  4710c0968793 (draft): bathroom stuff
   |
-  o  a224f2a4fb9f (public): SPAM SPAM
+  o  57e9caedbcb8 (public): SPAM SPAM
   |
   o  41aff6a42b75 (public): adding fruit
   |
@@ -1016,12 +1014,12 @@
   adding file changes
   added 1 changesets with 1 changes to 1 files
   1 new obsolescence markers
-  new changesets 75954b8cd933
+  new changesets 4710c0968793
   (run 'hg update' to get a working copy)
   $ hg log -G
-  o  75954b8cd933 (public): bathroom stuff
+  o  4710c0968793 (public): bathroom stuff
   |
-  o  a224f2a4fb9f (public): SPAM SPAM
+  o  57e9caedbcb8 (public): SPAM SPAM
   |
   o  41aff6a42b75 (public): adding fruit
   |
@@ -1039,7 +1037,7 @@
   $ hg rollback
   repository tip rolled back to revision 4 (undo pull)
   $ hg log -G
-  o  a224f2a4fb9f (public): SPAM SPAM
+  o  57e9caedbcb8 (public): SPAM SPAM
   |
   o  41aff6a42b75 (public): adding fruit
   |
@@ -1072,12 +1070,12 @@
   adding file changes
   added 1 changesets with 1 changes to 1 files
   1 new obsolescence markers
-  new changesets 75954b8cd933
+  new changesets 4710c0968793
   (run 'hg update' to get a working copy)
   $ hg log -G
-  o  75954b8cd933 (draft): bathroom stuff
+  o  4710c0968793 (draft): bathroom stuff
   |
-  o  a224f2a4fb9f (public): SPAM SPAM
+  o  57e9caedbcb8 (public): SPAM SPAM
   |
   o  41aff6a42b75 (public): adding fruit
   |
@@ -1093,7 +1091,7 @@
 
 Remotely someone add a new changeset on top of the mutable "bathroom" on.
 
-  $ hg up 75954b8cd933 -q
+  $ hg up 4710c0968793 -q
   $ cat >> shopping << EOF
   > Giraffe
   > Rhino
@@ -1105,13 +1103,13 @@
 But at the same time, locally, this same "bathroom changeset" was updated.
 
   $ cd ../local
-  $ hg up 75954b8cd933 -q
+  $ hg up 4710c0968793 -q
   $ sed -i'' -e 's/... More bathroom stuff to come/Bath Robe/' shopping
   $ hg commit --amend
   $ hg log -G
-  @  a44c85f957d3 (draft): bathroom stuff
+  @  682004e81e71 (draft): bathroom stuff
   |
-  o  a224f2a4fb9f (public): SPAM SPAM
+  o  57e9caedbcb8 (public): SPAM SPAM
   |
   o  41aff6a42b75 (public): adding fruit
   |
@@ -1214,21 +1212,20 @@
   adding file changes
   added 1 changesets with 1 changes to 1 files
   1 new orphan changesets
-  new changesets bf1b0d202029
+  new changesets e4e4fa805d92
   (run 'hg update' to get a working copy)
 
-
 The new changeset "animal" is based on an old changeset of "bathroom". You can
 see both version showing up in the log.
 
   $ hg log -G
-  *  bf1b0d202029 (draft): animals
+  *  e4e4fa805d92 (draft): animals
   |
-  | @  a44c85f957d3 (draft): bathroom stuff
+  | @  682004e81e71 (draft): bathroom stuff
   | |
-  x |  75954b8cd933 (draft): bathroom stuff
+  x |  4710c0968793 (draft): bathroom stuff
   |/
-  o  a224f2a4fb9f (public): SPAM SPAM
+  o  57e9caedbcb8 (public): SPAM SPAM
   |
   o  41aff6a42b75 (public): adding fruit
   |
@@ -1361,7 +1358,7 @@
   $ hg push other
   pushing to $TESTTMP/other (glob)
   searching for changes
-  abort: push includes orphan changeset: bf1b0d202029!
+  abort: push includes orphan changeset: e4e4fa805d92!
   (use 'hg evolve' to get a stable history or --force to ignore warnings)
   [255]
  
@@ -1374,7 +1371,7 @@
   $ hg evolve --dry-run
   move:[13] animals
   atop:[12] bathroom stuff
-  hg rebase -r bf1b0d202029 -d a44c85f957d3
+  hg rebase -r e4e4fa805d92 -d 682004e81e71
 
 Let's do it
 
@@ -1382,16 +1379,16 @@
   move:[13] animals
   atop:[12] bathroom stuff
   merging shopping
-  working directory is now at ee942144f952
+  working directory is now at 2a2b36e14660
 
 The old version of bathroom is hidden again.
 
   $ hg log -G
-  @  ee942144f952 (draft): animals
+  @  2a2b36e14660 (draft): animals
   |
-  o  a44c85f957d3 (draft): bathroom stuff
+  o  682004e81e71 (draft): bathroom stuff
   |
-  o  a224f2a4fb9f (public): SPAM SPAM
+  o  57e9caedbcb8 (public): SPAM SPAM
   |
   o  41aff6a42b75 (public): adding fruit
   |
@@ -1520,13 +1517,13 @@
 Now let's see where we are, and update to the successor.
 
   $ hg parents
-  bf1b0d202029 (draft): animals
-  working directory parent is obsolete! (bf1b0d202029)
-  (use 'hg evolve' to update to its successor: ee942144f952)
+  e4e4fa805d92 (draft): animals
+  working directory parent is obsolete! (e4e4fa805d92)
+  (use 'hg evolve' to update to its successor: 2a2b36e14660)
   $ hg evolve
   update:[8] animals
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  working directory is now at ee942144f952
+  working directory is now at 2a2b36e14660
 
 Relocating unstable change after prune
 ----------------------------------------------
@@ -1546,16 +1543,16 @@
   adding manifests
   adding file changes
   added 1 changesets with 1 changes to 1 files
-  new changesets 99f039c5ec9e
+  new changesets fc41faf45288
   (run 'hg update' to get a working copy)
   $ hg log -G
-  o  99f039c5ec9e (draft): SPAM SPAM SPAM
+  o  fc41faf45288 (draft): SPAM SPAM SPAM
   |
-  @  ee942144f952 (draft): animals
+  @  2a2b36e14660 (draft): animals
   |
-  o  a44c85f957d3 (draft): bathroom stuff
+  o  682004e81e71 (draft): bathroom stuff
   |
-  o  a224f2a4fb9f (public): SPAM SPAM
+  o  57e9caedbcb8 (public): SPAM SPAM
   |
   o  41aff6a42b75 (public): adding fruit
   |
@@ -1674,9 +1671,9 @@
 
 In the mean time I noticed you can't buy animals in a super market and I prune the animal changeset:
 
-  $ hg prune ee942144f952
+  $ hg prune 2a2b36e14660
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  working directory now at a44c85f957d3
+  working directory now at 682004e81e71
   1 changesets pruned
   1 new orphan changesets
 
@@ -1685,13 +1682,13 @@
 is neither dead or obsolete. My repository is in an unstable state again.
 
   $ hg log -G
-  *  99f039c5ec9e (draft): SPAM SPAM SPAM
+  *  fc41faf45288 (draft): SPAM SPAM SPAM
   |
-  x  ee942144f952 (draft): animals
+  x  2a2b36e14660 (draft): animals
   |
-  @  a44c85f957d3 (draft): bathroom stuff
+  @  682004e81e71 (draft): bathroom stuff
   |
-  o  a224f2a4fb9f (public): SPAM SPAM
+  o  57e9caedbcb8 (public): SPAM SPAM
   |
   o  41aff6a42b75 (public): adding fruit
   |
@@ -1809,7 +1806,7 @@
 #endif
 
   $ hg log -r "orphan()"
-  99f039c5ec9e (draft): SPAM SPAM SPAM
+  fc41faf45288 (draft): SPAM SPAM SPAM
 
 #if docgraph-ext
   $ hg docgraph -r "orphan()" --sphinx-directive --rankdir LR #rest-ignore
@@ -1837,14 +1834,14 @@
   move:[15] SPAM SPAM SPAM
   atop:[12] bathroom stuff
   merging shopping
-  working directory is now at 40aa40daeefb
+  working directory is now at e6cfcb672150
 
   $ hg log -G
-  @  40aa40daeefb (draft): SPAM SPAM SPAM
+  @  e6cfcb672150 (draft): SPAM SPAM SPAM
   |
-  o  a44c85f957d3 (draft): bathroom stuff
+  o  682004e81e71 (draft): bathroom stuff
   |
-  o  a224f2a4fb9f (public): SPAM SPAM
+  o  57e9caedbcb8 (public): SPAM SPAM
   |
   o  41aff6a42b75 (public): adding fruit
   |