merge with future 6.1.0 mercurial-3.8
authorPierre-Yves David <pierre-yves.david@octobus.net>
Wed, 03 May 2017 13:52:19 +0200
branchmercurial-3.8
changeset 2320 979a8ea17e1e
parent 2280 28bda23bd8c5 (current diff)
parent 2319 ee321b87c548 (diff)
child 2434 37deace44b36
merge with future 6.1.0 No extra adjustment needed from hg-3.9 result
tests/test-obsolete-push.t
tests/test-obsolete.t
tests/test-prune.t
tests/test-uncommit.t
--- a/.hgtags	Thu Apr 20 12:50:22 2017 +0200
+++ b/.hgtags	Wed May 03 13:52:19 2017 +0200
@@ -47,3 +47,4 @@
 e7b6e9c4a5d4317f56c2862910c569723b6ea71b 5.6.0
 70694b2621ba9d919bc38303f8901e84caf5da0f 5.6.1
 165ad227993de4e7d819cc6c820d5b9f7b38b80d 6.0.0
+5ef112a6eb875633a7925cde61b7d2d9e65b3a56 6.0.1
--- a/README	Thu Apr 20 12:50:22 2017 +0200
+++ b/README	Wed May 03 13:52:19 2017 +0200
@@ -112,8 +112,16 @@
 Changelog
 =========
 
+6.1.0 - in progress
+-------------------
 
-6.0.1 - in progress
+ - improve message about obsolete working copy parent,
+ - improve message issued  when accessing hidden nodes (4.2 only),
+ - introduce a new caches to reduce the impact of evolution on read-only commands,
+ - add a 'experimental.auto-publish' config. See `hg help -e evolve` for details.
+ - fix the propagation of some some cache invalidation,
+
+6.0.1 -- 2017-04-20
 -------------------
 
  - template: adapt to change in 4.2,
@@ -121,7 +129,7 @@
  - checkheads: give priority to updated 4.2 code,
  - serveronly: fix repository initialization.
 
-6.0.0 -- 2017-02-31
+6.0.0 -- 2017-03-31
 -------------------
 
 - push: improved detection of obsoleted remote branch (issue4354),
--- a/debian/changelog	Thu Apr 20 12:50:22 2017 +0200
+++ b/debian/changelog	Wed May 03 13:52:19 2017 +0200
@@ -1,10 +1,14 @@
-mercurial-evolve (6.0.0-1) UNRELEASED; urgency=medium
+mercurial-evolve (6.0.1-1) UNRELEASED; urgency=medium
+
+  * New upstream version
+
+ -- Pierre-Yves David <marmoute@nodosa.octopoid.net>  Thu, 20 Apr 2017 12:58:35 +0200
+
+mercurial-evolve (6.0.0-1) unstable; urgency=medium
 
   * New Upstream Release
-  * new upstream version
-  * new upstream release
 
- -- Pierre-Yves David <marmoute@nodosa.octopoid.net>  Fri, 31 Mar 2017 15:50:12 +0200
+ -- Pierre-Yves David <marmoute@nodosa.octopoid.net>  Thu, 20 Apr 2017 12:58:03 +0200
 
 mercurial-evolve (5.5.0-1) unstable; urgency=medium
 
--- a/hgext3rd/evolve/__init__.py	Thu Apr 20 12:50:22 2017 +0200
+++ b/hgext3rd/evolve/__init__.py	Wed May 03 13:52:19 2017 +0200
@@ -7,15 +7,43 @@
 # GNU General Public License version 2 or any later version.
 """extends Mercurial feature related to Changeset Evolution
 
-This extension provides several commands to mutate history and deal with
-resulting issues.
-
-It also:
-
-    - enables the "Changeset Obsolescence" feature of Mercurial,
-    - alters core commands and extensions that rewrite history to use
-      this feature,
-    - improves some aspect of the early implementation in Mercurial core
+This extension:
+
+- provides several commands to mutate history and deal with resulting issues,
+- enable the changeset-evolution feature for Mercurial,
+- improves some aspect of the early implementation in Mercurial core,
+
+Note that a version dedicated to server usage only (no local working copy) is
+available as 'evolve.serveronly'.
+
+While many feature related to changeset evolution are directly handled by core
+this extensions contains significant additions recommended to any user of
+changeset evolution.
+
+With the extensions various evolution events will display warning (new unstable
+changesets, obsolete working copy parent, improved error when accessing hidden
+revision, etc).
+
+In addition, the extension contains better discovery protocol for obsolescence
+markers. This means less obs-markers will have to be pushed and pulled around,
+speeding up such operation.
+
+Some improvement and bug fixes available in newer version of Mercurial are also
+backported to older version of Mercurial by this extension. Some older
+experimental protocol are also supported for a longer time in the extensions to
+help people transitioning. (The extensions is currently compatible down to
+Mercurial version 3.8).
+
+New Config:
+
+    [experimental]
+    # Set to control the behavior when pushing draft changesets to a publishing
+    # repository. Possible value:
+    # * ignore: current core behavior (default)
+    # * warn: proceed with the push, but issue a warning
+    # * abort: abort the push
+    auto-publish = ignore
+
 """
 
 
@@ -104,6 +132,7 @@
     revset,
     scmutil,
     templatekw,
+    obsolete
 )
 
 from mercurial.commands import walkopts, commitopts, commitopts2, mergetoolopts
@@ -113,9 +142,11 @@
 from . import (
     checkheads,
     debugcmd,
-    obsexchange,
     exthelper,
     metadata,
+    obscache,
+    obsexchange,
+    safeguard,
     utility,
 )
 
@@ -147,6 +178,8 @@
 eh.merge(debugcmd.eh)
 eh.merge(obsexchange.eh)
 eh.merge(checkheads.eh)
+eh.merge(safeguard.eh)
+eh.merge(obscache.eh)
 uisetup = eh.final_uisetup
 extsetup = eh.final_extsetup
 reposetup = eh.final_reposetup
