changeset 2431:0405bbda7402 mercurial-4.1

merge with future 6.2.0 There are been (multiple) minor output changes.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Thu, 18 May 2017 23:12:52 +0200
parents d4ee0274a8ef (diff) 677dfbb8bdbf (current diff)
children 765b2561fa90 e5e502407ab0
files tests/test-discovery-obshashrange.t tests/test-evolve-obshistory.t tests/test-evolve-serveronly-bundle2.t tests/test-evolve.t tests/test-obsolete.t tests/test-prev-next.t tests/test-topic-stack.t tests/test-wireproto-bundle1.t tests/test-wireproto.t
diffstat 19 files changed, 2680 insertions(+), 250 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Thu May 18 22:53:01 2017 +0200
+++ b/.hgtags	Thu May 18 23:12:52 2017 +0200
@@ -48,3 +48,4 @@
 70694b2621ba9d919bc38303f8901e84caf5da0f 5.6.1
 165ad227993de4e7d819cc6c820d5b9f7b38b80d 6.0.0
 5ef112a6eb875633a7925cde61b7d2d9e65b3a56 6.0.1
+8510d3fd7c3b312dc731f4c29badc415d504558a 6.1.0
--- a/README	Thu May 18 22:53:01 2017 +0200
+++ b/README	Thu May 18 23:12:52 2017 +0200
@@ -112,7 +112,24 @@
 Changelog
 =========
 
-6.1.0 - in progress
+6.2.0 -- 2017-05-18
+-------------------
+
+ - olog: a new command to inspect the obs-history of a changeset (hg-4.0 + only),
+ - topic: have thg display topic name if possible,
+ - blackbox: log more information about discovery and cache computation,
+ - obscache: more efficient update in the (rare) case of a transaction adding
+   markers without changesets,
+ - obscache: fix more cache invalidation propagation,
+ - obscache: also enable the new cache (from 6.1.0) for 'evolve.server-only',
+ - obshashrange-cache: update incrementally in the (common) case of a
+   transaction not affecting existing range,
+ - obshashrange-cache: keep the cache warm after each transaction,
+ - topic: now requires Mercurial 4.0 or above,
+ - stack: now display if current revision is in bad state (issue5533),
+ - stack: fix json output to be valid json.
+
+6.1.0 -- 2017-05-03
 -------------------
 
  - improve message about obsolete working copy parent,
--- a/debian/changelog	Thu May 18 22:53:01 2017 +0200
+++ b/debian/changelog	Thu May 18 23:12:52 2017 +0200
@@ -1,14 +1,26 @@
-mercurial-evolve (6.0.1-1) UNRELEASED; urgency=medium
+mercurial-evolve (6.2.0-1) unstable; urgency=medium
+
+  * new upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@ens-lyon.org>  Thu, 18 May 2017 22:24:10 +0200
+
+mercurial-evolve (6.1.0-1) unstable; urgency=medium
+
+  * New upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@ens-lyon.org>  Wed, 03 May 2017 13:57:15 +0200
+
+mercurial-evolve (6.0.1-1) unstable; urgency=medium
 
   * New upstream version
 
- -- Pierre-Yves David <marmoute@nodosa.octopoid.net>  Thu, 20 Apr 2017 12:58:35 +0200
+ -- Pierre-Yves David <pierre-yves.david@ens-lyon.org>  Thu, 20 Apr 2017 12:58:35 +0200
 
 mercurial-evolve (6.0.0-1) unstable; urgency=medium
 
   * New Upstream Release
 
- -- Pierre-Yves David <marmoute@nodosa.octopoid.net>  Thu, 20 Apr 2017 12:58:03 +0200
+ -- Pierre-Yves David <pierre-yves.david@ens-lyon.org>  Thu, 20 Apr 2017 12:58:03 +0200
 
 mercurial-evolve (5.5.0-1) unstable; urgency=medium
 
--- a/hgext3rd/evolve/__init__.py	Thu May 18 22:53:01 2017 +0200
+++ b/hgext3rd/evolve/__init__.py	Thu May 18 23:12:52 2017 +0200
@@ -148,6 +148,7 @@
     obsexchange,
     safeguard,
     utility,
+    obshistory
 )
 
 __version__ = metadata.__version__
@@ -162,6 +163,15 @@
 
 obsexcmsg = utility.obsexcmsg
 
+colortable = {'evolve.node': 'yellow',
+              'evolve.user': 'green',
+              'evolve.rev': 'blue',
+              'evolve.short_description': '',
+              'evolve.date': 'cyan',
+              'evolve.current_rev': 'bold',
+              'evolve.verb': '',
+              }
+
 _pack = struct.pack
 _unpack = struct.unpack
 
@@ -180,6 +190,7 @@
 eh.merge(checkheads.eh)
 eh.merge(safeguard.eh)
 eh.merge(obscache.eh)
+eh.merge(obshistory.eh)
 uisetup = eh.final_uisetup
 extsetup = eh.final_extsetup
 reposetup = eh.final_reposetup
@@ -269,9 +280,6 @@
         ui.setconfig('alias', 'pstatus', 'status --rev .^', 'evolve')
     if ui.config('alias', 'pdiff', None) is None:
         ui.setconfig('alias', 'pdiff', 'diff --rev .^', 'evolve')
-    if ui.config('alias', 'olog', None) is None:
-        ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden",
-                     'evolve')
     if ui.config('alias', 'odiff', None) is None:
         ui.setconfig('alias', 'odiff',
                      "diff --hidden --rev 'limit(precursors(.),1)' --rev .",
@@ -1881,7 +1889,7 @@
     raise error.Abort("base of divergent changeset %s not found" % ctx,
                       hint='this case is not yet handled')
 
-shorttemplate = '[{rev}] {desc|firstline}\n'
+shorttemplate = "[{label('evolve.rev', rev)}] {desc|firstline}\n"
 
 @eh.command(
     '^previous',
@@ -2055,7 +2063,8 @@
                 result = _solveone(ui, repo, repo[aspchildren[0]], dryrunopt,
                                    False, lambda: None, category='unstable')
                 if not result:
-                    ui.status(_('working directory now at %s\n') % repo['.'])
+                    ui.status(_('working directory now at %s\n')
+                              % ui.label(repo['.'], 'evolve.node'))
                 return result
             return 1
         return result
@@ -2261,7 +2270,8 @@
                     repo._bookmarks[bookactive] = newnode.node()
                     repo._bookmarks.recordchange(tr)
                 commands.update(ui, repo, newnode.rev())
-                ui.status(_('working directory now at %s\n') % newnode)
+                ui.status(_('working directory now at %s\n')
+                          % ui.label(newnode, 'evolve.node'))
                 if movebookmark:
                     bookmarksmod.activate(repo, bookactive)
 
--- a/hgext3rd/evolve/metadata.py	Thu May 18 22:53:01 2017 +0200
+++ b/hgext3rd/evolve/metadata.py	Thu May 18 23:12:52 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.1.0.dev'
-testedwith = '3.8.4 3.9.2 4.0.2 4.1.1 4.2'
+__version__ = '6.2.0'
+testedwith = '3.8.4 3.9.2 4.0.2 4.1.2 4.2'
 minimumhgversion = '3.8'
 buglink = 'https://bz.mercurial-scm.org/'
--- a/hgext3rd/evolve/obscache.py	Thu May 18 22:53:01 2017 +0200
+++ b/hgext3rd/evolve/obscache.py	Thu May 18 23:12:52 2017 +0200
@@ -7,25 +7,42 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
+import errno
 import hashlib
+import os
 import struct
+import time
 import weakref
-import errno
 
 from mercurial import (
+    error,
     localrepo,
     obsolete,
     phases,
+    pycompat,
     node,
     util,
 )
 
+from mercurial.i18n import _
+
 from . import (
     exthelper,
 )
 
 eh = exthelper.exthelper()
 
+# prior to hg-4.2 there are not util.timer
+if util.safehasattr(util, 'timer'):
+    timer = util.timer
+elif util.safehasattr(time, "perf_counter"):
+    timer = time.perf_counter
+elif getattr(pycompat, 'osname', os.name) == 'nt':
+    timer = time.clock
+else:
+    timer = time.time
+
+
 try:
     obsstorefilecache = localrepo.localrepository.obsstore
 except AttributeError:
@@ -76,91 +93,235 @@
             except (OSError, IOError) as e:
                 if e.errno != errno.ENOENT:
                     raise
-            key = hashlib.sha1(keydata).digest()
+            if keydata:
+                key = hashlib.sha1(keydata).digest()
+            else:
+                # reusing an existing "empty" value make it easier to define a
+                # default cachekey for 'no data'.
+                key = node.nullid
             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.
+# XXX copied as is from Mercurial 4.2 and added the "offset" parameters
+@util.nogc
+def _readmarkers(data, offset=None):
+    """Read and enumerate markers from raw data"""
+    off = 0
+    diskversion = struct.unpack('>B', data[off:off + 1])[0]
+    if offset is None:
+        off += 1
+    else:
+        assert 1 <= offset
+        off = offset
+    if diskversion not in obsolete.formats:
+        raise error.Abort(_('parsing obsolete marker: unknown version %r')
+                          % diskversion)
+    return diskversion, obsolete.formats[diskversion][0](data, off)
 
-    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 markersfrom(obsstore, byteoffset, firstmarker):
+    if not firstmarker:
+        return list(obsstore)
+    elif '_all' in vars(obsstore):
+        # if the data are in memory, just use that
+        return obsstore._all[firstmarker:]
+    else:
+        obsdata = obsstore.svfs.tryread('obsstore')
+        return _readmarkers(obsdata, byteoffset)[1]
 
-def upgradeneeded(repo, key):
-    """return (valid, start-rev, start-obs-idx)
 
-    'valid': is "False" if older cache value needs invalidation,
+class dualsourcecache(object):
+    """An abstract class for cache that needs both changelog and obsstore
 
-    '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.
+    This class handle the tracking of changelog and obsstore update. It provide
+    data to performs incremental update (see the 'updatefrom' function for
+    details).  This class can also detect stripping of the changelog or the
+    obsstore and can reset the cache in this cache (see the 'clear' function
+    for details).
     """
 
-    # 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.
+    # default key used for an empty cache
+    #
+    # The cache key covering the changesets and obsmarkers content
     #
-    # 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
+    # The cache key parts are:
+    # - tip-rev,
+    # - tip-node,
+    # - obsstore-length (nb markers),
+    # - obsstore-file-size (in bytes),
+    # - obsstore "cache key"
+    emptykey = (node.nullrev, node.nullid, 0, 0, node.nullid)
+    _cachename = None # used for error message
+
+    def __init__(self):
+        super(dualsourcecache, self).__init__()
+        self._cachekey = None
+
+    def _updatefrom(self, repo, revs, obsmarkers):
+        """override this method to update your cache data incrementally
+
+        revs:      list of new revision in the changelog
+        obsmarker: list of new obsmarkers in the obsstore
+        """
+        raise NotImplementedError
+
+    def clear(self, reset=False):
+        """invalidate the cache content
+
+        if 'reset' is passed, we detected a strip and the cache will have to be
+        recomputed.
+        """
+        # /!\ IMPORTANT /!\
+        # You must overide this method to actually
+        if reset:
+            self._cachekey = self.emptykey if reset else None
+        else:
+            self._cachekey = None
+
+    def load(self, repo):
+        """Load data from disk
+
+        Do not forget to restore the "cachekey" attribute while doing so.
+        """
+        raise NotImplementedError
+
+    # Useful public function (no need to override them)
+
+    def uptodate(self, repo):
+        """return True if the cache content is up to date False otherwise
+
+        This method can be used to detect of the cache is lagging behind new
+        data in either changelog or obsstore.
+        """
+        if self._cachekey is None:
+            self.load(repo)
+        status = self._checkkey(repo.changelog, repo.obsstore)
+        return (status is not None
+                and status[0] == self._cachekey[0] # tiprev
+                and status[1] == self._cachekey[3]) # obssize
+
+    def update(self, repo):
+        """update the cache with new repository data
+
+        The update will be incremental when possible"""
+        repo = repo.unfiltered()
+        # If we do not have any data, try loading from disk
+        if self._cachekey is None:
+            self.load(repo)
+
+        assert repo.filtername is None
+        cl = repo.changelog
+
+        upgrade = self._upgradeneeded(repo)
+        if upgrade is None:
+            return
+
+        reset, revs, obsmarkers, obskeypair = upgrade
+        if reset or self._cachekey is None:
+            repo.ui.log('evoext-cache', 'strip detected, %s cache reset\n' % self._cachename)
+            self.clear(reset=True)
+
+        starttime = timer()
+        self._updatefrom(repo, revs, obsmarkers)
+        duration = timer() - starttime
+        repo.ui.log('evoext-cache', 'updated %s in %.4f seconds (%sr, %so)\n',
+                    self._cachename, duration, len(revs), len(obsmarkers))
 
-    ### 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
+        # update the key from the new data
+        key = list(self._cachekey)
+        if revs:
+            key[0] = len(cl) - 1
+            key[1] = cl.node(key[0])
+        if obsmarkers:
+            key[2] += len(obsmarkers)
+            key[3], key[4] = obskeypair
+        self._cachekey = tuple(key)
+
+    # from here, there are internal function only
+
+    def _checkkey(self, changelog, obsstore):
+        """internal function"""
+        key = self._cachekey
+        if key is None:
+            return None
+
+        ### Is the cache valid ?
+        keytiprev, keytipnode, keyobslength, keyobssize, keyobskey = key
+        # check for changelog strip
+        tiprev = len(changelog) - 1
+        if (tiprev < keytiprev
+                or changelog.node(keytiprev) != keytipnode):
+            return None
+        # check for obsstore strip
+        obssize, obskey = obsstore.cachekey(index=keyobssize)
+        if obskey != keyobskey:
+            return None
+        if obssize != keyobssize:
+            # we want to return the obskey for the new size
+            __, obskey = obsstore.cachekey(index=obssize)
+        return tiprev, obssize, obskey
+
+    def _upgradeneeded(self, repo):
+        """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.
+        """
 
-    ### cache is valid, is there anything to update
+        # We need to ensure we use the same changelog and obsstore through the
+        # processing. Otherwise some invalidation could update the object and their
+        # content after we computed the cache key.
+        cl = repo.changelog
+        obsstore = repo.obsstore
+        key = self._cachekey
+
+        reset = False
 
-    # any new changesets ?
-    startrev = None
-    if keytiprev < tiprev:
-        startrev = keytiprev + 1
+        status = self._checkkey(cl, obsstore)
+        if status is None:
+            reset = True
+            key = self.emptykey
+            obssize, obskey = obsstore.cachekey()
+            tiprev = len(cl) - 1
+        else:
+            tiprev, obssize, obskey = status
+
+        keytiprev, keytipnode, keyobslength, keyobssize, keyobskey = key
+
+        if not reset and keytiprev == tiprev and keyobssize == obssize:
+            return None # nothing to upgrade
 
-    # any new markers
-    startidx = None
-    if keyobssize < obssize:
-        startidx = keyobslength
+        ### cache is valid, is there anything to update
+
+        # any new changesets ?
+        revs = ()
+        if keytiprev < tiprev:
+            revs = list(cl.revs(start=keytiprev + 1, stop=tiprev))
 
-    return True, startrev, startidx
+        # any new markers
+        markers = ()
+        if keyobssize < obssize:
+            # XXX Three are a small race change here. Since the obsstore might have
+            # move forward between the time we computed the cache key and we access
+            # the data. To fix this we need so "up to" argument when fetching the
+            # markers here. Otherwise we might return more markers than covered by
+            # the cache key.
+            #
+            # 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).
+            markers = markersfrom(obsstore, keyobssize, keyobslength)
 
-class obscache(object):
+        return reset, revs, markers, (obssize, obskey)
+
+
+class obscache(dualsourcecache):
     """cache the "does a rev" is the precursors of some obsmarkers data
 
     This is not directly holding the "is this revision obsolete" information,
@@ -197,16 +358,12 @@
     _filepath = 'cache/evoext-obscache-00'
     _headerformat = '>q20sQQ20s'
 
+    _cachename = 'evo-ext-obscache' # used for error message
+
     def __init__(self, repo):
+        super(obscache, self).__init__()
+        self._ondiskkey = None
         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):
@@ -215,90 +372,56 @@
         Make sure the cache has been updated to match the repository content before using it"""
         return self._data[rev]
 
-    def clear(self):
+    def clear(self, reset=False):
         """invalidate the cache content"""
-        self._cachekey = None
+        super(obscache, self).clear(reset=reset)
         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 _updatefrom(self, repo, revs, obsmarkers):
+        if revs:
+            self._updaterevs(repo, revs)
+        if obsmarkers:
+            self._updatemarkers(repo, obsmarkers)
+
+    def _updaterevs(self, repo, revs):
+        """update the cache with new revisions
+
+        Newly added changeset might be affected by obsolescence markers
+        we already have locally. So we needs to have some global
+        knowledge about the markers to handle that question.
+
+        Right now this requires parsing all markers in the obsstore. We could
+        imagine using various optimisation (eg: another cache, network
+        exchange, etc).
 
-    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)
+        A possible approach to this is to build a set of all node used as
+        precursors in `obsstore._obscandidate`. If markers are not loaded yet,
+        we could initialize it by doing a quick scan through the obsstore data
+        and filling a (pre-sized) set. Doing so would be much faster than
+        parsing all the obsmarkers since we would access less data, not create
+        any object beside the nodes and not have to decode any complex data.
 
