merge with future 6.1.0 mercurial-4.0
authorPierre-Yves David <pierre-yves.david@ens-lyon.org>
Wed, 03 May 2017 13:18:08 +0200
branchmercurial-4.0
changeset 2318 523855eb28c1
parent 2317 7263463ae79a (diff)
parent 2278 2db189baf8b7 (current diff)
child 2319 ee321b87c548
child 2432 765b2561fa90
merge with future 6.1.0 In addition to the output reverted for 4.1, we also drop the "troubles:" entry from the standard log.
tests/test-evolve-obshistory.t
tests/test-obsolete.t
--- a/.hgtags	Thu Apr 20 12:45:02 2017 +0200
+++ b/.hgtags	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/README	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/debian/changelog	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/hgext3rd/evolve/__init__.py	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/hgext3rd/evolve/metadata.py	Wed May 03 13:18:08 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:18:08 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:45:02 2017 +0200
+++ b/hgext3rd/evolve/obsdiscovery.py	Wed May 03 13:18:08 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:18:08 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:45:02 2017 +0200
+++ b/hgext3rd/evolve/stablerange.py	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/hgext3rd/topic/__init__.py	Wed May 03 13:18:08 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:18:08 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:45:02 2017 +0200
+++ b/tests/test-inhibit.t	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/tests/test-obsolete-push.t	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/tests/test-obsolete.t	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/tests/test-stabilize-result.t	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/tests/test-touch.t	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/tests/test-tutorial.t	Wed May 03 13:18:08 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:45:02 2017 +0200
+++ b/tests/test-uncommit.t	Wed May 03 13:18:08 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