@@ -449,11 +482,102 @@
 # This section take care of issue warning to the user when troubles appear
 
 
+def _getobsoletereason(repo, revnode):
+    """ Return a tuple containing:
+    - the reason a revision is obsolete (diverged, pruned or superseed)
+    - the list of successors short node if the revision is neither pruned
+    or has diverged
+    """
+    successorssets = obsolete.successorssets(repo, revnode)
+
+    if len(successorssets) == 0:
+        # The commit has been pruned
+        return ('pruned', [])
+    elif len(successorssets) > 1:
+        return ('diverged', [])
+    else:
+        # No divergence, only one set of successors
+        successors = [node.short(node_id) for node_id in successorssets[0]]
+
+        if len(successors) == 1:
+            return ('superseed', successors)
+        else:
+            return ('superseed_split', successors)
+
 def _warnobsoletewc(ui, repo):
-    if repo['.'].obsolete():
-        ui.warn(_('working directory parent is obsolete!\n'))
-        if (not ui.quiet) and obsolete.isenabled(repo, commandopt):
-            ui.warn(_("(use 'hg evolve' to update to its successor)\n"))
+    rev = repo['.']
+
+    if not rev.obsolete():
+        return
+
+    msg = _("working directory parent is obsolete! (%s)\n")
+    shortnode = node.short(rev.node())
+
+    ui.warn(msg % shortnode)
+
+    # Check that evolve is activated for performance reasons
+    if ui.quiet or not obsolete.isenabled(repo, commandopt):
+        return
+
+    # Show a warning for helping the user to solve the issue
+    reason, successors = _getobsoletereason(repo, rev.node())
+
+    if reason == 'pruned':
+        solvemsg = _("use 'hg evolve' to update to its parent successor")
+    elif reason == 'diverged':
+        debugcommand = "hg evolve -list --divergent"
+        basemsg = _("%s has diverged, use '%s' to resolve the issue")
+        solvemsg = basemsg % (shortnode, debugcommand)
+    elif reason == 'superseed':
+        msg = _("use 'hg evolve' to update to its successor: %s")
+        solvemsg = msg % successors[0]
+    elif reason == 'superseed_split':
+        msg = _("use 'hg evolve' to update to its tipmost successor: %s")
+
+        if len(successors) <= 2:
+            solvemsg = msg % ", ".join(successors)
+        else:
+            firstsuccessors = ", ".join(successors[:2])
+            remainingnumber = len(successors) - 2
+            successorsmsg = _("%s and %d more") % (firstsuccessors, remainingnumber)
+            solvemsg = msg % successorsmsg
+    else:
+        raise ValueError(reason)
+
+    ui.warn("(%s)\n" % solvemsg)
+
+if util.safehasattr(context, '_filterederror'):
+    # if < hg-4.2 we do not update the message
+    @eh.wrapfunction(context, '_filterederror')
+    def evolve_filtererror(original, repo, changeid):
+        """build an exception to be raised about a filtered changeid
+
+        This is extracted in a function to help extensions (eg: evolve) to
+        experiment with various message variants."""
+        if repo.filtername.startswith('visible'):
+
+            unfilteredrepo = repo.unfiltered()
+            rev = unfilteredrepo[changeid]
+            reason, successors = _getobsoletereason(unfilteredrepo, rev.node())
+
+            # Be more precise in cqse the revision is superseed
+            if reason == 'superseed':
+                reason = _("successor: %s") % successors[0]
+            elif reason == 'superseed_split':
+                if len(successors) <= 2:
+                    reason = _("successors: %s") % ", ".join(successors)
+                else:
+                    firstsuccessors = ", ".join(successors[:2])
+                    remainingnumber = len(successors) - 2
+                    successorsmsg = _("%s and %d more") % (firstsuccessors, remainingnumber)
+                    reason = _("successors: %s") % successorsmsg
+
+            msg = _("hidden revision '%s'") % changeid
+            hint = _('use --hidden to access hidden revisions; %s') % reason
+            return error.FilteredRepoLookupError(msg, hint=hint)
+        msg = _("filtered revision '%s' (not in '%s' subset)")
+        msg %= (changeid, repo.filtername)
+        return error.FilteredRepoLookupError(msg)
 
 @eh.wrapcommand("update")
 @eh.wrapcommand("pull")
--- a/hgext3rd/evolve/metadata.py	Thu Apr 20 12:50:22 2017 +0200
+++ b/hgext3rd/evolve/metadata.py	Wed May 03 13:52:19 2017 +0200
@@ -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__ = '6.0.0'
-testedwith = '3.8.4 3.9.2 4.0.2 4.1.1'
+__version__ = '6.1.0.dev'
+testedwith = '3.8.4 3.9.2 4.0.2 4.1.1 4.2'
 minimumhgversion = '3.8'
 buglink = 'https://bz.mercurial-scm.org/'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/obscache.py	Wed May 03 13:52:19 2017 +0200