-        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
+        For now we stick to the simpler approach of paying the
+        performance cost on new changesets.
+        """
+        node = repo.changelog.node
+        succs = repo.obsstore.successors
+        for r in revs:
+            if node(r) in succs:
+                val = 1
+            else:
+                val = 0
+            self._data.append(val)
         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 _updatemarkers(self, repo, obsmarkers):
+        """update the cache with new markers"""
+        rev = repo.changelog.nodemap.get
+        for m in obsmarkers:
+            r = rev(m[0])
+            if r is not None:
+                self._data[r] = 1
 
     def save(self, repo):
         """save the data to disk"""
@@ -319,7 +442,7 @@
 
         data = repo.vfs.tryread(self._filepath)
         if not data:
-            self._cachekey = emptykey
+            self._cachekey = self.emptykey
             self._data = bytearray()
         else:
             headersize = struct.calcsize(self._headerformat)
@@ -339,7 +462,7 @@
     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
+        # should be up to date. However a non-enabled client might have touched
         # the repository.
         #
         # Updating the cache without a lock is sloppy, so we fallback to the
@@ -348,17 +471,18 @@
         #
         # 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..
+        # will be about as fast...
         if not obscache.uptodate(repo):
             if repo.currenttransaction() is None:
-                repo.ui.log('evoext-obscache',
+                repo.ui.log('evoext-cache',
                             '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.
+                # If a transaction is open, it is worthwhile to update and use
+                # the cache, the lock prevent race and it will be written on
+                # disk when the transaction close.
                 obscache.update(repo)
         isobs = obscache.get
     for r in notpublic:
@@ -381,6 +505,7 @@
         def destroyed(self):
             if 'obsstore' in vars(self):
                 self.obsstore.obscache.clear()
+            super(obscacherepo, self).destroyed()
 
         def transaction(self, *args, **kwargs):
             tr = super(obscacherepo, self).transaction(*args, **kwargs)
@@ -391,11 +516,10 @@
                 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.
+                # 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 writing on disk.
                 self.obsstore.obscache.update(repo)
                 self.obsstore.obscache.save(repo)
 
--- a/hgext3rd/evolve/obsdiscovery.py	Thu May 18 22:53:01 2017 +0200
+++ b/hgext3rd/evolve/obsdiscovery.py	Thu May 18 23:12:52 2017 +0200
@@ -24,8 +24,10 @@
 
 import hashlib
 import heapq
+import os
 import sqlite3
 import struct
+import time
 import weakref
 
 from mercurial import (
@@ -36,6 +38,7 @@
     localrepo,
     node,
     obsolete,
+    pycompat,
     scmutil,
     setdiscovery,
     util,
@@ -46,10 +49,21 @@
 
 from . import (
     exthelper,
+    obscache,
     utility,
     stablerange,
 )
 
+# prior to hg-4.2 there are not util.timer
+if util.safehasattr(util, 'timer'):
+    timer = util.timer
+elif util.safehasattr(time, "perf_counter"):
+    timer = time.perf_counter
+elif getattr(pycompat, 'osname', os.name) == 'nt':
+    timer = time.clock
+else:
+    timer = time.time
+
 _pack = struct.pack
 _unpack = struct.unpack
 _calcsize = struct.calcsize
@@ -214,6 +228,7 @@
                      initialsamplesize=100,
                      fullsamplesize=200):
     missing = set()
+    starttime = timer()
 
     heads = local.revs('heads(%ld)', probeset)
     local.stablerange.warmup(local)
@@ -241,6 +256,7 @@
         entry = (h, 0)
         addentry(entry)
 
+    local.obsstore.rangeobshashcache.update(local)
     querycount = 0
     ui.progress(_("comparing obsmarker with other"), querycount)
     overflow = []
@@ -300,6 +316,12 @@
         ui.progress(_("comparing obsmarker with other"), querycount)
     ui.progress(_("comparing obsmarker with other"), None)
     local.obsstore.rangeobshashcache.save(local)
+    duration = timer() - starttime
+    logmsg = ('obsdiscovery, %d/%d mismatch'
+              ' - %d obshashrange queries in %.4f seconds\n')
+    logmsg %= (len(missing), len(probeset), querycount, duration)
+    ui.log('evoext-obsdiscovery', logmsg)
+    ui.debug(logmsg)
     return sorted(missing)
 
 def _queryrange(ui, repo, remote, allentries):
@@ -342,6 +364,7 @@
     linetemplate = '%12d %12s %12d %12d %12d %12s\n'
     headertemplate = linetemplate.replace('d', 's')
     ui.status(headertemplate % headers)
+    repo.obsstore.rangeobshashcache.update(repo)
     for r in ranges:
         d = (r[0],
              s(cl.node(r[0])),
@@ -392,10 +415,11 @@
 
 _sqliteschema = [
     """CREATE TABLE meta(schemaversion INTEGER NOT NULL,
+                         tiprev        INTEGER NOT NULL,
+                         tipnode       BLOB    NOT NULL,
                          nbobsmarker   INTEGER NOT NULL,
-                         obstipdata    BLOB    NOT NULL,
-                         tiprev        INTEGER NOT NULL,
-                         tipnode       BLOB    NOT NULL
+                         obssize       BLOB    NOT NULL,
+                         obskey        BLOB    NOT NULL
                         );""",
     """CREATE TABLE obshashrange(rev     INTEGER NOT NULL,
                                  idx     INTEGER NOT NULL,
@@ -404,53 +428,132 @@
     "CREATE INDEX range_index ON obshashrange(rev, idx);",
 ]
 _queryexist = "SELECT name FROM sqlite_master WHERE type='table' AND name='meta';"
-_newmeta = """INSERT INTO meta (schemaversion, nbobsmarker, obstipdata, tiprev, tipnode)
-            VALUES (?,?,?,?,?);"""
+_clearmeta = """DELETE FROM meta;"""
+_newmeta = """INSERT INTO meta (schemaversion, tiprev, tipnode, nbobsmarker, obssize, obskey)
+            VALUES (?,?,?,?,?,?);"""
 _updateobshash = "INSERT INTO obshashrange(rev, idx, obshash) VALUES (?,?,?);"
-_querymeta = "SELECT schemaversion, nbobsmarker, obstipdata, tiprev, tipnode FROM meta;"
+_querymeta = "SELECT schemaversion, tiprev, tipnode, nbobsmarker, obssize, obskey FROM meta;"
 _queryobshash = "SELECT obshash FROM obshashrange WHERE (rev = ? AND idx = ?);"
 
-class _obshashcache(dict):
+_reset = "DELETE FROM obshashrange;"
+
+class _obshashcache(obscache.dualsourcecache):
 
-    _schemaversion = 0
+    _schemaversion = 1
+
+    _cachename = 'evo-ext-obshashrange' # used for error message
 
     def __init__(self, repo):
         super(_obshashcache, self).__init__()
-        self._path = repo.vfs.join('cache/evoext_obshashrange_v0.sqlite')
+        self._vfs = repo.vfs
+        self._path = repo.vfs.join('cache/evoext_obshashrange_v1.sqlite')
         self._new = set()
         self._valid = True
         self._repo = weakref.ref(repo.unfiltered())
         # cache status
         self._ondiskcachekey = None
+        self._data = {}
 
-    def clear(self):
-        self._valid = False
-        super(_obshashcache, self).clear()
+    def clear(self, reset=False):
+        super(_obshashcache, self).clear(reset=reset)
+        self._data.clear()
         self._new.clear()
+        if reset:
+            self._valid = False
+        if '_con' in vars(self):
+            del self._con
 
     def get(self, rangeid):
-        value = super(_obshashcache, self).get(rangeid)
+        # revision should be covered by the tiprev
+        #
+        # XXX there are issue with cache warming, we hack around it for now
+        if not getattr(self, '_updating', False):
+            if self._cachekey[0] < rangeid[0]:
+                msg = ('using unwarmed obshashrangecache (%s %s)'
+                       % (rangeid[0], self._cachekey[0]))
+                raise error.ProgrammingError(msg)
+
+        value = self._data.get(rangeid)
         if value is None and self._con is not None:
             nrange = (rangeid[0], rangeid[1])
             obshash = self._con.execute(_queryobshash, nrange).fetchone()
             if obshash is not None:
                 value = obshash[0]
+            self._data[rangeid] = value
         return value
 
     def __setitem__(self, rangeid, obshash):
         self._new.add(rangeid)
-        super(_obshashcache, self).__setitem__(rangeid, obshash)
+        self._data[rangeid] = obshash
+
+    def _updatefrom(self, repo, revs, obsmarkers):
+        """override this method to update your cache data incrementally
+
+        revs:      list of new revision in the changelog
+        obsmarker: list of new obsmarkers in the obsstore
+        """
+        # XXX for now, we'll not actually update the cache, but we'll be
+        # smarter at invalidating it.
+        #
+        # 1) new revisions does not get their entry updated (not update)
+        # 2) if we detect markers affecting non-new revision we reset the cache
+
+        self._updating = True
+
+        setrevs = set(revs)
+        rev = repo.changelog.nodemap.get
+        # if we have a new markers affecting a node already covered by the
+        # cache, we must abort.
+        affected = set()
+        for m in obsmarkers:
+            # check successors and parent
+            for l in (m[1], m[5]):
+                if l is None:
+                    continue
+                for p in l:
+                    r = rev(p)
+                    if r is not None and r not in setrevs:
+                        # XXX should check < min(setrevs) or tiprevs
+                        affected.add(r)
 