@@ -0,0 +1,405 @@
+# Code dedicated to an cache around obsolescence property
+#
+# This module content aims at being upstreamed.
+#
+# Copyright 2017 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+import hashlib
+import struct
+import weakref
+import errno
+
+from mercurial import (
+    localrepo,
+    obsolete,
+    phases,
+    node,
+    util,
+)
+
+from . import (
+    exthelper,
+)
+
+eh = exthelper.exthelper()
+
+try:
+    obsstorefilecache = localrepo.localrepository.obsstore
+except AttributeError:
+    # XXX hg-3.8 compat
+    #
+    # mercurial 3.8 has issue with accessing file cache property from their
+    # cache. This is fix by 36fbd72c2f39fef8ad52d7c559906c2bc388760c in core
+    # and shipped in 3.9
+    obsstorefilecache = localrepo.localrepository.__dict__['obsstore']
+
+# obsstore is a filecache so we have do to some spacial dancing
+@eh.wrapfunction(obsstorefilecache, 'func')
+def obsstorewithcache(orig, repo):
+    obsstore = orig(repo)
+    obsstore.obscache = obscache(repo.unfiltered())
+
+    class cachekeyobsstore(obsstore.__class__):
+
+        _obskeysize = 200
+
+        def cachekey(self, index=None):
+            """return (current-length, cachekey)
+
+            'current-length': is the current length of the obsstore storage file,
+            'cachekey' is the hash of the last 200 bytes ending at 'index'.
+
+            if 'index' is unspecified, current obsstore length is used.
+            Cacheckey will be set to null id if the obstore is empty.
+
+            If the index specified is higher than the current obsstore file
+            length, cachekey will be set to None."""
+            # default value
+            obsstoresize = 0
+            keydata = ''
+            # try to get actual data from the obsstore
+            try:
+                with self.svfs('obsstore') as obsfile:
+                    obsfile.seek(0, 2)
+                    obsstoresize = obsfile.tell()
+                    if index is None:
+                        index = obsstoresize
+                    elif obsstoresize < index:
+                        return obsstoresize, None
+                    actualsize = min(index, self._obskeysize)
+                    if actualsize:
+                        obsfile.seek(index - actualsize, 0)
+                        keydata = obsfile.read(actualsize)
+            except (OSError, IOError) as e:
+                if e.errno != errno.ENOENT:
+                    raise
+            key = hashlib.sha1(keydata).digest()
+            return obsstoresize, key
+
+    obsstore.__class__ = cachekeyobsstore
+
+    return obsstore
+
+emptykey = (node.nullrev, node.nullid, 0, 0, node.nullid)
+
+def getcachekey(repo):
+    """get a cache key covering the changesets and obsmarkers content
+
+    IT contains the following data. Combined with 'upgradeneeded' it allows to
+    do iterative upgrade for cache depending of theses two data.
+
+    The cache key parts are"
+    - tip-rev,
+    - tip-node,
+    - obsstore-length (nb markers),
+    - obsstore-file-size (in bytes),
+    - obsstore "cache key"
+    """
+    assert repo.filtername is None
+    cl = repo.changelog
+    index, key = repo.obsstore.cachekey()
+    tiprev = len(cl) - 1
+    return (tiprev,
+            cl.node(tiprev),
+            len(repo.obsstore),
+            index,
+            key)
+
+def upgradeneeded(repo, key):
+    """return (valid, start-rev, start-obs-idx)
+
+    'valid': is "False" if older cache value needs invalidation,
+
+    'start-rev': first revision not in the cache. None if cache is up to date,
+
+    'start-obs-idx': index of the first obs-markers not in the cache. None is
+                     up to date.
+    """
+
+    # XXX ideally, this function would return a bounded amount of changeset and
+    # obsmarkers and the associated new cache key. Otherwise we are exposed to
+    # a race condition between the time the cache is updated and the new cache
+    # key is computed. (however, we do not want to compute the full new cache
+    # key in all case because we want to skip reading the obsstore content. We
+    # could have a smarter implementation here.
+    #
+    # In pratice the cache is only updated after each transaction within a
+    # lock. So we should be fine. We could enforce this with a new repository
+    # requirement (or fix the race, that is not too hard).
+    invalid = (False, 0, 0)
+    if key is None:
+        return invalid
+
+    ### Is the cache valid ?
+    keytiprev, keytipnode, keyobslength, keyobssize, keyobskey = key
+    # check for changelog strip
+    cl = repo.changelog
+    tiprev = len(cl) - 1
+    if (tiprev < keytiprev
+            or cl.node(keytiprev) != keytipnode):
+        return invalid
+    # check for obsstore strip
+    obssize, obskey = repo.obsstore.cachekey(index=keyobssize)
+    if obskey != keyobskey:
+        return invalid
+
+    ### cache is valid, is there anything to update
+
+    # any new changesets ?
+    startrev = None
+    if keytiprev < tiprev:
+        startrev = keytiprev + 1
+
+    # any new markers
+    startidx = None
+    if keyobssize < obssize:
+        startidx = keyobslength
+
+    return True, startrev, startidx
+
+class obscache(object):
+    """cache the "does a rev" is the precursors of some obsmarkers data
+
+    This is not directly holding the "is this revision obsolete" information,
+    because phases data gets into play here. However, it allow to compute the
+    "obsolescence" set without reading the obsstore content.
+
+    Implementation note #1:
+
+      The obsstore is implementing only half of the transaction logic it
+      should. It properly record the starting point of the obsstore to allow
+      clean rollback. However it still write to the obsstore file directly
+      during the transaction. Instead it should be keeping data in memory and
+      write to a '.pending' file to make the data vailable for hooks.
+
+      This cache is not going futher than what the obstore is doing, so it does
+      not has any '.pending' logic. When the obsstore gains proper '.pending'
+      support, adding it to this cache should not be too hard. As the flag
+      always move from 0 to 1, we could have a second '.pending' cache file to
+      be read. If flag is set in any of them, the value is 1. For the same
+      reason, updating the file in place should be possible.
+
+    Implementation note #2:
+
+      Instead of having a large final update run, we could update this cache at
+      the level adding a new changeset or a new obsmarkers. More on this in the
+      'update code'.
+
+    Implementation note #3:
+
+        Storage-wise, we could have a "start rev" to avoid storing useless
+        zero. That would be especially useful for the '.pending' overlay.
+    """
+
+    _filepath = 'cache/evoext-obscache-00'
+    _headerformat = '>q20sQQ20s'
+
+    def __init__(self, repo):
+        self._vfs = repo.vfs
+        # The cache key parts are"
+        # - tip-rev,
+        # - tip-node,
+        # - obsstore-length (nb markers),
+        # - obsstore-file-size (in bytes),
+        # - obsstore "cache key"
+        self._cachekey = None
+        self._ondiskkey = None
+        self._data = bytearray()
+
+    def get(self, rev):
+        """return True if "rev" is used as "precursors for any obsmarkers
+
+        Make sure the cache has been updated to match the repository content before using it"""
+        return self._data[rev]
+
+    def clear(self):
+        """invalidate the cache content"""
+        self._cachekey = None
+        self._data = bytearray()
+
+    def uptodate(self, repo):
+        if self._cachekey is None:
+            self.load(repo)
+        valid, startrev, startidx = upgradeneeded(repo, self._cachekey)
+        return (valid and startrev is None and startidx is None)
+
+    def update(self, repo):
+        """Iteratively update the cache with new repository data"""
+        # If we do not have any data, try loading from disk
+        if self._cachekey is None:
+            self.load(repo)
+
+        valid, startrev, startidx = upgradeneeded(repo, self._cachekey)
+        if not valid:
+            self.clear()
+
+        if startrev is None and startidx is None:
+            return
+
+        # process the new changesets
+        cl = repo.changelog
+        if startrev is not None:
+            node = cl.node
+            # Note:
+            #
+            #  Newly added changeset might be affected by obsolescence markers
+            #  we already have locally. So we needs to have soem global
+            #  knowledge about the markers to handle that question. Right this
+            #  requires parsing all markers in the obsstore. However, we could
+            #  imagine using various optimisation (eg: bloom filter, other on
+            #  disk cache) to remove this full parsing.
+            #
+            #  For now we stick to the simpler approach or paying the
+            #  performance cost on new changesets.
+            succs = repo.obsstore.successors
+            for r in cl.revs(startrev):
+                if node(r) in succs:
+                    val = 1
+                else:
+                    val = 0
+                self._data.append(val)
+        assert len(self._data) == len(cl), (len(self._data), len(cl))
+
+        # process the new obsmarkers
+        if startidx is not None:
+            rev = cl.nodemap.get
+            markers = repo.obsstore._all
+            # Note:
+            #
+            #   There are no actually needs to load the full obsstore here,
+            #   since we only read the latest ones.  We do it for simplicity in
+            #   the first implementation. Loading the full obsstore has a
+            #   performance cost and should go away in this case too. We have
+            #   two simples options for that:
+            #
+            #   1) provide and API to start reading markers from a byte offset
+            #      (we have that data in the cache key)
+            #
+            #   2) directly update the cache at a lower level, in the code
+            #      responsible for adding a markers.
+            #
+            #   Option 2 is probably a bit more invasive, but more solid on the long run
+
+            for i in xrange(startidx, len(repo.obsstore)):
+                r = rev(markers[i][0])
+                # If markers affect a newly added nodes, it would have been
+                # caught in the previous loop, (so we skip < startrev)
+                if r is not None and (startrev is None or r < startrev):
+                    self._data[r] = 1
+
+        assert repo._currentlock(repo._lockref) is not None
+        # XXX note that there are a potential race condition here, since the
+        # repo "might" have changed side the cache update above. However, this
+        # code will only be running in a lock so we ignore the issue for now.
+        #
+        # To work around this, 'upgradeneeded' should return a bounded amount
+        # of changeset and markers to read with their associated cachekey. see
+        # 'upgradeneeded' for detail.
+        self._cachekey = getcachekey(repo)
+
+    def save(self, repo):
+        """save the data to disk"""
+
+        # XXX it happens that the obsstore is (buggilly) always up to date on disk
+        if self._cachekey is None or self._cachekey == self._ondiskkey:
+            return
+
+        cachefile = repo.vfs(self._filepath, 'w', atomictemp=True)
+        headerdata = struct.pack(self._headerformat, *self._cachekey)
+        cachefile.write(headerdata)
+        cachefile.write(self._data)
+        cachefile.close()
+
+    def load(self, repo):
+        """load data from disk"""
+        assert repo.filtername is None
+
+        data = repo.vfs.tryread(self._filepath)
+        if not data:
+            self._cachekey = emptykey
+            self._data = bytearray()
+        else:
+            headersize = struct.calcsize(self._headerformat)
+            self._cachekey = struct.unpack(self._headerformat, data[:headersize])
+            self._data = bytearray(data[headersize:])
+        self._ondiskkey = self._cachekey
+
+def _computeobsoleteset(orig, repo):
+    """the set of obsolete revisions"""
+    obs = set()
+    repo = repo.unfiltered()
+    if util.safehasattr(repo._phasecache, 'getrevset'):
+        notpublic = repo._phasecache.getrevset(repo, (phases.draft, phases.secret))
+    else:
+        # < hg-4.2 compat
+        notpublic = repo.revs("not public()")
+    if notpublic:
+        obscache = repo.obsstore.obscache
+        # Since we warm the cache at the end of every transaction, the cache
+        # should be up to date. However a non-enabled client might have touced
+        # the repository.
+        #
+        # Updating the cache without a lock is sloppy, so we fallback to the
+        # old method and rely on the fact the next transaction will write the
+        # cache down anyway.
+        #
+        # With the current implementation updating the cache will requires to
+        # load the obsstore anyway. Once loaded, hitting the obsstore directly
+        # will be about as fast..
+        if not obscache.uptodate(repo):
+            if repo.currenttransaction() is None:
+                repo.ui.log('evoext-obscache',
+                            'obscache is out of date, '
+                            'falling back to slower obsstore version\n')
+                repo.ui.debug('obscache is out of date')
+                return orig(repo)
+            else:
+                # If a transaction is open, it is worthwhile to update and use the
+                # cache as it will be written on disk when the transaction close.
+                obscache.update(repo)
+        isobs = obscache.get
+    for r in notpublic:
+        if isobs(r):
+            obs.add(r)
+    return obs
+
+@eh.uisetup
+def cachefuncs(ui):
+    orig = obsolete.cachefuncs['obsolete']
+    wrapped = lambda repo: _computeobsoleteset(orig, repo)
+    obsolete.cachefuncs['obsolete'] = wrapped
+
+@eh.reposetup
+def setupcache(ui, repo):
+
+    class obscacherepo(repo.__class__):
+
+        @localrepo.unfilteredmethod
+        def destroyed(self):
+            if 'obsstore' in vars(self):
+                self.obsstore.obscache.clear()
+
+        def transaction(self, *args, **kwargs):
+            tr = super(obscacherepo, self).transaction(*args, **kwargs)
+            reporef = weakref.ref(self)
+
+            def _warmcache(tr):
+                repo = reporef()
+                if repo is None:
+                    return
+                repo = repo.unfiltered()
+                # As pointed in 'obscache.update', we could have the
+                # changelog and the obsstore in charge of updating the
+                # cache when new items goes it. The tranaction logic would
+                # then only be involved for the 'pending' and final saving
+                # logic.
+                self.obsstore.obscache.update(repo)
+                self.obsstore.obscache.save(repo)
+
+            tr.addpostclose('warmcache-obscache', _warmcache)
+            return tr
+
+    repo.__class__ = obscacherepo
--- a/hgext3rd/evolve/obsdiscovery.py	Thu Apr 20 12:50:22 2017 +0200
+++ b/hgext3rd/evolve/obsdiscovery.py	Wed May 03 13:52:19 2017 +0200
@@ -551,6 +551,7 @@
         def destroyed(self):
             if 'stablerange' in vars(self):
                 del self.stablerange
+            super(obshashrepo, self).destroyed()
 
     repo.__class__ = obshashrepo
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/safeguard.py	Wed May 03 13:52:19 2017 +0200
@@ -0,0 +1,42 @@
+# Code dedicated to adding various "safeguard" around evolution
+#
+# Some of these will be pollished and upstream when mature. Some other will be
+# replaced by better alternative later.
+#
+# Copyright 2017 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from mercurial import error
+
+from mercurial.i18n import _
+
+from . import exthelper
+
+eh = exthelper.exthelper()
+
+@eh.reposetup
+def setuppublishprevention(ui, repo):
+
+    class noautopublishrepo(repo.__class__):
+
+        def checkpush(self, pushop):
+            super(noautopublishrepo, self).checkpush(pushop)
+            behavior = repo.ui.config('experimental', 'auto-publish', 'default')
+            remotephases = pushop.remote.listkeys('phases')
+            publishing = remotephases.get('publishing', False)
+            if behavior in ('warn', 'abort') and publishing:
+                if pushop.revs is None:
+                    published = repo.filtered('served').revs("not public()")
+                else:
+                    published = repo.revs("::%ln - public()", pushop.revs)
+                if published:
+                    if behavior == 'warn':
+                        repo.ui.warn(_('%i changesets about to be published\n') % len(published))
+                    elif behavior == 'abort':
+                        msg = _('push would publish 1 changesets')
+                        hint = _("behavior controlled by 'experimental.auto-publish' config")
+                        raise error.Abort(msg, hint=hint)
+
+    repo.__class__ = noautopublishrepo
--- a/hgext3rd/evolve/stablerange.py	Thu Apr 20 12:50:22 2017 +0200
+++ b/hgext3rd/evolve/stablerange.py	Wed May 03 13:52:19 2017 +0200
@@ -886,6 +886,7 @@
         def destroyed(self):
             if 'stablerange' in vars(self):
                 del self.stablerange