-    def _cachekey(self, repo):
-        # XXX for now the cache is very volatile, but this is still a win
-        nbobsmarker = len(repo.obsstore._all)
-        if nbobsmarker:
-            tipdata = obsolete._fm1encodeonemarker(repo.obsstore._all[-1])
-        else:
-            tipdata = node.nullid
-        tiprev = len(repo.changelog) - 1
-        tipnode = repo.changelog.node(tiprev)
-        return (self._schemaversion, nbobsmarker, tipdata, tiprev, tipnode)
+        if affected:
+            repo.ui.log('evoext-cache', 'obshashcache reset - '
+                        'new markers affect cached ranges\n')
+            # XXX the current reset is too strong we could just drop the affected range
+            con = self._con
+            if con is not None:
+                con.execute(_reset)
+            # rewarm the whole cache
+            stop = self._cachekey[0] # tiprev
+            if revs:
+                stop = max(revs)
+            if 0 <= stop:
+                revs = repo.changelog.revs(stop=stop)
+
+        # warm the cache for the new revs
+        for r in revs:
+            _obshashrange(repo, (r, 0))
+
+        del self._updating
+
+    @property
+    def _fullcachekey(self):
+        return (self._schemaversion, ) + self._cachekey
+
+    def load(self, repo):
+        if self._con is None:
+            self._cachekey = self.emptykey
+            self._ondiskcachekey = self.emptykey
+        assert self._cachekey is not None
+
+    def _db(self):
+        try:
+            util.makedirs(self._vfs.dirname(self._path))
+        except OSError:
+            return None
+        con = sqlite3.connect(self._path)
+        con.text_factory = str
+        return con
 
     @util.propertycache
     def _con(self):
@@ -459,25 +562,27 @@
         repo = self._repo()
         if repo is None:
             return None
-        cachekey = self._cachekey(repo)
-        con = sqlite3.connect(self._path)
-        con.text_factory = str
+        con = self._db()
+        if con is None:
+            return None
         cur = con.execute(_queryexist)
         if cur.fetchone() is None:
             self._valid = False
             return None
         meta = con.execute(_querymeta).fetchone()
-        if meta != cachekey:
+        if meta is None or meta[0] != self._schemaversion:
             self._valid = False
             return None
-        self._ondiskcachekey = meta
+        self._cachekey = self._ondiskcachekey = meta[1:]
         return con
 
     def save(self, repo):
+        if self._cachekey is None:
+            return
+        if self._cachekey == self._ondiskcachekey and not self._new:
+            return
         repo = repo.unfiltered()
         try:
-            if not self._new:
-                return
             with repo.lock():
                 self._save(repo)
         except error.LockError:
@@ -493,32 +598,38 @@
             if '_con' in vars(self):
                 del self._con
 
-            con = sqlite3.connect(self._path)
-            con.text_factory = str
+            con = self._db()
+            if con is None:
+                repo.ui.log('evoext-cache', 'unable to write obshashrange cache'
+                            ' - cannot create database')
+                return
             with con:
                 for req in _sqliteschema:
                     con.execute(req)
 
-                con.execute(_newmeta, self._cachekey(repo))
+                con.execute(_newmeta, self._fullcachekey)
         else:
             con = self._con
             if self._ondiskcachekey is not None:
                 meta = con.execute(_querymeta).fetchone()
-                if meta != self._ondiskcachekey:
+                if meta[1:] != self._ondiskcachekey:
                     # drifting is currently an issue because this means another
                     # process might have already added the cache line we are about
                     # to add. This will confuse sqlite
                     msg = _('obshashrange cache: skipping write, '
                             'database drifted under my feet\n')
-                    data = (meta[2], meta[1], self._ondisktiprev, self._ondisktipnode)
+                    data = (meta[2], meta[1], self._ondiskcachekey[0], self._ondiskcachekey[1])
                     repo.ui.warn(msg)
-        data = ((rangeid[0], rangeid[1], self[rangeid]) for rangeid in self._new)
+                    return
+        data = ((rangeid[0], rangeid[1], self.get(rangeid)) for rangeid in self._new)
         con.executemany(_updateobshash, data)
-        cachekey = self._cachekey(repo)
+        cachekey = self._fullcachekey
+        con.execute(_clearmeta) # remove the older entry
         con.execute(_newmeta, cachekey)
         con.commit()
         self._new.clear()
-        self._ondiskcachekey = cachekey
+        self._ondiskcachekey = self._cachekey
+        self._valid = True
 
 @eh.wrapfunction(obsolete.obsstore, '_addmarkers')
 def _addmarkers(orig, obsstore, *args, **kwargs):
@@ -549,10 +660,31 @@
     class obshashrepo(repo.__class__):
         @localrepo.unfilteredmethod
         def destroyed(self):
-            if 'stablerange' in vars(self):
-                del self.stablerange
+            if 'obsstore' in vars(self):
+                self.obsstore.rangeobshashcache.clear()
             super(obshashrepo, self).destroyed()
 
+        def transaction(self, *args, **kwargs):
+            tr = super(obshashrepo, self).transaction(*args, **kwargs)
+            reporef = weakref.ref(self)
+
+            def _warmcache(tr):
+                repo = reporef()
+                if repo is None:
+                    return
+                if not repo.ui.configbool('experimental', 'obshashrange', False):
+                    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 writing on disk.
+                self.obsstore.rangeobshashcache.update(repo)
+                self.obsstore.rangeobshashcache.save(repo)
+
+            tr.addpostclose('warmcache-20-obscacherange', _warmcache)
+            return tr
+
     repo.__class__ = obshashrepo
 
 ### wire protocol commands
@@ -570,6 +702,7 @@
         if maxrev is not None:
             repo.stablerange.warmup(repo, upto=maxrev)
     result = []
+    repo.obsstore.rangeobshashcache.update(repo)
     for r in ranges:
         if r[0] is None:
             result.append(node.wdirid)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/obshistory.py	Thu May 18 23:12:52 2017 +0200
@@ -0,0 +1,357 @@
+# Code dedicated to display and exploration of the obsolescence history
+#
+# This module content aims at being upstreamed enventually.
+#
+# Copyright 2017 Octobus SAS <contact@octobus.net>
+#
+# 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 (
+    cmdutil,
+    commands,
+    error,
+    graphmod,
+    node as nodemod,
+    scmutil,
+)
+
+from mercurial.i18n import _
+
+from . import (
+    exthelper,
+)
+
+eh = exthelper.exthelper()
+
+@eh.command(
+    'olog',
+    [('G', 'graph', True, _("show the revision DAG")),
+     ('r', 'rev', [], _('show the specified revision or revset'), _('REV'))
+    ] + commands.formatteropts,
+    _('hg olog [OPTION]... [REV]'))
+def cmdobshistory(ui, repo, *revs, **opts):
+    """show the obsolescence history of the specified revisions.
+
+    If no revision range is specified, we display the log for the current
+    working copy parent.
+
+    By default this command prints the selected revisions and all its
+    precursors. For precursors pointing on existing revisions in the repository,
+    it will display revisions node id, revision number and the first line of the
+    description. For precursors pointing on non existing revisions in the
+    repository (that can happen when exchanging obsolescence-markers), display
+    only the node id.
+
+    In both case, for each node, its obsolescence marker will be displayed with
+    the obsolescence operation (rewritten or pruned) in addition of the user and
+    date of the operation.
+
+    The output is a graph by default but can deactivated with the option '--no-
+    graph'.
+
+    'o' is a changeset, '@' is a working directory parent, 'x' is obsolete,
+    and '+' represents a fork where the changeset from the lines below is a
+    parent of the 'o' merge on the same line.
+
+    Paths in the DAG are represented with '|', '/' and so forth.
+
+    Returns 0 on success.
+    """
+    revs = list(revs) + opts['rev']
+    if not revs:
+        revs = ['.']
+    revs = scmutil.revrange(repo, revs)
+
+    if opts['graph']:
+        return _debugobshistorygraph(ui, repo, revs, opts)
+
+    fm = ui.formatter('debugobshistory', opts)
+    revs.reverse()
+    _debugobshistorysingle(fm, repo, revs)
+
+    fm.end()
+
+class obsmarker_printer(cmdutil.changeset_printer):
+    """show (available) information about a node
+
+    We display the node, description (if available) and various information
+    about obsolescence markers affecting it"""
+
+    def show(self, ctx, copies=None, matchfn=None, **props):
+        if self.buffered:
+            self.ui.pushbuffer(labeled=True)
+
+            changenode = ctx.node()
+
+            fm = self.ui.formatter('debugobshistory', props)
+            _debugobshistorydisplaynode(fm, self.repo, changenode)
+
+            succs = self.repo.obsstore.successors.get(changenode, ())
+
+            markerfm = fm.nested("debugobshistory.markers")
+            for successor in sorted(succs):
+                _debugobshistorydisplaymarker(markerfm, self.repo, successor)
+            markerfm.end()
+
+            markerfm.plain('\n')
+
+            self.hunk[ctx.node()] = self.ui.popbuffer()
+        else:
+            ### graph output is buffered only
+            msg = 'cannot be used outside of the graphlog (yet)'
+            raise error.ProgrammingError(msg)
+
+    def flush(self, ctx):
+        ''' changeset_printer has some logic around buffering data
+        in self.headers that we don't use
+        '''
+        pass
+
+class missingchangectx(object):
+    ''' a minimal object mimicking changectx for change contexts
+    references by obs markers but not available locally '''
+
+    def __init__(self, repo, nodeid):
+        self._repo = repo
+        self._node = nodeid
+
+    def node(self):
+        return self._node
+
+    def obsolete(self):
+        # If we don't have it locally, it's obsolete
+        return True
+
+def cyclic(graph):
+    """Return True if the directed graph has a cycle.
+    The graph must be represented as a dictionary mapping vertices to
+    iterables of neighbouring vertices. For example:
+
+    >>> cyclic({1: (2,), 2: (3,), 3: (1,)})
+    True
+    >>> cyclic({1: (2,), 2: (3,), 3: (4,)})
+    False
+
+    Taken from: https://codereview.stackexchange.com/a/86067
+
+    """
+    visited = set()
+    o = object()
+    path = [o]
+    path_set = set(path)
+    stack = [iter(graph)]
+    while stack:
+        for v in sorted(stack[-1]):
+            if v in path_set:
+                path_set.remove(o)
+                return path_set
+            elif v not in visited:
+                visited.add(v)
+                path.append(v)
+                path_set.add(v)
+                stack.append(iter(graph.get(v, ())))
+                break
+        else:
+            path_set.remove(path.pop())
+            stack.pop()
+    return False
+
+def _obshistorywalker(repo, revs):
+    """ Directly inspired by graphmod.dagwalker,
+    walk the obs marker tree and yield
+    (id, CHANGESET, ctx, [parentinfo]) tuples
+    """
+
+    # Get the list of nodes and links between them
+    candidates, nodesucc, nodeprec = _obshistorywalker_links(repo, revs)
+
+    # Shown, set of nodes presents in items
+    shown = set()
+
+    def isvalidcandidate(candidate):
+        """ Function to filter candidates, check the candidate succ are
+        in shown set
+        """
+        return nodesucc.get(candidate, set()).issubset(shown)
+
+    # While we have some nodes to show
+    while candidates:
+
+        # Filter out candidates, returns only nodes with all their successors
+        # already shown
+        validcandidates = filter(isvalidcandidate, candidates)
+
+        # If we likely have a cycle
+        if not validcandidates:
+            cycle = cyclic(nodesucc)
+            assert cycle
+
+            # Then choose a random node from the cycle
+            breaknode = sorted(cycle)[0]
+            # And display it by force
+            repo.ui.debug('obs-cycle detected, forcing display of %s\n'
+                          % nodemod.short(breaknode))
+            validcandidates = [breaknode]
+
+        # Display all valid candidates
+        for cand in sorted(validcandidates):
+            # Remove candidate from candidates set
+            candidates.remove(cand)
+            # And remove it from nodesucc in case of future cycle detected
+            try:
+                del nodesucc[cand]
+            except KeyError:
+                pass
+
+            shown.add(cand)
+
+            # Add the right changectx class
+            if cand in repo:
+                changectx = repo[cand]
+            else:
+                changectx = missingchangectx(repo, cand)
+
+            childrens = [(graphmod.PARENT, x) for x in nodeprec.get(cand, ())]
+            yield (cand, 'M', changectx, childrens)
+
+def _obshistorywalker_links(repo, revs):
+    """ Iterate the obs history tree starting from revs, traversing
+    each revision precursors recursively.
+    Return a tuple of:
+    - The list of node crossed
+    - The dictionnary of each node successors, values are a set
+    - The dictionnary of each node precursors, values are a list
+    """
+    precursors = repo.obsstore.precursors
+    nodec = repo.changelog.node
+
+    # Parents, set of parents nodes seen during walking the graph for node
+    nodesucc = dict()
+    # Childrens
+    nodeprec = dict()
+
+    nodes = [nodec(r) for r in revs]
+    seen = set(nodes)
+
+    # Iterate on each node
+    while nodes:
+        node = nodes.pop()
+
+        precs = precursors.get(node, ())
+
+        nodeprec[node] = []
+
+        for prec in sorted(precs):
+            precnode = prec[0]
+
+            # Mark node as prec successor
+            nodesucc.setdefault(precnode, set()).add(node)
+
+            # Mark precnode as node precursor
+            nodeprec[node].append(precnode)
+
+            # Add prec for future processing if not node already processed
+            if precnode not in seen:
+                seen.add(precnode)
+                nodes.append(precnode)
+
+    return sorted(seen), nodesucc, nodeprec
+
+def _debugobshistorygraph(ui, repo, revs, opts):
+    displayer = obsmarker_printer(ui, repo.unfiltered(), None, opts, buffered=True)
+    edges = graphmod.asciiedges
+    cmdutil.displaygraph(ui, repo, _obshistorywalker(repo.unfiltered(), revs), displayer, edges)
+
+def _debugobshistorysingle(fm, repo, revs):
+    """ Display the obsolescence history for a single revision
+    """
+    precursors = repo.obsstore.precursors
+    successors = repo.obsstore.successors
+    nodec = repo.changelog.node
+    nodes = [nodec(r) for r in revs]
+
+    seen = set(nodes)
+
+    while nodes:
+        ctxnode = nodes.pop()
+
+        _debugobshistorydisplaynode(fm, repo, ctxnode)
+
+        succs = successors.get(ctxnode, ())
+
+        markerfm = fm.nested("debugobshistory.markers")
+        for successor in sorted(succs):
+            _debugobshistorydisplaymarker(markerfm, repo, successor)
+        markerfm.end()
+
+        precs = precursors.get(ctxnode, ())
+        for p in sorted(precs):
+            # Only show nodes once
+            if p[0] not in seen:
+                seen.add(p[0])
+                nodes.append(p[0])
+
+def _debugobshistorydisplaynode(fm, repo, node):
+    if node in repo.unfiltered():
+        _debugobshistorydisplayctx(fm, repo.unfiltered()[node])
+    else:
+        _debugobshistorydisplaymissingctx(fm, node)
+
+def _debugobshistorydisplayctx(fm, ctx):
+    shortdescription = ctx.description().splitlines()[0]
+
+    fm.startitem()
+    fm.write('debugobshistory.node', '%s', str(ctx),
+             label="evolve.node")
+    fm.plain(' ')
+
+    fm.write('debugobshistory.rev', '(%d)', int(ctx),
+             label="evolve.rev")
+    fm.plain(' ')
+
+    fm.write('debugobshistory.shortdescription', '%s', shortdescription,
+             label="evolve.short_description")
+    fm.plain('\n')
+
+def _debugobshistorydisplaymissingctx(fm, nodewithoutctx):
+    hexnode = nodemod.short(nodewithoutctx)
+    fm.startitem()
+    fm.write('debugobshistory.node', '%s', hexnode,
+             label="evolve.node evolve.missing_change_ctx")
+    fm.plain('\n')
+
+def _debugobshistorydisplaymarker(fm, repo, marker):
+    succnodes = marker[1]
+    date = marker[4]
+    metadata = dict(marker[3])
+
+    fm.startitem()
+    fm.plain('  ')
+
+    # Detect pruned revisions
+    if len(succnodes) == 0:
+        verb = 'pruned'
+    else:
+        verb = 'rewritten'
+
+    fm.write('debugobshistory.verb', '%s', verb,
+             label="evolve.verb")
+    fm.plain(' by ')
+
+    fm.write('debugobshistory.marker_user', '%s', metadata['user'],
+             label="evolve.user")
+    fm.plain(' ')
+
+    fm.write('debugobshistory.marker_date', '(%s)', fm.formatdate(date),
+             label="evolve.date")
+
+    if len(succnodes) > 0:
+        fm.plain(' as ')
+
+        shortsnodes = (nodemod.short(succnode) for succnode in sorted(succnodes))
+        nodes = fm.formatlist(shortsnodes, 'debugobshistory.succnodes', sep=', ')
+        fm.write('debugobshistory.succnodes', '%s', nodes,
+                 label="evolve.node")
+
+    fm.plain("\n")
--- a/hgext3rd/evolve/serveronly.py	Thu May 18 22:53:01 2017 +0200
+++ b/hgext3rd/evolve/serveronly.py	Thu May 18 23:12:52 2017 +0200
@@ -17,6 +17,7 @@
     from . import (
         exthelper,
         metadata,
+        obscache,
         obsexchange,
     )
 except ValueError as exc:
@@ -27,6 +28,7 @@
     from evolve import (
         exthelper,
         metadata,
+        obscache,
         obsexchange,
     )
 
@@ -36,6 +38,7 @@
 buglink = metadata.buglink
 
 eh = exthelper.exthelper()
+eh.merge(obscache.eh)
 eh.merge(obsexchange.eh)
 uisetup = eh.final_uisetup
 extsetup = eh.final_extsetup
--- a/hgext3rd/evolve/stablerange.py	Thu May 18 22:53:01 2017 +0200
+++ b/hgext3rd/evolve/stablerange.py	Thu May 18 23:12:52 2017 +0200
@@ -10,7 +10,9 @@
 import collections
 import heapq
 import math
+import os
 import sqlite3
+import time
 import weakref
 
 from mercurial import (
@@ -19,6 +21,7 @@
     error,
     localrepo,
     node as nodemod,
+    pycompat,
     scmutil,
     util,
 )
@@ -31,6 +34,16 @@
 
 eh = exthelper.exthelper()
 
+# prior to hg-4.2 there are not util.timer
+if util.safehasattr(util, 'timer'):
+    timer = util.timer
+elif util.safehasattr(time, "perf_counter"):
+    timer = time.perf_counter
+elif getattr(pycompat, 'osname', os.name) == 'nt':
+    timer = time.clock
+else:
+    timer = time.time
+
 ##################################
 ### Stable topological sorting ###
 ##################################
@@ -263,6 +276,7 @@
         #
         # we use the revnumber as an approximation for depth
         ui = repo.ui
+        starttime = timer()
 
         if upto is None:
             upto = len(cl) - 1
@@ -308,6 +322,10 @@
         self._tiprev = upto
         self._tipnode = cl.node(upto)
 
+        duration = timer() - starttime
+        repo.ui.log('evoext-cache', 'updated stablerange cache in %.4f seconds\n',
+                    duration)
+
     def depthrev(self, repo, rev):
         repo = repo.unfiltered()
         cl = repo.changelog
@@ -714,6 +732,7 @@
 
     def __init__(self, repo):
         super(sqlstablerange, self).__init__()
+        self._vfs = repo.vfs
         self._path = repo.vfs.join('cache/evoext_stablerange_v0.sqlite')
         self._cl = repo.unfiltered().changelog # (okay to keep an old one)
         self._ondisktiprev = None
@@ -777,10 +796,20 @@
         self._loaddepth()
         return super(sqlstablerange, self)._inheritancepoint(*args, **kwargs)
 
+    def _db(self):
+        try:
+            util.makedirs(self._vfs.dirname(self._path))
+        except OSError:
+            return None
+        con = sqlite3.connect(self._path)
+        con.text_factory = str
+        return con
+
     @util.propertycache
     def _con(self):
-        con = sqlite3.connect(self._path)
-        con.text_factory = str
+        con = self._db()
+        if con is None:
+            return None
         cur = con.execute(_queryexist)
         if cur.fetchone() is None:
             return None
@@ -810,8 +839,9 @@
             if '_con' in vars(self):
                 del self._con
 
-            con = sqlite3.connect(self._path)
-            con.text_factory = str
+            con = self._db()
+            if con is None:
+                return
             with con:
                 for req in _sqliteschema:
                     con.execute(req)
@@ -902,7 +932,7 @@
                     # new nodes !
                     repo.stablerange.warmup(repo)
 
-            tr.addpostclose('warmcache-stablerange', _warmcache)
+            tr.addpostclose('warmcache-10-stablerange', _warmcache)
             return tr
 
     repo.__class__ = stablerangerepo
--- a/hgext3rd/topic/__init__.py	Thu May 18 22:53:01 2017 +0200
+++ b/hgext3rd/topic/__init__.py	Thu May 18 23:12:52 2017 +0200
@@ -100,9 +100,13 @@
               'topic.stack.summary.behindcount': 'cyan',
               'topic.stack.summary.behinderror': 'red',
               'topic.stack.summary.headcount.multiple': 'yellow',
+              # default color to help log output and thg
+              # (first pick I could think off, update as needed
+              'log.topic': 'green_background',
+              'topic.active': 'green',
              }
 
-testedwith = '3.9'
+testedwith = '4.0.2 4.1.3 4.2'
 
 def _contexttopic(self):
     return self.extra().get(constants.extrakey, '')
@@ -157,6 +161,10 @@
     if not isinstance(repo, localrepo.localrepository):
         return # this can be a peer in the ssh case (puzzling)
 
+    if repo.ui.config('experimental', 'thg.displaynames', None) is None:
+        repo.ui.setconfig('experimental', 'thg.displaynames', 'topics',
+                          source='topic-extension')
+
     class topicrepo(repo.__class__):
 
         def _restrictcapabilities(self, caps):
--- a/hgext3rd/topic/stack.py	Thu May 18 22:53:01 2017 +0200
+++ b/hgext3rd/topic/stack.py	Thu May 18 23:12:52 2017 +0200
@@ -15,6 +15,12 @@
     trevs = repo.revs("topic(%s) - obsolete()", topic)
     return _orderrevs(repo, trevs)
 
+def labelsgen(prefix, labelssuffix):
+    """ Takes a label prefix and a list of suffixes. Returns a string of the prefix
+    formatted with each suffix separated with a space.
+    """
+    return ' '.join(prefix % suffix for suffix in labelssuffix)
+
 def showstack(ui, repo, topic, opts):
     fm = ui.formatter('topicstack', opts)
     prev = None
@@ -59,35 +65,44 @@
 
     # super crude initial version
     for idx, isentry, ctx in entries[::-1]:
+
+        states = []
+        iscurrentrevision = repo.revs('%d and parents()', ctx.rev())
+
+        if iscurrentrevision:
+            states.append('current')
+
         if not isentry:
             symbol = '^'
-            state = 'base'
-        elif repo.revs('%d and parents()', ctx.rev()):
+            # "base" is kind of a "ghost" entry
+            # skip other label for them (no current, no unstable)
+            states = ['base']
+        elif iscurrentrevision:
             symbol = '@'
-            state = 'current'
         elif repo.revs('%d and unstable()', ctx.rev()):
             symbol = '$'
-            state = 'unstable'
+            states.append('unstable')
         else:
             symbol = ':'
-            state = 'clean'
+            states.append('clean')
         fm.startitem()
         fm.data(isentry=isentry)
+
         if idx is None:
             fm.plain('  ')
         else:
             fm.write('topic.stack.index', 't%d', idx,
-                     label='topic.stack.index topic.stack.index.%s' % state)
+                     label='topic.stack.index ' + labelsgen('topic.stack.index.%s', states))
         fm.write('topic.stack.state.symbol', '%s', symbol,
-                 label='topic.stack.state topic.stack.state.%s' % state)
+                 label='topic.stack.state ' + labelsgen('topic.stack.state.%s', states))
         fm.plain(' ')
         fm.write('topic.stack.desc', '%s', ctx.description().splitlines()[0],
-                 label='topic.stack.desc topic.stack.desc.%s' % state)
-        fm.condwrite(state != 'clean' and idx is not None, 'topic.stack.state',
-                     ' (%s)', state,
-                     label='topic.stack.state topic.stack.state.%s' % state)
+                 label='topic.stack.desc ' + labelsgen('topic.stack.desc.%s', states))
+        fm.condwrite(states != ['clean'] and idx is not None, 'topic.stack.state',
+                     ' (%s)', fm.formatlist(states, 'topic.stack.state'),
+                     label='topic.stack.state ' + labelsgen('topic.stack.state.%s', states))
         fm.plain('\n')