+            super(stablerangerepo, self).destroyed()
 
         def transaction(self, *args, **kwargs):
             tr = super(stablerangerepo, self).transaction(*args, **kwargs)
--- a/hgext3rd/topic/__init__.py	Thu Apr 20 12:50:22 2017 +0200
+++ b/hgext3rd/topic/__init__.py	Wed May 03 13:52:19 2017 +0200
@@ -154,7 +154,6 @@
 
 
 def reposetup(ui, repo):
-    orig = repo.__class__
     if not isinstance(repo, localrepo.localrepository):
         return # this can be a peer in the ssh case (puzzling)
 
@@ -171,7 +170,7 @@
                 if repo.currenttopic != repo['.'].topic():
                     # bypass the core "nothing changed" logic
                     self.ui.setconfig('ui', 'allowemptycommit', True)
-                return orig.commit(self, *args, **kwargs)
+                return super(topicrepo, self).commit(*args, **kwargs)
             finally:
                 self.ui.restoreconfig(backup)
 
@@ -187,7 +186,7 @@
                 # we are amending and need to remove a topic
                 del ctx.extra()[constants.extrakey]
             with topicmap.usetopicmap(self):
-                return orig.commitctx(self, ctx, error=error)
+                return super(topicrepo, self).commitctx(ctx, error=error)
 
         @property
         def topics(self):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-evolve-obshistory.t	Wed May 03 13:52:19 2017 +0200
@@ -0,0 +1,514 @@
+This test file test the various messages when accessing obsolete
+revisions.
+
+Global setup
+============
+
+  $ . $TESTDIR/testlib/common.sh
+  $ cat >> $HGRCPATH <<EOF
+  > [ui]
+  > interactive = true
+  > [phases]
+  > publish=False
+  > [extensions]
+  > evolve =
+  > EOF
+
+Test output on amended commit
+=============================
+
+Test setup
+----------
+
+  $ hg init $TESTTMP/local-amend
+  $ cd $TESTTMP/local-amend
+  $ mkcommit ROOT
+  $ mkcommit A0
+  $ echo 42 >> A0
+  $ hg amend -m "A1"
+  $ hg log --hidden -G
+  @  changeset:   3:a468dc9b3633
+  |  tag:         tip
+  |  parent:      0:ea207398892e
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A1
+  |
+  | x  changeset:   2:f137d23bb3e1
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     temporary amend commit for 471f378eab4c
+  | |
+  | x  changeset:   1:471f378eab4c
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     A0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+Actual test
+-----------
+
+  $ hg update 471f378eab4c
+  abort: hidden revision '471f378eab4c'!
+  (use --hidden to access hidden revisions)
+  [255]
+  $ hg update --hidden "desc(A0)"
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  working directory parent is obsolete! (471f378eab4c)
+  (use 'hg evolve' to update to its successor: a468dc9b3633)
+
+Test output with pruned commit
+==============================
+
+Test setup
+----------
+
+  $ hg init $TESTTMP/local-prune
+  $ cd $TESTTMP/local-prune
+  $ mkcommit ROOT
+  $ mkcommit A0 # 0
+  $ mkcommit B0 # 1
+  $ hg log --hidden -G
+  @  changeset:   2:0dec01379d3b
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B0
+  |
+  o  changeset:   1:471f378eab4c
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+  $ hg prune -r 'desc(B0)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  working directory now at 471f378eab4c
+  1 changesets pruned
+
+Actual test
+-----------
+
+  $ hg up 1
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg up 0dec01379d3b
+  abort: hidden revision '0dec01379d3b'!
+  (use --hidden to access hidden revisions)
+  [255]
+  $ hg up --hidden -r 'desc(B0)'
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  working directory parent is obsolete! (0dec01379d3b)
+  (use 'hg evolve' to update to its parent successor)
+
+Test output with splitted commit
+================================
+
+Test setup
+----------
+
+  $ hg init $TESTTMP/local-split
+  $ cd $TESTTMP/local-split
+  $ mkcommit ROOT
+  $ echo 42 >> a
+  $ echo 43 >> b
+  $ hg commit -A -m "A0"
+  adding a
+  adding b
+  $ hg log --hidden -G
+  @  changeset:   1:471597cad322
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+  $ hg split -r 'desc(A0)' -d "0 0" << EOF
+  > y
+  > y
+  > n
+  > n
+  > y
+  > y
+  > EOF
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  adding a
+  adding b
+  diff --git a/a b/a
+  new file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +42
+  record change 1/2 to 'a'? [Ynesfdaq?] y
+  
+  diff --git a/b b/b
+  new file mode 100644
+  examine changes to 'b'? [Ynesfdaq?] n
+  
+  created new head
+  Done splitting? [yN] n
+  diff --git a/b b/b
+  new file mode 100644
+  examine changes to 'b'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +43
+  record this change to 'b'? [Ynesfdaq?] y
+  
+  no more change to split
+
+  $ hg log --hidden -G
+  @  changeset:   3:f257fde29c7a
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A0
+  |
+  o  changeset:   2:337fec4d2edc
+  |  parent:      0:ea207398892e
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A0
+  |
+  | x  changeset:   1:471597cad322
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     A0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+Actual test
+-----------
+
+  $ hg update 471597cad322
+  abort: hidden revision '471597cad322'!
+  (use --hidden to access hidden revisions)
+  [255]
+  $ hg update --hidden 'min(desc(A0))'
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  working directory parent is obsolete! (471597cad322)
+  (use 'hg evolve' to update to its tipmost successor: 337fec4d2edc, f257fde29c7a)
+
+Test output with lots of splitted commit
+========================================
+
+Test setup
+----------
+
+  $ hg init $TESTTMP/local-lots-split
+  $ cd $TESTTMP/local-lots-split
+  $ mkcommit ROOT
+  $ echo 42 >> a
+  $ echo 43 >> b
+  $ echo 44 >> c
+  $ echo 45 >> d
+  $ hg commit -A -m "A0"
+  adding a
+  adding b
+  adding c
+  adding d
+  $ hg log --hidden -G
+  @  changeset:   1:de7290d8b885
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+
+  $ hg split -r 'desc(A0)' -d "0 0" << EOF
+  > y
+  > y
+  > n
+  > n
+  > n
+  > n
+  > y
+  > y
+  > n
+  > n
+  > n
+  > y
+  > y
+  > n
+  > n
+  > y
+  > y
+  > EOF
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  adding a
+  adding b
+  adding c
+  adding d
+  diff --git a/a b/a
+  new file mode 100644
+  examine changes to 'a'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +42
+  record change 1/4 to 'a'? [Ynesfdaq?] y
+  
+  diff --git a/b b/b
+  new file mode 100644
+  examine changes to 'b'? [Ynesfdaq?] n
+  
+  diff --git a/c b/c
+  new file mode 100644
+  examine changes to 'c'? [Ynesfdaq?] n
+  
+  diff --git a/d b/d
+  new file mode 100644
+  examine changes to 'd'? [Ynesfdaq?] n
+  
+  created new head
+  Done splitting? [yN] n
+  diff --git a/b b/b
+  new file mode 100644
+  examine changes to 'b'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +43
+  record change 1/3 to 'b'? [Ynesfdaq?] y
+  
+  diff --git a/c b/c
+  new file mode 100644
+  examine changes to 'c'? [Ynesfdaq?] n
+  
+  diff --git a/d b/d
+  new file mode 100644
+  examine changes to 'd'? [Ynesfdaq?] n
+  
+  Done splitting? [yN] n
+  diff --git a/c b/c
+  new file mode 100644
+  examine changes to 'c'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +44
+  record change 1/2 to 'c'? [Ynesfdaq?] y
+  
+  diff --git a/d b/d
+  new file mode 100644
+  examine changes to 'd'? [Ynesfdaq?] n
+  
+  Done splitting? [yN] n
+  diff --git a/d b/d
+  new file mode 100644
+  examine changes to 'd'? [Ynesfdaq?] y
+  
+  @@ -0,0 +1,1 @@
+  +45
+  record this change to 'd'? [Ynesfdaq?] y
+  
+  no more change to split
+
+  $ hg log --hidden -G
+  @  changeset:   5:c7f044602e9b
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A0
+  |
+  o  changeset:   4:1ae8bc733a14
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A0
+  |
+  o  changeset:   3:f257fde29c7a
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A0
+  |
+  o  changeset:   2:337fec4d2edc
+  |  parent:      0:ea207398892e
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A0
+  |
+  | x  changeset:   1:de7290d8b885
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     A0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+Actual test
+-----------
+
+  $ hg update de7290d8b885
+  abort: hidden revision 'de7290d8b885'!
+  (use --hidden to access hidden revisions)
+  [255]
+  $ hg update --hidden 'min(desc(A0))'
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  working directory parent is obsolete! (de7290d8b885)
+  (use 'hg evolve' to update to its tipmost successor: 337fec4d2edc, f257fde29c7a and 2 more)
+
+Test output with folded commit
+==============================
+
+Test setup
+----------
+
+  $ hg init $TESTTMP/local-fold
+  $ cd $TESTTMP/local-fold
+  $ mkcommit ROOT
+  $ mkcommit A0
+  $ mkcommit B0
+  $ hg log --hidden -G
+  @  changeset:   2:0dec01379d3b
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B0
+  |
+  o  changeset:   1:471f378eab4c
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+  $ hg fold --exact -r 'desc(A0) + desc(B0)' --date "0 0" -m "C0"
+  2 changesets folded
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg log --hidden -G
+  @  changeset:   3:eb5a0daa2192
+  |  tag:         tip
+  |  parent:      0:ea207398892e
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     C0
+  |
+  | x  changeset:   2:0dec01379d3b
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     B0
+  | |
+  | x  changeset:   1:471f378eab4c
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     A0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+ Actual test
+ -----------
+
+  $ hg update 471f378eab4c
+  abort: hidden revision '471f378eab4c'!
+  (use --hidden to access hidden revisions)
+  [255]
+  $ hg update --hidden 'desc(A0)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  working directory parent is obsolete! (471f378eab4c)
+  (use 'hg evolve' to update to its successor: eb5a0daa2192)
+  $ hg update 0dec01379d3b
+  working directory parent is obsolete! (471f378eab4c)
+  (use 'hg evolve' to update to its successor: eb5a0daa2192)
+  abort: hidden revision '0dec01379d3b'!
+  (use --hidden to access hidden revisions)
+  [255]
+  $ hg update --hidden 'desc(B0)'
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  working directory parent is obsolete! (0dec01379d3b)
+  (use 'hg evolve' to update to its successor: eb5a0daa2192)
+
+Test output with divergence
+===========================
+
+Test setup
+----------
+
+  $ hg init $TESTTMP/local-divergence
+  $ cd $TESTTMP/local-divergence
+  $ mkcommit ROOT
+  $ mkcommit A0
+  $ hg amend -m "A1"
+  $ hg log --hidden -G
+  @  changeset:   2:fdf9bde5129a
+  |  tag:         tip
+  |  parent:      0:ea207398892e
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A1
+  |
+  | x  changeset:   1:471f378eab4c
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     A0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+  $ hg update --hidden 'desc(A0)'
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  working directory parent is obsolete! (471f378eab4c)
+  (use 'hg evolve' to update to its successor: fdf9bde5129a)
+  $ hg amend -m "A2"
+  2 new divergent changesets
+  $ hg log --hidden -G
+  @  changeset:   3:65b757b745b9
+  |  tag:         tip
+  |  parent:      0:ea207398892e
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A2
+  |
+  | o  changeset:   2:fdf9bde5129a
+  |/   parent:      0:ea207398892e
+  |    user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     A1
+  |
+  | x  changeset:   1:471f378eab4c
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     A0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+Actual test
+-----------
+
+  $ hg update 471f378eab4c
+  abort: hidden revision '471f378eab4c'!
+  (use --hidden to access hidden revisions)
+  [255]
+  $ hg update --hidden 'desc(A0)'
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  working directory parent is obsolete! (471f378eab4c)
+  (471f378eab4c has diverged, use 'hg evolve -list --divergent' to resolve the issue)
--- a/tests/test-inhibit.t	Thu Apr 20 12:50:22 2017 +0200
+++ b/tests/test-inhibit.t	Wed May 03 13:52:19 2017 +0200
@@ -699,7 +699,7 @@
 
   $ hg up 15
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  working directory parent is obsolete!
+  working directory parent is obsolete! (2d66e189f5b5)
   $ cat >> $HGRCPATH <<EOF
   > [experimental]
   > evolution=all
--- a/tests/test-obsolete-push.t	Thu Apr 20 12:50:22 2017 +0200
+++ b/tests/test-obsolete-push.t	Wed May 03 13:52:19 2017 +0200
@@ -44,3 +44,50 @@
   comparing with ../clone
   searching for changes
   0:1994f17a630e@default(obsolete/draft) A
+  $ cd ..
+
+Test options to prevent implicite publishing of changesets
+----------------------------------------------------------
+
+
+  $ hg clone source strict-publish-client --pull
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  1 new obsolescence markers
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd strict-publish-client
+  $ echo c > c
+  $ hg ci -qAm C c
+
+abort behavior
+
+  $ cat >> .hg/hgrc <<eof
+  > [experimental]
+  > auto-publish = abort
+  > eof
+  $ hg push -r .
+  pushing to $TESTTMP/source
+  abort: push would publish 1 changesets
+  (behavior controlled by 'experimental.auto-publish' config)
+  [255]
+  $ hg push
+  pushing to $TESTTMP/source
+  abort: push would publish 1 changesets
+  (behavior controlled by 'experimental.auto-publish' config)
+  [255]
+
+warning behavior
+
+  $ echo 'auto-publish = warn' >> .hg/hgrc
+  $ hg push
+  pushing to $TESTTMP/source
+  1 changesets about to be published
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 0 changesets with 0 changes to 1 files
--- a/tests/test-obsolete.t	Thu Apr 20 12:50:22 2017 +0200
+++ b/tests/test-obsolete.t	Wed May 03 13:52:19 2017 +0200
@@ -121,7 +121,7 @@
   4
   - 725c380fe99b
   $ hg up --hidden 3 -q
-  working directory parent is obsolete!
+  working directory parent is obsolete! (0d3f46688ccc)
 (reported by parents too)
   $ hg parents
   changeset:   3:0d3f46688ccc
@@ -130,8 +130,8 @@
   date:        Thu Jan 01 00:00:00 1970 +0000
   summary:     add obsol_c
   
-  working directory parent is obsolete!
-  (use 'hg evolve' to update to its successor)
+  working directory parent is obsolete! (0d3f46688ccc)
+  (use 'hg evolve' to update to its successor: 725c380fe99b)
   $ mkcommit d # 5 (on 3)
   1 new unstable changesets
   $ qlog -r 'obsolete()'
@@ -206,7 +206,7 @@
   0
   - 1f0dee641bb7
   $ hg up --hidden 3 -q
-  working directory parent is obsolete!
+  working directory parent is obsolete! (0d3f46688ccc)
   $ mkcommit obsol_d # 6
   created new head
   1 new unstable changesets
@@ -263,7 +263,7 @@
   [1]
 
   $ hg up --hidden -q .^ # 3
-  working directory parent is obsolete!
+  working directory parent is obsolete! (0d3f46688ccc)
   $ mkcommit "obsol_d'" # 7
   created new head
   1 new unstable changesets
@@ -351,7 +351,7 @@
 Test rollback support
 
   $ hg up --hidden .^ -q # 3
-  working directory parent is obsolete!
+  working directory parent is obsolete! (0d3f46688ccc)
   $ mkcommit "obsol_d''"
   created new head
   1 new unstable changesets
@@ -683,7 +683,7 @@
   
 
   $ hg up --hidden 3 -q
-  working directory parent is obsolete!
+  working directory parent is obsolete! (0d3f46688ccc)
   $ hg evolve
   parent is obsolete with multiple successors:
   [4] add obsol_c'
@@ -700,8 +700,8 @@
 
   $ hg up --hidden 2
   1 files updated, 0 files merged, 1 files removed, 0 files unresolved
-  working directory parent is obsolete!
-  (use 'hg evolve' to update to its successor)
+  working directory parent is obsolete! (4538525df7e2)
+  (4538525df7e2 has diverged, use 'hg evolve -list --divergent' to resolve the issue)
   $ hg export 9468a5f5d8b2 | hg import -
   applying patch from stdin
   1 new unstable changesets
--- a/tests/test-stabilize-result.t	Thu Apr 20 12:50:22 2017 +0200
+++ b/tests/test-stabilize-result.t	Wed May 03 13:52:19 2017 +0200
@@ -222,8 +222,8 @@
   $ hg amend
   $ hg up --hidden 15
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  working directory parent is obsolete!
-  (use 'hg evolve' to update to its successor)
+  working directory parent is obsolete! (3932c176bbaa)
+  (use 'hg evolve' to update to its successor: d2f173e25686)
   $ mv a a.old
   $ echo 'jungle' > a
   $ cat a.old >> a
@@ -335,8 +335,8 @@
 
   $ hg up --hidden 15
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  working directory parent is obsolete!
-  (use 'hg evolve' to update to its successor)
+  working directory parent is obsolete! (3932c176bbaa)
+  (use 'hg evolve' to update to its successor: f344982e63c4)
   $ echo 'gotta break' >> a
   $ hg amend
   2 new divergent changesets
--- a/tests/test-touch.t	Thu Apr 20 12:50:22 2017 +0200
+++ b/tests/test-touch.t	Wed May 03 13:52:19 2017 +0200
@@ -33,8 +33,8 @@
   $ hg commit -m ab --amend
   $ hg up --hidden 1
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
-  working directory parent is obsolete!
-  (use 'hg evolve' to update to its successor)
+  working directory parent is obsolete! (*) (glob)
+  (use 'hg evolve' to update to its successor: *) (glob)
   $ hg log -G
   o  3:[0-9a-f]{12} ab (re)
   
--- a/tests/test-tutorial.t	Thu Apr 20 12:50:22 2017 +0200
+++ b/tests/test-tutorial.t	Wed May 03 13:52:19 2017 +0200
@@ -741,15 +741,15 @@
   pulling from $TESTTMP/local (glob)
   searching for changes
   no changes found
-  working directory parent is obsolete!
-  (use 'hg evolve' to update to its successor)
+  working directory parent is obsolete! (bf1b0d202029)
+  (use 'hg evolve' to update to its successor: ee942144f952)
 
 now let's see where we are, and update to the successor
 
   $ hg parents
   bf1b0d202029 (draft): animals
-  working directory parent is obsolete!
-  (use 'hg evolve' to update to its successor)
+  working directory parent is obsolete! (bf1b0d202029)
+  (use 'hg evolve' to update to its successor: ee942144f952)
   $ hg evolve
   update:[8] animals
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
--- a/tests/test-uncommit.t	Thu Apr 20 12:50:22 2017 +0200
+++ b/tests/test-uncommit.t	Wed May 03 13:52:19 2017 +0200
@@ -239,8 +239,8 @@
   $ hg up -C 3 --hidden
   8 files updated, 0 files merged, 1 files removed, 0 files unresolved
   (leaving bookmark touncommit-bm)
-  working directory parent is obsolete!
-  (use 'hg evolve' to update to its successor)
+  working directory parent is obsolete! (5eb72dbe0cb4)
+  (use 'hg evolve' to update to its successor: e8db4aa611f6)
   $ hg --config extensions.purge= purge
   $ hg uncommit -I 'set:added() and e'
   2 new divergent changesets
@@ -285,8 +285,8 @@
 
   $ hg up -C 3 --hidden
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  working directory parent is obsolete!
-  (use 'hg evolve' to update to its successor)
+  working directory parent is obsolete! (5eb72dbe0cb4)
+  (5eb72dbe0cb4 has diverged, use 'hg evolve -list --divergent' to resolve the issue)
   $ hg --config extensions.purge= purge
   $ hg uncommit --all -X e
   1 new divergent changesets