-        fm.end()
+    fm.end()
 
 def stackdata(repo, topic):
     """get various data about a stack
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-check-commit.t	Thu May 18 23:12:52 2017 +0200
@@ -0,0 +1,22 @@
+#require test-repo
+
+Enable obsolescence to avoid the warning issue when obsmarker are found
+
+  $ cat << EOF >> $HGRCPATH
+  > [experimental]
+  > evolution=all
+  > EOF
+
+Go back in the hg repo
+
+  $ cd $TESTDIR/..
+
+  $ for node in `hg log --rev 'not public() and ::. and not desc("# no-check-commit")' --template '{node|short}\n'`; do
+  >    hg export $node | ${RUNTESTDIR}/../contrib/check-commit > ${TESTTMP}/check-commit.out
+  >    if [ $? -ne 0 ]; then
+  >        echo "Revision $node does not comply with rules"
+  >        echo '------------------------------------------------------'
+  >        cat ${TESTTMP}/check-commit.out
+  >        echo
+  >   fi
+  > done
--- a/tests/test-discovery-obshashrange.t	Thu May 18 22:53:01 2017 +0200
+++ b/tests/test-discovery-obshashrange.t	Thu May 18 23:12:52 2017 +0200
@@ -6,6 +6,9 @@
   $ cat << EOF >> $HGRCPATH
   > [extensions]
   > hgext3rd.evolve =
+  > blackbox =
+  > [defaults]
+  > blackbox = -l 100
   > [experimental]
   > obshashrange=1
   > verbose-obsolescence-exchange=1
@@ -17,7 +20,7 @@
   > EOF
 
   $ getid() {
-  >     hg log --hidden --template '{node}\n' --rev "$1"
+  >     hg log --hidden --template '{node}\n' --rev "$1" --config 'extensions.blackbox=!'
   > }
 
   $ hg init server
@@ -27,6 +30,31 @@
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ cd server
   $ hg debugbuilddag '.+7'
+  $ hg blackbox
+  * @0000000000000000000000000000000000000000 (*)> serve --stdio (glob)
+  * @0000000000000000000000000000000000000000 (*)> -R server serve --stdio exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugbuilddag .+7 (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 1 nodes (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 1 nodes (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 1 nodes (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 1 nodes (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 1 nodes (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 1 nodes (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 1 nodes (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 1 nodes (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (8r, 0o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (8r, 0o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugbuilddag .+7 exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> blackbox (glob)
+  $ rm .hg/blackbox.log
   $ hg log -G
   o  7 4de32a90b66c r7 tip
   |
@@ -57,6 +85,45 @@
   dddddddddddddddddddddddddddddddddddddddd c8d03c1b5e94af74b772900c58259d2e08917735 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 4de32a90b66cd083ebf3c00b41277aa7abca51dd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
 
+  $ hg blackbox
+  * @0000000000000000000000000000000000000000 (*)> log -G (glob)
+  * @0000000000000000000000000000000000000000 (*)> writing .hg/cache/tags2-visible with 0 tags (glob)
+  * @0000000000000000000000000000000000000000 (*)> log -G exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd (glob)
+  * @0000000000000000000000000000000000000000 (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @0000000000000000000000000000000000000000 (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 (glob)
+  * @0000000000000000000000000000000000000000 (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @0000000000000000000000000000000000000000 (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d (glob)
+  * @0000000000000000000000000000000000000000 (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @0000000000000000000000000000000000000000 (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete dddddddddddddddddddddddddddddddddddddddd c8d03c1b5e94af74b772900c58259d2e08917735 (glob)
+  * @0000000000000000000000000000000000000000 (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @0000000000000000000000000000000000000000 (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete dddddddddddddddddddddddddddddddddddddddd c8d03c1b5e94af74b772900c58259d2e08917735 exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 4de32a90b66cd083ebf3c00b41277aa7abca51dd (glob)
+  * @0000000000000000000000000000000000000000 (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @0000000000000000000000000000000000000000 (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 4de32a90b66cd083ebf3c00b41277aa7abca51dd exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete (glob)
+  * @0000000000000000000000000000000000000000 (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> blackbox (glob)
+  $ rm .hg/blackbox.log
   $ hg debugobshashrange --subranges --rev tip
            rev         node        index         size        depth      obshash
              7 4de32a90b66c            0            8            8 38d1e7ad86ea
@@ -88,10 +155,30 @@
   added 5 changesets with 0 changes to 0 files
   3 new obsolescence markers
   (run 'hg update' to get a working copy)
+  $ hg -R ../server blackbox
+  * @0000000000000000000000000000000000000000 (*)> debugobshashrange --subranges --rev tip (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated stablerange cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobshashrange --subranges --rev tip exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> serve --stdio (glob)
+  * @0000000000000000000000000000000000000000 (*)> -R server serve --stdio exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> blackbox (glob)
+  $ rm ../server/.hg/blackbox.log
   $ hg -R ../server/ debugobsolete --rev ::4 | sort
   aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
+  $ rm ../server/.hg/blackbox.log
+  $ hg blackbox
+  * @0000000000000000000000000000000000000000 (*)> pull --rev 4 (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 1 nodes (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated stablerange cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (5r, 3o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (5r, 3o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> 5 incoming changes - new heads: bebd167eb94d (glob)
+  * @0000000000000000000000000000000000000000 (*)> pull --rev 4 exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> blackbox (glob)
+  $ rm .hg/blackbox.log
   $ hg debugobsolete | sort
   aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
@@ -106,17 +193,75 @@
   $ hg add foo
   $ hg commit -m foo
   $ hg debugobsolete ffffffffffffffffffffffffffffffffffffffff `getid '.'`
-  $ hg push -f
+  $ hg push -f --debug
   pushing to ssh://user@dummy/server
+  running python "*/dummyssh" user@dummy 'hg -R server serve --stdio' (glob)
+  sending hello command
+  sending between command
+  remote: 516
+  remote: capabilities: _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_obshashrange_v0 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch branchmap bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1%2Csha512%0Aerror%3Dabort%2Cunsupportedcontent%2Cpushraced%2Cpushkey%0Ahgtagsfnodes%0Alistkeys%0Aobsmarkers%3DV0%2CV1%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps changegroupsubset getbundle known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
+  remote: 1
+  preparing listkeys for "phases"
+  sending listkeys command
+  received listkey for "phases": 58 bytes
+  query 1; heads
+  sending batch command
   searching for changes
+  taking quick initial sample
+  query 2; still undecided: 5, sample size is: 5
+  sending known command
+  2 total queries
+  preparing listkeys for "phases"
+  sending listkeys command
+  received listkey for "phases": 58 bytes
+  preparing listkeys for "namespaces"
+  sending listkeys command
+  received listkey for "namespaces": 40 bytes
   OBSEXC: computing relevant nodes
   OBSEXC: looking for common markers in 6 nodes
+  query 0; add more sample (target 100, current 1)
+  query 0; sample size is 9, largest range 5
+  sending evoext_obshashrange_v0 command
+  obsdiscovery, 0/5 mismatch - 1 obshashrange queries in *.???? seconds (glob)
   OBSEXC: computing markers relevant to 1 nodes
+  checking for updated bookmarks
+  preparing listkeys for "bookmarks"
+  sending listkeys command
+  received listkey for "bookmarks": 0 bytes
+  1 changesets found
+  list of changesets:
+  45f8b879de922f6a6e620ba04205730335b6fc7e
+  sending unbundle command
+  bundle2-output-bundle: "HG20", 4 parts total
+  bundle2-output-part: "replycaps" 172 bytes payload
+  bundle2-output-part: "changegroup" (params: 1 mandatory) streamed payload
+  bundle2-output-part: "pushkey" (params: 4 mandatory) empty payload
+  bundle2-output-part: "obsmarkers" streamed payload
   remote: adding changesets
   remote: adding manifests
   remote: adding file changes
   remote: added 1 changesets with 1 changes to 1 files (+1 heads)
   remote: 1 new obsolescence markers
+  bundle2-input-bundle: with-transaction
+  bundle2-input-part: "reply:changegroup" (advisory) (params: 0 advisory) supported
+  bundle2-input-part: "reply:pushkey" (params: 0 advisory) supported
+  bundle2-input-part: "reply:obsmarkers" (params: 0 advisory) supported
+  bundle2-input-bundle: 2 parts total
+  preparing listkeys for "phases"
+  sending listkeys command
+  received listkey for "phases": 58 bytes
+  $ hg -R ../server blackbox
+  * @0000000000000000000000000000000000000000 (*)> serve --stdio (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (1r, 0o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated served branch cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> wrote served branch cache with 1 labels and 2 nodes (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated stablerange cache in *.???? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (1r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> 1 incoming changes - new heads: 45f8b879de92 (glob)
+  * @0000000000000000000000000000000000000000 (*)> -R server serve --stdio exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> blackbox (glob)
+  $ rm ../server/.hg/blackbox.log
 
 testing push with extra local markers
 =====================================
@@ -145,6 +290,14 @@
   no changes found
   remote: 2 new obsolescence markers
   [1]
+  $ hg -R ../server blackbox
+  * @0000000000000000000000000000000000000000 (*)> serve --stdio (glob)
+  * @0000000000000000000000000000000000000000 (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 2o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (0r, 2o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> -R server serve --stdio exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> blackbox (glob)
+  $ rm ../server/.hg/blackbox.log
   $ hg -R ../server/ debugobsolete --rev ::tip | sort
   111111111111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   22222222222222222bbbbbbbbbbbbb2222222222 2dc09a01254db841290af0538aa52f6f52c776e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
@@ -152,6 +305,50 @@
   bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   ffffffffffffffffffffffffffffffffffffffff 45f8b879de922f6a6e620ba04205730335b6fc7e 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
+  $ hg blackbox
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete (glob)
+  * @0000000000000000000000000000000000000000 (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> up (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> up exited 0 after *.?? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> add foo (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> add foo exited 0 after *.?? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> commit -m foo (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obscache in *.???? seconds (1r, 0o) (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated served branch cache in *.???? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> wrote served branch cache with 1 labels and 1 nodes (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (1r, 0o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> commit -m foo exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete ffffffffffffffffffffffffffffffffffffffff 45f8b879de922f6a6e620ba04205730335b6fc7e (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete ffffffffffffffffffffffffffffffffffffffff 45f8b879de922f6a6e620ba04205730335b6fc7e exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> push -f --debug (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated stablerange cache in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obsdiscovery, 0/5 mismatch - 1 obshashrange queries in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> push -f --debug exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> log -G (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> writing .hg/cache/tags2-visible with 0 tags (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> log -G exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete 111111111111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete 111111111111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete 22222222222222222bbbbbbbbbbbbb2222222222 2dc09a01254db841290af0538aa52f6f52c776e3 (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete 22222222222222222bbbbbbbbbbbbb2222222222 2dc09a01254db841290af0538aa52f6f52c776e3 exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> push (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obsdiscovery, 2/6 mismatch - 1 obshashrange queries in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> push exited True after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> blackbox (glob)
+  $ rm .hg/blackbox.log
   $ hg debugobsolete | sort
   111111111111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   22222222222222222bbbbbbbbbbbbb2222222222 2dc09a01254db841290af0538aa52f6f52c776e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
@@ -190,6 +387,27 @@
   3 new obsolescence markers
   (run 'hg heads' to see heads, 'hg merge' to merge)
 
+  $ hg -R ../server blackbox
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete --rev ::tip (glob)
+  * @0000000000000000000000000000000000000000 (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @0000000000000000000000000000000000000000 (*)> writing .hg/cache/tags2-visible with 0 tags (glob)
+  * @0000000000000000000000000000000000000000 (*)> -R ../server/ debugobsolete --rev ::tip exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete aaaaaaa11111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd (glob)
+  * @0000000000000000000000000000000000000000 (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @0000000000000000000000000000000000000000 (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> -R ../server debugobsolete aaaaaaa11111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> debugobsolete bbbbbbb2222222222bbbbbbbbbbbbb2222222222 bebd167eb94d257ace0e814aeb98e6972ed2970d (glob)
+  * @0000000000000000000000000000000000000000 (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @0000000000000000000000000000000000000000 (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @0000000000000000000000000000000000000000 (*)> -R ../server debugobsolete bbbbbbb2222222222bbbbbbbbbbbbb2222222222 bebd167eb94d257ace0e814aeb98e6972ed2970d exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> serve --stdio (glob)
+  * @0000000000000000000000000000000000000000 (*)> -R server serve --stdio exited 0 after *.?? seconds (glob)
+  * @0000000000000000000000000000000000000000 (*)> blackbox (glob)
+  $ rm ../server/.hg/blackbox.log
   $ hg -R ../server/ debugobsolete --rev '::6' | sort
   111111111111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   22222222222222222bbbbbbbbbbbbb2222222222 2dc09a01254db841290af0538aa52f6f52c776e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
@@ -199,6 +417,25 @@
   bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   dddddddddddddddddddddddddddddddddddddddd c8d03c1b5e94af74b772900c58259d2e08917735 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
+  $ hg blackbox
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> log -G (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> log -G exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull -r 6 (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obsdiscovery, 2/6 mismatch - 1 obshashrange queries in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (2r, 0o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated served branch cache in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> wrote served branch cache with 1 labels and 2 nodes (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated stablerange cache in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (2r, 3o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (0r, 3o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> 2 incoming changes - new heads: f69452c5b1af (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull -r 6 exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> blackbox (glob)
+  $ rm .hg/blackbox.log
   $ hg debugobsolete --rev '::6' | sort
   111111111111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   22222222222222222bbbbbbbbbbbbb2222222222 2dc09a01254db841290af0538aa52f6f52c776e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
@@ -209,3 +446,337 @@
   cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
   dddddddddddddddddddddddddddddddddddddddd c8d03c1b5e94af74b772900c58259d2e08917735 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
 
+Test cache behavior
+===================
+
+Adding markers affecting already used range:
+--------------------------------------------
+
+  $ hg debugobshashrange --subranges --rev 'heads(all())'
+           rev         node        index         size        depth      obshash
+             7 f69452c5b1af            0            7            7 000000000000
+             5 45f8b879de92            0            6            6 1643971dbe2d
+             3 2dc09a01254d            0            4            4 6be48f31976a
+             7 f69452c5b1af            4            3            7 000000000000
+             3 2dc09a01254d            2            2            4 9522069ae085
+             5 45f8b879de92            4            2            6 9c26c72819c0
+             1 66f7d451a68b            0            2            2 853c77a32154
+             6 c8d03c1b5e94            4            2            6 ec8a3e92c525
+             2 01241442b3c2            2            1            3 1ed3c61fb39a
+             0 1ea73414a91b            0            1            1 000000000000
+             3 2dc09a01254d            3            1            4 8a2acf8e1cde
+             5 45f8b879de92            5            1            6 1a0c08180b65
+             1 66f7d451a68b            1            1            2 853c77a32154
+             4 bebd167eb94d            4            1            5 20a2cc572e4b
+             6 c8d03c1b5e94            5            1            6 446c2dc3bce5
+             7 f69452c5b1af            6            1            7 000000000000
+  $ hg -R ../server debugobsolete aaaa333333333aaaaa333a3a3a3a3a3a3a3a3a3a `getid 'desc(r1)'`
+  $ hg -R ../server debugobsolete bb4b4b4b4b4b4b4b44b4b4b4b4b4b4b4b4b4b4b4 `getid 'desc(r3)'`
+  $ hg pull -r `getid 'desc(r6)'`
+  pulling from ssh://user@dummy/server
+  no changes found
+  OBSEXC: looking for common markers in 7 nodes
+  OBSEXC: request obsmarkers for 2 common nodes
+  2 new obsolescence markers
+  $ hg debugobshashrange --subranges --rev 'desc("r3")' -R ../server
+           rev         node        index         size        depth      obshash
+             3 2dc09a01254d            0            4            4 8932bf980bb4
+             3 2dc09a01254d            2            2            4 ce1937ca1278
+             1 66f7d451a68b            0            2            2 327c7dd73d29
+             2 01241442b3c2            2            1            3 1ed3c61fb39a
+             0 1ea73414a91b            0            1            1 000000000000
+             3 2dc09a01254d            3            1            4 26f996446ecb
+             1 66f7d451a68b            1            1            2 327c7dd73d29
+  $ hg debugobshashrange --subranges --rev 'desc("r3")'
+           rev         node        index         size        depth      obshash
+             3 2dc09a01254d            0            4            4 8932bf980bb4
+             3 2dc09a01254d            2            2            4 ce1937ca1278
+             1 66f7d451a68b            0            2            2 327c7dd73d29
+             2 01241442b3c2            2            1            3 1ed3c61fb39a
+             0 1ea73414a91b            0            1            1 000000000000
+             3 2dc09a01254d            3            1            4 26f996446ecb
+             1 66f7d451a68b            1            1            2 327c7dd73d29
+  $ hg blackbox
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete --rev ::6 (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> alias 'debugobsolete' expands to 'debugobsolete -d '0 0'' (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> writing .hg/cache/tags2-visible with 0 tags (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete --rev ::6 exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev heads(all()) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev heads(all()) exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull -r f69452c5b1af6cbaaa56ef50cf94fff5bcc6ca23 (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obsdiscovery, 2/7 mismatch - 1 obshashrange queries in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 2o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (0r, 2o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull -r f69452c5b1af6cbaaa56ef50cf94fff5bcc6ca23 exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev desc("r3") (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev desc("r3") exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> blackbox (glob)
+  $ rm .hg/blackbox.log
+
+Adding prune markers on existing changeset
+------------------------------------------
+
+  $ hg -R ../server debugobsolete --record-parents `getid 'desc(foo)'`
+  $ hg pull -r `getid 'desc(r4)'`
+  pulling from ssh://user@dummy/server
+  no changes found
+  OBSEXC: looking for common markers in 5 nodes
+  OBSEXC: request obsmarkers for 1 common nodes
+  1 new obsolescence markers
+  $ hg blackbox
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull -r bebd167eb94d257ace0e814aeb98e6972ed2970d (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obsdiscovery, 1/5 mismatch - 1 obshashrange queries in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obshashcache reset - new markers affect cached ranges (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (0r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull -r bebd167eb94d257ace0e814aeb98e6972ed2970d exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> blackbox (glob)
+  $ rm .hg/blackbox.log
+  $ hg debugobshashrange --subranges --rev 'heads(all())'
+           rev         node        index         size        depth      obshash
+             7 f69452c5b1af            0            7            7 000000000000
+             5 45f8b879de92            0            6            6 b8a4206b0fc6
+             3 2dc09a01254d            0            4            4 8932bf980bb4
+             7 f69452c5b1af            4            3            7 000000000000
+             3 2dc09a01254d            2            2            4 ce1937ca1278
+             5 45f8b879de92            4            2            6 31fc49d36a59
+             1 66f7d451a68b            0            2            2 327c7dd73d29
+             6 c8d03c1b5e94            4            2            6 89755fd39e6d
+             2 01241442b3c2            2            1            3 1ed3c61fb39a
+             0 1ea73414a91b            0            1            1 000000000000
+             3 2dc09a01254d            3            1            4 26f996446ecb
+             5 45f8b879de92            5            1            6 1a0c08180b65
+             1 66f7d451a68b            1            1            2 327c7dd73d29
+             4 bebd167eb94d            4            1            5 b21465ecb790
+             6 c8d03c1b5e94            5            1            6 446c2dc3bce5
+             7 f69452c5b1af            6            1            7 000000000000
+
+Recover after rollback
+
+  $ hg pull
+  pulling from ssh://user@dummy/server
+  searching for changes
+  OBSEXC: looking for common markers in 8 nodes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 0 changes to 0 files
+  1 new obsolescence markers
+  (run 'hg update' to get a working copy)
+  $ hg rollback
+  repository tip rolled back to revision 7 (undo pull)
+  $ hg blackbox
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev heads(all()) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev heads(all()) exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obsdiscovery, 0/8 mismatch - 1 obshashrange queries in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (1r, 0o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated served branch cache in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> wrote served branch cache with 1 labels and 2 nodes (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated stablerange cache in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (1r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> 1 incoming changes - new heads: 4de32a90b66c (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> rollback (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated base branch cache in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> wrote base branch cache with 1 labels and 2 nodes (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> rollback exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> blackbox (glob)
+  $ rm .hg/blackbox.log
+  $ hg debugobshashrange --subranges --rev 'heads(all())'
+           rev         node        index         size        depth      obshash
+             7 f69452c5b1af            0            7            7 000000000000
+             5 45f8b879de92            0            6            6 b8a4206b0fc6
+             3 2dc09a01254d            0            4            4 8932bf980bb4
+             7 f69452c5b1af            4            3            7 000000000000
+             3 2dc09a01254d            2            2            4 ce1937ca1278
+             5 45f8b879de92            4            2            6 31fc49d36a59
+             1 66f7d451a68b            0            2            2 327c7dd73d29
+             6 c8d03c1b5e94            4            2            6 89755fd39e6d
+             2 01241442b3c2            2            1            3 1ed3c61fb39a
+             0 1ea73414a91b            0            1            1 000000000000
+             3 2dc09a01254d            3            1            4 26f996446ecb
+             5 45f8b879de92            5            1            6 1a0c08180b65
+             1 66f7d451a68b            1            1            2 327c7dd73d29
+             4 bebd167eb94d            4            1            5 b21465ecb790
+             6 c8d03c1b5e94            5            1            6 446c2dc3bce5
+             7 f69452c5b1af            6            1            7 000000000000
+  $ hg pull
+  pulling from ssh://user@dummy/server
+  searching for changes
+  OBSEXC: looking for common markers in 8 nodes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 0 changes to 0 files
+  1 new obsolescence markers
+  (run 'hg update' to get a working copy)
+  $ hg blackbox
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev heads(all()) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated stablerange cache in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> strip detected, evo-ext-obshashrange cache reset (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (8r, 12o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev heads(all()) exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> obsdiscovery, 0/8 mismatch - 1 obshashrange queries in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> strip detected, evo-ext-obscache cache reset (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (9r, 12o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated stablerange cache in *.???? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obshashrange in *.???? seconds (1r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> 1 incoming changes - new heads: 4de32a90b66c (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> pull exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> blackbox (glob)
+  $ rm .hg/blackbox.log
+  $ hg debugobshashrange --subranges --rev 'heads(all())'
+           rev         node        index         size        depth      obshash
+             8 4de32a90b66c            0            8            8 c7f1f7e9925b
+             5 45f8b879de92            0            6            6 b8a4206b0fc6
+             3 2dc09a01254d            0            4            4 8932bf980bb4
+             8 4de32a90b66c            4            4            8 c681c3e58c27
+             3 2dc09a01254d            2            2            4 ce1937ca1278
+             5 45f8b879de92            4            2            6 31fc49d36a59
+             8 4de32a90b66c            6            2            8 033544c939f0
+             1 66f7d451a68b            0            2            2 327c7dd73d29
+             6 c8d03c1b5e94            4            2            6 89755fd39e6d
+             2 01241442b3c2            2            1            3 1ed3c61fb39a
+             0 1ea73414a91b            0            1            1 000000000000
+             3 2dc09a01254d            3            1            4 26f996446ecb
+             5 45f8b879de92            5            1            6 1a0c08180b65
+             8 4de32a90b66c            7            1            8 033544c939f0
+             1 66f7d451a68b            1            1            2 327c7dd73d29
+             4 bebd167eb94d            4            1            5 b21465ecb790
+             6 c8d03c1b5e94            5            1            6 446c2dc3bce5
+             7 f69452c5b1af            6            1            7 000000000000
+
+Recover after stripping (in the middle of the repo)
+
+We strip a branch that is not the tip of the reporiosy so part of the affected
+revision are reapplied after the target is stripped.
+
+  $ hg log -G
+  o  8 4de32a90b66c r7 tip
+  |
+  o  7 f69452c5b1af r6
+  |
+  o  6 c8d03c1b5e94 r5
+  |
+  | @  5 45f8b879de92 foo
+  |/
+  o  4 bebd167eb94d r4
+  |
+  o  3 2dc09a01254d r3
+  |
+  o  2 01241442b3c2 r2
+  |
+  o  1 66f7d451a68b r1
+  |
+  o  0 1ea73414a91b r0
+  
+  $ hg --config extensions.strip= strip -r 'desc("foo")'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  saved backup bundle to $TESTTMP/client/.hg/strip-backup/45f8b879de92-94c82517-backup.hg (glob)
+  $ hg log -G
+  o  7 4de32a90b66c r7 tip
+  |
+  o  6 f69452c5b1af r6
+  |
+  o  5 c8d03c1b5e94 r5
+  |
+  @  4 bebd167eb94d r4
+  |
+  o  3 2dc09a01254d r3
+  |
+  o  2 01241442b3c2 r2
+  |
+  o  1 66f7d451a68b r1
+  |
+  o  0 1ea73414a91b r0
+  
+  $ hg pull
+  pulling from ssh://user@dummy/server
+  searching for changes
+  OBSEXC: looking for common markers in 8 nodes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+  $ hg log -G
+  o  8 45f8b879de92 foo tip
+  |
+  | o  7 4de32a90b66c r7
+  | |
+  | o  6 f69452c5b1af r6
+  | |
+  | o  5 c8d03c1b5e94 r5
+  |/
+  @  4 bebd167eb94d r4
+  |
+  o  3 2dc09a01254d r3
+  |
+  o  2 01241442b3c2 r2
+  |
+  o  1 66f7d451a68b r1
+  |
+  o  0 1ea73414a91b r0
+  
+  $ hg blackbox
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev heads(all()) (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> writing .hg/cache/tags2-visible with 0 tags (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobshashrange --subranges --rev heads(all()) exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> log -G (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> log -G exited 0 after *.?? seconds (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> strip -r desc("foo") (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> saved backup bundle to $TESTTMP/client/.hg/strip-backup/45f8b879de92-94c82517-backup.hg (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> strip detected, evo-ext-obshashrange cache reset (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obshashrange in *.???? seconds (5r, 13o) (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> strip detected, evo-ext-obscache cache reset (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obscache in *.???? seconds (5r, 13o) (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated stablerange cache in *.???? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obshashrange in *.???? seconds (3r, 0o) (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obscache in *.???? seconds (3r, 0o) (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated base branch cache in *.???? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> wrote base branch cache with 1 labels and 1 nodes (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> 3 incoming changes - new heads: 4de32a90b66c (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> --config extensions.strip= strip -r desc("foo") exited 0 after *.?? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> log -G (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> writing .hg/cache/tags2-visible with 0 tags (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> log -G exited 0 after *.?? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> pull (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> obsdiscovery, 0/8 mismatch - 1 obshashrange queries in *.???? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obscache in *.???? seconds (1r, 0o) (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated stablerange cache in *.???? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated evo-ext-obshashrange in *.???? seconds (1r, 0o) (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> updated served branch cache in *.???? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> wrote served branch cache with 1 labels and 2 nodes (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> 1 incoming changes - new heads: 45f8b879de92 (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> pull exited 0 after *.?? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> log -G (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> writing .hg/cache/tags2-visible with 0 tags (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> log -G exited 0 after *.?? seconds (glob)
+  * @bebd167eb94d257ace0e814aeb98e6972ed2970d (*)> blackbox (glob)
+  $ rm .hg/blackbox.log
+  $ hg debugobshashrange --subranges --rev 'heads(all())'
+           rev         node        index         size        depth      obshash
+             7 4de32a90b66c            0            8            8 c7f1f7e9925b
+             8 45f8b879de92            0            6            6 b8a4206b0fc6
+             3 2dc09a01254d            0            4            4 8932bf980bb4
+             7 4de32a90b66c            4            4            8 c681c3e58c27
+             3 2dc09a01254d            2            2            4 ce1937ca1278
+             8 45f8b879de92            4            2            6 31fc49d36a59
+             7 4de32a90b66c            6            2            8 033544c939f0
+             1 66f7d451a68b            0            2            2 327c7dd73d29
+             5 c8d03c1b5e94            4            2            6 89755fd39e6d
+             2 01241442b3c2            2            1            3 1ed3c61fb39a
+             0 1ea73414a91b            0            1            1 000000000000
+             3 2dc09a01254d            3            1            4 26f996446ecb
+             8 45f8b879de92            5            1            6 1a0c08180b65
+             7 4de32a90b66c            7            1            8 033544c939f0
+             1 66f7d451a68b            1            1            2 327c7dd73d29
+             4 bebd167eb94d            4            1            5 b21465ecb790
+             5 c8d03c1b5e94            5            1            6 446c2dc3bce5
+             6 f69452c5b1af            6            1            7 000000000000
+
--- a/tests/test-evolve-obshistory.t	Thu May 18 22:53:01 2017 +0200
+++ b/tests/test-evolve-obshistory.t	Thu May 18 23:12:52 2017 +0200
@@ -26,9 +26,11 @@
   $ mkcommit ROOT
   $ mkcommit A0
   $ echo 42 >> A0
-  $ hg amend -m "A1"
+  $ hg amend -m "A1
+  > 
+  > Better commit message"
   $ hg log --hidden -G
-  @  changeset:   3:a468dc9b3633
+  @  changeset:   3:4ae3a4151de9
   |  tag:         tip
   |  parent:      0:ea207398892e
   |  user:        test
@@ -52,7 +54,64 @@
   
 Actual test
 -----------
-
+  $ hg olog 4ae3a4151de9
+  @  4ae3a4151de9 (3) A1
+  |
+  x  471f378eab4c (1) A0
+       rewritten by test (*20*) as 4ae3a4151de9 (glob)
+  
+  $ hg olog 4ae3a4151de9 --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [],
+          "debugobshistory.node": "4ae3a4151de9",
+          "debugobshistory.rev": 3,
+          "debugobshistory.shortdescription": "A1"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "4ae3a4151de9"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "471f378eab4c",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A0"
+      }
+  ]
+  $ hg olog --hidden 471f378eab4c
+  x  471f378eab4c (1) A0
+       rewritten by test (*20*) as 4ae3a4151de9 (glob)
+  
+  $ hg olog --hidden 471f378eab4c --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "4ae3a4151de9"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "471f378eab4c",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A0"
+      }
+  ]
   $ hg update 471f378eab4c
   abort: hidden revision '471f378eab4c'!
   (use --hidden to access hidden revisions)
@@ -60,7 +119,7 @@
   $ 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)
+  (use 'hg evolve' to update to its successor: 4ae3a4151de9)
 
 Test output with pruned commit
 ==============================
@@ -94,10 +153,61 @@
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   working directory now at 471f378eab4c
   1 changesets pruned
+  $ hg log --hidden -G
+  x  changeset:   2:0dec01379d3b
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B0
+  |
+  @  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 olog 'desc(B0)' --hidden
+  x  0dec01379d3b (2) B0
+       pruned by test (*20*) (glob)
+  
+  $ hg olog 'desc(B0)' --hidden --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.verb": "pruned"
+              }
+          ],
+          "debugobshistory.node": "0dec01379d3b",
+          "debugobshistory.rev": 2,
+          "debugobshistory.shortdescription": "B0"
+      }
+  ]
+  $ hg olog 'desc(A0)'
+  @  471f378eab4c (1) A0
+  
+  $ hg olog 'desc(A0)' --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [],
+          "debugobshistory.node": "471f378eab4c",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A0"
+      }
+  ]
   $ hg up 1
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg up 0dec01379d3b
@@ -196,6 +306,59 @@
 Actual test
 -----------
 
+Check that debugobshistory on splitted commit show both targets
+  $ hg olog 471597cad322 --hidden
+  x  471597cad322 (1) A0
+       rewritten by test (*20*) as 337fec4d2edc, f257fde29c7a (glob)
+  
+  $ hg olog 471597cad322 --hidden --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "337fec4d2edc",
+                      "f257fde29c7a"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "471597cad322",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A0"
+      }
+  ]
+Check that debugobshistory on the first successor after split show
+the revision plus the splitted one
+  $ hg olog 337fec4d2edc
+  o  337fec4d2edc (2) A0
+  |
+  x  471597cad322 (1) A0
+       rewritten by test (*20*) as 337fec4d2edc, f257fde29c7a (glob)
+  
+Check that debugobshistory on the second successor after split show
+the revision plus the splitted one
+  $ hg olog f257fde29c7a
+  @  f257fde29c7a (3) A0
+  |
+  x  471597cad322 (1) A0
+       rewritten by test (*20*) as 337fec4d2edc, f257fde29c7a (glob)
+  
+Check that debugobshistory on both successors after split show
+a coherent graph
+  $ hg olog 'f257fde29c7a+337fec4d2edc'
+  o  337fec4d2edc (2) A0
+  |
+  | @  f257fde29c7a (3) A0
+  |/
+  x  471597cad322 (1) A0
+       rewritten by test (*20*) as 337fec4d2edc, f257fde29c7a (glob)
+  
   $ hg update 471597cad322
   abort: hidden revision '471597cad322'!
   (use --hidden to access hidden revisions)
@@ -358,6 +521,83 @@
 Actual test
 -----------
 
+  $ hg olog de7290d8b885 --hidden
+  x  de7290d8b885 (1) A0
+       rewritten by test (*20*) as 1ae8bc733a14, 337fec4d2edc, c7f044602e9b, f257fde29c7a (glob)
+  
+  $ hg olog de7290d8b885 --hidden --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "1ae8bc733a14",
+                      "337fec4d2edc",
+                      "c7f044602e9b",
+                      "f257fde29c7a"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "de7290d8b885",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A0"
+      }
+  ]
+  $ hg olog c7f044602e9b
+  @  c7f044602e9b (5) A0
+  |
+  x  de7290d8b885 (1) A0
+       rewritten by test (*20*) as 1ae8bc733a14, 337fec4d2edc, c7f044602e9b, f257fde29c7a (glob)
+  
+  $ hg olog c7f044602e9b --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [],
+          "debugobshistory.node": "c7f044602e9b",
+          "debugobshistory.rev": 5,
+          "debugobshistory.shortdescription": "A0"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "1ae8bc733a14",
+                      "337fec4d2edc",
+                      "c7f044602e9b",
+                      "f257fde29c7a"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "de7290d8b885",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A0"
+      }
+  ]
+Check that debugobshistory on all heads show a coherent graph
+  $ hg olog 2::5
+  o  1ae8bc733a14 (4) A0
+  |
+  | o  337fec4d2edc (2) A0
+  |/
+  | @  c7f044602e9b (5) A0
+  |/
+  | o  f257fde29c7a (3) A0
+  |/
+  x  de7290d8b885 (1) A0
+       rewritten by test (*20*) as 1ae8bc733a14, 337fec4d2edc, c7f044602e9b, f257fde29c7a (glob)
+  
   $ hg update de7290d8b885
   abort: hidden revision 'de7290d8b885'!
   (use --hidden to access hidden revisions)
@@ -424,6 +664,74 @@
  Actual test
  -----------
 
+Check that debugobshistory on the first folded revision show only
+the revision with the target
+  $ hg olog --hidden 471f378eab4c
+  x  471f378eab4c (1) A0
+       rewritten by test (*20*) as eb5a0daa2192 (glob)
+  
+Check that debugobshistory on the second folded revision show only
+the revision with the target
+  $ hg olog --hidden 0dec01379d3b
+  x  0dec01379d3b (2) B0
+       rewritten by test (*20*) as eb5a0daa2192 (glob)
+  
+Check that debugobshistory on the successor revision show a coherent
+graph
+  $ hg olog eb5a0daa2192
+  @    eb5a0daa2192 (3) C0
+  |\
+  x |  0dec01379d3b (2) B0
+   /     rewritten by test (*20*) as eb5a0daa2192 (glob)
+  |
+  x  471f378eab4c (1) A0
+       rewritten by test (*20*) as eb5a0daa2192 (glob)
+  
+  $ hg olog eb5a0daa2192 --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [],
+          "debugobshistory.node": "eb5a0daa2192",
+          "debugobshistory.rev": 3,
+          "debugobshistory.shortdescription": "C0"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "eb5a0daa2192"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "471f378eab4c",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A0"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "eb5a0daa2192"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "0dec01379d3b",
+          "debugobshistory.rev": 2,
+          "debugobshistory.shortdescription": "B0"
+      }
+  ]
   $ hg update 471f378eab4c
   abort: hidden revision '471f378eab4c'!
   (use --hidden to access hidden revisions)
@@ -507,6 +815,117 @@
 Actual test
 -----------
 
+Check that debugobshistory on the divergent revision show both destinations
+  $ hg olog --hidden 471f378eab4c
+  x  471f378eab4c (1) A0
+       rewritten by test (*20*) as 65b757b745b9 (glob)
+       rewritten by test (*20*) as fdf9bde5129a (glob)
+  
+  $ hg olog --hidden 471f378eab4c --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "65b757b745b9"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              },
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "fdf9bde5129a"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "471f378eab4c",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A0"
+      }
+  ]
+Check that debugobshistory on the first diverged revision show the revision
+and the diverent one
+  $ hg olog fdf9bde5129a
+  o  fdf9bde5129a (2) A1
+  |
+  x  471f378eab4c (1) A0
+       rewritten by test (*20*) as 65b757b745b9 (glob)
+       rewritten by test (*20*) as fdf9bde5129a (glob)
+  
+Check that debugobshistory on the second diverged revision show the revision
+and the diverent one
+  $ hg olog 65b757b745b9
+  @  65b757b745b9 (3) A2
+  |
+  x  471f378eab4c (1) A0
+       rewritten by test (*20*) as 65b757b745b9 (glob)
+       rewritten by test (*20*) as fdf9bde5129a (glob)
+  
+Check that debugobshistory on the both diverged revision show a coherent
+graph
+  $ hg olog '65b757b745b9+fdf9bde5129a'
+  @  65b757b745b9 (3) A2
+  |
+  | o  fdf9bde5129a (2) A1
+  |/
+  x  471f378eab4c (1) A0
+       rewritten by test (*20*) as 65b757b745b9 (glob)
+       rewritten by test (*20*) as fdf9bde5129a (glob)
+  
+  $ hg olog '65b757b745b9+fdf9bde5129a' --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [],
+          "debugobshistory.node": "65b757b745b9",
+          "debugobshistory.rev": 3,
+          "debugobshistory.shortdescription": "A2"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "65b757b745b9"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              },
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "fdf9bde5129a"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "471f378eab4c",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A0"
+      },
+      {
+          "debugobshistory.markers": [],
+          "debugobshistory.node": "fdf9bde5129a",
+          "debugobshistory.rev": 2,
+          "debugobshistory.shortdescription": "A1"
+      }
+  ]
   $ hg update 471f378eab4c
   abort: hidden revision '471f378eab4c'!
   (use --hidden to access hidden revisions)
@@ -515,3 +934,630 @@
   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)
+
+Test output with amended + folded commit
+========================================
+
+Test setup
+----------
+
+  $ hg init $TESTTMP/local-amend-fold
+  $ cd $TESTTMP/local-amend-fold
+  $ mkcommit ROOT
+  $ mkcommit A0
+  $ mkcommit B0
+  $ hg amend -m "B1"
+  $ hg log --hidden -G
+  @  changeset:   3:b7ea6d14e664
+  |  tag:         tip
+  |  parent:      1:471f378eab4c
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B1
+  |
+  | x  changeset:   2:0dec01379d3b
+  |/   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(B1)' --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:   4:eb5a0daa2192
+  |  tag:         tip
+  |  parent:      0:ea207398892e
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     C0
+  |
+  | x  changeset:   3:b7ea6d14e664
+  | |  parent:      1:471f378eab4c
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     B1
+  | |
+  | | 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
+ -----------
+
+Check that debugobshistory on head show a coherent graph
+  $ hg olog eb5a0daa2192
+  @    eb5a0daa2192 (4) C0
+  |\
+  x |  471f378eab4c (1) A0
+   /     rewritten by test (*20*) as eb5a0daa2192 (glob)
+  |
+  x  b7ea6d14e664 (3) B1
+  |    rewritten by test (*20*) as eb5a0daa2192 (glob)
+  |
+  x  0dec01379d3b (2) B0
+       rewritten by test (*20*) as b7ea6d14e664 (glob)
+  
+  $ hg olog eb5a0daa2192 --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [],
+          "debugobshistory.node": "eb5a0daa2192",
+          "debugobshistory.rev": 4,
+          "debugobshistory.shortdescription": "C0"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "eb5a0daa2192"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "b7ea6d14e664",
+          "debugobshistory.rev": 3,
+          "debugobshistory.shortdescription": "B1"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "b7ea6d14e664"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "0dec01379d3b",
+          "debugobshistory.rev": 2,
+          "debugobshistory.shortdescription": "B0"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0 (glob)
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "eb5a0daa2192"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "471f378eab4c",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A0"
+      }
+  ]
+  $ 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 --hidden 0dec01379d3b
+  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)
+  $ hg update 0dec01379d3b
+  0 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)
+  $ hg update --hidden 'desc(B0)'
+  0 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 pushed and pulled obs markers
+==============================================
+
+Test setup
+----------
+
+  $ hg init $TESTTMP/local-remote-markers-1
+  $ cd $TESTTMP/local-remote-markers-1
+  $ mkcommit ROOT
+  $ mkcommit A0
+  $ hg log --hidden -G
+  @  changeset:   1:471f378eab4c
+  |  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 clone $TESTTMP/local-remote-markers-1 $TESTTMP/local-remote-markers-2
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd $TESTTMP/local-remote-markers-2
+  $ hg log --hidden -G
+  @  changeset:   1:471f378eab4c
+  |  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
+  
+  $ cd $TESTTMP/local-remote-markers-1
+  $ hg amend -m "A1"
+  $ hg amend -m "A2"
+  $ hg log --hidden -G
+  @  changeset:   3:7a230b46bf61
+  |  tag:         tip
+  |  parent:      0:ea207398892e
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A2
+  |
+  | x  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 olog 7a230b46bf61
+  @  7a230b46bf61 (3) A2
+  |
+  x  fdf9bde5129a (2) A1
+  |    rewritten by test (*20*) as 7a230b46bf61 (glob)
+  |
+  x  471f378eab4c (1) A0
+       rewritten by test (*20*) as fdf9bde5129a (glob)
+  
+  $ cd $TESTTMP/local-remote-markers-2
+  $ hg pull
+  pulling from $TESTTMP/local-remote-markers-1
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 0 changes to 1 files (+1 heads)
+  2 new obsolescence markers
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+  working directory parent is obsolete! (471f378eab4c)
+  (use 'hg evolve' to update to its successor: 7a230b46bf61)
+Check that debugobshistory works with markers pointing to missing local
+changectx
+  $ hg olog 7a230b46bf61
+  o  7a230b46bf61 (2) A2
+  |
+  x  fdf9bde5129a
+  |    rewritten by test (*20*) as 7a230b46bf61 (glob)
+  |
+  @  471f378eab4c (1) A0
+       rewritten by test (*20*) as fdf9bde5129a (glob)
+  
+  $ hg olog 7a230b46bf61 --color=debug
+  o  [evolve.node|7a230b46bf61] [evolve.rev|(2)] [evolve.short_description|A2]
+  |
+  x  [evolve.node evolve.missing_change_ctx|fdf9bde5129a]
+  |    [evolve.verb|rewritten] by [evolve.user|test] [evolve.date|(*20*)] as [evolve.node|7a230b46bf61] (glob)
+  |
+  @  [evolve.node|471f378eab4c] [evolve.rev|(1)] [evolve.short_description|A0]
+       [evolve.verb|rewritten] by [evolve.user|test] [evolve.date|(*20*)] as [evolve.node|fdf9bde5129a] (glob)
+  
+
+Test with cycle
+===============
+
+Test setup
+----------
+
+  $ hg init $TESTTMP/cycle
+  $ cd $TESTTMP/cycle
+  $ mkcommit ROOT
+  $ mkcommit A
+  $ mkcommit B
+  $ mkcommit C
+  $ hg log -G
+  @  changeset:   3:a8df460dbbfe
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     C
+  |
+  o  changeset:   2:c473644ee0e9
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B
+  |
+  o  changeset:   1:2a34000d3544
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+Create a cycle
+  $ hg prune -s "desc(B)" "desc(A)"
+  1 changesets pruned
+  2 new unstable changesets
+  $ hg prune -s "desc(C)" "desc(B)"
+  1 changesets pruned
+  $ hg prune -s "desc(A)" "desc(C)"
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  working directory now at 2a34000d3544
+  1 changesets pruned
+  $ hg log --hidden -G
+  x  changeset:   3:a8df460dbbfe
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     C
+  |
+  x  changeset:   2:c473644ee0e9
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B
+  |
+  @  changeset:   1:2a34000d3544
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+Actual test
+-----------
+
+Check that debugobshistory never crash on a cycle
+
+  $ hg olog "desc(A)" --hidden
+  @  2a34000d3544 (1) A
+  |    rewritten by test (*20*) as c473644ee0e9 (glob)
+  |
+  x  a8df460dbbfe (3) C
+  |    rewritten by test (*20*) as 2a34000d3544 (glob)
+  |
+  x  c473644ee0e9 (2) B
+  |    rewritten by test (*20*) as a8df460dbbfe (glob)
+  |
+
+  $ hg olog "desc(B)" --hidden
+  @  2a34000d3544 (1) A
+  |    rewritten by test (*20*) as c473644ee0e9 (glob)
+  |
+  x  a8df460dbbfe (3) C
+  |    rewritten by test (*20*) as 2a34000d3544 (glob)
+  |
+  x  c473644ee0e9 (2) B
+  |    rewritten by test (*20*) as a8df460dbbfe (glob)
+  |
+
+  $ hg olog "desc(C)" --hidden
+  @  2a34000d3544 (1) A
+  |    rewritten by test (*20*) as c473644ee0e9 (glob)
+  |
+  x  a8df460dbbfe (3) C
+  |    rewritten by test (*20*) as 2a34000d3544 (glob)
+  |
+  x  c473644ee0e9 (2) B
+  |    rewritten by test (*20*) as a8df460dbbfe (glob)
+  |
+
+Test with multiple cyles
+========================
+
+Test setup
+----------
+
+  $ hg init $TESTTMP/multiple-cycle
+  $ cd $TESTTMP/multiple-cycle
+  $ mkcommit ROOT
+  $ mkcommit A
+  $ mkcommit B
+  $ mkcommit C
+  $ mkcommit D
+  $ mkcommit E
+  $ mkcommit F
+  $ hg log -G
+  @  changeset:   6:d9f908fde1a1
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     F
+  |
+  o  changeset:   5:0da815c333f6
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     E
+  |
+  o  changeset:   4:868d2e0eb19c
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     D
+  |
+  o  changeset:   3:a8df460dbbfe
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     C
+  |
+  o  changeset:   2:c473644ee0e9
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B
+  |
+  o  changeset:   1:2a34000d3544
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+Create a first cycle
+  $ hg prune -s "desc(B)" "desc(A)"
+  1 changesets pruned
+  5 new unstable changesets
+  $ hg prune -s "desc(C)" "desc(B)"
+  1 changesets pruned
+  $ hg prune --split -s "desc(A)" -s "desc(D)" "desc(C)"
+  1 changesets pruned
+And create a second one
+  $ hg prune -s "desc(E)" "desc(D)"
+  1 changesets pruned
+  $ hg prune -s "desc(F)" "desc(E)"
+  1 changesets pruned
+  $ hg prune -s "desc(D)" "desc(F)"
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  working directory now at 868d2e0eb19c
+  1 changesets pruned
+  $ hg log --hidden -G
+  x  changeset:   6:d9f908fde1a1
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     F
+  |
+  x  changeset:   5:0da815c333f6
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     E
+  |
+  @  changeset:   4:868d2e0eb19c
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     D
+  |
+  x  changeset:   3:a8df460dbbfe
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     C
+  |
+  x  changeset:   2:c473644ee0e9
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     B
+  |
+  x  changeset:   1:2a34000d3544
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+Actual test
+-----------
+
+Check that debugobshistory never crash on a cycle
+
+  $ hg olog "desc(D)" --hidden
+  x  0da815c333f6 (5) E
+  |    rewritten by test (*20*) as d9f908fde1a1 (glob)
+  |
+  @    868d2e0eb19c (4) D
+  |\     rewritten by test (*20*) as 0da815c333f6 (glob)
+  | |
+  | x  d9f908fde1a1 (6) F
+  | |    rewritten by test (*20*) as 868d2e0eb19c (glob)
+  | |
+  +---x  2a34000d3544 (1) A
+  | |      rewritten by test (*20*) as c473644ee0e9 (glob)
+  | |
+  x |  a8df460dbbfe (3) C
+  | |    rewritten by test (*20*) as 2a34000d3544, 868d2e0eb19c (glob)
+  | |
+  x |  c473644ee0e9 (2) B
+  | |    rewritten by test (*20*) as a8df460dbbfe (glob)
+  | |
+
+Check the json output is valid in this case
+
+  $ hg olog "desc(D)" --hidden --no-graph -Tjson | python -m json.tool
+  [
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "0da815c333f6"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "868d2e0eb19c",
+          "debugobshistory.rev": 4,
+          "debugobshistory.shortdescription": "D"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "868d2e0eb19c"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "d9f908fde1a1",
+          "debugobshistory.rev": 6,
+          "debugobshistory.shortdescription": "F"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "d9f908fde1a1"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "0da815c333f6",
+          "debugobshistory.rev": 5,
+          "debugobshistory.shortdescription": "E"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "2a34000d3544",
+                      "868d2e0eb19c"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "a8df460dbbfe",
+          "debugobshistory.rev": 3,
+          "debugobshistory.shortdescription": "C"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "a8df460dbbfe"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "c473644ee0e9",
+          "debugobshistory.rev": 2,
+          "debugobshistory.shortdescription": "B"
+      },
+      {
+          "debugobshistory.markers": [
+              {
+                  "debugobshistory.marker_date": [
+                      *, (glob)
+                      0
+                  ],
+                  "debugobshistory.marker_user": "test",
+                  "debugobshistory.succnodes": [
+                      "c473644ee0e9"
+                  ],
+                  "debugobshistory.verb": "rewritten"
+              }
+          ],
+          "debugobshistory.node": "2a34000d3544",
+          "debugobshistory.rev": 1,
+          "debugobshistory.shortdescription": "A"
+      }
+  ]
--- a/tests/test-evolve.t	Thu May 18 22:53:01 2017 +0200
+++ b/tests/test-evolve.t	Thu May 18 23:12:52 2017 +0200
@@ -769,9 +769,17 @@
 
 Test olog
 
-  $ hg olog
-  4	: add 4 - test
-  11	: add 3 - test
+  $ hg olog | head -n 10 # hg touch makes the output unstable (fix it with devel option for more stable touch)
+  @    d26d339c513f (12) add 4
+  |\
+  x |    af636757ce3b (11) add 3
+  |\ \     rewritten by test (*) as d26d339c513f (glob)
+  | | |
+  | \ \
+  | |\ \
+  | | | x  ce341209337f (4) add 4
+  | | |      rewritten by test (*) as d26d339c513f (glob)
+  | | |
 
 Test obsstore stat
 
--- a/tests/test-obsolete.t	Thu May 18 22:53:01 2017 +0200
+++ b/tests/test-obsolete.t	Thu May 18 23:12:52 2017 +0200
@@ -1,3 +1,5 @@
+
+  $ . $TESTDIR/testlib/common.sh
   $ cat >> $HGRCPATH <<EOF
   > [web]
   > push_ssl = false
@@ -16,10 +18,6 @@
   >    hg add "$1"
   >    hg ci -m "add $1"
   > }
-  $ getid() {
-  >    hg id --hidden --debug -ir "$1"
-  > }
-
   $ alias qlog="hg log --template='{rev}\n- {node|short}\n'"
   $ hg init local
   $ cd local
@@ -695,10 +693,12 @@
   [10] add obsol_c
   [2]
   $ hg olog
-  changeset:   2:4538525df7e2
-  user:        test
-  date:        Thu Jan 01 00:00:00 1970 +0000
-  summary:     add c
+  @  0d3f46688ccc (3) add obsol_c
+  |    rewritten by test (*) as 2033b4e49474 (glob)
+  |    rewritten by test (Thu Jan 01 00:00:00 1970 +0000) as 725c380fe99b
+  |
+  x  4538525df7e2 (2) add c
+       rewritten by test (Thu Jan 01 00:00:00 1970 +0000) as 0d3f46688ccc
   
 
 Check import reports new unstable changeset:
--- a/tests/test-prev-next.t	Thu May 18 22:53:01 2017 +0200
+++ b/tests/test-prev-next.t	Thu May 18 23:12:52 2017 +0200
@@ -48,6 +48,9 @@
   $ hg bookmarks
      mark                      1:6e742c9127b3
    * mark2                     0:a154386e50d1
+  $ hg next --dry-run --color=debug
+  hg update 1;
+  [[evolve.rev|1]] added b
   $ hg next
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   [1] added b
@@ -148,6 +151,9 @@
   $ hg next --evolve
   no children
   [1]
+  $ hg prev --dry-run --color=debug
+  hg update 1;
+  [[evolve.rev|1]] added b
   $ hg prev
   0 files updated, 0 files merged, 1 files removed, 0 files unresolved
   [1] added b
--- a/tests/test-topic-stack.t	Thu May 18 22:53:01 2017 +0200
+++ b/tests/test-topic-stack.t	Thu May 18 23:12:52 2017 +0200
@@ -79,6 +79,53 @@
   t2: c_d
   t1: c_c
     ^ c_b
+  $ hg stack -Tjson | python -m json.tool
+  [
+      {
+          "isentry": true,
+          "topic.stack.desc": "c_f",
+          "topic.stack.index": 4,
+          "topic.stack.state": [
+              "current"
+          ],
+          "topic.stack.state.symbol": "@"
+      },
+      {
+          "isentry": true,
+          "topic.stack.desc": "c_e",
+          "topic.stack.index": 3,
+          "topic.stack.state": [
+              "clean"
+          ],
+          "topic.stack.state.symbol": ":"
+      },
+      {
+          "isentry": true,
+          "topic.stack.desc": "c_d",
+          "topic.stack.index": 2,
+          "topic.stack.state": [
+              "clean"
+          ],
+          "topic.stack.state.symbol": ":"
+      },
+      {
+          "isentry": true,
+          "topic.stack.desc": "c_c",
+          "topic.stack.index": 1,
+          "topic.stack.state": [
+              "clean"
+          ],
+          "topic.stack.state.symbol": ":"
+      },
+      {
+          "isentry": false,
+          "topic.stack.desc": "c_b",
+          "topic.stack.state": [
+              "base"
+          ],
+          "topic.stack.state.symbol": "^"
+      }
+  ]
 
 error case, nothing to list
 
@@ -137,6 +184,26 @@
   t2@ c_d (current)
   t1: c_c
     ^ c_b
+  $ hg up t3
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg topic --list
+  ### topic: foo
+  ### branch: default
+  t4$ c_f (unstable)
+  t3@ c_e (current)
+  t2: c_d
+  t1: c_c
+    ^ c_b
+  $ hg topic --list --color=debug
+  [topic.stack.summary.topic|### topic: [topic.active|foo]]
+  [topic.stack.summary.branches|### branch: default]
+  [topic.stack.index topic.stack.index.unstable|t4][topic.stack.state topic.stack.state.unstable|$] [topic.stack.desc topic.stack.desc.unstable|c_f][topic.stack.state topic.stack.state.unstable| (unstable)]
+  [topic.stack.index topic.stack.index.current|t3][topic.stack.state topic.stack.state.current|@] [topic.stack.desc topic.stack.desc.current|c_e][topic.stack.state topic.stack.state.current| (current)]
+  [topic.stack.index topic.stack.index.clean|t2][topic.stack.state topic.stack.state.clean|:] [topic.stack.desc topic.stack.desc.clean|c_d]
+  [topic.stack.index topic.stack.index.clean|t1][topic.stack.state topic.stack.state.clean|:] [topic.stack.desc topic.stack.desc.clean|c_c]
+    [topic.stack.state topic.stack.state.base|^] [topic.stack.desc topic.stack.desc.base|c_b]
+  $ hg up t2
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
 
 Also test the revset: