changeset 2261:3e339f6717c7 mercurial-3.9

test-compat-hg-3.9: merge with future 6.0
author Pierre-Yves David <pierre-yves.david@ens-lyon.org>
date Fri, 31 Mar 2017 15:44:10 +0200
parents f1ffd093ef30 (current diff) e200dbfb4515 (diff)
children d65318bf1782 347849e17876
files tests/_exc-util.sh tests/test-evolve.t tests/test-exchange-A3.t tests/test-sharing.t tests/test-topic-push.t tests/test-topic-tutorial.t
diffstat 75 files changed, 4794 insertions(+), 1280 deletions(-) [+]
line wrap: on
line diff
--- a/README	Tue Mar 14 14:47:20 2017 -0700
+++ b/README	Fri Mar 31 15:44:10 2017 +0200
@@ -57,7 +57,7 @@
 
     $ hg clone https://www.mercurial-scm.org/repo/evolve/
     $ cd evolve
-    $ make install-home
+    $ pip install --user .
 
 Then just enable it in you hgrc::
 
@@ -67,8 +67,8 @@
 
 Documentation lives in ``doc/``.
 
-Server Only Version
-===================
+Server Only Setup
+=================
 
 It is possible to enable a smaller subset of the extensions aimed at server
 serving repository. It skips the additions of the new commands and local UI
@@ -87,8 +87,9 @@
 
 .. _evolution: https://bz.mercurial-scm.org/buglist.cgi?component=evolution&query_format=advanced&resolution=---
 
-Please use the patchbomb extension to send email to mercurial devel. Please
-make sure to use the evolve-ext flag when doing so. You can use a command like
+Please use the patchbomb extension to send email to `mercurial devel
+<https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel>`_. Please make
+sure to use the evolve-ext flag when doing so. You can use a command like
 this::
 
     $ hg email --to mercurial-devel@mercurial-scm.org --flag evolve-ext --rev '<your patches>'
@@ -114,6 +115,7 @@
 6.0.0 -- In progress
 --------------------
 
+- push: improved detection of obsoleted remote branch (issue4354),
 - drop compatibility for Mercurial < 3.8,
 - removed old (unpackaged) pushexperiment extension,
 - move all extensions in the official 'hgext3rd' namespace package,
@@ -125,6 +127,24 @@
   to disable obsmarkers echange.  The old '__temporary__.advertiseobsolete'
   option is no longer supported.
 
+- a new prototype of obsmarker discovery is available. The prototype is still
+  at early stage and not recommended for production.
+  Examples of current limitations:
+
+  - write access to the repo is highly recommanded for all operation,
+  - large memory footprint,
+  - initial caching is slow,
+  - unusable on large repo (because of various issue pointed earlier),
+  - likely to constains various bugs.
+
+  It can be tested by setting `experimental.obshashrange=1` on both client and
+  server. It is recommanded to get in touch with the evolve maintainer if you
+  decide to test it.
+
+- the 'debugrecordpruneparents' have been moved into the 'evolve.legacy'
+  separate extension. enable that extentions if you need to convert/update
+  markers in an old repository.
+
 5.6.1 -- 2017-02-28
 -------------------
 
--- a/hgext3rd/evolve/__init__.py	Tue Mar 14 14:47:20 2017 -0700
+++ b/hgext3rd/evolve/__init__.py	Fri Mar 31 15:44:10 2017 +0200
@@ -111,6 +111,8 @@
 from mercurial.node import nullid
 
 from . import (
+    checkheads,
+    debugcmd,
     obsdiscovery,
     obsexchange,
     exthelper,
@@ -143,8 +145,10 @@
 # - Older format compat
 
 eh = exthelper.exthelper()
+eh.merge(debugcmd.eh)
 eh.merge(obsdiscovery.eh)
 eh.merge(obsexchange.eh)
+eh.merge(checkheads.eh)
 uisetup = eh.final_uisetup
 extsetup = eh.final_extsetup
 reposetup = eh.final_reposetup
@@ -814,153 +818,6 @@
     _deprecatealias('gup', 'next')
     _deprecatealias('gdown', 'previous')
 
-@eh.command('debugrecordpruneparents', [], '')
-def cmddebugrecordpruneparents(ui, repo):
-    """add parent data to prune markers when possible
-
-    This command searches the repo for prune markers without parent information.
-    If the pruned node is locally known, it creates a new marker with parent
-    data.
-    """
-    pgop = 'reading markers'
-
-    # lock from the beginning to prevent race
-    wlock = lock = tr = None
-    try:
-        wlock = repo.wlock()
-        lock = repo.lock()
-        tr = repo.transaction('recordpruneparents')
-        unfi = repo.unfiltered()
-        nm = unfi.changelog.nodemap
-        store = repo.obsstore
-        pgtotal = len(store._all)
-        for idx, mark in enumerate(list(store._all)):
-            if not mark[1]:
-                rev = nm.get(mark[0])
-                if rev is not None:
-                    ctx = unfi[rev]
-                    parents = tuple(p.node() for p in ctx.parents())
-                    before = len(store._all)
-                    store.create(tr, mark[0], mark[1], mark[2], mark[3],
-                                 parents=parents)
-                    if len(store._all) - before:
-                        ui.write(_('created new markers for %i\n') % rev)
-            ui.progress(pgop, idx, total=pgtotal)
-        tr.close()
-        ui.progress(pgop, None)
-    finally:
-        lockmod.release(tr, lock, wlock)
-
-@eh.command('debugobsstorestat', [], '')
-def cmddebugobsstorestat(ui, repo):
-    """print statistics about obsolescence markers in the repo"""
-    def _updateclustermap(nodes, mark, clustersmap):
-        c = (set(nodes), set([mark]))
-        toproceed = set(nodes)
-        while toproceed:
-            n = toproceed.pop()
-            other = clustersmap.get(n)
-            if (other is not None
-                and other is not c):
-                other[0].update(c[0])
-                other[1].update(c[1])
-                for on in c[0]:
-                    if on in toproceed:
-                        continue
-                    clustersmap[on] = other
-                c = other
-            clustersmap[n] = c
-
-    store = repo.obsstore
-    unfi = repo.unfiltered()
-    nm = unfi.changelog.nodemap
-    ui.write(_('markers total:              %9i\n') % len(store._all))
-    sucscount = [0, 0, 0, 0]
-    known = 0
-    parentsdata = 0
-    metakeys = {}
-    # node -> cluster mapping
-    #   a cluster is a (set(nodes), set(markers)) tuple
-    clustersmap = {}
-    # same data using parent information
-    pclustersmap = {}
-    for mark in store:
-        if mark[0] in nm:
-            known += 1
-        nbsucs = len(mark[1])
-        sucscount[min(nbsucs, 3)] += 1
-        meta = mark[3]
-        for key, value in meta:
-            metakeys.setdefault(key, 0)
-            metakeys[key] += 1
-        meta = dict(meta)
-        parents = [meta.get('p1'), meta.get('p2')]
-        parents = [node.bin(p) for p in parents if p is not None]
-        if parents:
-            parentsdata += 1
-        # cluster handling
-        nodes = set(mark[1])
-        nodes.add(mark[0])
-        _updateclustermap(nodes, mark, clustersmap)
-        # same with parent data
-        nodes.update(parents)
-        _updateclustermap(nodes, mark, pclustersmap)
-
-    # freezing the result
-    for c in clustersmap.values():
-        fc = (frozenset(c[0]), frozenset(c[1]))
-        for n in fc[0]:
-            clustersmap[n] = fc
-    # same with parent data
-    for c in pclustersmap.values():
-        fc = (frozenset(c[0]), frozenset(c[1]))
-        for n in fc[0]:
-            pclustersmap[n] = fc
-    ui.write(('    for known precursors:   %9i\n' % known))
-    ui.write(('    with parents data:      %9i\n' % parentsdata))
-    # successors data
-    ui.write(('markers with no successors: %9i\n' % sucscount[0]))
-    ui.write(('              1 successors: %9i\n' % sucscount[1]))
-    ui.write(('              2 successors: %9i\n' % sucscount[2]))
-    ui.write(('    more than 2 successors: %9i\n' % sucscount[3]))
-    # meta data info
-    ui.write(('    available  keys:\n'))
-    for key in sorted(metakeys):
-        ui.write(('    %15s:        %9i\n' % (key, metakeys[key])))
-
-    allclusters = list(set(clustersmap.values()))
-    allclusters.sort(key=lambda x: len(x[1]))
-    ui.write(('disconnected clusters:      %9i\n' % len(allclusters)))
-
-    ui.write('        any known node:     %9i\n'
-             % len([c for c in allclusters
-                    if [n for n in c[0] if nm.get(n) is not None]]))
-    if allclusters:
-        nbcluster = len(allclusters)
-        ui.write(('        smallest length:    %9i\n' % len(allclusters[0][1])))
-        ui.write(('        longer length:      %9i\n'
-                 % len(allclusters[-1][1])))
-        median = len(allclusters[nbcluster // 2][1])
-        ui.write(('        median length:      %9i\n' % median))
-        mean = sum(len(x[1]) for x in allclusters) // nbcluster
-        ui.write(('        mean length:        %9i\n' % mean))
-    allpclusters = list(set(pclustersmap.values()))
-    allpclusters.sort(key=lambda x: len(x[1]))
-    ui.write(('    using parents data:     %9i\n' % len(allpclusters)))
-    ui.write('        any known node:     %9i\n'
-             % len([c for c in allclusters
-                    if [n for n in c[0] if nm.get(n) is not None]]))
-    if allpclusters:
-        nbcluster = len(allpclusters)
-        ui.write(('        smallest length:    %9i\n'
-                 % len(allpclusters[0][1])))
-        ui.write(('        longer length:      %9i\n'
-                 % len(allpclusters[-1][1])))
-        median = len(allpclusters[nbcluster // 2][1])
-        ui.write(('        median length:      %9i\n' % median))
-        mean = sum(len(x[1]) for x in allpclusters) // nbcluster
-        ui.write(('        mean length:        %9i\n' % mean))
-
 def _solveone(ui, repo, ctx, dryrun, confirm, progresscb, category):
     """Resolve the troubles affecting one revision"""
     wlock = lock = tr = None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/checkheads.py	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,281 @@
+# Code dedicated to the postprocessing new heads check with obsolescence
+#
+# Copyright 2017 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+import functools
+
+from mercurial import (
+    discovery,
+    error,
+    extensions,
+    node as nodemod,
+    phases,
+    util,
+)
+
+from mercurial.i18n import _
+
+from . import exthelper
+
+nullid = nodemod.nullid
+short = nodemod.short
+_headssummary = discovery._headssummary
+_oldheadssummary = discovery._oldheadssummary
+_nowarnheads = discovery._nowarnheads
+
+eh = exthelper.exthelper()
+
+@eh.uisetup
+def setupcheckheadswrapper(ui):
+    if util.safehasattr(discovery, '_postprocessobsolete'):
+        extensions.wrapfunction(discovery, '_postprocessobsolete',
+                                checkheadslightoverlay)
+    else:
+        extensions.wrapfunction(discovery, 'checkheads',
+                                checkheadsfulloverlay)
+
+# have dedicated wrapper to keep the rest as close as core as possible
+def checkheadsfulloverlay(orig, pushop):
+    if pushop.repo.obsstore:
+        return corecheckheads(pushop)
+    else:
+        return orig(pushop)
+
+def checkheadslightoverlay(orig, *args, **kwargs):
+    return _postprocessobsolete(*args, **kwargs)
+
+# copied from mercurial.discovery.checkheads as in a5bad127128d (4.1)
+#
+# The only differences are:
+# * the _postprocessobsolete section have been extracted,
+# * minor test adjustment to please flake8
+def corecheckheads(pushop):
+    """Check that a push won't add any outgoing head
+
+    raise Abort error and display ui message as needed.
+    """
+
+    repo = pushop.repo.unfiltered()
+    remote = pushop.remote
+    outgoing = pushop.outgoing
+    remoteheads = pushop.remoteheads
+    newbranch = pushop.newbranch
+    inc = bool(pushop.incoming)
+
+    # Check for each named branch if we're creating new remote heads.
+    # To be a remote head after push, node must be either:
+    # - unknown locally
+    # - a local outgoing head descended from update
+    # - a remote head that's known locally and not
+    #   ancestral to an outgoing head
+    if remoteheads == [nullid]:
+        # remote is empty, nothing to check.
+        return
+
+    if remote.capable('branchmap'):
+        headssum = _headssummary(repo, remote, outgoing)
+    else:
+        headssum = _oldheadssummary(repo, remoteheads, outgoing, inc)
+    newbranches = [branch for branch, heads in headssum.iteritems()
+                   if heads[0] is None]
+    # 1. Check for new branches on the remote.
+    if newbranches and not newbranch:  # new branch requires --new-branch
+        branchnames = ', '.join(sorted(newbranches))
+        raise error.Abort(_("push creates new remote branches: %s!")
+                          % branchnames,
+                          hint=_("use 'hg push --new-branch' to create"
+                                 " new remote branches"))
+
+    # 2. Find heads that we need not warn about
+    nowarnheads = _nowarnheads(pushop)
+
+    # 3. Check for new heads.
+    # If there are more heads after the push than before, a suitable
+    # error message, depending on unsynced status, is displayed.
+    errormsg = None
+    # If there is no obsstore, allfuturecommon won't be used, so no
+    # need to compute it.
+    if repo.obsstore:
+        allmissing = set(outgoing.missing)
+        cctx = repo.set('%ld', outgoing.common)
+        allfuturecommon = set(c.node() for c in cctx)
+        allfuturecommon.update(allmissing)
+    for branch, heads in sorted(headssum.iteritems()):
+        remoteheads, newheads, unsyncedheads = heads
+        candidate_newhs = set(newheads)
+        # add unsynced data
+        if remoteheads is None:
+            oldhs = set()
+        else:
+            oldhs = set(remoteheads)
+        oldhs.update(unsyncedheads)
+        candidate_newhs.update(unsyncedheads)
+        dhs = None # delta heads, the new heads on branch
+        if not repo.obsstore:
+            discardedheads = set()
+            newhs = candidate_newhs
+        else:
+            newhs, discardedheads = _postprocessobsolete(pushop,
+                                                         allfuturecommon,
+                                                         candidate_newhs)
+        unsynced = sorted(h for h in unsyncedheads if h not in discardedheads)
+        if unsynced:
+            if None in unsynced:
+                # old remote, no heads data
+                heads = None
+            elif len(unsynced) <= 4 or repo.ui.verbose:
+                heads = ' '.join(short(h) for h in unsynced)
+            else:
+                heads = (' '.join(short(h) for h in unsynced[:4]) +
+                         ' ' + _("and %s others") % (len(unsynced) - 4))
+            if heads is None:
+                repo.ui.status(_("remote has heads that are "
+                                 "not known locally\n"))
+            elif branch is None:
+                repo.ui.status(_("remote has heads that are "
+                                 "not known locally: %s\n") % heads)
+            else:
+                repo.ui.status(_("remote has heads on branch '%s' that are "
+                                 "not known locally: %s\n") % (branch, heads))
+        if remoteheads is None:
+            if len(newhs) > 1:
+                dhs = list(newhs)
+                if errormsg is None:
+                    errormsg = (_("push creates new branch '%s' "
+                                  "with multiple heads") % (branch))
+                    hint = _("merge or"
+                             " see 'hg help push' for details about"
+                             " pushing new heads")
+        elif len(newhs) > len(oldhs):
+            # remove bookmarked or existing remote heads from the new heads list
+            dhs = sorted(newhs - nowarnheads - oldhs)
+        if dhs:
+            if errormsg is None:
+                if branch not in ('default', None):
+                    errormsg = _("push creates new remote head %s "
+                                 "on branch '%s'!") % (short(dhs[0]), branch)
+                elif repo[dhs[0]].bookmarks():
+                    errormsg = (_("push creates new remote head %s "
+                                  "with bookmark '%s'!")
+                                % (short(dhs[0]), repo[dhs[0]].bookmarks()[0]))
+                else:
+                    errormsg = _("push creates new remote head %s!"
+                                 ) % short(dhs[0])
+                if unsyncedheads:
+                    hint = _("pull and merge or"
+                             " see 'hg help push' for details about"
+                             " pushing new heads")
+                else:
+                    hint = _("merge or"
+                             " see 'hg help push' for details about"
+                             " pushing new heads")
+            if branch is None:
+                repo.ui.note(_("new remote heads:\n"))
+            else:
+                repo.ui.note(_("new remote heads on branch '%s':\n") % branch)
+            for h in dhs:
+                repo.ui.note((" %s\n") % short(h))
+    if errormsg:
+        raise error.Abort(errormsg, hint=hint)
+
+def _postprocessobsolete(pushop, futurecommon, candidate):
+    """post process the list of new heads with obsolescence information
+
+    Exist as a subfunction to contains the complexity and allow extensions to
+    experiment with smarter logic.
+    Returns (newheads, discarded_heads) tuple
+    """
+    # remove future heads which are actually obsoleted by another
+    # pushed element:
+    #
+    # known issue
+    #
+    # * We "silently" skip processing on all changeset unknown locally
+    #
+    # * if <nh> is public on the remote, it won't be affected by obsolete
+    #     marker and a new is created
+    repo = pushop.repo
+    unfi = repo.unfiltered()
+    tonode = unfi.changelog.node
+    public = phases.public
+    getphase = unfi._phasecache.phase
+    ispublic = (lambda r: getphase(unfi, r) == public)
+    hasoutmarker = functools.partial(pushingmarkerfor, unfi.obsstore, futurecommon)
+    successorsmarkers = unfi.obsstore.successors
+    newhs = set()
+    discarded = set()
+    # I leave the print in the code because they are so handy at debugging
+    # and I keep getting back to this piece of code.
+    #
+    localcandidate = set()
+    unknownheads = set()
+    for h in candidate:
+        if h in unfi:
+            localcandidate.add(h)
+        else:
+            if successorsmarkers.get(h) is not None:
+                msg = ('checkheads: remote head unknown locally has'
+                       ' local marker: %s\n')
+                repo.ui.debug(msg % nodemod.hex(h))
+            unknownheads.add(h)
+    if len(localcandidate) == 1:
+        return unknownheads | set(candidate), set()
+    while localcandidate:
+        nh = localcandidate.pop()
+        # run this check early to skip the revset on the whole branch
+        if (nh in futurecommon
+                or unfi[nh].phase() <= public):
+            newhs.add(nh)
+            continue
+        # XXX there is a corner case if there is a merge in the branch. we
+        # might end up with -more- heads.  However, these heads are not "added"
+        # by the push, but more by the "removal" on the remote so I think is a
+        # okay to ignore them,
+        branchrevs = unfi.revs('only(%n, (%ln+%ln))',
+                               nh, localcandidate, newhs)
+        branchnodes = [tonode(r) for r in branchrevs]
+
+        # The branch will still exist on the remote if
+        # * any part of it is public,
+        # * any part of it is considered part of the result by previous logic,
+        # * if we have no markers to push to obsolete it.
+        if (any(ispublic(r) for r in branchrevs)
+                or any(n in futurecommon for n in branchnodes)
+                or any(not hasoutmarker(n) for n in branchnodes)):
+            newhs.add(nh)
+        else:
+            discarded.add(nh)
+    newhs |= unknownheads
+    return newhs, discarded
+
+def pushingmarkerfor(obsstore, pushset, node):
+    """True if some markers are to be pushed for node
+
+    We cannot just look in to the pushed obsmarkers from the pushop because
+    discover might have filtered relevant markers. In addition listing all
+    markers relevant to all changeset in the pushed set would be too expensive.
+
+    The is probably some cache opportunity in this function. but it would
+    requires a two dimentions stack.
+    """
+    successorsmarkers = obsstore.successors
+    stack = [node]
+    seen = set(stack)
+    while stack:
+        current = stack.pop()
+        if current in pushset:
+            return True
+        markers = successorsmarkers.get(current, ())
+        # markers fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
+        for m in markers:
+            nexts = m[1] # successors
+            if not nexts: # this is a prune marker
+                nexts = m[5] # parents
+            for n in nexts:
+                if n not in seen:
+                    seen.add(n)
+                    stack.append(n)
+    return False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/debugcmd.py	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,129 @@
+# Code dedicated to debug commands around evolution
+#
+# Copyright 2017 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+# Status: Ready to Upstream
+#
+#  * We could have the same code in core as `hg debugobsolete --stat`,
+#  * We probably want a way for the extension to hook in for extra data.
+
+from mercurial import node
+
+from mercurial.i18n import _
+
+from . import exthelper
+
+eh = exthelper.exthelper()
+
+@eh.command('debugobsstorestat', [], '')
+def cmddebugobsstorestat(ui, repo):
+    """print statistics about obsolescence markers in the repo"""
+    def _updateclustermap(nodes, mark, clustersmap):
+        c = (set(nodes), set([mark]))
+        toproceed = set(nodes)
+        while toproceed:
+            n = toproceed.pop()
+            other = clustersmap.get(n)
+            if (other is not None
+                and other is not c):
+                other[0].update(c[0])
+                other[1].update(c[1])
+                for on in c[0]:
+                    if on in toproceed:
+                        continue
+                    clustersmap[on] = other
+                c = other
+            clustersmap[n] = c
+
+    store = repo.obsstore
+    unfi = repo.unfiltered()
+    nm = unfi.changelog.nodemap
+    ui.write(_('markers total:              %9i\n') % len(store._all))
+    sucscount = [0, 0, 0, 0]
+    known = 0
+    parentsdata = 0
+    metakeys = {}
+    # node -> cluster mapping
+    #   a cluster is a (set(nodes), set(markers)) tuple
+    clustersmap = {}
+    # same data using parent information
+    pclustersmap = {}
+    for mark in store:
+        if mark[0] in nm:
+            known += 1
+        nbsucs = len(mark[1])
+        sucscount[min(nbsucs, 3)] += 1
+        meta = mark[3]
+        for key, value in meta:
+            metakeys.setdefault(key, 0)
+            metakeys[key] += 1
+        meta = dict(meta)
+        parents = [meta.get('p1'), meta.get('p2')]
+        parents = [node.bin(p) for p in parents if p is not None]
+        if parents:
+            parentsdata += 1
+        # cluster handling
+        nodes = set(mark[1])
+        nodes.add(mark[0])
+        _updateclustermap(nodes, mark, clustersmap)
+        # same with parent data
+        nodes.update(parents)
+        _updateclustermap(nodes, mark, pclustersmap)
+
+    # freezing the result
+    for c in clustersmap.values():
+        fc = (frozenset(c[0]), frozenset(c[1]))
+        for n in fc[0]:
+            clustersmap[n] = fc
+    # same with parent data
+    for c in pclustersmap.values():
+        fc = (frozenset(c[0]), frozenset(c[1]))
+        for n in fc[0]:
+            pclustersmap[n] = fc
+    ui.write(('    for known precursors:   %9i\n' % known))
+    ui.write(('    with parents data:      %9i\n' % parentsdata))
+    # successors data
+    ui.write(('markers with no successors: %9i\n' % sucscount[0]))
+    ui.write(('              1 successors: %9i\n' % sucscount[1]))
+    ui.write(('              2 successors: %9i\n' % sucscount[2]))
+    ui.write(('    more than 2 successors: %9i\n' % sucscount[3]))
+    # meta data info
+    ui.write(('    available  keys:\n'))
+    for key in sorted(metakeys):
+        ui.write(('    %15s:        %9i\n' % (key, metakeys[key])))
+
+    allclusters = list(set(clustersmap.values()))
+    allclusters.sort(key=lambda x: len(x[1]))
+    ui.write(('disconnected clusters:      %9i\n' % len(allclusters)))
+
+    ui.write('        any known node:     %9i\n'
+             % len([c for c in allclusters
+                    if [n for n in c[0] if nm.get(n) is not None]]))
+    if allclusters:
+        nbcluster = len(allclusters)
+        ui.write(('        smallest length:    %9i\n' % len(allclusters[0][1])))
+        ui.write(('        longer length:      %9i\n'
+                 % len(allclusters[-1][1])))
+        median = len(allclusters[nbcluster // 2][1])
+        ui.write(('        median length:      %9i\n' % median))
+        mean = sum(len(x[1]) for x in allclusters) // nbcluster
+        ui.write(('        mean length:        %9i\n' % mean))
+    allpclusters = list(set(pclustersmap.values()))
+    allpclusters.sort(key=lambda x: len(x[1]))
+    ui.write(('    using parents data:     %9i\n' % len(allpclusters)))
+    ui.write('        any known node:     %9i\n'
+             % len([c for c in allclusters
+                    if [n for n in c[0] if nm.get(n) is not None]]))
+    if allpclusters:
+        nbcluster = len(allpclusters)
+        ui.write(('        smallest length:    %9i\n'
+                 % len(allpclusters[0][1])))
+        ui.write(('        longer length:      %9i\n'
+                 % len(allpclusters[-1][1])))
+        median = len(allpclusters[nbcluster // 2][1])
+        ui.write(('        median length:      %9i\n' % median))
+        mean = sum(len(x[1]) for x in allpclusters) // nbcluster
+        ui.write(('        mean length:        %9i\n' % mean))
--- a/hgext3rd/evolve/legacy.py	Tue Mar 14 14:47:20 2017 -0700
+++ b/hgext3rd/evolve/legacy.py	Fri Mar 31 15:44:10 2017 +0200
@@ -26,6 +26,7 @@
 
 from mercurial import cmdutil
 from mercurial.i18n import _
+from mercurial import lock as lockmod
 from mercurial.node import bin, nullid
 from mercurial import util
 
@@ -163,3 +164,40 @@
     ui.status('%i obsolete marker converted\n' % cnt)
     if err:
         ui.write_err('%i conversion failed. check you graph!\n' % err)
+
+@command('debugrecordpruneparents', [], '')
+def cmddebugrecordpruneparents(ui, repo):
+    """add parent data to prune markers when possible
+
+    This command searches the repo for prune markers without parent information.
+    If the pruned node is locally known, it creates a new marker with parent
+    data.
+    """
+    pgop = 'reading markers'
+
+    # lock from the beginning to prevent race
+    wlock = lock = tr = None
+    try:
+        wlock = repo.wlock()
+        lock = repo.lock()
+        tr = repo.transaction('recordpruneparents')
+        unfi = repo.unfiltered()
+        nm = unfi.changelog.nodemap
+        store = repo.obsstore
+        pgtotal = len(store._all)
+        for idx, mark in enumerate(list(store._all)):
+            if not mark[1]:
+                rev = nm.get(mark[0])
+                if rev is not None:
+                    ctx = unfi[rev]
+                    parents = tuple(p.node() for p in ctx.parents())
+                    before = len(store._all)
+                    store.create(tr, mark[0], mark[1], mark[2], mark[3],
+                                 parents=parents)
+                    if len(store._all) - before:
+                        ui.write(_('created new markers for %i\n') % rev)
+            ui.progress(pgop, idx, total=pgtotal)
+        tr.close()
+        ui.progress(pgop, None)
+    finally:
+        lockmod.release(tr, lock, wlock)
--- a/hgext3rd/evolve/metadata.py	Tue Mar 14 14:47:20 2017 -0700
+++ b/hgext3rd/evolve/metadata.py	Fri Mar 31 15:44:10 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__ = '5.6.0'
+__version__ = '6.0.0.dev'
 testedwith = '3.8.4 3.9.2 4.0.2 4.1'
 minimumhgversion = '3.8'
 buglink = 'https://bz.mercurial-scm.org/'
--- a/hgext3rd/evolve/obsdiscovery.py	Tue Mar 14 14:47:20 2017 -0700
+++ b/hgext3rd/evolve/obsdiscovery.py	Fri Mar 31 15:44:10 2017 +0200
@@ -5,6 +5,14 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
+# Status: Experiment in progress // open question
+#
+#   The final discovery algorithm and protocol will go into core when we'll be
+#   happy with it.
+#
+#   Some of the code in this module is for compatiblity with older version
+#   of evolve and will be eventually dropped.
+
 from __future__ import absolute_import
 
 try:
@@ -14,16 +22,13 @@
     import io
     StringIO = io.StringIO
 
-import collections
 import hashlib
 import heapq
-import math
+import sqlite3
 import struct
+import weakref
 
 from mercurial import (
-    bundle2,
-    cmdutil,
-    commands,
     dagutil,
     error,
     exchange,
@@ -42,12 +47,15 @@
 from . import (
     exthelper,
     utility,
+    stablerange,
 )
 
 _pack = struct.pack
 _unpack = struct.unpack
+_calcsize = struct.calcsize
 
 eh = exthelper.exthelper()
+eh.merge(stablerange.eh)
 obsexcmsg = utility.obsexcmsg
 
 ##########################################
@@ -145,32 +153,6 @@
 ###  Code performing discovery ###
 ##################################
 
-def _canobshashrange(local, remote):
-    return (local.ui.configbool('experimental', 'obshashrange', False)
-            and remote.capable('_donotusemeever_evoext_obshashrange_1'))
-
-
-def _obshashrange_capabilities(orig, repo, proto):
-    """wrapper to advertise new capability"""
-    caps = orig(repo, proto)
-    enabled = repo.ui.configbool('experimental', 'obshashrange', False)
-    if obsolete.isenabled(repo, obsolete.exchangeopt) and enabled:
-        caps = caps.split()
-        caps.append('_donotusemeever_evoext_obshashrange_1')
-        caps.sort()
-        caps = ' '.join(caps)
-    return caps
-
-@eh.extsetup
-def obshashrange_extsetup(ui):
-    extensions.wrapfunction(wireproto, 'capabilities', _obshashrange_capabilities)
-    # wrap command content
-    oldcap, args = wireproto.commands['capabilities']
-
-    def newcap(repo, proto):
-        return _obshashrange_capabilities(oldcap, repo, proto)
-    wireproto.commands['capabilities'] = (newcap, args)
-
 def findcommonobsmarkers(ui, local, remote, probeset,
                          initialsamplesize=100,
                          fullsamplesize=200):
@@ -234,7 +216,10 @@
     missing = set()
 
     heads = local.revs('heads(%ld)', probeset)
+    local.stablerange.warmup(local)
 
+    rangelength = local.stablerange.rangelength
+    subranges = local.stablerange.subranges
     # size of slice ?
     heappop = heapq.heappop
     heappush = heapq.heappush
@@ -253,7 +238,7 @@
         return True
 
     for h in heads:
-        entry = _range(local, h, 0)
+        entry = (h, 0)
         addentry(entry)
 
     querycount = 0
@@ -269,21 +254,23 @@
             overflow = sample[samplesize:]
             sample = sample[:samplesize]
         elif len(sample) < samplesize:
+            ui.debug("query %i; add more sample (target %i, current %i)\n"
+                     % (querycount, samplesize, len(sample)))
             # we need more sample !
             needed = samplesize - len(sample)
             sliceme = []
             heapify(sliceme)
             for entry in sample:
-                if 1 < len(entry):
-                    heappush(sliceme, (-len(entry), entry))
+                if 1 < rangelength(local, entry):
+                    heappush(sliceme, (-rangelength(local, entry), entry))
 
             while sliceme and 0 < needed:
                 _key, target = heappop(sliceme)
-                for new in target.subranges():
+                for new in subranges(local, target):
                     # XXX we could record hierarchy to optimise drop
-                    if addentry(entry):
-                        if 1 < len(entry):
-                            heappush(sliceme, (-len(entry), entry))
+                    if addentry(new):
+                        if 1 < len(new):
+                            heappush(sliceme, (-rangelength(local, new), new))
                         needed -= 1
                         if needed <= 0:
                             break
@@ -292,181 +279,37 @@
         samplesize = fullsamplesize
 
         nbsample = len(sample)
-        maxsize = max([len(r) for r in sample])
+        maxsize = max([rangelength(local, r) for r in sample])
         ui.debug("query %i; sample size is %i, largest range %i\n"
-                 % (querycount, maxsize, nbsample))
+                 % (querycount, nbsample, maxsize))
         nbreplies = 0
         replies = list(_queryrange(ui, local, remote, sample))
         sample = []
+        n = local.changelog.node
         for entry, remotehash in replies:
             nbreplies += 1
-            if remotehash == entry.obshash:
+            if remotehash == _obshashrange(local, entry):
                 continue
-            elif 1 == len(entry):
-                missing.add(entry.node)
+            elif 1 == rangelength(local, entry):
+                missing.add(n(entry[0]))
             else:
-                for new in entry.subranges():
+                for new in subranges(local, entry):
                     addentry(new)
         assert nbsample == nbreplies
         querycount += 1
         ui.progress(_("comparing obsmarker with other"), querycount)
     ui.progress(_("comparing obsmarker with other"), None)
+    local.obsstore.rangeobshashcache.save(local)
     return sorted(missing)
 
 def _queryrange(ui, repo, remote, allentries):
-    mapping = {}
-
-    def gen():
-        for entry in allentries:
-            key = entry.node + _pack('>I', entry.index)
-            mapping[key] = entry
-            yield key
-
-    bundler = bundle2.bundle20(ui, bundle2.bundle2caps(remote))
-    capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
-    bundler.newpart('replycaps', data=capsblob)
-    bundler.newpart('_donotusemeever_evoext_obshashrange_1', data=gen())
-
-    stream = util.chunkbuffer(bundler.getchunks())
-    try:
-        reply = remote.unbundle(
-            stream, ['force'], remote.url())
-    except error.BundleValueError as exc:
-        raise error.Abort(_('missing support for %s') % exc)
-    try:
-        op = bundle2.processbundle(repo, reply)
-    except error.BundleValueError as exc:
-        raise error.Abort(_('missing support for %s') % exc)
-    except bundle2.AbortFromPart as exc:
-        ui.status(_('remote: %s\n') % exc)
-        if exc.hint is not None:
-            ui.status(_('remote: %s\n') % ('(%s)' % exc.hint))
-        raise error.Abort(_('push failed on remote'))
-    for rep in op.records['_donotusemeever_evoext_obshashrange_1']:
-        yield mapping[rep['key']], rep['value']
-
-
-@bundle2.parthandler('_donotusemeever_evoext_obshashrange_1', ())
-def _processqueryrange(op, inpart):
-    assert op.reply is not None
-    replies = []
-    data = inpart.read(24)
-    while data:
-        n = data[:20]
-        index = _unpack('>I', data[20:])[0]
-        r = op.repo.changelog.rev(n)
-        rhash = _range(op.repo, r, index).obshash
-        replies.append(data + rhash)
-        data = inpart.read(24)
-    op.reply.newpart('reply:_donotusemeever_evoext_obshashrange_1', data=iter(replies))
-
-
-@bundle2.parthandler('reply:_donotusemeever_evoext_obshashrange_1', ())
-def _processqueryrangereply(op, inpart):
-    data = inpart.read(44)
-    while data:
-        key = data[:24]
-        rhash = data[24:]
-        op.records.add('_donotusemeever_evoext_obshashrange_1', {'key': key, 'value': rhash})
-        data = inpart.read(44)
-
-##################################
-### Stable topological sorting ###
-##################################
-@eh.command(
-    'debugstablesort',
-    [
-        ('', 'rev', [], 'heads to start from'),
-    ] + commands.formatteropts,
-    _(''))
-def debugstablesort(ui, repo, **opts):
-    """display the ::REVS set topologically sorted in a stable way
-    """
-    revs = scmutil.revrange(repo, opts['rev'])
-    displayer = cmdutil.show_changeset(ui, repo, opts, buffered=True)
-    for r in _stablesort(repo, revs):
-        ctx = repo[r]
-        displayer.show(ctx)
-        displayer.flush(ctx)
-    displayer.close()
-
-def _stablesort(repo, revs):
-    """return '::revs' topologically sorted in "stable" order
-
-    This is a depth first traversal starting from 'nullrev', using node as a
-    tie breaker.
-    """
-    # Various notes:
-    #
-    # * Bitbucket is used dates as tie breaker, that might be a good idea.
-    #
-    # * It seemds we can traverse in the same order from (one) head to bottom,
-    #   if we the following record data for each merge:
-    #
-    #  - highest (stablesort-wise) common ancestors,
-    #  - order of parents (tablesort-wise)
-    cl = repo.changelog
-    parents = cl.parentrevs
-    nullrev = node.nullrev
-    n = cl.node
-    # step 1: We need a parents -> children mapping for 2 reasons.
-    #
-    # * we build the order from nullrev to tip
-    #
-    # * we need to detect branching
-    children = collections.defaultdict(list)
-    for r in cl.ancestors(revs, inclusive=True):
-        p1, p2 = parents(r)
-        children[p1].append(r)
-        if p2 != nullrev:
-            children[p2].append(r)
-    # step two: walk back up
-    # * pick lowest node in case of branching
-    # * stack disregarded part of the branching
-    # * process merge when both parents are yielded
-
-    # track what changeset has been
-    seen = [0] * (max(revs) + 2)
-    seen[-1] = True # nullrev is known
-    # starts from repository roots
-    # reuse the list form the mapping as we won't need it again anyway
-    stack = children[nullrev]
-    if not stack:
-        return []
-    if 1 < len(stack):
-        stack.sort(key=n, reverse=True)
-
-    # list of rev, maybe we should yield, but since we built a children mapping we are 'O(N)' already
+    #  question are asked with node
+    n = repo.changelog.node
+    noderanges = [(n(entry[0]), entry[1]) for entry in allentries]
+    replies = remote.evoext_obshashrange_v0(noderanges)
     result = []
-
-    current = stack.pop()
-    while current is not None or stack:
-        if current is None:
-            # previous iteration reached a merge or an unready merge,
-            current = stack.pop()
-            if seen[current]:
-                current = None
-                continue
-        p1, p2 = parents(current)
-        if not (seen[p1] and seen[p2]):
-            # we can't iterate on this merge yet because other child is not
-            # yielded yet (and we are topo sorting) we can discard it for now
-            # because it will be reached from the other child.
-            current = None
-            continue
-        assert not seen[current]
-        seen[current] = True
-        result.append(current) # could be yield, cf earlier comment
-        cs = children[current]
-        if not cs:
-            current = None
-        elif 1 == len(cs):
-            current = cs[0]
-        else:
-            cs.sort(key=n, reverse=True)
-            current = cs.pop() # proceed on smallest
-            stack.extend(cs)   # stack the rest for later
-    assert len(result) == len(set(result))
+    for idx, entry in enumerate(allentries):
+        result.append((entry, replies[idx]))
     return result
 
 ##############################
@@ -474,188 +317,330 @@
 ##############################
 
 @eh.command(
-    'debugstablerange',
+    'debugobshashrange',
     [
-        ('', 'rev', [], 'heads to start from'),
+        ('', 'rev', [], 'display obshash for all (rev, 0) range in REVS'),
+        ('', 'subranges', False, 'display all subranges'),
     ],
     _(''))
-def debugstablerange(ui, repo, **opts):
+def debugobshashrange(ui, repo, **opts):
     """display the ::REVS set topologically sorted in a stable way
     """
     s = node.short
     revs = scmutil.revrange(repo, opts['rev'])
     # prewarm depth cache
-    for r in repo.revs("::%ld", revs):
-        utility.depth(repo, r)
-    toproceed = [_range(repo, r, 0, ) for r in revs]
-    ranges = set(toproceed)
-    while toproceed:
-        entry = toproceed.pop()
-        for r in entry.subranges():
-            if r not in ranges:
-                ranges.add(r)
-                toproceed.append(r)
-    ranges = list(ranges)
-    ranges.sort(key=lambda r: (-len(r), r.node))
-    ui.status('rev         node index size depth      obshash\n')
+    if revs:
+        repo.stablerange.warmup(repo, max(revs))
+    cl = repo.changelog
+    rangelength = repo.stablerange.rangelength
+    depthrev = repo.stablerange.depthrev
+    if opts['subranges']:
+        ranges = stablerange.subrangesclosure(repo, revs)
+    else:
+        ranges = [(r, 0) for r in revs]
+    headers = ('rev', 'node', 'index', 'size', 'depth', 'obshash')
+    linetemplate = '%12d %12s %12d %12d %12d %12s\n'
+    headertemplate = linetemplate.replace('d', 's')
+    ui.status(headertemplate % headers)
     for r in ranges:
-        d = (r.head, s(r.node), r.index, len(r), r.depth, node.short(r.obshash))
-        ui.status('%3d %s %5d %4d %5d %s\n' % d)
+        d = (r[0],
+             s(cl.node(r[0])),
+             r[1],
+             rangelength(repo, r),
+             depthrev(repo, r[0]),
+             node.short(_obshashrange(repo, r)))
+        ui.status(linetemplate % d)
+    repo.obsstore.rangeobshashcache.save(repo)
 
-def _hlp2(i):
-    """return highest power of two lower than 'i'"""
-    return 2 ** int(math.log(i - 1, 2))
+def _obshashrange(repo, rangeid):
+    """return the obsolete hash associated to a range"""
+    cache = repo.obsstore.rangeobshashcache
+    cl = repo.changelog
+    obshash = cache.get(rangeid)
+    if obshash is not None:
+        return obshash
+    pieces = []
+    nullid = node.nullid
+    if repo.stablerange.rangelength(repo, rangeid) == 1:
+        rangenode = cl.node(rangeid[0])
+        tmarkers = repo.obsstore.relevantmarkers([rangenode])
+        pieces = []
+        for m in tmarkers:
+            mbin = obsolete._fm1encodeonemarker(m)
+            pieces.append(mbin)
+        pieces.sort()
+    else:
+        for subrange in repo.stablerange.subranges(repo, rangeid):
+            obshash = _obshashrange(repo, subrange)
+            if obshash != nullid:
+                pieces.append(obshash)
 
-class _range(object):
+    sha = hashlib.sha1()
+    # note: if there is only one subrange with actual data, we'll just
+    # reuse the same hash.
+    if not pieces:
+        obshash = node.nullid
+    elif len(pieces) != 1 or obshash is None:
+        sha = hashlib.sha1()
+        for p in pieces:
+            sha.update(p)
+        obshash = sha.digest()
+    cache[rangeid] = obshash
+    return obshash
+
+### sqlite caching
 
-    def __init__(self, repo, head, index, revs=None):
-        self._repo = repo.unfiltered()
-        self.head = head
-        self.index = index
-        if revs is not None:
-            assert len(revs) == len(self)
-            self._revs = revs
-        assert index < self.depth, (head, index, self.depth, revs)
+_sqliteschema = [
+    """CREATE TABLE meta(schemaversion INTEGER NOT NULL,
+                         nbobsmarker   INTEGER NOT NULL,
+                         obstipdata    BLOB    NOT NULL,
+                         tiprev        INTEGER NOT NULL,
+                         tipnode       BLOB    NOT NULL
+                        );""",
+    """CREATE TABLE obshashrange(rev     INTEGER NOT NULL,
+                                 idx     INTEGER NOT NULL,
+                                 obshash BLOB    NOT NULL,
+                                 PRIMARY KEY(rev, idx));""",
+    "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 (?,?,?,?,?);"""
+_updateobshash = "INSERT INTO obshashrange(rev, idx, obshash) VALUES (?,?,?);"
+_querymeta = "SELECT schemaversion, nbobsmarker, obstipdata, tiprev, tipnode FROM meta;"
+_queryobshash = "SELECT obshash FROM obshashrange WHERE (rev = ? AND idx = ?);"
 
-    def __repr__(self):
-        return '%s %d %d %s' % (node.short(self.node), self.depth, self.index, node.short(self.obshash))
+class _obshashcache(dict):
 
-    def __hash__(self):
-        return self._id
+    _schemaversion = 0
 
-    def __eq__(self, other):
-        if type(self) != type(other):
-            raise NotImplementedError()
-        return self.stablekey == other.stablekey
+    def __init__(self, repo):
+        super(_obshashcache, self).__init__()
+        self._path = repo.vfs.join('cache/evoext_obshashrange_v0.sqlite')
+        self._new = set()
+        self._valid = True
+        self._repo = weakref.ref(repo.unfiltered())
+        # cache status
+        self._ondiskcachekey = None
 
-    @util.propertycache
-    def _id(self):
-        return hash(self.stablekey)
+    def clear(self):
+        self._valid = False
+        super(_obshashcache, self).clear()
+        self._new.clear()
 
-    @util.propertycache
-    def stablekey(self):
-        return (self.node, self.index)
+    def get(self, rangeid):
+        value = super(_obshashcache, self).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]
+        return value
 
-    @util.propertycache
-    def node(self):
-        return self._repo.changelog.node(self.head)
+    def __setitem__(self, rangeid, obshash):
+        self._new.add(rangeid)
+        super(_obshashcache, self).__setitem__(rangeid, obshash)
 
-    def __len__(self):
-        return self.depth - self.index
-
-    @util.propertycache
-    def depth(self):
-        return utility.depth(self._repo, self.head)
+    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)
 
     @util.propertycache
-    def _revs(self):
-        r = _stablesort(self._repo, [self.head])[self.index:]
-        assert len(r) == len(self), (self.head, self.index, len(r), len(self))
-        return r
-
-    def _slicesat(self, globalindex):
-        localindex = globalindex - self.index
-
-        cl = self._repo.changelog
-
-        result = []
-        bottom = self._revs[:localindex]
-        top = _range(self._repo, self.head, globalindex, self._revs[localindex:])
-        #
-        toprootdepth = utility.depth(self._repo, top._revs[0])
-        if toprootdepth + len(top) == self.depth + 1:
-            bheads = [bottom[-1]]
-        else:
-            bheads = set(bottom)
-            parentrevs = cl.parentrevs
-            du = bheads.difference_update
-            for r in bottom:
-                du(parentrevs(r))
-            # if len(bheads) == 1:
-            #     assert 1 == len(self._repo.revs('roots(%ld)', top._revs))
-        if len(bheads) == 1:
-            newhead = bottom[-1]
-            bottomdepth = utility.depth(self._repo, newhead)
-            newstart = bottomdepth - len(bottom)
-            result.append(_range(self._repo, newhead, newstart, bottom))
-        else:
-            # assert 1 < len(bheads), (toprootdepth, len(top), len(self))
-            cl = self._repo.changelog
-            for h in bheads:
-                subset = cl.ancestors([h], inclusive=True)
-                hrevs = [r for r in bottom if r in subset]
-                start = utility.depth(self._repo, h) - len(hrevs)
-                entry = _range(self._repo, h, start, [r for r in bottom if r in subset])
-                result.append(entry)
-        result.append(top)
-        return result
+    def _con(self):
+        if not self._valid:
+            return None
+        repo = self._repo()
+        if repo is None:
+            return None
+        cachekey = self._cachekey(repo)
+        con = sqlite3.connect(self._path)
+        con.text_factory = str
+        cur = con.execute(_queryexist)
+        if cur.fetchone() is None:
+            self._valid = False
+            return None
+        meta = con.execute(_querymeta).fetchone()
+        if meta != cachekey:
+            self._valid = False
+            return None
+        self._ondiskcachekey = meta
+        return con
 
-    def subranges(self):
-        if not util.safehasattr(self._repo, '_subrangecache'):
-            self._repo._subrangecache = {}
-        cached = self._repo._subrangecache.get(self)
-        if cached is not None:
-            return cached
-        if len(self) == 1:
-            return []
-        step = _hlp2(self.depth)
-        standard_start = 0
-        while standard_start < self.index and 0 < step:
-            if standard_start + step < self.depth:
-                standard_start += step
-            step //= 2
-        if self.index == standard_start:
-            slicesize = _hlp2(len(self))
-            slicepoint = self.index + slicesize
-        else:
-            assert standard_start < self.depth
-            slicepoint = standard_start
-        result = self._slicesat(slicepoint)
-        self._repo._subrangecache[self] = result
-        return result
+    def save(self, repo):
+        repo = repo.unfiltered()
+        try:
+            if not self._new:
+                return
+            with repo.lock():
+                self._save(repo)
+        except error.LockError:
+            # Exceptionnally we are noisy about it since performance impact
+            # is large We should address that before using this more
+            # widely.
+            msg = _('obshashrange cache: skipping save unable to lock repo\n')
+            repo.ui.warn(msg)
+
+    def _save(self, repo):
+        if self._con is None:
+            util.unlinkpath(self._path, ignoremissing=True)
+            if '_con' in vars(self):
+                del self._con
 
-    @util.propertycache
-    def obshash(self):
-        cache = self._repo.obsstore.rangeobshashcache
-        obshash = cache.get(self)
-        if obshash is not None:
-            return obshash
-        pieces = []
-        nullid = node.nullid
-        if len(self) == 1:
-            tmarkers = self._repo.obsstore.relevantmarkers([self.node])
-            pieces = []
-            for m in tmarkers:
-                mbin = obsolete._fm1encodeonemarker(m)
-                pieces.append(mbin)
-            pieces.sort()
+            con = sqlite3.connect(self._path)
+            con.text_factory = str
+            with con:
+                for req in _sqliteschema:
+                    con.execute(req)
+
+                con.execute(_newmeta, self._cachekey(repo))
         else:
-            for subrange in self.subranges():
-                obshash = subrange.obshash
-                if obshash != nullid:
-                    pieces.append(obshash)
-
-        sha = hashlib.sha1()
-        # note: if there is only one subrange with actual data, we'll just
-        # reuse the same hash.
-        if not pieces:
-            obshash = node.nullid
-        elif len(pieces) != 1 or obshash is None:
-            sha = hashlib.sha1()
-            for p in pieces:
-                sha.update(p)
-            obshash = cache[self] = sha.digest()
-        return obshash
+            con = self._con
+            if self._ondiskcachekey is not None:
+                meta = con.execute(_querymeta).fetchone()
+                if meta != 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)
+                    repo.ui.warn(msg)
+        data = ((rangeid[0], rangeid[1], self[rangeid]) for rangeid in self._new)
+        con.executemany(_updateobshash, data)
+        cachekey = self._cachekey(repo)
+        con.execute(_newmeta, cachekey)
+        con.commit()
+        self._new.clear()
+        self._ondiskcachekey = cachekey
 
 @eh.wrapfunction(obsolete.obsstore, '_addmarkers')
 def _addmarkers(orig, obsstore, *args, **kwargs):
     obsstore.rangeobshashcache.clear()
     return orig(obsstore, *args, **kwargs)
 
-@eh.addattr(obsolete.obsstore, 'rangeobshashcache')
-@util.propertycache
-def rangeobshashcache(obsstore):
-    return {}
+try:
+    obsstorefilecache = localrepo.localrepository.obsstore
+except AttributeError:
+    # XXX hg-3.8 compat
+    #
+    # mercurial 3.8 has issue with accessing file cache property from their
+    # cache. This is fix by 36fbd72c2f39fef8ad52d7c559906c2bc388760c in core
+    # and shipped in 3.9
+    obsstorefilecache = localrepo.localrepository.__dict__['obsstore']
+
+
+# obsstore is a filecache so we have do to some spacial dancing
+@eh.wrapfunction(obsstorefilecache, 'func')
+def obsstorewithcache(orig, repo):
+    obsstore = orig(repo)
+    obsstore.rangeobshashcache = _obshashcache(repo.unfiltered())
+    return obsstore
+
+@eh.reposetup
+def setupcache(ui, repo):
+
+    class obshashrepo(repo.__class__):
+        @localrepo.unfilteredmethod
+        def destroyed(self):
+            if 'stablerange' in vars(self):
+                del self.stablerange
+
+    repo.__class__ = obshashrepo
+
+### wire protocol commands
+
+def _obshashrange_v0(repo, ranges):
+    """return a list of hash from a list of range
+
+    The range have the id encoded as a node
+
+    return 'wdirid' for unknown range"""
+    nm = repo.changelog.nodemap
+    ranges = [(nm.get(n), idx) for n, idx in ranges]
+    if ranges:
+        maxrev = max(r for r, i in ranges)
+        if maxrev is not None:
+            repo.stablerange.warmup(repo, upto=maxrev)
+    result = []
+    for r in ranges:
+        if r[0] is None:
+            result.append(node.wdirid)
+        else:
+            result.append(_obshashrange(repo, r))
+    repo.obsstore.rangeobshashcache.save(repo)
+    return result
+
+@eh.addattr(localrepo.localpeer, 'evoext_obshashrange_v0')
+def local_obshashrange_v0(peer, ranges):
+    return _obshashrange_v0(peer._repo, ranges)
+
+
+_indexformat = '>I'
+_indexsize = _calcsize(_indexformat)
+def _encrange(node_rangeid):
+    """encode a (node) range"""
+    headnode, index = node_rangeid
+    return headnode + _pack(_indexformat, index)
+
+def _decrange(data):
+    """encode a (node) range"""
+    assert _indexsize < len(data), len(data)
+    headnode = data[:-_indexsize]
+    index = _unpack(_indexformat, data[-_indexsize:])[0]
+    return (headnode, index)
+
+@eh.addattr(wireproto.wirepeer, 'evoext_obshashrange_v0')
+def peer_obshashrange_v0(self, ranges):
+    binranges = [_encrange(r) for r in ranges]
+    encranges = wireproto.encodelist(binranges)
+    d = self._call("evoext_obshashrange_v0", ranges=encranges)
+    try:
+        return wireproto.decodelist(d)
+    except ValueError:
+        self._abort(error.ResponseError(_("unexpected response:"), d))
+
+def srv_obshashrange_v0(repo, proto, ranges):
+    ranges = wireproto.decodelist(ranges)
+    ranges = [_decrange(r) for r in ranges]
+    hashes = _obshashrange_v0(repo, ranges)
+    return wireproto.encodelist(hashes)
+
+
+def _canobshashrange(local, remote):
+    return (local.ui.configbool('experimental', 'obshashrange', False)
+            and remote.capable('_evoext_obshashrange_v0'))
+
+def _obshashrange_capabilities(orig, repo, proto):
+    """wrapper to advertise new capability"""
+    caps = orig(repo, proto)
+    enabled = repo.ui.configbool('experimental', 'obshashrange', False)
+    if obsolete.isenabled(repo, obsolete.exchangeopt) and enabled:
+        caps = caps.split()
+        caps.append('_evoext_obshashrange_v0')
+        caps.sort()
+        caps = ' '.join(caps)
+    return caps
+
+@eh.extsetup
+def obshashrange_extsetup(ui):
+    hgweb_mod.perms['evoext_obshashrange_v0'] = 'pull'
+
+    wireproto.commands['evoext_obshashrange_v0'] = (srv_obshashrange_v0, 'ranges')
+    ###
+    extensions.wrapfunction(wireproto, 'capabilities', _obshashrange_capabilities)
+    # wrap command content
+    oldcap, args = wireproto.commands['capabilities']
+
+    def newcap(repo, proto):
+        return _obshashrange_capabilities(oldcap, repo, proto)
+    wireproto.commands['capabilities'] = (newcap, args)
 
 #############################
 ### Tree Hash computation ###
--- a/hgext3rd/evolve/obsexchange.py	Tue Mar 14 14:47:20 2017 -0700
+++ b/hgext3rd/evolve/obsexchange.py	Fri Mar 31 15:44:10 2017 +0200
@@ -34,7 +34,6 @@
 
 from . import (
     exthelper,
-    serveronly,
     utility,
     obsdiscovery,
 )
@@ -338,7 +337,7 @@
 @eh.addattr(localrepo.localpeer, 'evoext_pushobsmarkers_0')
 def local_pushobsmarkers(peer, obsfile):
     data = obsfile.read()
-    serveronly._pushobsmarkers(peer._repo, data)
+    _pushobsmarkers(peer._repo, data)
 
 # compat-code: _pullobsolete
 #
@@ -417,8 +416,8 @@
 
 @eh.addattr(localrepo.localpeer, 'evoext_pullobsmarkers_0')
 def local_pullobsmarkers(self, heads=None, common=None):
-    return serveronly._getobsmarkersstream(self._repo, heads=heads,
-                                           common=common)
+    return _getobsmarkersstream(self._repo, heads=heads,
+                                common=common)
 
 def _legacypush_capabilities(orig, repo, proto):
     """wrapper to advertise new capability"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/stablerange.py	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,907 @@
+# Code dedicated to the computation and properties of "stable ranges"
+#
+# These stable ranges are use for obsolescence markers discovery
+#
+# Copyright 2017 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+import collections
+import heapq
+import math
+import sqlite3
+import weakref
+
+from mercurial import (
+    commands,
+    cmdutil,
+    error,
+    localrepo,
+    node as nodemod,
+    scmutil,
+    util,
+)
+
+from mercurial.i18n import _
+
+from . import (
+    exthelper,
+)
+
+eh = exthelper.exthelper()
+
+##################################
+### Stable topological sorting ###
+##################################
+@eh.command(
+    'debugstablesort',
+    [
+        ('', 'rev', [], 'heads to start from'),
+    ] + commands.formatteropts,
+    _(''))
+def debugstablesort(ui, repo, **opts):
+    """display the ::REVS set topologically sorted in a stable way
+    """
+    revs = scmutil.revrange(repo, opts['rev'])
+    displayer = cmdutil.show_changeset(ui, repo, opts, buffered=True)
+    for r in stablesort(repo, revs):
+        ctx = repo[r]
+        displayer.show(ctx)
+        displayer.flush(ctx)
+    displayer.close()
+
+def stablesort(repo, revs, mergecallback=None):
+    """return '::revs' topologically sorted in "stable" order
+
+    This is a depth first traversal starting from 'nullrev', using node as a
+    tie breaker.
+    """
+    # Various notes:
+    #
+    # * Bitbucket is used dates as tie breaker, that might be a good idea.
+    #
+    # * It seemds we can traverse in the same order from (one) head to bottom,
+    #   if we the following record data for each merge:
+    #
+    #  - highest (stablesort-wise) common ancestors,
+    #  - order of parents (tablesort-wise)
+    cl = repo.changelog
+    parents = cl.parentrevs
+    nullrev = nodemod.nullrev
+    n = cl.node
+    # step 1: We need a parents -> children mapping for 2 reasons.
+    #
+    # * we build the order from nullrev to tip
+    #
+    # * we need to detect branching
+    children = collections.defaultdict(list)
+    for r in cl.ancestors(revs, inclusive=True):
+        p1, p2 = parents(r)
+        children[p1].append(r)
+        if p2 != nullrev:
+            children[p2].append(r)
+    # step two: walk back up
+    # * pick lowest node in case of branching
+    # * stack disregarded part of the branching
+    # * process merge when both parents are yielded
+
+    # track what changeset has been
+    seen = [0] * (max(revs) + 2)
+    seen[-1] = True # nullrev is known
+    # starts from repository roots
+    # reuse the list form the mapping as we won't need it again anyway
+    stack = children[nullrev]
+    if not stack:
+        return []
+    if 1 < len(stack):
+        stack.sort(key=n, reverse=True)
+
+    # list of rev, maybe we should yield, but since we built a children mapping we are 'O(N)' already
+    result = []
+
+    current = stack.pop()
+    while current is not None or stack:
+        if current is None:
+            # previous iteration reached a merge or an unready merge,
+            current = stack.pop()
+            if seen[current]:
+                current = None
+                continue
+        p1, p2 = parents(current)
+        if not (seen[p1] and seen[p2]):
+            # we can't iterate on this merge yet because other child is not
+            # yielded yet (and we are topo sorting) we can discard it for now
+            # because it will be reached from the other child.
+            current = None
+            continue
+        assert not seen[current]
+        seen[current] = True
+        result.append(current) # could be yield, cf earlier comment
+        if mergecallback is not None and p2 != nullrev:
+            mergecallback(result, current)
+        cs = children[current]
+        if not cs:
+            current = None
+        elif 1 == len(cs):
+            current = cs[0]
+        else:
+            cs.sort(key=n, reverse=True)
+            current = cs.pop() # proceed on smallest
+            stack.extend(cs)   # stack the rest for later
+    assert len(result) == len(set(result))
+    return result
+
+#################################
+### Stable Range computation  ###
+#################################
+
+def _hlp2(i):
+    """return highest power of two lower than 'i'"""
+    return 2 ** int(math.log(i - 1, 2))
+
+def subrangesclosure(repo, heads):
+    """set of all standard subrange under heads
+
+    This is intended for debug purposes. Range are returned from largest to
+    smallest in terms of number of revision it contains."""
+    subranges = repo.stablerange.subranges
+    toproceed = [(r, 0, ) for r in heads]
+    ranges = set(toproceed)
+    while toproceed:
+        entry = toproceed.pop()
+        for r in subranges(repo, entry):
+            if r not in ranges:
+                ranges.add(r)
+                toproceed.append(r)
+    ranges = list(ranges)
+    n = repo.changelog.node
+    rangelength = repo.stablerange.rangelength
+    ranges.sort(key=lambda r: (-rangelength(repo, r), n(r[0])))
+    return ranges
+
+@eh.command(
+    'debugstablerange',
+    [
+        ('', 'rev', [], 'operate on (rev, 0) ranges for rev in REVS'),
+        ('', 'subranges', False, 'recursively display data for subranges too'),
+        ('', 'verify', False, 'checks subranges content (EXPENSIVE)'),
+    ],
+    _(''))
+def debugstablerange(ui, repo, **opts):
+    """display standard stable subrange for a set of ranges
+
+    Range as displayed as '<node>-<index> (<rev>, <depth>, <length>)', use
+    --verbose to get the extra details in ().
+    """
+    short = nodemod.short
+    revs = scmutil.revrange(repo, opts['rev'])
+    # prewarm depth cache
+    unfi = repo.unfiltered()
+    node = unfi.changelog.node
+    stablerange = unfi.stablerange
+    depth = stablerange.depthrev
+    length = stablerange.rangelength
+    subranges = stablerange.subranges
+    repo.stablerange.warmup(repo, max(revs))
+    if opts['subranges']:
+        ranges = subrangesclosure(repo, revs)
+    else:
+        ranges = [(r, 0) for r in revs]
+    if ui.verbose:
+        template = '%s-%d (%d, %d, %d)'
+
+        def _rangestring(repo, rangeid):
+            return template % (
+                short(node(rangeid[0])),
+                rangeid[1],
+                rangeid[0],
+                depth(unfi, rangeid[0]),
+                length(unfi, rangeid)
+            )
+    else:
+        template = '%s-%d'
+
+        def _rangestring(repo, rangeid):
+            return template % (
+                short(node(rangeid[0])),
+                rangeid[1],
+            )
+
+    for r in ranges:
+        subs = subranges(unfi, r)
+        subsstr = ', '.join(_rangestring(unfi, s) for s in subs)
+        rstr = _rangestring(unfi, r)
+        if opts['verify']:
+            status = 'leaf'
+            if 1 < length(unfi, r):
+                status = 'complete'
+                revs = set(stablerange.revsfromrange(unfi, r))
+                subrevs = set()
+                for s in subs:
+                    subrevs.update(stablerange.revsfromrange(unfi, s))
+                if revs != subrevs:
+                    status = 'missing'
+            ui.status('%s [%s] - %s\n' % (rstr, status, subsstr))
+        else:
+            ui.status('%s - %s\n' % (rstr, subsstr))
+
+class stablerange(object):
+
+    def __init__(self):
+        # The point up to which we have data in cache
+        self._tiprev = None
+        self._tipnode = None
+        # cache the 'depth' of a changeset, the size of '::rev'
+        self._depthcache = {}
+        # cache the standard stable subranges or a range
+        self._subrangescache = {}
+        # To slices merge, we need to walk their descendant in reverse stable
+        # sort order. For now we perform a full stable sort their descendant
+        # and then use the relevant top most part. This order is going to be
+        # the same for all ranges headed at the same merge. So we cache these
+        # value to reuse them accross the same invocation.
+        self._stablesortcache = {}
+        # something useful to compute the above
+        # mergerev -> stablesort, length
+        self._stablesortprepared = {}
+        # caching parent call # as we do so many of them
+        self._parentscache = {}
+        # The first part of the stable sorted list of revision of a merge will
+        # shared with the one of others. This means we can reuse subranges
+        # computed from that point to compute some of the subranges from the
+        # merge.
+        self._inheritancecache = {}
+
+    def warmup(self, repo, upto=None):
+        """warm the cache up"""
+        repo = repo.unfiltered()
+        cl = repo.changelog
+        # subrange should be warmed from head to range to be able to benefit
+        # from revsfromrange cache. otherwise each merge will trigger its own
+        # stablesort.
+        #
+        # we use the revnumber as an approximation for depth
+        ui = repo.ui
+
+        if upto is None:
+            upto = len(cl) - 1
+        if self._tiprev is None:
+            revs = cl.revs(stop=upto)
+            nbrevs = upto + 1
+        else:
+            assert cl.node(self._tiprev) == self._tipnode
+            if upto <= self._tiprev:
+                return
+            revs = cl.revs(start=self._tiprev + 1, stop=upto)
+            nbrevs = upto - self._tiprev
+        rangeheap = []
+        for idx, r in enumerate(revs):
+            if not idx % 1000:
+                ui.progress(_("filling depth cache"), idx, total=nbrevs)
+            # warm up depth
+            self.depthrev(repo, r)
+            rangeheap.append((-r, (r, 0)))
+        ui.progress(_("filling depth cache"), None, total=nbrevs)
+
+        heappop = heapq.heappop
+        heappush = heapq.heappush
+        heapify = heapq.heapify
+
+        original = set(rangeheap)
+        seen = 0
+        heapify(rangeheap)
+        while rangeheap:
+            value = heappop(rangeheap)
+            if value in original:
+                if not seen % 1000:
+                    ui.progress(_("filling stablerange cache"), seen, total=nbrevs)
+                seen += 1
+                original.remove(value) # might have been added from other source
+            __, rangeid = value
+            if self._getsub(rangeid) is None:
+                for sub in self.subranges(repo, rangeid):
+                    if self._getsub(sub) is None:
+                        heappush(rangeheap, (-sub[0], sub))
+        ui.progress(_("filling stablerange cache"), None, total=nbrevs)
+
+        self._tiprev = upto
+        self._tipnode = cl.node(upto)
+
+    def depthrev(self, repo, rev):
+        repo = repo.unfiltered()
+        cl = repo.changelog
+        depth = self._getdepth
+        nullrev = nodemod.nullrev
+        stack = [rev]
+        while stack:
+            revdepth = None
+            current = stack[-1]
+            revdepth = depth(current)
+            if revdepth is not None:
+                stack.pop()
+                continue
+            p1, p2 = self._parents(current, cl.parentrevs)
+            if p1 == nullrev:
+                # root case
+                revdepth = 1
+            elif p2 == nullrev:
+                # linear commit case
+                parentdepth = depth(p1)
+                if parentdepth is None:
+                    stack.append(p1)
+                else:
+                    revdepth = parentdepth + 1
+            else:
+                # merge case
+                revdepth = self._depthmerge(cl, current, p1, p2, stack)
+            if revdepth is not None:
+                self._setdepth(current, revdepth)
+                stack.pop()
+        # actual_depth = len(list(cl.ancestors([rev], inclusive=True)))
+        # assert revdepth == actual_depth, (rev, revdepth, actual_depth)
+        return revdepth
+
+    def rangelength(self, repo, rangeid):
+        headrev, index = rangeid[0], rangeid[1]
+        return self.depthrev(repo, headrev) - index
+
+    def subranges(self, repo, rangeid):
+        cached = self._getsub(rangeid)
+        if cached is not None:
+            return cached
+        value = self._subranges(repo, rangeid)
+        self._setsub(rangeid, value)
+        return value
+
+    def revsfromrange(self, repo, rangeid):
+        headrev, index = rangeid
+        rangelength = self.rangelength(repo, rangeid)
+        if rangelength == 1:
+            revs = [headrev]
+        else:
+            # get all revs under heads in stable order
+            #
+            # note: In the general case we can just walk down and then request
+            # data about the merge. But I'm not sure this function will be even
+            # call for the general case.
+            allrevs = self._stablesortcache.get(headrev)
+            if allrevs is None:
+                allrevs = self._getrevsfrommerge(repo, headrev)
+                if allrevs is None:
+                    allrevs = stablesort(repo, [headrev],
+                                         mergecallback=self._filestablesortcache)
+                self._stablesortcache[headrev] = allrevs
+            # takes from index
+            revs = allrevs[index:]
+        # sanity checks
+        assert len(revs) == rangelength
+        return revs
+
+    def _parents(self, rev, func):
+        parents = self._parentscache.get(rev)
+        if parents is None:
+            parents = func(rev)
+            self._parentscache[rev] = parents
+        return parents
+
+    def _getdepth(self, rev):
+        """utility function used to access the depth cache
+
+        This mostly exist to help the on disk persistence."""
+        return self._depthcache.get(rev)
+
+    def _setdepth(self, rev, value):
+        """utility function used to set the depth cache
+
+        This mostly exist to help the on disk persistence."""
+        self._depthcache[rev] = value
+
+    def _getsub(self, rev):
+        """utility function used to access the subranges cache
+
+        This mostly exist to help the on disk persistence"""
+        return self._subrangescache.get(rev)
+
+    def _setsub(self, rev, value):
+        """utility function used to set the subranges cache
+
+        This mostly exist to help the on disk persistence."""
+        self._subrangescache[rev] = value
+
+    def _filestablesortcache(self, sortedrevs, merge):
+        if merge not in self._stablesortprepared:
+            self._stablesortprepared[merge] = (sortedrevs, len(sortedrevs))
+
+    def _getrevsfrommerge(self, repo, merge):
+        prepared = self._stablesortprepared.get(merge)
+        if prepared is None:
+            return None
+
+        mergedepth = self.depthrev(repo, merge)
+        allrevs = prepared[0][:prepared[1]]
+        nbextrarevs = prepared[1] - mergedepth
+        if not nbextrarevs:
+            return allrevs
+
+        anc = repo.changelog.ancestors([merge], inclusive=True)
+        top = []
+        counter = nbextrarevs
+        for rev in reversed(allrevs):
+            if rev in anc:
+                top.append(rev)
+            else:
+                counter -= 1
+                if counter <= 0:
+                    break
+
+        bottomidx = prepared[1] - (nbextrarevs + len(top))
+        revs = allrevs[:bottomidx]
+        revs.extend(reversed(top))
+        return revs
+
+    def _inheritancepoint(self, repo, merge):
+        """Find the inheritance point of a Merge
+
+        The first part of the stable sorted list of revision of a merge will shared with
+        the one of others. This means we can reuse subranges computed from that point to
+        compute some of the subranges from the merge.
+
+        That point is latest point in the stable sorted list where the depth of the
+        revisions match its index (that means all revision earlier in the stable sorted
+        list are its ancestors, no dangling unrelated branches exists).
+        """
+        value = self._inheritancecache.get(merge)
+        if value is None:
+            revs = self.revsfromrange(repo, (merge, 0))
+            i = reversed(revs)
+            i.next() # pop the merge
+            expected = len(revs) - 1
+            # Since we do warmup properly, we can expect the cache to be hot
+            # for everythin under the merge we investigate
+            cache = self._depthcache
+            # note: we cannot do a binary search because element under the
+            # inherited point might have mismatching depth because of inner
+            # branching.
+            for rev in i:
+                if cache[rev] == expected:
+                    break
+                expected -= 1
+            value = (expected - 1, rev)
+            self._inheritancecache[merge] = value
+        return value
+
+    def _depthmerge(self, cl, rev, p1, p2, stack):
+        # sub method to simplify the main 'depthrev' one
+        revdepth = None
+        depth = self._getdepth
+        depth_p1 = depth(p1)
+        depth_p2 = depth(p2)
+        missingparent = False
+        if depth_p1 is None:
+            stack.append(p1)
+            missingparent = True
+        if depth_p2 is None:
+            stack.append(p2)
+            missingparent = True
+        if missingparent:
+            return None
+        # computin depth of a merge
+        # XXX the common ancestors heads could be cached
+        ancnodes = cl.commonancestorsheads(cl.node(p1), cl.node(p2))
+        ancrevs = [cl.rev(a) for a in ancnodes]
+        anyunkown = False
+        ancdepth = []
+        for r in ancrevs:
+            d = depth(r)
+            if d is None:
+                anyunkown = True
+                stack.append(r)
+            ancdepth.append((r, d))
+        if anyunkown:
+            return None
+        if not ancrevs:
+            # unrelated branch, (no common root)
+            revdepth = depth_p1 + depth_p2 + 1
+        elif len(ancrevs) == 1:
+            # one unique branch point:
+            # we can compute depth without any walk
+            depth_anc = ancdepth[0][1]
+            revdepth = depth_p1 + (depth_p2 - depth_anc) + 1
+        else:
+            # multiple ancestors, we pick one that is
+            # * the deepest (less changeset outside of it),
+            # * lowest revs because more chance to have descendant of other "above"
+            anc, revdepth = max(ancdepth, key=lambda x: (x[1], -x[0]))
+            revdepth += len(cl.findmissingrevs(common=[anc], heads=[rev]))
+        return revdepth
+
+    def _subranges(self, repo, rangeid):
+        if self.rangelength(repo, rangeid) == 1:
+            return []
+        slicepoint = self._slicepoint(repo, rangeid)
+
+        # make sure we have cache for all relevant parent first to prevent
+        # recursion (python is bad with recursion
+        stack = []
+        current = rangeid
+        while current is not None:
+            current = self._cold_reusable(repo, current, slicepoint)
+            if current is not None:
+                stack.append(current)
+        while stack:
+            # these call will directly compute the subranges
+            self.subranges(repo, stack.pop())
+        return self._slicesrangeat(repo, rangeid, slicepoint)
+
+    def _cold_reusable(self, repo, rangeid, slicepoint):
+        """return parent range that it would be useful to prepare to slice
+        rangeid at slicepoint
+
+        This function also have the important task to update the revscache of
+        the parent rev s if possible and needed"""
+        p1, p2 = self._parents(rangeid[0], repo.changelog.parentrevs)
+        if p2 == nodemod.nullrev:
+            # regular changesets, we pick the parent
+            reusablerev = p1
+        else:
+            # merge, we try the inheritance point
+            # if it is too low, it will be ditched by the depth check anyway
+            index, reusablerev = self._inheritancepoint(repo, rangeid[0])
+
+        # if we reached the slicepoint, no need to go further
+        if self.depthrev(repo, reusablerev) <= slicepoint:
+            return None
+
+        reurange = (reusablerev, rangeid[1])
+        # if we have an entry for the current range, lets update the cache
+        # if we already have subrange for this range, no need to prepare it.
+        if self._getsub(reurange) is not None:
+            return None
+
+        # look like we found a relevent parentrange with no cache yet
+        return reurange
+
+    def _slicepoint(self, repo, rangeid):
+        rangedepth = self.depthrev(repo, rangeid[0])
+        step = _hlp2(rangedepth)
+        standard_start = 0
+        while standard_start < rangeid[1] and 0 < step:
+            if standard_start + step < rangedepth:
+                standard_start += step
+            step //= 2
+        if rangeid[1] == standard_start:
+            slicesize = _hlp2(self.rangelength(repo, rangeid))
+            slicepoint = rangeid[1] + slicesize
+        else:
+            assert standard_start < rangedepth
+            slicepoint = standard_start
+        return slicepoint
+
+    def _slicesrangeat(self, repo, rangeid, globalindex):
+        p1, p2 = self._parents(rangeid[0], repo.changelog.parentrevs)
+        if p2 == nodemod.nullrev:
+            reuserev = p1
+        else:
+            index, reuserev = self._inheritancepoint(repo, rangeid[0])
+            if index < globalindex:
+                return self._slicesrangeatmerge(repo, rangeid, globalindex)
+
+        assert reuserev != nodemod.nullrev
+
+        reuserange = (reuserev, rangeid[1])
+        top = (rangeid[0], globalindex)
+
+        if rangeid[1] + self.rangelength(repo, reuserange) == globalindex:
+            return [reuserange, top]
+        # This will not initiate a recursion since we took appropriate
+        # precaution in the caller of this method to ensure it will be so.
+        # It the parent is a merge that will not be the case but computing
+        # subranges from a merge will not recurse.
+        reusesubranges = self.subranges(repo, reuserange)
+        slices = reusesubranges[:-1] # pop the top
+        slices.append(top)
+        return slices
+
+    def _slicesrangeatmerge(self, repo, rangeid, globalindex):
+        localindex = globalindex - rangeid[1]
+        cl = repo.changelog
+
+        result = []
+        allrevs = self.revsfromrange(repo, rangeid)
+        bottomrevs = allrevs[:localindex]
+
+        if globalindex == self.depthrev(repo, bottomrevs[-1]):
+            # simple case, top revision in the bottom set contains exactly the
+            # revision we needs
+            result.append((bottomrevs[-1], rangeid[1]))
+        else:
+            parentrevs = cl.parentrevs
+            parents = self._parents
+            bheads = set(bottomrevs)
+            du = bheads.difference_update
+            reachableroots = repo.changelog.reachableroots
+            minrev = min(bottomrevs)
+            for r in bottomrevs:
+                du(parents(r, parentrevs))
+            for h in bheads:
+                # reachable roots is fast because is C
+                #
+                # It is worth noting that will use this kind of filtering from
+                # "h" multiple time in a warming run. So using "ancestors" and
+                # caching that should be faster. But python code filtering on
+                # the ancestors end up being slower.
+                hrevs = reachableroots(minrev, [h], bottomrevs, True)
+                start = self.depthrev(repo, h) - len(hrevs)
+                entry = (h, start)
+                result.append(entry)
+
+            # Talking about python code being slow, the following code is an
+            # alternative implementation.
+            #
+            # It complexity is better since is does a single traversal on the
+            # bottomset. However since it is all python it end up being
+            # slower.
+            # I'm keeping it here as an inspiration for a future C version
+            # branches = []
+            # for current in reversed(bottomrevs):
+            #     ps = parents(current, parentrevs)
+            #     found = False
+            #     for brevs, bexpect in branches:
+            #         if current in bexpect:
+            #             found = True
+            #             brevs.append(current)
+            #             bexpect.discard(current)
+            #             bexpect.update(ps)
+            #     if not found:
+            #         branches.append(([current], set(ps)))
+            # for revs, __ in reversed(branches):
+            #     head = revs[0]
+            #     index = self.depthrev(repo, head) - len(revs)
+            #     result.append((head, index))
+
+        # top part is trivial
+        top = (rangeid[0], globalindex)
+        result.append(top)
+        return result
+
+#############################
+### simple sqlite caching ###
+#############################
+
+_sqliteschema = [
+    """CREATE TABLE meta(schemaversion INTEGER NOT NULL,
+                         tiprev        INTEGER NOT NULL,
+                         tipnode       BLOB    NOT NULL
+                        );""",
+    "CREATE TABLE depth(rev INTEGER NOT NULL PRIMARY KEY, depth INTEGER NOT NULL);",
+    """CREATE TABLE range(rev INTEGER  NOT NULL,
+                          idx INTEGER NOT NULL,
+                          PRIMARY KEY(rev, idx));""",
+    """CREATE TABLE subranges(listidx INTEGER NOT NULL,
+                              suprev  INTEGER NOT NULL,
+                              supidx  INTEGER NOT NULL,
+                              subrev  INTEGER NOT NULL,
+                              subidx  INTEGER NOT NULL,
+                              PRIMARY KEY(listidx, suprev, supidx),
+                              FOREIGN KEY (suprev, supidx) REFERENCES range(rev, idx),
+                              FOREIGN KEY (subrev, subidx) REFERENCES range(rev, idx)
+    );""",
+    "CREATE INDEX subranges_index ON subranges (suprev, supidx);",
+    "CREATE INDEX range_index ON range (rev, idx);",
+    "CREATE INDEX depth_index ON depth (rev);"
+]
+_newmeta = "INSERT INTO meta (schemaversion, tiprev, tipnode) VALUES (?,?,?);"
+_updatemeta = "UPDATE meta SET tiprev = ?, tipnode = ?;"
+_updatedepth = "INSERT INTO depth(rev, depth) VALUES (?,?);"
+_updaterange = "INSERT INTO range(rev, idx) VALUES (?,?);"
+_updatesubranges = """INSERT
+                       INTO subranges(listidx, suprev, supidx, subrev, subidx)
+                       VALUES (?,?,?,?,?);"""
+_queryexist = "SELECT name FROM sqlite_master WHERE type='table' AND name='meta';"
+_querymeta = "SELECT schemaversion, tiprev, tipnode FROM meta;"
+_querydepth = "SELECT depth FROM depth WHERE rev = ?;"
+_batchdepth = "SELECT rev, depth FROM depth;"
+_queryrange = "SELECT * FROM range WHERE (rev = ? AND idx = ?);"
+_querysubranges = """SELECT subrev, subidx
+                     FROM subranges
+                     WHERE (suprev = ? AND supidx = ?)
+                     ORDER BY listidx;"""
+
+class sqlstablerange(stablerange):
+
+    _schemaversion = 0
+
+    def __init__(self, repo):
+        super(sqlstablerange, self).__init__()
+        self._path = repo.vfs.join('cache/evoext_stablerange_v0.sqlite')
+        self._cl = repo.unfiltered().changelog # (okay to keep an old one)
+        self._ondisktiprev = None
+        self._ondisktipnode = None
+        self._unsaveddepth = {}
+        self._unsavedsubranges = {}
+        self._fulldepth = False
+
+    def warmup(self, repo, upto=None):
+        self._con # make sure the data base is loaded
+        try:
+            # samelessly lock the repo to ensure nobody will update the repo
+            # concurently. This should not be too much of an issue if we warm
+            # at the end of the transaction.
+            #
+            # XXX However, we lock even if we are up to date so we should check
+            # before locking
+            with repo.lock():
+                super(sqlstablerange, self).warmup(repo, upto)
+                self._save(repo)
+        except error.LockError:
+            # Exceptionnally we are noisy about it since performance impact is
+            # large We should address that before using this more widely.
+            repo.ui.warn('stable-range cache: unable to lock repo while warming\n')
+            repo.ui.warn('(cache will not be saved)\n')
+            super(sqlstablerange, self).warmup(repo, upto)
+
+    def _getdepth(self, rev):
+        cache = self._depthcache
+        if rev not in cache and rev <= self._ondisktiprev and self._con is not None:
+            value = None
+            result = self._con.execute(_querydepth, (rev,)).fetchone()
+            if result is not None:
+                value = result[0]
+            # in memory caching of the value
+            cache[rev] = value
+        return cache.get(rev)
+
+    def _setdepth(self, rev, depth):
+        assert rev not in self._unsaveddepth
+        self._unsaveddepth[rev] = depth
+        super(sqlstablerange, self)._setdepth(rev, depth)
+
+    def _getsub(self, rangeid):
+        cache = self._subrangescache
+        if rangeid not in cache and rangeid[0] <= self._ondisktiprev and self._con is not None:
+            value = None
+            result = self._con.execute(_queryrange, rangeid).fetchone()
+            if result is not None: # database know about this node (skip in the future?)
+                value = self._con.execute(_querysubranges, rangeid).fetchall()
+            # in memory caching of the value
+            cache[rangeid] = value
+        return cache.get(rangeid)
+
+    def _setsub(self, rangeid, value):
+        assert rangeid not in self._unsavedsubranges
+        self._unsavedsubranges[rangeid] = value
+        super(sqlstablerange, self)._setsub(rangeid, value)
+
+    def _inheritancepoint(self, *args, **kwargs):
+        self._loaddepth()
+        return super(sqlstablerange, self)._inheritancepoint(*args, **kwargs)
+
+    @util.propertycache
+    def _con(self):
+        con = sqlite3.connect(self._path)
+        con.text_factory = str
+        cur = con.execute(_queryexist)
+        if cur.fetchone() is None:
+            return None
+        meta = con.execute(_querymeta).fetchone()
+        if meta is None:
+            return None
+        if meta[0] != self._schemaversion:
+            return None
+        if len(self._cl) <= meta[1]:
+            return None
+        if self._cl.node(meta[1]) != meta[2]:
+            return None
+        self._ondisktiprev = meta[1]
+        self._ondisktipnode = meta[2]
+        if self._tiprev < self._ondisktiprev:
+            self._tiprev = self._ondisktiprev
+            self._tipnode = self._ondisktipnode
+        return con
+
+    def _save(self, repo):
+        repo = repo.unfiltered()
+        if not (self._unsavedsubranges or self._unsaveddepth):
+            return # no new data
+
+        if self._con is None:
+            util.unlinkpath(self._path, ignoremissing=True)
+            if '_con' in vars(self):
+                del self._con
+
+            con = sqlite3.connect(self._path)
+            con.text_factory = str
+            with con:
+                for req in _sqliteschema:
+                    con.execute(req)
+
+                meta = [self._schemaversion,
+                        self._tiprev,
+                        self._tipnode,
+                ]
+                con.execute(_newmeta, meta)
+        else:
+            con = self._con
+            meta = con.execute(_querymeta).fetchone()
+            if meta[2] != self._ondisktipnode or meta[1] != self._ondisktiprev:
+                # 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 = _('stable-range cache: skipping write, '
+                        'database drifted under my feet\n')
+                hint = _('(disk: %s-%s vs mem: %s%s)\n')
+                data = (meta[2], meta[1], self._ondisktiprev, self._ondisktipnode)
+                repo.ui.warn(msg)
+                repo.ui.warn(hint % data)
+                return
+            meta = [self._tiprev,
+                    self._tipnode,
+            ]
+            con.execute(_updatemeta, meta)
+
+        self._savedepth(con, repo)
+        self._saverange(con, repo)
+        con.commit()
+        self._ondisktiprev = self._tiprev
+        self._ondisktipnode = self._tipnode
+        self._unsaveddepth.clear()
+        self._unsavedsubranges.clear()
+
+    def _savedepth(self, con, repo):
+        repo = repo.unfiltered()
+        data = self._unsaveddepth.items()
+        con.executemany(_updatedepth, data)
+
+    def _loaddepth(self):
+        """batch load all data about depth"""
+        if not (self._fulldepth or self._con is None):
+            result = self._con.execute(_batchdepth)
+            self._depthcache.update(result.fetchall())
+            self._fulldepth = True
+
+    def _saverange(self, con, repo):
+        repo = repo.unfiltered()
+        data = []
+        allranges = set()
+        for key, value in self._unsavedsubranges.items():
+            allranges.add(key)
+            for idx, sub in enumerate(value):
+                data.append((idx, key[0], key[1], sub[0], sub[1]))
+
+        con.executemany(_updaterange, allranges)
+        con.executemany(_updatesubranges, data)
+
+
+@eh.reposetup
+def setupcache(ui, repo):
+
+    class stablerangerepo(repo.__class__):
+
+        @localrepo.unfilteredpropertycache
+        def stablerange(self):
+            return sqlstablerange(repo)
+
+        @localrepo.unfilteredmethod
+        def destroyed(self):
+            if 'stablerange' in vars(self):
+                del self.stablerange
+
+        def transaction(self, *args, **kwargs):
+            tr = super(stablerangerepo, self).transaction(*args, **kwargs)
+            if not repo.ui.configbool('experimental', 'obshashrange', False):
+                return tr
+            reporef = weakref.ref(self)
+
+            def _warmcache(tr):
+                repo = reporef()
+                if repo is None:
+                    return
+                if 'node' in tr.hookargs:
+                    # new nodes !
+                    repo.stablerange.warmup(repo)
+
+            tr.addpostclose('warmcache-stablerange', _warmcache)
+            return tr
+
+    repo.__class__ = stablerangerepo
--- a/hgext3rd/evolve/utility.py	Tue Mar 14 14:47:20 2017 -0700
+++ b/hgext3rd/evolve/utility.py	Fri Mar 31 15:44:10 2017 +0200
@@ -4,7 +4,6 @@
 #
 # 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 node
 
 def obsexcmsg(ui, message, important=False):
     verbose = ui.configbool('experimental', 'verbose-obsolescence-exchange',
@@ -19,36 +18,3 @@
     if ui.configbool('experimental', 'verbose-obsolescence-exchange', False):
         topic = 'OBSEXC'
     ui.progress(topic, *args, **kwargs)
-
-_depthcache = {}
-def depth(repo, rev):
-    cl = repo.changelog
-    n = cl.node(rev)
-    revdepth = _depthcache.get(n, None)
-    if revdepth is None:
-        p1, p2 = cl.parentrevs(rev)
-        if p1 == node.nullrev:
-            revdepth = 1
-        elif p2 == node.nullrev:
-            revdepth = depth(repo, p1) + 1
-        else:
-            ancs = cl.commonancestorsheads(cl.node(p1), cl.node(p2))
-            depth_p1 = depth(repo, p1)
-            depth_p2 = depth(repo, p2)
-            if not ancs:
-                revdepth = depth_p1 + depth_p2 + 1
-            elif len(ancs) == 1:
-                anc = cl.rev(ancs[0])
-                revdepth = depth_anc = depth(repo, anc)
-                revdepth += depth_p1 - depth_anc
-                revdepth += depth_p2 - depth_anc
-                revdepth += 1
-            else:
-                # multiple ancestors, we pick the highest and search all missing bits
-                anc = max(cl.rev(a) for a in ancs)
-                revdepth = depth(repo, anc)
-                revdepth += len(cl.findmissingrevs(common=[anc], heads=[rev]))
-        _depthcache[n] = revdepth
-    # actual_depth = len(list(cl.ancestors([rev], inclusive=True)))
-    # assert revdepth == actual_depth, (rev, revdepth, actual_depth)
-    return revdepth
--- a/tests/_exc-util.sh	Tue Mar 14 14:47:20 2017 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-#!/bin/sh
-
-cat >> $HGRCPATH <<EOF
-[web]
-push_ssl = false
-allow_push = *
-
-[ui]
-logtemplate ="{node|short} ({phase}): {desc}\n"
-
-[phases]
-publish=False
-
-[experimental]
-verbose-obsolescence-exchange=false
-bundle2-exp=true
-bundle2-output-capture=True
-
-[alias]
-debugobsolete=debugobsolete -d '0 0'
-
-[extensions]
-hgext.strip=
-EOF
-echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
-
-mkcommit() {
-   echo "$1" > "$1"
-   hg add "$1"
-   hg ci -m "$1"
-}
-getid() {
-   hg log --hidden --template '{node}\n' --rev "$1"
-}
-
-setuprepos() {
-    echo creating test repo for test case $1
-    mkdir $1
-    cd $1
-    echo - pulldest
-    hg init pushdest
-    cd pushdest
-    mkcommit O
-    hg phase --public .
-    cd ..
-    echo - main
-    hg clone -q pushdest main
-    echo - pushdest
-    hg clone -q main pulldest
-    echo 'cd into `main` and proceed with env setup'
-}
-
-dotest() {
-# dotest TESTNAME [TARGETNODE]
-
-    testcase=$1
-    shift
-    target="$1"
-    if [ $# -gt 0 ]; then
-        shift
-    fi
-    targetnode=""
-    desccall=""
-    cd $testcase
-    echo "## Running testcase $testcase"
-    if [ -n "$target" ]; then
-        desccall="desc("\'"$target"\'")"
-        targetnode="`hg -R main id -qr \"$desccall\"`"
-        echo "# testing echange of \"$target\" ($targetnode)"
-    fi
-    echo "## initial state"
-    echo "# obstore: main"
-    hg -R main     debugobsolete | sort
-    echo "# obstore: pushdest"
-    hg -R pushdest debugobsolete | sort
-    echo "# obstore: pulldest"
-    hg -R pulldest debugobsolete | sort
-
-    if [ -n "$target" ]; then
-        echo "## pushing \"$target\"" from main to pushdest
-        hg -R main push -r "$desccall" $@ pushdest
-    else
-        echo "## pushing from main to pushdest"
-        hg -R main push pushdest $@
-    fi
-    echo "## post push state"
-    echo "# obstore: main"
-    hg -R main     debugobsolete | sort
-    echo "# obstore: pushdest"
-    hg -R pushdest debugobsolete | sort
-    echo "# obstore: pulldest"
-    hg -R pulldest debugobsolete | sort
-    if [ -n "$target" ]; then
-        echo "## pulling \"$targetnode\"" from main into pulldest
-        hg -R pulldest pull -r $targetnode $@ main
-    else
-        echo "## pulling from main into pulldest"
-        hg -R pulldest pull main $@
-    fi
-    echo "## post pull state"
-    echo "# obstore: main"
-    hg -R main     debugobsolete | sort
-    echo "# obstore: pushdest"
-    hg -R pushdest debugobsolete | sort
-    echo "# obstore: pulldest"
-    hg -R pulldest debugobsolete | sort
-
-    cd ..
-
-}
--- a/tests/test-check-flake8.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-check-flake8.t	Fri Mar 31 15:44:10 2017 +0200
@@ -14,5 +14,5 @@
 
 run flake8 if it exists; if it doesn't, then just skip
 
-  $ hg files -0 'set:(**.py or grep("^!#.*python")) - removed()' 2>/dev/null \
+  $ hg files -0 'set:(**.py or grep("^#!.*python")) - removed()' 2>/dev/null \
   > | xargs -0 flake8
--- a/tests/test-check-pyflakes.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-check-pyflakes.t	Fri Mar 31 15:44:10 2017 +0200
@@ -7,5 +7,5 @@
 run pyflakes on all tracked files ending in .py or without a file ending
 (skipping binary file random-seed)
 
-  $ hg locate 'set:(**.py or grep("^!#.*python")) - removed()' 2>/dev/null \
+  $ hg locate 'set:(**.py or grep("^#!.*python")) - removed()' 2>/dev/null \
   > | xargs pyflakes 2>/dev/null
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-check-setup-manifest.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,19 @@
+#require test-repo
+
+  $ checkcm() {
+  >   if ! (which check-manifest > /dev/null); then
+  >     echo skipped: missing tool: check-manifest;
+  >     exit 80;
+  >   fi;
+  > };
+  $ checkcm
+  $ cat << EOF >> $HGRCPATH
+  > [experimental]
+  > evolution=all
+  > EOF
+
+Run check manifest:
+
+  $ cd $TESTDIR/..
+  $ check-manifest
+  lists of files in version control and sdist match
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-partial-C1.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,79 @@
+====================================
+Testing head checking code: Case C-1
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category C: checking case were the branch is only partially obsoleted.
+TestCase 1: 2 changeset branch, only the head is rewritten
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * 1 new changesets branches superceeding only the head of the old one
+.. * base of the old branch is still alive
+..
+.. expected-result:
+..
+.. * push denied
+..
+.. graph-summary:
+..
+..   B ø⇠◔ B'
+..     | |
+..   A ○ |
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B1
+  created new head
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg log -G --hidden
+  @  25c56d33e4c4 (draft): B1
+  |
+  | x  d73caddc5533 (draft): B0
+  | |
+  | o  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 25c56d33e4c4!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-partial-C2.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,79 @@
+====================================
+Testing head checking code: Case C-2
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category C: checking case were the branch is only partially obsoleted.
+TestCase 2: 2 changeset branch, only the base is rewritten
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * 1 new changesets branches superceeding only the base of the old one
+.. * The old branch is still alive (base is obsolete, head is alive)
+..
+.. expected-result:
+..
+.. * push denied
+..
+.. graph-summary:
+..
+..   B ○
+..     |
+..   A ø⇠◔ A'
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg log -G --hidden
+  @  f6082bc4ffef (draft): A1
+  |
+  | o  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push --rev 'desc(A1)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head f6082bc4ffef!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-partial-C3.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,78 @@
+====================================
+Testing head checking code: Case C-3
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category C: checking case were the branch is only partially obsoleted.
+TestCase 3: 2 changeset branch, only the head is pruned
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * old head is pruned
+.. * 1 new unrelated branch
+..
+.. expected-result:
+..
+.. * push denied
+..
+.. graph-summary:
+..
+..   B ⊗
+..     |
+..   A ◔ ◔ C
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ hg debugobsolete --record-parents `getid "desc(B0)"`
+  $ hg log -G --hidden
+  @  0f88766e02d6 (draft): C0
+  |
+  | x  d73caddc5533 (draft): B0
+  | |
+  | o  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 0f88766e02d6!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-partial-C4.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,78 @@
+====================================
+Testing head checking code: Case C-4
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category C: checking case were the branch is only partially obsoleted.
+TestCase 4: 2 changeset branch, only the base is pruned
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * old base is pruned
+.. * 1 new unrelated branch
+..
+.. expected-result:
+..
+.. * push denied
+..
+.. graph-summary:
+..
+..   B ◔
+..     |
+..   A ⊗ ◔ C
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ hg debugobsolete --record-parents `getid "desc(A0)"`
+  $ hg log -G --hidden
+  @  0f88766e02d6 (draft): C0
+  |
+  | o  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push --rev 'desc(C0)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 0f88766e02d6!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-pruned-B1.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,71 @@
+====================================
+Testing head checking code: Case B-1
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category B: checking simple case involving pruned changesets
+TestCase 1: single pruned changeset
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * old branch is pruned
+.. * 1 new unrelated branch
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..       ◔ B
+..       |
+..   A ⊗ |
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B0
+  created new head
+  $ hg debugobsolete --record-parents `getid "desc(A0)"`
+  $ hg log -G --hidden
+  @  74ff5441d343 (draft): B0
+  |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  1 new obsolescence markers
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-pruned-B2.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,82 @@
+====================================
+Testing head checking code: Case B-2
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category B: checking simple case involving pruned changesets
+TestCase 2: multi-changeset branch, head is pruned, rest is superceeded
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * old head is pruned
+.. * 1 new branch succeeding to the other changeset in the old branch
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ⊗
+..     |
+..   A ø⇠◔ A'
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete --record-parents `getid "desc(B0)"`
+  $ hg log -G --hidden
+  @  f6082bc4ffef (draft): A1
+  |
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  2 new obsolescence markers
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-pruned-B3.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,82 @@
+====================================
+Testing head checking code: Case B-3
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category B: checking simple case involving pruned changesets
+TestCase 3: multi-changeset branch, other is pruned, rest is superceeded
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * old head is superceeded
+.. * old other is pruned
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ø⇠◔ B'
+..     | |
+..   A ⊗ |
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B1
+  created new head
+  $ hg debugobsolete --record-parents `getid "desc(A0)"`
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg log -G --hidden
+  @  25c56d33e4c4 (draft): B1
+  |
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  2 new obsolescence markers
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-pruned-B4.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,83 @@
+====================================
+Testing head checking code: Case B-4
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category B: checking simple case involving pruned changesets
+TestCase 4: multi-changeset branch, all are pruned
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * old branch is pruned
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ⊗
+..     |
+..   A ⊗
+..     |
+..     | ◔ C
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ hg debugobsolete --record-parents `getid "desc(A0)"`
+  $ hg debugobsolete --record-parents `getid "desc(B0)"`
+  $ hg log -G --hidden
+  @  0f88766e02d6 (draft): C0
+  |
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  2 new obsolescence markers
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-pruned-B5.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,89 @@
+====================================
+Testing head checking code: Case B-5
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category B: checking simple case involving pruned changesets
+TestCase 5: multi-changeset branch, mix of pruned and superceeded
+
+.. old-state:
+..
+.. * 3 changeset branch
+..
+.. new-state:
+..
+.. * old head is pruned
+.. * old mid is superceeded
+.. * old root is pruned
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ⊗
+..     |
+..   A ø⇠◔ A'
+..     | |
+..   B ⊗ |
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ mkcommit C0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B1
+  created new head
+  $ hg debugobsolete --record-parents `getid "desc(A0)"`
+  $ hg debugobsolete `getid "desc(B0)"` `getid "desc(B1)"`
+  $ hg debugobsolete --record-parents `getid "desc(C0)"`
+  $ hg log -G --hidden
+  @  25c56d33e4c4 (draft): B1
+  |
+  | x  821fb21d0dd2 (draft): C0
+  | |
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  3 new obsolescence markers
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-pruned-B6.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,74 @@
+====================================
+Testing head checking code: Case B-6
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category B: checking simple case involving pruned changesets
+TestCase 6: single changesets, pruned then superseeded (on a new changeset)
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * old branch is rewritten onto another one,
+.. * the new version is then pruned.
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   A ø⇠⊗ A'
+..     | |
+..     | ◔ B
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B0
+  created new head
+  $ mkcommit A1
+  $ hg up 'desc(B0)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg debugobsolete `getid "desc(A0)"` `getid "desc(A1)"`
+  $ hg debugobsolete --record-parents `getid "desc(A1)"`
+  $ hg log -G --hidden
+  x  ba93660aff8d (draft): A1
+  |
+  @  74ff5441d343 (draft): B0
+  |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  2 new obsolescence markers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-pruned-B7.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,73 @@
+====================================
+Testing head checking code: Case B-7
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category B: checking simple case involving pruned changesets
+TestCase 7: single changesets, pruned then superseeded (on an existing changeset)
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * old branch is rewritten onto the common set,
+.. * the new version is then pruned.
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   A ø⇠⊗ A'
+.. B ◔ | |
+..    \|/
+..     ●
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B0
+  created new head
+  $ mkcommit A1
+  $ hg up 'desc(B0)'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg debugobsolete `getid "desc(A0)"` `getid "desc(A1)"`
+  $ hg debugobsolete --record-parents `getid "desc(A1)"`
+  $ hg log -G --hidden
+  x  ba93660aff8d (draft): A1
+  |
+  @  74ff5441d343 (draft): B0
+  |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  2 new obsolescence markers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-pruned-B8.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,95 @@
+====================================
+Testing head checking code: Case B-2
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category B: checking simple case involving pruned changesets
+TestCase 2: multi-changeset branch, head is pruned, rest is superceeded, through other
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * old head is rewritten then pruned
+.. * 1 new branch succeeding to the other changeset in the old branch (through another obsolete branch)
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ø⇠⊗ B'
+..     | | A'
+..   A ø⇠ø⇠◔ A''
+..     |/ /
+..     | /
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ mkcommit B1
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ mkcommit A2
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg debugobsolete --record-parents `getid "desc(B1)"`
+  $ hg debugobsolete `getid "desc(A1)" ` `getid "desc(A2)"`
+  $ hg log -G --hidden
+  @  c1f8d089020f (draft): A2
+  |
+  | x  262c8c798096 (draft): B1
+  | |
+  | x  f6082bc4ffef (draft): A1
+  |/
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  4 new obsolescence markers
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-superceed-A1.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,68 @@
+====================================
+Testing head checking code: Case A-1
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category A: checking simple case invoving a branch being superceeded by another.
+TestCase 1: single-changeset branch
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * 1 changeset branch succeeding to A
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   A ø⇠◔ A'
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg log -G --hidden
+  @  f6082bc4ffef (draft): A1
+  |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  1 new obsolescence markers
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-superceed-A2.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,84 @@
+====================================
+Testing head checking code: Case A-2
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category A: checking simple case invoving a branch being superceeded by another.
+TestCase 2: multi-changeset branch
+
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * 2 changeset branch succeeding the old one
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ø⇠◔ B'
+..     | |
+..   A ø⇠◔ A'
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ mkcommit B1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg log -G --hidden
+  @  262c8c798096 (draft): B1
+  |
+  o  f6082bc4ffef (draft): A1
+  |
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  2 new obsolescence markers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-superceed-A3.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,87 @@
+====================================
+Testing head checking code: Case A-3
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category A: checking simple case invoving a branch being superceeded by another.
+TestCase 3: multi-changeset branch with reordering
+
+Push should be allowed
+.. old-state:
+..
+.. * 2 changeset branch
+..
+.. new-state:
+..
+.. * 2 changeset branch succeeding the old one with reordering
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ø⇠⇠
+..     | ⇡
+..   A ø⇠⇠⇠○ A'
+..     | ⇡/
+..     | ○ B'
+..     |/
+..     ● O
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B1
+  created new head
+  $ mkcommit A1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg log -G --hidden
+  @  c1c7524e9488 (draft): A1
+  |
+  o  25c56d33e4c4 (draft): B1
+  |
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  2 new obsolescence markers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-superceed-A4.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,74 @@
+====================================
+Testing head checking code: Case A-4
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category A: checking simple case invoving a branch being superceeded by another.
+TestCase 4: New changeset as children of the successor
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * 2 changeset branch, first is a successor, but head is new
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..       ◔ B
+..       |
+..   A ø⇠◔ A'
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ mkcommit B0
+  $ hg log -G --hidden
+  @  f40ded968333 (draft): B0
+  |
+  o  f6082bc4ffef (draft): A1
+  |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  1 new obsolescence markers
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-superceed-A5.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,74 @@
+====================================
+Testing head checking code: Case A-5
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category A: checking simple case invoving a branch being superceeded by another.
+TestCase 5: New changeset as parent of the successor
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * 2 changeset branch, head is a successor, but other is new
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   A ø⇠◔ A'
+..     | |
+..     | ◔ B
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B0
+  created new head
+  $ mkcommit A1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg log -G --hidden
+  @  ba93660aff8d (draft): A1
+  |
+  o  74ff5441d343 (draft): B0
+  |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  1 new obsolescence markers
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-superceed-A6.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,95 @@
+====================================
+Testing head checking code: Case A-6
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category A: checking simple case invoving a branch being superceeded by another.
+TestCase 6: multi-changeset branch, split on multiple other, (base on its own branch)
+
+.. old-state:
+..
+.. * 2 branch (1 changeset, and 2 changesets)
+..
+.. new-state:
+..
+.. * 1 new branch superceeding the base of the old-2-changesets-branch,
+.. * 1 new changesets on the old-1-changeset-branch superceeding the head of the other
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+.. B'◔⇢ø B
+..   | |
+.. A | ø⇠◔ A'
+..   | |/
+.. C ● |
+..    \|
+..     ●
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg up 'desc(C0)'
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg log -G --hidden
+  @  d70a1f75a020 (draft): B1
+  |
+  | o  f6082bc4ffef (draft): A1
+  | |
+  o |  0f88766e02d6 (draft): C0
+  |/
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  2 new obsolescence markers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-superceed-A7.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,95 @@
+====================================
+Testing head checking code: Case A-7
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category A: checking simple case invoving a branch being superceeded by another.
+TestCase 7: multi-changeset branch, split on multiple other, (head on its own branch)
+
+.. old-state:
+..
+.. * 2 branch (1 changeset, and 2 changesets)
+..
+.. new-state:
+..
+.. * 1 new branch superceeding the head of the old-2-changesets-branch,
+.. * 1 new changesets on the old-1-changeset-branch superceeding the base of the other
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ø⇠◔ B'
+..     | |
+.. A'◔⇢ø |
+..   | |/
+.. C ● |
+..    \|
+..     ●
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+  $ hg up 'desc(C0)'
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ mkcommit B1
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg log -G --hidden
+  @  25c56d33e4c4 (draft): B1
+  |
+  | o  a0802eb7fc1b (draft): A1
+  | |
+  | o  0f88766e02d6 (draft): C0
+  |/
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  2 new obsolescence markers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-superceed-A8.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,78 @@
+====================================
+Testing head checking code: Case A-8
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category A: checking simple case invoving a branch being superceeded by another.
+TestCase 8: single-changeset branch indirect rewrite
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * 1 changeset branch succeeding to A, through another unpushed changesets
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..       A'
+..   A ø⇠ø⇠◔ A''
+..     |/ /
+..     | /
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A2
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete `getid "desc(A1)" ` `getid "desc(A2)"`
+  $ hg log -G --hidden
+  @  c1f8d089020f (draft): A2
+  |
+  | x  f6082bc4ffef (draft): A1
+  |/
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  2 new obsolescence markers
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-unpushed-D1.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,75 @@
+====================================
+Testing head checking code: Case D-1
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category D: remote head is "obs-affected" locally, but result is not part of the push.
+TestCase 1: remote head is rewritten, but successors is not part of the push
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * 1 changeset branch succeeding the old branch
+.. * 1 new unrelated branch
+..
+.. expected-result:
+..
+.. * pushing only the unrelated branch: denied
+..
+.. graph-summary:
+..
+..   A ø⇠○ A'
+..     |/
+..     | ◔ B
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B0
+  created new head
+  $ hg log -G --hidden
+  @  74ff5441d343 (draft): B0
+  |
+  | o  f6082bc4ffef (draft): A1
+  |/
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc(B0)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 74ff5441d343!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-unpushed-D2.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,90 @@
+====================================
+Testing head checking code: Case D-2
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category D: remote head is "obs-affected" locally, but result is not part of the push.
+TestCase 1: remote branch has 2 changes, head is pruned, second is rewritten but result is not pushed
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * old head is pruned
+.. * 1 new branch succeeding to the other changeset in the old branch
+.. * 1 new unrelated branch
+..
+.. expected-result:
+..
+.. * push allowed
+.. * pushing only the unrelated branch: denied
+..
+.. graph-summary:
+..
+..   B ⊗
+..     |
+..   A ø⇠○ A'
+..     |/
+..     | ◔ C
+..     |/
+..     ○
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete --record-parents `getid "desc(B0)"`
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ hg log -G --hidden
+  @  0f88766e02d6 (draft): C0
+  |
+  | o  f6082bc4ffef (draft): A1
+  |/
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push --rev 'desc(C0)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 0f88766e02d6!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-unpushed-D3.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,107 @@
+====================================
+Testing head checking code: Case D-3
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category D: remote head is "obs-affected" locally, but result is not part of the push.
+TestCase 3: multi-changeset branch, split on multiple new others, only one of them is pushed
+
+.. old-state:
+..
+.. * 2 changesets branch
+..
+.. new-state:
+..
+.. * 2 new branches, each superseding one changeset in the old one.
+..
+.. expected-result:
+..
+.. * pushing only one of the resulting branch (either of them)
+.. * push denied
+..
+.. graph-summary:
+..
+.. B'◔⇢ø B
+..   | |
+.. A | ø⇠◔ A'
+..   | |/
+..    \|
+..     ●
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  (run 'hg update' to get a working copy)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg up '0'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B1
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg log -G --hidden
+  @  25c56d33e4c4 (draft): B1
+  |
+  | o  f6082bc4ffef (draft): A1
+  |/
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push --rev 'desc(A1)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head f6082bc4ffef!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+  $ hg push --rev 'desc(B1)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 25c56d33e4c4!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
+Extra testing
+-------------
+
+In this case, even a bare push is creating more heads
+
+  $ hg push
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 25c56d33e4c4!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-unpushed-D4.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,102 @@
+====================================
+Testing head checking code: Case D-4
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category D: remote head is "obs-affected" locally, but result is not part of the push.
+TestCase 4: multi-changeset branch, split on multiple other, (base on its own new branch)
+
+.. old-state:
+..
+.. * 2 branch (1 changeset, and 2 changesets)
+..
+.. new-state:
+..
+.. * 1 new branch superceeding the base of the old-2-changesets-branch,
+.. * 1 new changesets on the old-1-changeset-branch superceeding the head of the other
+..
+.. expected-result:
+..
+.. * push the new branch only -> push denied
+.. * push the existing branch only -> push allowed
+..
+.. graph-summary:
+..
+.. B'◔⇢ø B
+..   | |
+.. A | ø⇠◔ A'
+..   | |/
+.. C ● |
+..    \|
+..     ●
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  $ hg up 'desc(C0)'
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg log -G --hidden
+  @  d70a1f75a020 (draft): B1
+  |
+  | o  f6082bc4ffef (draft): A1
+  | |
+  o |  0f88766e02d6 (draft): C0
+  |/
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push --rev 'desc(A1)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head f6082bc4ffef!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+  $ hg push --rev 'desc(B1)'
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  1 new obsolescence markers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-unpushed-D5.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,104 @@
+====================================
+Testing head checking code: Case D-5
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category D: remote head is "obs-affected" locally, but result is not part of the push.
+TestCase 5: multi-changeset branch, split on multiple other, (head on its own new branch)
+
+.. old-state:
+..
+.. * 2 branch (1 changeset, and 2 changesets)
+..
+.. new-state:
+..
+.. * 1 new branch superceeding the head of the old-2-changesets-branch,
+.. * 1 new changesets on the old-1-changeset-branch superceeding the base of the other
+..
+.. expected-result:
+..
+.. * push the new branch only -> push denied
+.. * push the existing branch only -> push allowed
+..   /!\ This push create unstability/orphaning on the other hand and we should
+..  probably detect/warn agains that.
+..
+.. graph-summary:
+..
+..   B ø⇠◔ B'
+..     | |
+.. A'◔⇢ø |
+..   | |/
+.. C ● |
+..    \|
+..     ●
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit B0
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ cd ../client
+  $ hg pull
+  pulling from $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+  $ hg up 'desc(C0)'
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A1
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ mkcommit B1
+  created new head
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  $ hg log -G --hidden
+  @  25c56d33e4c4 (draft): B1
+  |
+  | o  a0802eb7fc1b (draft): A1
+  | |
+  | o  0f88766e02d6 (draft): C0
+  |/
+  | x  d73caddc5533 (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+
+Actual testing
+--------------
+
+  $ hg push --rev 'desc(B1)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 25c56d33e4c4!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+  $ hg push --rev 'desc(A1)'
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  1 new obsolescence markers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-unpushed-D6.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,78 @@
+====================================
+Testing head checking code: Case D-6
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category D: remote head is "obs-affected" locally, but result is not part of the push.
+TestCase 6: single changesets, superseeded then pruned (on a new changeset unpushed) changeset
+
+This is a partial push variation of B6
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * old branch is rewritten onto another one,
+.. * the new version is then pruned.
+..
+.. expected-result:
+..
+.. * push denied
+..
+.. graph-summary:
+..
+..   A ø⇠⊗ A'
+..     | |
+.. C ◔ | ◔ B
+..    \|/
+..     ●
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B0
+  created new head
+  $ mkcommit A1
+  $ hg up '0'
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ hg debugobsolete `getid "desc(A0)"` `getid "desc(A1)"`
+  $ hg debugobsolete --record-parents `getid "desc(A1)"`
+  $ hg log -G --hidden
+  @  0f88766e02d6 (draft): C0
+  |
+  | x  ba93660aff8d (draft): A1
+  | |
+  | o  74ff5441d343 (draft): B0
+  |/
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push --rev 'desc(C0)'
+  pushing to $TESTTMP/server
+  searching for changes
+  abort: push creates new remote head 0f88766e02d6!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-checkheads-unpushed-D7.t	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,92 @@
+====================================
+Testing head checking code: Case D-7
+====================================
+
+Mercurial checks for the introduction of multiple heads on push. Evolution
+comes into play to detect if existing heads on the server are being replaced by
+some of the new heads we push.
+
+This test file is part of a series of tests checking this behavior.
+
+Category D: remote head is "obs-affected" locally, but result is not part of the push.
+TestCase 7: single changesets, superseeded multiple time then pruned (on a new changeset unpushed) changeset
+
+This is a partial push variation of B6
+
+.. old-state:
+..
+.. * 1 changeset branch
+..
+.. new-state:
+..
+.. * old branch is rewritten onto another one,
+.. * The rewriting it again rewritten on the root
+.. * the new version is then pruned.
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..       A'
+..   A ø⇠ø⇠⊗ A''
+..     | | |
+.. C ◔ | ◔ | B
+..    \|/ /
+..     | /
+..     |/
+..     |
+..     ●
+
+  $ . $TESTDIR/testlib/checkheads-util.sh
+
+Test setup
+----------
+
+  $ setuprepos
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit B0
+  created new head
+  $ mkcommit A1
+  $ hg up '0'
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ mkcommit A2
+  created new head
+  $ hg up '0'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit C0
+  created new head
+  $ hg debugobsolete `getid "desc(A0)"` `getid "desc(A1)"`
+  $ hg debugobsolete `getid "desc(A1)"` `getid "desc(A2)"`
+  $ hg debugobsolete --record-parents `getid "desc(A2)"`
+  $ hg log -G --hidden
+  @  0f88766e02d6 (draft): C0
+  |
+  | x  c1f8d089020f (draft): A2
+  |/
+  | x  ba93660aff8d (draft): A1
+  | |
+  | o  74ff5441d343 (draft): B0
+  |/
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push --rev 'desc(C0)'
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  3 new obsolescence markers
--- a/tests/test-discovery-obshashrange.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-discovery-obshashrange.t	Fri Mar 31 15:44:10 2017 +0200
@@ -57,23 +57,23 @@
   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 debugstablerange --rev tip
-  rev         node index size depth      obshash
-    7 4de32a90b66c     0    8     8 38d1e7ad86ea
-    3 2dc09a01254d     0    4     4 000000000000
-    7 4de32a90b66c     4    4     8 38d1e7ad86ea
-    3 2dc09a01254d     2    2     4 000000000000
-    7 4de32a90b66c     6    2     8 033544c939f0
-    1 66f7d451a68b     0    2     2 17ff8dd63509
-    5 c8d03c1b5e94     4    2     6 57f6cf3757a2
-    2 01241442b3c2     2    1     3 1ed3c61fb39a
-    0 1ea73414a91b     0    1     1 000000000000
-    3 2dc09a01254d     3    1     4 000000000000
-    7 4de32a90b66c     7    1     8 033544c939f0
-    1 66f7d451a68b     1    1     2 17ff8dd63509
-    4 bebd167eb94d     4    1     5 bbe4d7fe27a8
-    5 c8d03c1b5e94     5    1     6 446c2dc3bce5
-    6 f69452c5b1af     6    1     7 000000000000
+  $ hg debugobshashrange --subranges --rev tip
+           rev         node        index         size        depth      obshash
+             7 4de32a90b66c            0            8            8 38d1e7ad86ea
+             3 2dc09a01254d            0            4            4 000000000000
+             7 4de32a90b66c            4            4            8 38d1e7ad86ea
+             3 2dc09a01254d            2            2            4 000000000000
+             7 4de32a90b66c            6            2            8 033544c939f0
+             1 66f7d451a68b            0            2            2 17ff8dd63509
+             5 c8d03c1b5e94            4            2            6 57f6cf3757a2
+             2 01241442b3c2            2            1            3 1ed3c61fb39a
+             0 1ea73414a91b            0            1            1 000000000000
+             3 2dc09a01254d            3            1            4 000000000000
+             7 4de32a90b66c            7            1            8 033544c939f0
+             1 66f7d451a68b            1            1            2 17ff8dd63509
+             4 bebd167eb94d            4            1            5 bbe4d7fe27a8
+             5 c8d03c1b5e94            5            1            6 446c2dc3bce5
+             6 f69452c5b1af            6            1            7 000000000000
   $ cd .. 
 
 testing simple pull
--- a/tests/test-evolve.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-evolve.t	Fri Mar 31 15:44:10 2017 +0200
@@ -171,11 +171,11 @@
 
 Smoketest stablerange.obshash:
 
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    1 7c3bad9141dc     0    2     2 * (glob)
-    0 1f0dee641bb7     0    1     1 000000000000
-    1 7c3bad9141dc     1    1     2 * (glob)
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             1 7c3bad9141dc            0            2            2 * (glob)
+             0 1f0dee641bb7            0            1            1 000000000000
+             1 7c3bad9141dc            1            1            2 * (glob)
 
   $ cd ..
 
--- a/tests/test-exchange-A1.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-A1.t	Fri Mar 31 15:44:10 2017 +0200
@@ -1,7 +1,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 ==== A.1.1 pushing a single head ====
 ..
@@ -48,11 +48,11 @@
   $ hg debugobsrelsethashtree
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 50656e04a95ecdfed94659dd61f663b2caa55e98
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    1 f5bc6836db60     0    2     2 50656e04a95e
-    0 a9bdc8b26820     0    1     1 000000000000
-    1 f5bc6836db60     1    1     2 50656e04a95e
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             1 f5bc6836db60            0            2            2 50656e04a95e
+             0 a9bdc8b26820            0            1            1 000000000000
+             1 f5bc6836db60            1            1            2 50656e04a95e
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-A2.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-A2.t	Fri Mar 31 15:44:10 2017 +0200
@@ -2,7 +2,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === A.2 Two heads ===
 
@@ -61,13 +61,13 @@
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 50656e04a95ecdfed94659dd61f663b2caa55e98
   35b1839966785d5703a01607229eea932db42f87 b9c8f20eef8938ebab939fe6a592587feacf3245
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    2 35b183996678     0    2     2 b9c8f20eef89
-    1 f5bc6836db60     0    2     2 50656e04a95e
-    2 35b183996678     1    1     2 b9c8f20eef89
-    0 a9bdc8b26820     0    1     1 000000000000
-    1 f5bc6836db60     1    1     2 50656e04a95e
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             2 35b183996678            0            2            2 b9c8f20eef89
+             1 f5bc6836db60            0            2            2 50656e04a95e
+             2 35b183996678            1            1            2 b9c8f20eef89
+             0 a9bdc8b26820            0            1            1 000000000000
+             1 f5bc6836db60            1            1            2 50656e04a95e
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-A3.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-A3.t	Fri Mar 31 15:44:10 2017 +0200
@@ -1,7 +1,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === A.3 new branch created ===
 
@@ -76,13 +76,13 @@
   6e72f0a95b5e01a7504743aa941f69cb1fbef8b0 0000000000000000000000000000000000000000
   e5ea8f9c73143125d36658e90ef70c6d2027a5b7 3bc2ee626e11a7cf8fee7a66d069271e17d5a597
   f6298a8ac3a4b78bbeae5f1d3dc5bc3c3812f0f3 91716bfd671b5a5854a47ac5d392edfdd25e431a
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    3 e5ea8f9c7314     0    2     2 3bc2ee626e11
-    4 f6298a8ac3a4     0    2     2 91716bfd671b
-    0 a9bdc8b26820     0    1     1 000000000000
-    3 e5ea8f9c7314     1    1     2 3bc2ee626e11
-    4 f6298a8ac3a4     1    1     2 91716bfd671b
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             3 e5ea8f9c7314            0            2            2 3bc2ee626e11
+             4 f6298a8ac3a4            0            2            2 91716bfd671b
+             0 a9bdc8b26820            0            1            1 000000000000
+             3 e5ea8f9c7314            1            1            2 3bc2ee626e11
+             4 f6298a8ac3a4            1            1            2 91716bfd671b
   $ cd ..
   $ cd ..
 
@@ -181,7 +181,7 @@
   pushing to pushdest
   searching for changes
   abort: push creates new remote head e5ea8f9c7314!
-  (merge or see "hg help push" for details about pushing new heads)
+  (merge or see 'hg help push' for details about pushing new heads)
   [255]
   $ cd ..
 
--- a/tests/test-exchange-A4.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-A4.t	Fri Mar 31 15:44:10 2017 +0200
@@ -1,7 +1,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 
 === A.4 Push in the middle of the obsolescence chain ===
@@ -68,15 +68,15 @@
   28b51eb45704506b5c603decd6bf7ac5e0f6a52f 5d69322fad9eb1ba8f8f2c2312346ed347fdde76
   06055a7959d4128e6e3bccfd01482e83a2db8a3a fd3e5712c9c2d216547d7a1b87ac815ee1fb7542
   e5ea8f9c73143125d36658e90ef70c6d2027a5b7 cf518031fa753e9b049d727e6b0e19f645bab38f
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    2 06055a7959d4     0    3     3 000000000000
-    1 28b51eb45704     0    2     2 5d69322fad9e
-    3 e5ea8f9c7314     0    2     2 cf518031fa75
-    2 06055a7959d4     2    1     3 000000000000
-    1 28b51eb45704     1    1     2 5d69322fad9e
-    0 a9bdc8b26820     0    1     1 000000000000
-    3 e5ea8f9c7314     1    1     2 cf518031fa75
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             2 06055a7959d4            0            3            3 000000000000
+             1 28b51eb45704            0            2            2 5d69322fad9e
+             3 e5ea8f9c7314            0            2            2 cf518031fa75
+             2 06055a7959d4            2            1            3 000000000000
+             1 28b51eb45704            1            1            2 5d69322fad9e
+             0 a9bdc8b26820            0            1            1 000000000000
+             3 e5ea8f9c7314            1            1            2 cf518031fa75
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-A5.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-A5.t	Fri Mar 31 15:44:10 2017 +0200
@@ -2,7 +2,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 
 === A.5 partial reordering ===
@@ -75,13 +75,13 @@
   6e72f0a95b5e01a7504743aa941f69cb1fbef8b0 fd3e5712c9c2d216547d7a1b87ac815ee1fb7542
   f6298a8ac3a4b78bbeae5f1d3dc5bc3c3812f0f3 91716bfd671b5a5854a47ac5d392edfdd25e431a
   8c0a98c8372212c6efde4bfdcef006f27ff759d3 6e8c8c71c47a2bfc27c7cf2b1f4174977ede9f21
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    4 8c0a98c83722     0    3     3 70185b996296
-    3 f6298a8ac3a4     0    2     2 91716bfd671b
-    4 8c0a98c83722     2    1     3 4d835a45c1e9
-    0 a9bdc8b26820     0    1     1 000000000000
-    3 f6298a8ac3a4     1    1     2 91716bfd671b
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             4 8c0a98c83722            0            3            3 70185b996296
+             3 f6298a8ac3a4            0            2            2 91716bfd671b
+             4 8c0a98c83722            2            1            3 4d835a45c1e9
+             0 a9bdc8b26820            0            1            1 000000000000
+             3 f6298a8ac3a4            1            1            2 91716bfd671b
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-A6.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-A6.t	Fri Mar 31 15:44:10 2017 +0200
@@ -3,7 +3,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 
 === A.6 between existing changeset ===
@@ -63,11 +63,11 @@
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000
   28b51eb45704506b5c603decd6bf7ac5e0f6a52f 0000000000000000000000000000000000000000
   e5ea8f9c73143125d36658e90ef70c6d2027a5b7 3bc2ee626e11a7cf8fee7a66d069271e17d5a597
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    2 e5ea8f9c7314     0    2     2 3bc2ee626e11
-    0 a9bdc8b26820     0    1     1 000000000000
-    2 e5ea8f9c7314     1    1     2 3bc2ee626e11
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             2 e5ea8f9c7314            0            2            2 3bc2ee626e11
+             0 a9bdc8b26820            0            1            1 000000000000
+             2 e5ea8f9c7314            1            1            2 3bc2ee626e11
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-A7.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-A7.t	Fri Mar 31 15:44:10 2017 +0200
@@ -1,7 +1,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === A.7 Non targeted common changeset ===
 
@@ -45,11 +45,11 @@
   $ hg debugobsrelsethashtree
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 50656e04a95ecdfed94659dd61f663b2caa55e98
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    1 f5bc6836db60     0    2     2 50656e04a95e
-    0 a9bdc8b26820     0    1     1 000000000000
-    1 f5bc6836db60     1    1     2 50656e04a95e
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             1 f5bc6836db60            0            2            2 50656e04a95e
+             0 a9bdc8b26820            0            1            1 000000000000
+             1 f5bc6836db60            1            1            2 50656e04a95e
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-B1.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-B1.t	Fri Mar 31 15:44:10 2017 +0200
@@ -1,7 +1,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === B.1 Prune on non-targeted common changeset ===
 
@@ -50,11 +50,11 @@
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 926d9d84b97b3483891ae983990ad87c1f7827e9
   f6fbb35d8ac958bbe70035e4c789c18471cdc0af e041f7ff1c7bd5501c7ab602baa35f0873128021
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    1 f5bc6836db60     0    2     2 926d9d84b97b
-    0 a9bdc8b26820     0    1     1 000000000000
-    1 f5bc6836db60     1    1     2 926d9d84b97b
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             1 f5bc6836db60            0            2            2 926d9d84b97b
+             0 a9bdc8b26820            0            1            1 000000000000
+             1 f5bc6836db60            1            1            2 926d9d84b97b
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-B2.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-B2.t	Fri Mar 31 15:44:10 2017 +0200
@@ -1,7 +1,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === B.2 Pruned changeset on head: nothing pushed ===
 
@@ -44,9 +44,9 @@
   $ hg debugobsrelsethashtree
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 52a5380bc04783a9ad43bb2ab2f47a02ef02adcc
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 c5a567339e205e8cc4c494e4fb82944daaec449c
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    0 a9bdc8b26820     0    1     1 52a5380bc047
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             0 a9bdc8b26820            0            1            1 52a5380bc047
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-B3.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-B3.t	Fri Mar 31 15:44:10 2017 +0200
@@ -2,7 +2,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === B.3 Pruned changeset on non-pushed part of the history ===
 
@@ -62,13 +62,13 @@
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 0000000000000000000000000000000000000000
   35b1839966785d5703a01607229eea932db42f87 631ab4cd02ffa1d144dc8f32a18be574076031e3
   e56289ab6378dc752fd7965f8bf66b58bda740bd 47c9d2d8db5d4b1eddd0266329ad260ccc84772c
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    2 35b183996678     0    2     2 631ab4cd02ff
-    1 f5bc6836db60     0    2     2 000000000000
-    2 35b183996678     1    1     2 631ab4cd02ff
-    0 a9bdc8b26820     0    1     1 000000000000
-    1 f5bc6836db60     1    1     2 000000000000
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             2 35b183996678            0            2            2 631ab4cd02ff
+             1 f5bc6836db60            0            2            2 000000000000
+             2 35b183996678            1            1            2 631ab4cd02ff
+             0 a9bdc8b26820            0            1            1 000000000000
+             1 f5bc6836db60            1            1            2 000000000000
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-B4.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-B4.t	Fri Mar 31 15:44:10 2017 +0200
@@ -1,7 +1,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === B.4 Pruned changeset on common part of history ===
 
@@ -72,13 +72,13 @@
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 c27e764c783f451ef3aa40daf2a3795e6674cd06
   f6fbb35d8ac958bbe70035e4c789c18471cdc0af 907beff79fdff2b82b5d3bed7989107a6d744508
   7f7f229b13a629a5b20581c6cb723f4e2ca54bed c27e764c783f451ef3aa40daf2a3795e6674cd06
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    2 f6fbb35d8ac9     0    3     3 000000000000
-    1 f5bc6836db60     0    2     2 000000000000
-    0 a9bdc8b26820     0    1     1 1900882e85db
-    1 f5bc6836db60     1    1     2 000000000000
-    2 f6fbb35d8ac9     2    1     3 000000000000
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             2 f6fbb35d8ac9            0            3            3 000000000000
+             1 f5bc6836db60            0            2            2 000000000000
+             0 a9bdc8b26820            0            1            1 1900882e85db
+             1 f5bc6836db60            1            1            2 000000000000
+             2 f6fbb35d8ac9            2            1            3 000000000000
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-B5.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-B5.t	Fri Mar 31 15:44:10 2017 +0200
@@ -3,7 +3,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 
 === B.5 Push of a children of changeset which successors is pruned ===
@@ -71,13 +71,13 @@
   28b51eb45704506b5c603decd6bf7ac5e0f6a52f 5c81c58ce0a8ad61dd9cf4c6949846b5990af30d
   06055a7959d4128e6e3bccfd01482e83a2db8a3a 201e20697f2a6b0752335af7cd813f140e9e653e
   e5ea8f9c73143125d36658e90ef70c6d2027a5b7 ae1ac676a5e6d6f4216595c53da763d588929970
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    2 06055a7959d4     0    3     3 000000000000
-    1 28b51eb45704     0    2     2 000000000000
-    2 06055a7959d4     2    1     3 000000000000
-    1 28b51eb45704     1    1     2 000000000000
-    0 a9bdc8b26820     0    1     1 554c0b12f7d9
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             2 06055a7959d4            0            3            3 000000000000
+             1 28b51eb45704            0            2            2 000000000000
+             2 06055a7959d4            2            1            3 000000000000
+             1 28b51eb45704            1            1            2 000000000000
+             0 a9bdc8b26820            0            1            1 554c0b12f7d9
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-B6.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-B6.t	Fri Mar 31 15:44:10 2017 +0200
@@ -4,7 +4,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 == B.6 Pruned changeset with ancestors not in pushed set ===
 
@@ -61,11 +61,11 @@
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 f2e05412d3f1d5bc1ae647cf9efc43e0399c26ca
   962ecf6b1afc94e15c7e48fdfb76ef8abd11372b 974507d1c466d0aa86d288836194339ed3b98736
   f6298a8ac3a4b78bbeae5f1d3dc5bc3c3812f0f3 04e03a8959d8a39984e6a8f4a16fba975b364747
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    1 f5bc6836db60     0    2     2 000000000000
-    0 a9bdc8b26820     0    1     1 86e41541149f
-    1 f5bc6836db60     1    1     2 000000000000
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             1 f5bc6836db60            0            2            2 000000000000
+             0 a9bdc8b26820            0            1            1 86e41541149f
+             1 f5bc6836db60            1            1            2 000000000000
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-B7.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-B7.t	Fri Mar 31 15:44:10 2017 +0200
@@ -1,7 +1,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 
 === B.7 Prune on non-targeted common changeset ===
@@ -53,11 +53,11 @@
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 926d9d84b97b3483891ae983990ad87c1f7827e9
   f6fbb35d8ac958bbe70035e4c789c18471cdc0af e041f7ff1c7bd5501c7ab602baa35f0873128021
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    1 f5bc6836db60     0    2     2 926d9d84b97b
-    0 a9bdc8b26820     0    1     1 000000000000
-    1 f5bc6836db60     1    1     2 926d9d84b97b
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             1 f5bc6836db60            0            2            2 926d9d84b97b
+             0 a9bdc8b26820            0            1            1 000000000000
+             1 f5bc6836db60            1            1            2 926d9d84b97b
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-C1.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-C1.t	Fri Mar 31 15:44:10 2017 +0200
@@ -1,7 +1,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === C.1 Multiple pruned changeset atop each other ===
 .. 
@@ -52,9 +52,9 @@
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 1ce18e5a71f78d443a80c819f2f7197c4706af70
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 92af733686ce7e0469d8b2b87b4612a4c2d33468
   f6fbb35d8ac958bbe70035e4c789c18471cdc0af 3800aeba3728457abb9c508c94f6abc59e698c55
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    0 a9bdc8b26820     0    1     1 1ce18e5a71f7
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             0 a9bdc8b26820            0            1            1 1ce18e5a71f7
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-C2.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-C2.t	Fri Mar 31 15:44:10 2017 +0200
@@ -2,7 +2,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === C.2 Pruned changeset on precursors ===
 
@@ -60,11 +60,11 @@
   28b51eb45704506b5c603decd6bf7ac5e0f6a52f 72f95b7b9fa12243aeb90433d211f2c38263da31
   06055a7959d4128e6e3bccfd01482e83a2db8a3a 58ecf9a107b10986d88da605eb0d03b7f24ae486
   e5ea8f9c73143125d36658e90ef70c6d2027a5b7 289cb0d058c81c763eca8bb438657dba9a7ba646
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    3 e5ea8f9c7314     0    2     2 289cb0d058c8
-    0 a9bdc8b26820     0    1     1 000000000000
-    3 e5ea8f9c7314     1    1     2 289cb0d058c8
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             3 e5ea8f9c7314            0            2            2 289cb0d058c8
+             0 a9bdc8b26820            0            1            1 000000000000
+             3 e5ea8f9c7314            1            1            2 289cb0d058c8
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-C3.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-C3.t	Fri Mar 31 15:44:10 2017 +0200
@@ -2,7 +2,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 
 === C.3 Pruned changeset on precursors of another pruned one ===
@@ -65,9 +65,9 @@
   28b51eb45704506b5c603decd6bf7ac5e0f6a52f beac7228bbe708bc7c9181c3c27f8a17f21dbd9f
   06055a7959d4128e6e3bccfd01482e83a2db8a3a 8b648bd67281e9e525919285ac7b3bb2836c2f02
   e5ea8f9c73143125d36658e90ef70c6d2027a5b7 dcd2b566ad0983333be704afdc205066e1a6b742
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    0 a9bdc8b26820     0    1     1 40be80b35671
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             0 a9bdc8b26820            0            1            1 40be80b35671
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-C4.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-C4.t	Fri Mar 31 15:44:10 2017 +0200
@@ -2,7 +2,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === C.4 multiple successors, one is pruned ===
 
@@ -75,11 +75,11 @@
   f5bc6836db60e308a17ba08bf050154ba9c4fad7 619b4d13bd9878f04d7208dcfcf1e89da826f6be
   35b1839966785d5703a01607229eea932db42f87 ddeb7b7a87378f59cecb36d5146df0092b6b3327
   7f7f229b13a629a5b20581c6cb723f4e2ca54bed 58ef2e726c5bd89bceffb6243294b38eadbf3d60
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    2 35b183996678     0    2     2 2a098b4a877f
-    2 35b183996678     1    1     2 916e804c50de
-    0 a9bdc8b26820     0    1     1 a9c02d134f5b
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             2 35b183996678            0            2            2 2a098b4a877f
+             2 35b183996678            1            1            2 916e804c50de
+             0 a9bdc8b26820            0            1            1 a9c02d134f5b
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-D1.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-D1.t	Fri Mar 31 15:44:10 2017 +0200
@@ -1,7 +1,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === D.1 Pruned changeset based on missing precursor of something not present ===
 
@@ -55,11 +55,11 @@
   $ hg debugobsrelsethashtree
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000
   e5ea8f9c73143125d36658e90ef70c6d2027a5b7 289cb0d058c81c763eca8bb438657dba9a7ba646
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    1 e5ea8f9c7314     0    2     2 289cb0d058c8
-    0 a9bdc8b26820     0    1     1 000000000000
-    1 e5ea8f9c7314     1    1     2 289cb0d058c8
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             1 e5ea8f9c7314            0            2            2 289cb0d058c8
+             0 a9bdc8b26820            0            1            1 000000000000
+             1 e5ea8f9c7314            1            1            2 289cb0d058c8
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-D2.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-D2.t	Fri Mar 31 15:44:10 2017 +0200
@@ -2,7 +2,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === D.2 missing prune target (prune in "pushed set") ===
 
@@ -52,9 +52,9 @@
   $ hg debugobsrelsethashtree
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 554c0b12f7d9fff20cb904c26e12eee337e3309c
   28b51eb45704506b5c603decd6bf7ac5e0f6a52f 5c81c58ce0a8ad61dd9cf4c6949846b5990af30d
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    0 a9bdc8b26820     0    1     1 554c0b12f7d9
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             0 a9bdc8b26820            0            1            1 554c0b12f7d9
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-D3.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-D3.t	Fri Mar 31 15:44:10 2017 +0200
@@ -3,7 +3,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === D.2 missing prune target (prune in "pushed set") ===
 
@@ -57,11 +57,11 @@
   a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000
   28b51eb45704506b5c603decd6bf7ac5e0f6a52f 0000000000000000000000000000000000000000
   35b1839966785d5703a01607229eea932db42f87 65a9f21dff0702355e973a8f31d3b3b7e59376fb
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    2 35b183996678     0    2     2 65a9f21dff07
-    2 35b183996678     1    1     2 65a9f21dff07
-    0 a9bdc8b26820     0    1     1 000000000000
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             2 35b183996678            0            2            2 65a9f21dff07
+             2 35b183996678            1            1            2 65a9f21dff07
+             0 a9bdc8b26820            0            1            1 000000000000
   $ cd ..
   $ cd ..
 
--- a/tests/test-exchange-D4.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-exchange-D4.t	Fri Mar 31 15:44:10 2017 +0200
@@ -2,7 +2,7 @@
 
 Initial setup
 
-  $ . $TESTDIR/_exc-util.sh
+  $ . $TESTDIR/testlib/exchange-util.sh
 
 === D.4 Unknown changeset in between known one ===
 
@@ -71,13 +71,13 @@
   6e72f0a95b5e01a7504743aa941f69cb1fbef8b0 0000000000000000000000000000000000000000
   e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0aacc2f86e8fca29f2d5fd8d0790644620acd58a
   069b05c3876d56f62895e853a501ea58ea85f68d 40b98bc2b5b1152416ea8e9665ae1c6a3ce32ba0
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-    4 069b05c3876d     0    3     3 a2b2331da650
-    3 e5ea8f9c7314     0    2     2 0aacc2f86e8f
-    4 069b05c3876d     2    1     3 901f118d4333
-    0 a9bdc8b26820     0    1     1 000000000000
-    3 e5ea8f9c7314     1    1     2 0aacc2f86e8f
+  $ hg debugobshashrange --subranges --rev 'head()'
+           rev         node        index         size        depth      obshash
+             4 069b05c3876d            0            3            3 a2b2331da650
+             3 e5ea8f9c7314            0            2            2 0aacc2f86e8f
+             4 069b05c3876d            2            1            3 901f118d4333
+             0 a9bdc8b26820            0            1            1 000000000000
+             3 e5ea8f9c7314            1            1            2 0aacc2f86e8f
   $ cd ..
   $ cd ..
 
--- a/tests/test-inhibit.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-inhibit.t	Fri Mar 31 15:44:10 2017 +0200
@@ -807,7 +807,7 @@
   [255]
 
 Visible commits can still be pushed
-  $ hg push -r 71eb4f100663 $pwd/inhibit2
+  $ hg push -fr 71eb4f100663 $pwd/inhibit2
   pushing to $TESTTMP/inhibit2
   searching for changes
   adding changesets
@@ -911,7 +911,7 @@
   $ cd not-inhibit
   $ hg book -d foo
   $ hg pull
-  pulling from $TESTTMP/inhibit
+  pulling from $TESTTMP/inhibit (glob)
   searching for changes
   no changes found
   adding remote bookmark foo
--- a/tests/test-sharing.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-sharing.t	Fri Mar 31 15:44:10 2017 +0200
@@ -342,7 +342,7 @@
   searching for changes
   remote has heads on branch 'default' that are not known locally: 540ba8f317e6
   abort: push creates new remote head cbdfbd5a5db2 with bookmark 'bug15'!
-  (pull and merge or see "hg help push" for details about pushing new heads)
+  (pull and merge or see 'hg help push' for details about pushing new heads)
   [255]
   $ hg pull ../public
   pulling from ../public
--- a/tests/test-stablerange.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-stablerange.t	Fri Mar 31 15:44:10 2017 +0200
@@ -16,114 +16,107 @@
   $ hg init repo_linear
   $ cd repo_linear
   $ hg debugbuilddag '.+6'
-  $ hg debugstablerange --rev 1
-  rev         node index size depth      obshash
-    1 66f7d451a68b     0    2     2 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-  $ hg debugstablerange --rev 1 > 1.range
+  $ hg debugstablerange --verify --verbose --subranges --rev 1
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 1 > 1.range
 
 bigger subset reuse most of the previous one
 
-  $ hg debugstablerange --rev 4
-  rev         node index size depth      obshash
-    4 bebd167eb94d     0    5     5 000000000000
-    3 2dc09a01254d     0    4     4 000000000000
-    3 2dc09a01254d     2    2     4 000000000000
-    1 66f7d451a68b     0    2     2 000000000000
-    2 01241442b3c2     2    1     3 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    3 2dc09a01254d     3    1     4 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-    4 bebd167eb94d     4    1     5 000000000000
-  $ hg debugstablerange --rev 4 > 4.range
+  $ hg debugstablerange --verify --verbose --subranges --rev 4
+  bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1)
+  2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  01241442b3c2-2 (2, 3, 1) [leaf] - 
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  2dc09a01254d-3 (3, 4, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  bebd167eb94d-4 (4, 5, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 4 > 4.range
   $ diff -u 1.range 4.range
   --- 1.range	* (glob)
   +++ 4.range	* (glob)
-  @@ -1,4 +1,10 @@
-   rev         node index size depth      obshash
-  +  4 bebd167eb94d     0    5     5 000000000000
-  +  3 2dc09a01254d     0    4     4 000000000000
-  +  3 2dc09a01254d     2    2     4 000000000000
-     1 66f7d451a68b     0    2     2 000000000000
-  +  2 01241442b3c2     2    1     3 000000000000
-     0 1ea73414a91b     0    1     1 000000000000
-  +  3 2dc09a01254d     3    1     4 000000000000
-     1 66f7d451a68b     1    1     2 000000000000
-  +  4 bebd167eb94d     4    1     5 000000000000
+  @@ -1,3 +1,9 @@
+  +bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1)
+  +2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  +2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+   66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  +01241442b3c2-2 (2, 3, 1) [leaf] - 
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  +2dc09a01254d-3 (3, 4, 1) [leaf] - 
+   66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  +bebd167eb94d-4 (4, 5, 1) [leaf] - 
   [1]
 
 Using a range not ending on 2**N boundary
 we fall back on 2**N as much as possible
 
-  $ hg debugstablerange --rev 5
-  rev         node index size depth      obshash
-    5 c8d03c1b5e94     0    6     6 000000000000
-    3 2dc09a01254d     0    4     4 000000000000
-    3 2dc09a01254d     2    2     4 000000000000
-    1 66f7d451a68b     0    2     2 000000000000
-    5 c8d03c1b5e94     4    2     6 000000000000
-    2 01241442b3c2     2    1     3 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    3 2dc09a01254d     3    1     4 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-    4 bebd167eb94d     4    1     5 000000000000
-    5 c8d03c1b5e94     5    1     6 000000000000
-  $ hg debugstablerange --rev 5 > 5.range
+  $ hg debugstablerange --verify --verbose --subranges --rev 5
+  c8d03c1b5e94-0 (5, 6, 6) [complete] - 2dc09a01254d-0 (3, 4, 4), c8d03c1b5e94-4 (5, 6, 2)
+  2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  c8d03c1b5e94-4 (5, 6, 2) [complete] - bebd167eb94d-4 (4, 5, 1), c8d03c1b5e94-5 (5, 6, 1)
+  01241442b3c2-2 (2, 3, 1) [leaf] - 
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  2dc09a01254d-3 (3, 4, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  bebd167eb94d-4 (4, 5, 1) [leaf] - 
+  c8d03c1b5e94-5 (5, 6, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 5 > 5.range
   $ diff -u 4.range 5.range
   --- 4.range	* (glob)
   +++ 5.range	* (glob)
-  @@ -1,10 +1,12 @@
-   rev         node index size depth      obshash
-  -  4 bebd167eb94d     0    5     5 000000000000
-  +  5 c8d03c1b5e94     0    6     6 000000000000
-     3 2dc09a01254d     0    4     4 000000000000
-     3 2dc09a01254d     2    2     4 000000000000
-     1 66f7d451a68b     0    2     2 000000000000
-  +  5 c8d03c1b5e94     4    2     6 000000000000
-     2 01241442b3c2     2    1     3 000000000000
-     0 1ea73414a91b     0    1     1 000000000000
-     3 2dc09a01254d     3    1     4 000000000000
-     1 66f7d451a68b     1    1     2 000000000000
-     4 bebd167eb94d     4    1     5 000000000000
-  +  5 c8d03c1b5e94     5    1     6 000000000000
+  @@ -1,9 +1,11 @@
+  -bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1)
+  +c8d03c1b5e94-0 (5, 6, 6) [complete] - 2dc09a01254d-0 (3, 4, 4), c8d03c1b5e94-4 (5, 6, 2)
+   2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+   2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+   66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  +c8d03c1b5e94-4 (5, 6, 2) [complete] - bebd167eb94d-4 (4, 5, 1), c8d03c1b5e94-5 (5, 6, 1)
+   01241442b3c2-2 (2, 3, 1) [leaf] - 
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+   2dc09a01254d-3 (3, 4, 1) [leaf] - 
+   66f7d451a68b-1 (1, 2, 1) [leaf] - 
+   bebd167eb94d-4 (4, 5, 1) [leaf] - 
+  +c8d03c1b5e94-5 (5, 6, 1) [leaf] - 
   [1]
 
 Even two unperfect range overlap a lot
 
-  $ hg debugstablerange --rev tip
-  rev         node index size depth      obshash
-    6 f69452c5b1af     0    7     7 000000000000
-    3 2dc09a01254d     0    4     4 000000000000
-    6 f69452c5b1af     4    3     7 000000000000
-    3 2dc09a01254d     2    2     4 000000000000
-    1 66f7d451a68b     0    2     2 000000000000
-    5 c8d03c1b5e94     4    2     6 000000000000
-    2 01241442b3c2     2    1     3 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    3 2dc09a01254d     3    1     4 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-    4 bebd167eb94d     4    1     5 000000000000
-    5 c8d03c1b5e94     5    1     6 000000000000
-    6 f69452c5b1af     6    1     7 000000000000
-  $ hg debugstablerange --rev tip > tip.range
+  $ hg debugstablerange --verify --verbose --subranges --rev tip
+  f69452c5b1af-0 (6, 7, 7) [complete] - 2dc09a01254d-0 (3, 4, 4), f69452c5b1af-4 (6, 7, 3)
+  2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  f69452c5b1af-4 (6, 7, 3) [complete] - c8d03c1b5e94-4 (5, 6, 2), f69452c5b1af-6 (6, 7, 1)
+  2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  c8d03c1b5e94-4 (5, 6, 2) [complete] - bebd167eb94d-4 (4, 5, 1), c8d03c1b5e94-5 (5, 6, 1)
+  01241442b3c2-2 (2, 3, 1) [leaf] - 
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  2dc09a01254d-3 (3, 4, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  bebd167eb94d-4 (4, 5, 1) [leaf] - 
+  c8d03c1b5e94-5 (5, 6, 1) [leaf] - 
+  f69452c5b1af-6 (6, 7, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev tip > tip.range
   $ diff -u 5.range tip.range
   --- 5.range	* (glob)
   +++ tip.range	* (glob)
-  @@ -1,6 +1,7 @@
-   rev         node index size depth      obshash
-  -  5 c8d03c1b5e94     0    6     6 000000000000
-  +  6 f69452c5b1af     0    7     7 000000000000
-     3 2dc09a01254d     0    4     4 000000000000
-  +  6 f69452c5b1af     4    3     7 000000000000
-     3 2dc09a01254d     2    2     4 000000000000
-     1 66f7d451a68b     0    2     2 000000000000
-     5 c8d03c1b5e94     4    2     6 000000000000
-  @@ -10,3 +11,4 @@
-     1 66f7d451a68b     1    1     2 000000000000
-     4 bebd167eb94d     4    1     5 000000000000
-     5 c8d03c1b5e94     5    1     6 000000000000
-  +  6 f69452c5b1af     6    1     7 000000000000
+  @@ -1,5 +1,6 @@
+  -c8d03c1b5e94-0 (5, 6, 6) [complete] - 2dc09a01254d-0 (3, 4, 4), c8d03c1b5e94-4 (5, 6, 2)
+  +f69452c5b1af-0 (6, 7, 7) [complete] - 2dc09a01254d-0 (3, 4, 4), f69452c5b1af-4 (6, 7, 3)
+   2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  +f69452c5b1af-4 (6, 7, 3) [complete] - c8d03c1b5e94-4 (5, 6, 2), f69452c5b1af-6 (6, 7, 1)
+   2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+   66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+   c8d03c1b5e94-4 (5, 6, 2) [complete] - bebd167eb94d-4 (4, 5, 1), c8d03c1b5e94-5 (5, 6, 1)
+  @@ -9,3 +10,4 @@
+   66f7d451a68b-1 (1, 2, 1) [leaf] - 
+   bebd167eb94d-4 (4, 5, 1) [leaf] - 
+   c8d03c1b5e94-5 (5, 6, 1) [leaf] - 
+  +f69452c5b1af-6 (6, 7, 1) [leaf] - 
   [1]
 
   $ cd ..
@@ -168,132 +161,123 @@
 
 (left branch)
 
-  $ hg debugstablerange --rev 'left~2'
-  rev         node index size depth      obshash
-    1 66f7d451a68b     0    2     2 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-  $ hg debugstablerange --rev 'left~2' > left-2.range
-  $ hg debugstablerange --rev left
-  rev         node index size depth      obshash
-    3 2dc09a01254d     0    4     4 000000000000
-    3 2dc09a01254d     2    2     4 000000000000
-    1 66f7d451a68b     0    2     2 000000000000
-    2 01241442b3c2     2    1     3 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    3 2dc09a01254d     3    1     4 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-  $ hg debugstablerange --rev 'left' > left.range
+  $ hg debugstablerange --verify --verbose --subranges --rev 'left~2'
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'left~2' > left-2.range
+  $ hg debugstablerange --verify --verbose --subranges --rev left
+  2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  01241442b3c2-2 (2, 3, 1) [leaf] - 
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  2dc09a01254d-3 (3, 4, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'left' > left.range
   $ diff -u left-2.range left.range
   --- left-2.range	* (glob)
   +++ left.range	* (glob)
-  @@ -1,4 +1,8 @@
-   rev         node index size depth      obshash
-  +  3 2dc09a01254d     0    4     4 000000000000
-  +  3 2dc09a01254d     2    2     4 000000000000
-     1 66f7d451a68b     0    2     2 000000000000
-  +  2 01241442b3c2     2    1     3 000000000000
-     0 1ea73414a91b     0    1     1 000000000000
-  +  3 2dc09a01254d     3    1     4 000000000000
-     1 66f7d451a68b     1    1     2 000000000000
+  @@ -1,3 +1,7 @@
+  +2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  +2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+   66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  +01241442b3c2-2 (2, 3, 1) [leaf] - 
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  +2dc09a01254d-3 (3, 4, 1) [leaf] - 
+   66f7d451a68b-1 (1, 2, 1) [leaf] - 
   [1]
 
 (right branch)
 
-  $ hg debugstablerange --rev right~2
-  rev         node index size depth      obshash
-    4 e7bd5218ca15     0    2     2 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    4 e7bd5218ca15     1    1     2 000000000000
-  $ hg debugstablerange --rev 'right~2' > right-2.range
-  $ hg debugstablerange --rev right
-  rev         node index size depth      obshash
-    6 a2f58e9c1e56     0    4     4 000000000000
-    6 a2f58e9c1e56     2    2     4 000000000000
-    4 e7bd5218ca15     0    2     2 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    5 3a367db1fabc     2    1     3 000000000000
-    6 a2f58e9c1e56     3    1     4 000000000000
-    4 e7bd5218ca15     1    1     2 000000000000
-  $ hg debugstablerange --rev 'right' > right.range
+  $ hg debugstablerange --verify --verbose --subranges --rev right~2
+  e7bd5218ca15-0 (4, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), e7bd5218ca15-1 (4, 2, 1)
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  e7bd5218ca15-1 (4, 2, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'right~2' > right-2.range
+  $ hg debugstablerange --verify --verbose --subranges --rev right
+  a2f58e9c1e56-0 (6, 4, 4) [complete] - e7bd5218ca15-0 (4, 2, 2), a2f58e9c1e56-2 (6, 4, 2)
+  a2f58e9c1e56-2 (6, 4, 2) [complete] - 3a367db1fabc-2 (5, 3, 1), a2f58e9c1e56-3 (6, 4, 1)
+  e7bd5218ca15-0 (4, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), e7bd5218ca15-1 (4, 2, 1)
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  3a367db1fabc-2 (5, 3, 1) [leaf] - 
+  a2f58e9c1e56-3 (6, 4, 1) [leaf] - 
+  e7bd5218ca15-1 (4, 2, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'right' > right.range
   $ diff -u right-2.range right.range
   --- right-2.range	* (glob)
   +++ right.range	* (glob)
-  @@ -1,4 +1,8 @@
-   rev         node index size depth      obshash
-  +  6 a2f58e9c1e56     0    4     4 000000000000
-  +  6 a2f58e9c1e56     2    2     4 000000000000
-     4 e7bd5218ca15     0    2     2 000000000000
-     0 1ea73414a91b     0    1     1 000000000000
-  +  5 3a367db1fabc     2    1     3 000000000000
-  +  6 a2f58e9c1e56     3    1     4 000000000000
-     4 e7bd5218ca15     1    1     2 000000000000
+  @@ -1,3 +1,7 @@
+  +a2f58e9c1e56-0 (6, 4, 4) [complete] - e7bd5218ca15-0 (4, 2, 2), a2f58e9c1e56-2 (6, 4, 2)
+  +a2f58e9c1e56-2 (6, 4, 2) [complete] - 3a367db1fabc-2 (5, 3, 1), a2f58e9c1e56-3 (6, 4, 1)
+   e7bd5218ca15-0 (4, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), e7bd5218ca15-1 (4, 2, 1)
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  +3a367db1fabc-2 (5, 3, 1) [leaf] - 
+  +a2f58e9c1e56-3 (6, 4, 1) [leaf] - 
+   e7bd5218ca15-1 (4, 2, 1) [leaf] - 
   [1]
 
 The merge reuse as much of the slicing created for one of the branch
 
-  $ hg debugstablerange --rev merge
-  rev         node index size depth      obshash
-    7 5f18015f9110     0    8     8 000000000000
-    3 2dc09a01254d     0    4     4 000000000000
-    7 5f18015f9110     4    4     8 000000000000
-    3 2dc09a01254d     2    2     4 000000000000
-    5 3a367db1fabc     1    2     3 000000000000
-    7 5f18015f9110     6    2     8 000000000000
-    1 66f7d451a68b     0    2     2 000000000000
-    2 01241442b3c2     2    1     3 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    3 2dc09a01254d     3    1     4 000000000000
-    5 3a367db1fabc     2    1     3 000000000000
-    7 5f18015f9110     7    1     8 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-    6 a2f58e9c1e56     3    1     4 000000000000
-    4 e7bd5218ca15     1    1     2 000000000000
-  $ hg debugstablerange --rev 'merge' > merge.range
+  $ hg debugstablerange --verify --verbose --subranges --rev merge
+  5f18015f9110-0 (7, 8, 8) [complete] - 2dc09a01254d-0 (3, 4, 4), 5f18015f9110-4 (7, 8, 4)
+  2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  5f18015f9110-4 (7, 8, 4) [complete] - 3a367db1fabc-1 (5, 3, 2), 5f18015f9110-6 (7, 8, 2)
+  2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  3a367db1fabc-1 (5, 3, 2) [complete] - e7bd5218ca15-1 (4, 2, 1), 3a367db1fabc-2 (5, 3, 1)
+  5f18015f9110-6 (7, 8, 2) [complete] - a2f58e9c1e56-3 (6, 4, 1), 5f18015f9110-7 (7, 8, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  01241442b3c2-2 (2, 3, 1) [leaf] - 
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  2dc09a01254d-3 (3, 4, 1) [leaf] - 
+  3a367db1fabc-2 (5, 3, 1) [leaf] - 
+  5f18015f9110-7 (7, 8, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  a2f58e9c1e56-3 (6, 4, 1) [leaf] - 
+  e7bd5218ca15-1 (4, 2, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'merge' > merge.range
   $ diff -u left.range merge.range
   --- left.range	* (glob)
   +++ merge.range	* (glob)
-  @@ -1,8 +1,16 @@
-   rev         node index size depth      obshash
-  +  7 5f18015f9110     0    8     8 000000000000
-     3 2dc09a01254d     0    4     4 000000000000
-  +  7 5f18015f9110     4    4     8 000000000000
-     3 2dc09a01254d     2    2     4 000000000000
-  +  5 3a367db1fabc     1    2     3 000000000000
-  +  7 5f18015f9110     6    2     8 000000000000
-     1 66f7d451a68b     0    2     2 000000000000
-     2 01241442b3c2     2    1     3 000000000000
-     0 1ea73414a91b     0    1     1 000000000000
-     3 2dc09a01254d     3    1     4 000000000000
-  +  5 3a367db1fabc     2    1     3 000000000000
-  +  7 5f18015f9110     7    1     8 000000000000
-     1 66f7d451a68b     1    1     2 000000000000
-  +  6 a2f58e9c1e56     3    1     4 000000000000
-  +  4 e7bd5218ca15     1    1     2 000000000000
+  @@ -1,7 +1,15 @@
+  +5f18015f9110-0 (7, 8, 8) [complete] - 2dc09a01254d-0 (3, 4, 4), 5f18015f9110-4 (7, 8, 4)
+   2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  +5f18015f9110-4 (7, 8, 4) [complete] - 3a367db1fabc-1 (5, 3, 2), 5f18015f9110-6 (7, 8, 2)
+   2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  +3a367db1fabc-1 (5, 3, 2) [complete] - e7bd5218ca15-1 (4, 2, 1), 3a367db1fabc-2 (5, 3, 1)
+  +5f18015f9110-6 (7, 8, 2) [complete] - a2f58e9c1e56-3 (6, 4, 1), 5f18015f9110-7 (7, 8, 1)
+   66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+   01241442b3c2-2 (2, 3, 1) [leaf] - 
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+   2dc09a01254d-3 (3, 4, 1) [leaf] - 
+  +3a367db1fabc-2 (5, 3, 1) [leaf] - 
+  +5f18015f9110-7 (7, 8, 1) [leaf] - 
+   66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  +a2f58e9c1e56-3 (6, 4, 1) [leaf] - 
+  +e7bd5218ca15-1 (4, 2, 1) [leaf] - 
   [1]
   $ diff -u right.range merge.range
   --- right.range	* (glob)
   +++ merge.range	* (glob)
-  @@ -1,8 +1,16 @@
-   rev         node index size depth      obshash
-  -  6 a2f58e9c1e56     0    4     4 000000000000
-  -  6 a2f58e9c1e56     2    2     4 000000000000
-  -  4 e7bd5218ca15     0    2     2 000000000000
-  +  7 5f18015f9110     0    8     8 000000000000
-  +  3 2dc09a01254d     0    4     4 000000000000
-  +  7 5f18015f9110     4    4     8 000000000000
-  +  3 2dc09a01254d     2    2     4 000000000000
-  +  5 3a367db1fabc     1    2     3 000000000000
-  +  7 5f18015f9110     6    2     8 000000000000
-  +  1 66f7d451a68b     0    2     2 000000000000
-  +  2 01241442b3c2     2    1     3 000000000000
-     0 1ea73414a91b     0    1     1 000000000000
-  +  3 2dc09a01254d     3    1     4 000000000000
-     5 3a367db1fabc     2    1     3 000000000000
-  +  7 5f18015f9110     7    1     8 000000000000
-  +  1 66f7d451a68b     1    1     2 000000000000
-     6 a2f58e9c1e56     3    1     4 000000000000
-     4 e7bd5218ca15     1    1     2 000000000000
+  @@ -1,7 +1,15 @@
+  -a2f58e9c1e56-0 (6, 4, 4) [complete] - e7bd5218ca15-0 (4, 2, 2), a2f58e9c1e56-2 (6, 4, 2)
+  -a2f58e9c1e56-2 (6, 4, 2) [complete] - 3a367db1fabc-2 (5, 3, 1), a2f58e9c1e56-3 (6, 4, 1)
+  -e7bd5218ca15-0 (4, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), e7bd5218ca15-1 (4, 2, 1)
+  +5f18015f9110-0 (7, 8, 8) [complete] - 2dc09a01254d-0 (3, 4, 4), 5f18015f9110-4 (7, 8, 4)
+  +2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  +5f18015f9110-4 (7, 8, 4) [complete] - 3a367db1fabc-1 (5, 3, 2), 5f18015f9110-6 (7, 8, 2)
+  +2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  +3a367db1fabc-1 (5, 3, 2) [complete] - e7bd5218ca15-1 (4, 2, 1), 3a367db1fabc-2 (5, 3, 1)
+  +5f18015f9110-6 (7, 8, 2) [complete] - a2f58e9c1e56-3 (6, 4, 1), 5f18015f9110-7 (7, 8, 1)
+  +66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  +01241442b3c2-2 (2, 3, 1) [leaf] - 
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  +2dc09a01254d-3 (3, 4, 1) [leaf] - 
+   3a367db1fabc-2 (5, 3, 1) [leaf] - 
+  +5f18015f9110-7 (7, 8, 1) [leaf] - 
+  +66f7d451a68b-1 (1, 2, 1) [leaf] - 
+   a2f58e9c1e56-3 (6, 4, 1) [leaf] - 
+   e7bd5218ca15-1 (4, 2, 1) [leaf] - 
   [1]
   $ cd ..
 
@@ -348,85 +332,79 @@
 
 (left branch)
 
-  $ hg debugstablerange --rev 'left~2'
-  rev         node index size depth      obshash
-    2 01241442b3c2     0    3     3 000000000000
-    1 66f7d451a68b     0    2     2 000000000000
-    2 01241442b3c2     2    1     3 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-  $ hg debugstablerange --rev 'left~2' > left-2.range
-  $ hg debugstablerange --rev left
-  rev         node index size depth      obshash
-    4 bebd167eb94d     0    5     5 000000000000
-    3 2dc09a01254d     0    4     4 000000000000
-    3 2dc09a01254d     2    2     4 000000000000
-    1 66f7d451a68b     0    2     2 000000000000
-    2 01241442b3c2     2    1     3 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    3 2dc09a01254d     3    1     4 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-    4 bebd167eb94d     4    1     5 000000000000
-  $ hg debugstablerange --rev 'left' > left.range
+  $ hg debugstablerange --verify --verbose --subranges --rev 'left~2'
+  01241442b3c2-0 (2, 3, 3) [complete] - 66f7d451a68b-0 (1, 2, 2), 01241442b3c2-2 (2, 3, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  01241442b3c2-2 (2, 3, 1) [leaf] - 
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'left~2' > left-2.range
+  $ hg debugstablerange --verify --verbose --subranges --rev left
+  bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1)
+  2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  01241442b3c2-2 (2, 3, 1) [leaf] - 
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  2dc09a01254d-3 (3, 4, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  bebd167eb94d-4 (4, 5, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'left' > left.range
   $ diff -u left-2.range left.range
   --- left-2.range	* (glob)
   +++ left.range	* (glob)
-  @@ -1,6 +1,10 @@
-   rev         node index size depth      obshash
-  -  2 01241442b3c2     0    3     3 000000000000
-  +  4 bebd167eb94d     0    5     5 000000000000
-  +  3 2dc09a01254d     0    4     4 000000000000
-  +  3 2dc09a01254d     2    2     4 000000000000
-     1 66f7d451a68b     0    2     2 000000000000
-     2 01241442b3c2     2    1     3 000000000000
-     0 1ea73414a91b     0    1     1 000000000000
-  +  3 2dc09a01254d     3    1     4 000000000000
-     1 66f7d451a68b     1    1     2 000000000000
-  +  4 bebd167eb94d     4    1     5 000000000000
+  @@ -1,5 +1,9 @@
+  -01241442b3c2-0 (2, 3, 3) [complete] - 66f7d451a68b-0 (1, 2, 2), 01241442b3c2-2 (2, 3, 1)
+  +bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1)
+  +2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  +2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+   66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+   01241442b3c2-2 (2, 3, 1) [leaf] - 
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  +2dc09a01254d-3 (3, 4, 1) [leaf] - 
+   66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  +bebd167eb94d-4 (4, 5, 1) [leaf] - 
   [1]
 
 (right branch)
 
-  $ hg debugstablerange --rev right~2
-  rev         node index size depth      obshash
-    7 42b07e8da27d     0    4     4 000000000000
-    7 42b07e8da27d     2    2     4 000000000000
-    5 de561312eff4     0    2     2 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    7 42b07e8da27d     3    1     4 000000000000
-    6 b9bc20507e0b     2    1     3 000000000000
-    5 de561312eff4     1    1     2 000000000000
-  $ hg debugstablerange --rev 'right~2' > right-2.range
-  $ hg debugstablerange --rev right
-  rev         node index size depth      obshash
-    9 f4b7da68b467     0    6     6 000000000000
-    7 42b07e8da27d     0    4     4 000000000000
-    7 42b07e8da27d     2    2     4 000000000000
-    5 de561312eff4     0    2     2 000000000000
-    9 f4b7da68b467     4    2     6 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    7 42b07e8da27d     3    1     4 000000000000
-    8 857477a9aebb     4    1     5 000000000000
-    6 b9bc20507e0b     2    1     3 000000000000
-    5 de561312eff4     1    1     2 000000000000
-    9 f4b7da68b467     5    1     6 000000000000
-  $ hg debugstablerange --rev 'right' > right.range
+  $ hg debugstablerange --verify --verbose --subranges --rev right~2
+  42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2)
+  42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1)
+  de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1)
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  42b07e8da27d-3 (7, 4, 1) [leaf] - 
+  b9bc20507e0b-2 (6, 3, 1) [leaf] - 
+  de561312eff4-1 (5, 2, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'right~2' > right-2.range
+  $ hg debugstablerange --verify --verbose --subranges --rev right
+  f4b7da68b467-0 (9, 6, 6) [complete] - 42b07e8da27d-0 (7, 4, 4), f4b7da68b467-4 (9, 6, 2)
+  42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2)
+  42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1)
+  de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1)
+  f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1)
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  42b07e8da27d-3 (7, 4, 1) [leaf] - 
+  857477a9aebb-4 (8, 5, 1) [leaf] - 
+  b9bc20507e0b-2 (6, 3, 1) [leaf] - 
+  de561312eff4-1 (5, 2, 1) [leaf] - 
+  f4b7da68b467-5 (9, 6, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'right' > right.range
   $ diff -u right-2.range right.range
   --- right-2.range	* (glob)
   +++ right.range	* (glob)
-  @@ -1,8 +1,12 @@
-   rev         node index size depth      obshash
-  +  9 f4b7da68b467     0    6     6 000000000000
-     7 42b07e8da27d     0    4     4 000000000000
-     7 42b07e8da27d     2    2     4 000000000000
-     5 de561312eff4     0    2     2 000000000000
-  +  9 f4b7da68b467     4    2     6 000000000000
-     0 1ea73414a91b     0    1     1 000000000000
-     7 42b07e8da27d     3    1     4 000000000000
-  +  8 857477a9aebb     4    1     5 000000000000
-     6 b9bc20507e0b     2    1     3 000000000000
-     5 de561312eff4     1    1     2 000000000000
-  +  9 f4b7da68b467     5    1     6 000000000000
+  @@ -1,7 +1,11 @@
+  +f4b7da68b467-0 (9, 6, 6) [complete] - 42b07e8da27d-0 (7, 4, 4), f4b7da68b467-4 (9, 6, 2)
+   42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2)
+   42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1)
+   de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1)
+  +f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1)
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+   42b07e8da27d-3 (7, 4, 1) [leaf] - 
+  +857477a9aebb-4 (8, 5, 1) [leaf] - 
+   b9bc20507e0b-2 (6, 3, 1) [leaf] - 
+   de561312eff4-1 (5, 2, 1) [leaf] - 
+  +f4b7da68b467-5 (9, 6, 1) [leaf] - 
   [1]
 
 In this case, the bottom of the split will have multiple heads,
@@ -435,148 +413,143 @@
 
 We are still able to reuse one of the branch however
 
-  $ hg debugstablerange --rev merge
-  rev         node index size depth      obshash
-   10 8aca7f8c9bd2     0   11    11 000000000000
-    4 bebd167eb94d     0    5     5 000000000000
-    3 2dc09a01254d     0    4     4 000000000000
-    7 42b07e8da27d     0    4     4 000000000000
-   10 8aca7f8c9bd2     8    3    11 000000000000
-    3 2dc09a01254d     2    2     4 000000000000
-    7 42b07e8da27d     2    2     4 000000000000
-    1 66f7d451a68b     0    2     2 000000000000
-    5 de561312eff4     0    2     2 000000000000
-    9 f4b7da68b467     4    2     6 000000000000
-    2 01241442b3c2     2    1     3 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    3 2dc09a01254d     3    1     4 000000000000
-    7 42b07e8da27d     3    1     4 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-    8 857477a9aebb     4    1     5 000000000000
-   10 8aca7f8c9bd2    10    1    11 000000000000
-    6 b9bc20507e0b     2    1     3 000000000000
-    4 bebd167eb94d     4    1     5 000000000000
-    5 de561312eff4     1    1     2 000000000000
-    9 f4b7da68b467     5    1     6 000000000000
-  $ hg debugstablerange --rev 'merge' > merge.range
+  $ hg debugstablerange --verify --verbose --subranges --rev merge
+  8aca7f8c9bd2-0 (10, 11, 11) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), 8aca7f8c9bd2-8 (10, 11, 3)
+  bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1)
+  2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2)
+  8aca7f8c9bd2-8 (10, 11, 3) [complete] - f4b7da68b467-4 (9, 6, 2), 8aca7f8c9bd2-10 (10, 11, 1)
+  2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1)
+  f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1)
+  01241442b3c2-2 (2, 3, 1) [leaf] - 
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  2dc09a01254d-3 (3, 4, 1) [leaf] - 
+  42b07e8da27d-3 (7, 4, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  857477a9aebb-4 (8, 5, 1) [leaf] - 
+  8aca7f8c9bd2-10 (10, 11, 1) [leaf] - 
+  b9bc20507e0b-2 (6, 3, 1) [leaf] - 
+  bebd167eb94d-4 (4, 5, 1) [leaf] - 
+  de561312eff4-1 (5, 2, 1) [leaf] - 
+  f4b7da68b467-5 (9, 6, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'merge' > merge.range
   $ diff -u left.range merge.range
   --- left.range	* (glob)
   +++ merge.range	* (glob)
-  @@ -1,10 +1,22 @@
-   rev         node index size depth      obshash
-  + 10 8aca7f8c9bd2     0   11    11 000000000000
-     4 bebd167eb94d     0    5     5 000000000000
-     3 2dc09a01254d     0    4     4 000000000000
-  +  7 42b07e8da27d     0    4     4 000000000000
-  + 10 8aca7f8c9bd2     8    3    11 000000000000
-     3 2dc09a01254d     2    2     4 000000000000
-  +  7 42b07e8da27d     2    2     4 000000000000
-     1 66f7d451a68b     0    2     2 000000000000
-  +  5 de561312eff4     0    2     2 000000000000
-  +  9 f4b7da68b467     4    2     6 000000000000
-     2 01241442b3c2     2    1     3 000000000000
-     0 1ea73414a91b     0    1     1 000000000000
-     3 2dc09a01254d     3    1     4 000000000000
-  +  7 42b07e8da27d     3    1     4 000000000000
-     1 66f7d451a68b     1    1     2 000000000000
-  +  8 857477a9aebb     4    1     5 000000000000
-  + 10 8aca7f8c9bd2    10    1    11 000000000000
-  +  6 b9bc20507e0b     2    1     3 000000000000
-     4 bebd167eb94d     4    1     5 000000000000
-  +  5 de561312eff4     1    1     2 000000000000
-  +  9 f4b7da68b467     5    1     6 000000000000
+  @@ -1,9 +1,21 @@
+  +8aca7f8c9bd2-0 (10, 11, 11) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), 8aca7f8c9bd2-8 (10, 11, 3)
+   bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1)
+   2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  +42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2)
+  +8aca7f8c9bd2-8 (10, 11, 3) [complete] - f4b7da68b467-4 (9, 6, 2), 8aca7f8c9bd2-10 (10, 11, 1)
+   2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  +42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1)
+   66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  +de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1)
+  +f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1)
+   01241442b3c2-2 (2, 3, 1) [leaf] - 
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+   2dc09a01254d-3 (3, 4, 1) [leaf] - 
+  +42b07e8da27d-3 (7, 4, 1) [leaf] - 
+   66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  +857477a9aebb-4 (8, 5, 1) [leaf] - 
+  +8aca7f8c9bd2-10 (10, 11, 1) [leaf] - 
+  +b9bc20507e0b-2 (6, 3, 1) [leaf] - 
+   bebd167eb94d-4 (4, 5, 1) [leaf] - 
+  +de561312eff4-1 (5, 2, 1) [leaf] - 
+  +f4b7da68b467-5 (9, 6, 1) [leaf] - 
   [1]
   $ diff -u right.range merge.range
   --- right.range	* (glob)
   +++ merge.range	* (glob)
-  @@ -1,12 +1,22 @@
-   rev         node index size depth      obshash
-  -  9 f4b7da68b467     0    6     6 000000000000
-  + 10 8aca7f8c9bd2     0   11    11 000000000000
-  +  4 bebd167eb94d     0    5     5 000000000000
-  +  3 2dc09a01254d     0    4     4 000000000000
-     7 42b07e8da27d     0    4     4 000000000000
-  + 10 8aca7f8c9bd2     8    3    11 000000000000
-  +  3 2dc09a01254d     2    2     4 000000000000
-     7 42b07e8da27d     2    2     4 000000000000
-  +  1 66f7d451a68b     0    2     2 000000000000
-     5 de561312eff4     0    2     2 000000000000
-     9 f4b7da68b467     4    2     6 000000000000
-  +  2 01241442b3c2     2    1     3 000000000000
-     0 1ea73414a91b     0    1     1 000000000000
-  +  3 2dc09a01254d     3    1     4 000000000000
-     7 42b07e8da27d     3    1     4 000000000000
-  +  1 66f7d451a68b     1    1     2 000000000000
-     8 857477a9aebb     4    1     5 000000000000
-  + 10 8aca7f8c9bd2    10    1    11 000000000000
-     6 b9bc20507e0b     2    1     3 000000000000
-  +  4 bebd167eb94d     4    1     5 000000000000
-     5 de561312eff4     1    1     2 000000000000
-     9 f4b7da68b467     5    1     6 000000000000
+  @@ -1,11 +1,21 @@
+  -f4b7da68b467-0 (9, 6, 6) [complete] - 42b07e8da27d-0 (7, 4, 4), f4b7da68b467-4 (9, 6, 2)
+  +8aca7f8c9bd2-0 (10, 11, 11) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), 8aca7f8c9bd2-8 (10, 11, 3)
+  +bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1)
+  +2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+   42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2)
+  +8aca7f8c9bd2-8 (10, 11, 3) [complete] - f4b7da68b467-4 (9, 6, 2), 8aca7f8c9bd2-10 (10, 11, 1)
+  +2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+   42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1)
+  +66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+   de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1)
+   f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1)
+  +01241442b3c2-2 (2, 3, 1) [leaf] - 
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  +2dc09a01254d-3 (3, 4, 1) [leaf] - 
+   42b07e8da27d-3 (7, 4, 1) [leaf] - 
+  +66f7d451a68b-1 (1, 2, 1) [leaf] - 
+   857477a9aebb-4 (8, 5, 1) [leaf] - 
+  +8aca7f8c9bd2-10 (10, 11, 1) [leaf] - 
+   b9bc20507e0b-2 (6, 3, 1) [leaf] - 
+  +bebd167eb94d-4 (4, 5, 1) [leaf] - 
+   de561312eff4-1 (5, 2, 1) [leaf] - 
+   f4b7da68b467-5 (9, 6, 1) [leaf] - 
   [1]
 
 Range above the merge, reuse subrange from the merge
 
-  $ hg debugstablerange --rev tip
-  rev         node index size depth      obshash
-   12 e6b8d5b46647     0   13    13 000000000000
-    4 bebd167eb94d     0    5     5 000000000000
-   12 e6b8d5b46647     8    5    13 000000000000
-    3 2dc09a01254d     0    4     4 000000000000
-    7 42b07e8da27d     0    4     4 000000000000
-   11 485383494a89     8    4    12 000000000000
-    3 2dc09a01254d     2    2     4 000000000000
-    7 42b07e8da27d     2    2     4 000000000000
-   11 485383494a89    10    2    12 000000000000
-    1 66f7d451a68b     0    2     2 000000000000
-    5 de561312eff4     0    2     2 000000000000
-    9 f4b7da68b467     4    2     6 000000000000
-    2 01241442b3c2     2    1     3 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    3 2dc09a01254d     3    1     4 000000000000
-    7 42b07e8da27d     3    1     4 000000000000
-   11 485383494a89    11    1    12 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-    8 857477a9aebb     4    1     5 000000000000
-   10 8aca7f8c9bd2    10    1    11 000000000000
-    6 b9bc20507e0b     2    1     3 000000000000
-    4 bebd167eb94d     4    1     5 000000000000
-    5 de561312eff4     1    1     2 000000000000
-   12 e6b8d5b46647    12    1    13 000000000000
-    9 f4b7da68b467     5    1     6 000000000000
-  $ hg debugstablerange --rev 'tip' > tip.range
+  $ hg debugstablerange --verify --verbose --subranges --rev tip
+  e6b8d5b46647-0 (12, 13, 13) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), e6b8d5b46647-8 (12, 13, 5)
+  bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1)
+  e6b8d5b46647-8 (12, 13, 5) [complete] - 485383494a89-8 (11, 12, 4), e6b8d5b46647-12 (12, 13, 1)
+  2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+  42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2)
+  485383494a89-8 (11, 12, 4) [complete] - f4b7da68b467-4 (9, 6, 2), 485383494a89-10 (11, 12, 2)
+  2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+  42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1)
+  485383494a89-10 (11, 12, 2) [complete] - 8aca7f8c9bd2-10 (10, 11, 1), 485383494a89-11 (11, 12, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1)
+  f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1)
+  01241442b3c2-2 (2, 3, 1) [leaf] - 
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  2dc09a01254d-3 (3, 4, 1) [leaf] - 
+  42b07e8da27d-3 (7, 4, 1) [leaf] - 
+  485383494a89-11 (11, 12, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  857477a9aebb-4 (8, 5, 1) [leaf] - 
+  8aca7f8c9bd2-10 (10, 11, 1) [leaf] - 
+  b9bc20507e0b-2 (6, 3, 1) [leaf] - 
+  bebd167eb94d-4 (4, 5, 1) [leaf] - 
+  de561312eff4-1 (5, 2, 1) [leaf] - 
+  e6b8d5b46647-12 (12, 13, 1) [leaf] - 
+  f4b7da68b467-5 (9, 6, 1) [leaf] - 
+  $ hg debugstablerange --verify --verbose --subranges --rev 'tip' > tip.range
   $ diff -u merge.range tip.range
   --- merge.range	* (glob)
   +++ tip.range	* (glob)
-  @@ -1,11 +1,13 @@
-   rev         node index size depth      obshash
-  - 10 8aca7f8c9bd2     0   11    11 000000000000
-  + 12 e6b8d5b46647     0   13    13 000000000000
-     4 bebd167eb94d     0    5     5 000000000000
-  + 12 e6b8d5b46647     8    5    13 000000000000
-     3 2dc09a01254d     0    4     4 000000000000
-     7 42b07e8da27d     0    4     4 000000000000
-  - 10 8aca7f8c9bd2     8    3    11 000000000000
-  + 11 485383494a89     8    4    12 000000000000
-     3 2dc09a01254d     2    2     4 000000000000
-     7 42b07e8da27d     2    2     4 000000000000
-  + 11 485383494a89    10    2    12 000000000000
-     1 66f7d451a68b     0    2     2 000000000000
-     5 de561312eff4     0    2     2 000000000000
-     9 f4b7da68b467     4    2     6 000000000000
-  @@ -13,10 +15,12 @@
-     0 1ea73414a91b     0    1     1 000000000000
-     3 2dc09a01254d     3    1     4 000000000000
-     7 42b07e8da27d     3    1     4 000000000000
-  + 11 485383494a89    11    1    12 000000000000
-     1 66f7d451a68b     1    1     2 000000000000
-     8 857477a9aebb     4    1     5 000000000000
-    10 8aca7f8c9bd2    10    1    11 000000000000
-     6 b9bc20507e0b     2    1     3 000000000000
-     4 bebd167eb94d     4    1     5 000000000000
-     5 de561312eff4     1    1     2 000000000000
-  + 12 e6b8d5b46647    12    1    13 000000000000
-     9 f4b7da68b467     5    1     6 000000000000
+  @@ -1,10 +1,12 @@
+  -8aca7f8c9bd2-0 (10, 11, 11) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), 8aca7f8c9bd2-8 (10, 11, 3)
+  +e6b8d5b46647-0 (12, 13, 13) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), e6b8d5b46647-8 (12, 13, 5)
+   bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1)
+  +e6b8d5b46647-8 (12, 13, 5) [complete] - 485383494a89-8 (11, 12, 4), e6b8d5b46647-12 (12, 13, 1)
+   2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2)
+   42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2)
+  -8aca7f8c9bd2-8 (10, 11, 3) [complete] - f4b7da68b467-4 (9, 6, 2), 8aca7f8c9bd2-10 (10, 11, 1)
+  +485383494a89-8 (11, 12, 4) [complete] - f4b7da68b467-4 (9, 6, 2), 485383494a89-10 (11, 12, 2)
+   2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1)
+   42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1)
+  +485383494a89-10 (11, 12, 2) [complete] - 8aca7f8c9bd2-10 (10, 11, 1), 485383494a89-11 (11, 12, 1)
+   66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+   de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1)
+   f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1)
+  @@ -12,10 +14,12 @@
+   1ea73414a91b-0 (0, 1, 1) [leaf] - 
+   2dc09a01254d-3 (3, 4, 1) [leaf] - 
+   42b07e8da27d-3 (7, 4, 1) [leaf] - 
+  +485383494a89-11 (11, 12, 1) [leaf] - 
+   66f7d451a68b-1 (1, 2, 1) [leaf] - 
+   857477a9aebb-4 (8, 5, 1) [leaf] - 
+   8aca7f8c9bd2-10 (10, 11, 1) [leaf] - 
+   b9bc20507e0b-2 (6, 3, 1) [leaf] - 
+   bebd167eb94d-4 (4, 5, 1) [leaf] - 
+   de561312eff4-1 (5, 2, 1) [leaf] - 
+  +e6b8d5b46647-12 (12, 13, 1) [leaf] - 
+   f4b7da68b467-5 (9, 6, 1) [leaf] - 
   [1]
 
   $ cd ..
@@ -632,41 +605,73 @@
   |/
   o  0 1ea73414a91b r0
   
-  $ hg debugstablerange --rev 'head()'
-  rev         node index size depth      obshash
-   15 1d8d22637c2d     0    8     8 000000000000
-    9 dcbb326fdec2     0    7     7 000000000000
-   10 ff43616e5d0f     0    7     7 000000000000
-   13 b4594d867745     0    6     6 000000000000
-   12 e46a4836065c     0    6     6 000000000000
-    6 2702dd0c91e7     0    5     5 000000000000
-   15 1d8d22637c2d     4    4     8 000000000000
-    3 2b6d669947cd     0    4     4 000000000000
-    5 f0f3ef9a6cd5     0    4     4 000000000000
-    9 dcbb326fdec2     4    3     7 000000000000
-   10 ff43616e5d0f     4    3     7 000000000000
-   15 1d8d22637c2d     6    2     8 000000000000
-    3 2b6d669947cd     2    2     4 000000000000
-    1 66f7d451a68b     0    2     2 000000000000
-   13 b4594d867745     4    2     6 000000000000
-    8 d62d843c9a01     4    2     6 000000000000
-   12 e46a4836065c     4    2     6 000000000000
-    5 f0f3ef9a6cd5     2    2     4 000000000000
-    2 fa942426a6fd     0    2     2 000000000000
-   15 1d8d22637c2d     7    1     8 000000000000
-    0 1ea73414a91b     0    1     1 000000000000
-    6 2702dd0c91e7     4    1     5 000000000000
-    3 2b6d669947cd     3    1     4 000000000000
-   14 43227190fef8     4    1     5 000000000000
-    4 4c748ffd1a46     2    1     3 000000000000
-    1 66f7d451a68b     1    1     2 000000000000
-   13 b4594d867745     5    1     6 000000000000
-   11 bab5d5bf48bd     4    1     5 000000000000
-    8 d62d843c9a01     5    1     6 000000000000
-    9 dcbb326fdec2     6    1     7 000000000000
-   12 e46a4836065c     5    1     6 000000000000
-    7 e7d9710d9fc6     4    1     5 000000000000
-    5 f0f3ef9a6cd5     3    1     4 000000000000
-    2 fa942426a6fd     1    1     2 000000000000
-   10 ff43616e5d0f     6    1     7 000000000000
+  $ hg debugstablerange --verify --verbose --subranges --rev 'head()'
+  1d8d22637c2d-0 (15, 8, 8) [complete] - 2b6d669947cd-0 (3, 4, 4), 1d8d22637c2d-4 (15, 8, 4)
+  dcbb326fdec2-0 (9, 7, 7) [complete] - 2b6d669947cd-0 (3, 4, 4), dcbb326fdec2-4 (9, 7, 3)
+  ff43616e5d0f-0 (10, 7, 7) [complete] - 2b6d669947cd-0 (3, 4, 4), ff43616e5d0f-4 (10, 7, 3)
+  b4594d867745-0 (13, 6, 6) [complete] - 2b6d669947cd-0 (3, 4, 4), b4594d867745-4 (13, 6, 2)
+  e46a4836065c-0 (12, 6, 6) [complete] - 2b6d669947cd-0 (3, 4, 4), e46a4836065c-4 (12, 6, 2)
+  2702dd0c91e7-0 (6, 5, 5) [complete] - f0f3ef9a6cd5-0 (5, 4, 4), 2702dd0c91e7-4 (6, 5, 1)
+  1d8d22637c2d-4 (15, 8, 4) [complete] - 4c748ffd1a46-2 (4, 3, 1), 43227190fef8-4 (14, 5, 1), 1d8d22637c2d-6 (15, 8, 2)
+  2b6d669947cd-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2b6d669947cd-2 (3, 4, 2)
+  f0f3ef9a6cd5-0 (5, 4, 4) [complete] - fa942426a6fd-0 (2, 2, 2), f0f3ef9a6cd5-2 (5, 4, 2)
+  dcbb326fdec2-4 (9, 7, 3) [complete] - d62d843c9a01-4 (8, 6, 2), dcbb326fdec2-6 (9, 7, 1)
+  ff43616e5d0f-4 (10, 7, 3) [complete] - d62d843c9a01-4 (8, 6, 2), ff43616e5d0f-6 (10, 7, 1)
+  1d8d22637c2d-6 (15, 8, 2) [complete] - f0f3ef9a6cd5-3 (5, 4, 1), 1d8d22637c2d-7 (15, 8, 1)
+  2b6d669947cd-2 (3, 4, 2) [complete] - fa942426a6fd-1 (2, 2, 1), 2b6d669947cd-3 (3, 4, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  b4594d867745-4 (13, 6, 2) [complete] - bab5d5bf48bd-4 (11, 5, 1), b4594d867745-5 (13, 6, 1)
+  d62d843c9a01-4 (8, 6, 2) [complete] - e7d9710d9fc6-4 (7, 5, 1), d62d843c9a01-5 (8, 6, 1)
+  e46a4836065c-4 (12, 6, 2) [complete] - bab5d5bf48bd-4 (11, 5, 1), e46a4836065c-5 (12, 6, 1)
+  f0f3ef9a6cd5-2 (5, 4, 2) [complete] - 4c748ffd1a46-2 (4, 3, 1), f0f3ef9a6cd5-3 (5, 4, 1)
+  fa942426a6fd-0 (2, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), fa942426a6fd-1 (2, 2, 1)
+  1d8d22637c2d-7 (15, 8, 1) [leaf] - 
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  2702dd0c91e7-4 (6, 5, 1) [leaf] - 
+  2b6d669947cd-3 (3, 4, 1) [leaf] - 
+  43227190fef8-4 (14, 5, 1) [leaf] - 
+  4c748ffd1a46-2 (4, 3, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  b4594d867745-5 (13, 6, 1) [leaf] - 
+  bab5d5bf48bd-4 (11, 5, 1) [leaf] - 
+  d62d843c9a01-5 (8, 6, 1) [leaf] - 
+  dcbb326fdec2-6 (9, 7, 1) [leaf] - 
+  e46a4836065c-5 (12, 6, 1) [leaf] - 
+  e7d9710d9fc6-4 (7, 5, 1) [leaf] - 
+  f0f3ef9a6cd5-3 (5, 4, 1) [leaf] - 
+  fa942426a6fd-1 (2, 2, 1) [leaf] - 
+  ff43616e5d0f-6 (10, 7, 1) [leaf] - 
   $ cd ..
+
+Tests range where a toprange is rooted on a merge
+=================================================
+
+  $ hg init slice_on_merge
+  $ cd slice_on_merge
+  $ hg debugbuilddag '
+  > ..:a   # 2 nodes, tagged "a"
+  > <2..:b   # another branch with two node based on 0, tagged b
+  > *a/b:m # merge -1 and -2 (1, 2), tagged "m"
+  > '
+  $ hg log -G
+  o    4 f37e476fba9a r4 m tip
+  |\
+  | o  3 36315563e2fa r3 b
+  | |
+  | o  2 fa942426a6fd r2
+  | |
+  o |  1 66f7d451a68b r1 a
+  |/
+  o  0 1ea73414a91b r0
+  
+  $ hg debugstablerange --verify --verbose --subranges --rev 'head()'
+  f37e476fba9a-0 (4, 5, 5) [complete] - 66f7d451a68b-0 (1, 2, 2), 36315563e2fa-0 (3, 3, 3), f37e476fba9a-4 (4, 5, 1)
+  36315563e2fa-0 (3, 3, 3) [complete] - fa942426a6fd-0 (2, 2, 2), 36315563e2fa-2 (3, 3, 1)
+  66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1)
+  fa942426a6fd-0 (2, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), fa942426a6fd-1 (2, 2, 1)
+  1ea73414a91b-0 (0, 1, 1) [leaf] - 
+  36315563e2fa-2 (3, 3, 1) [leaf] - 
+  66f7d451a68b-1 (1, 2, 1) [leaf] - 
+  f37e476fba9a-4 (4, 5, 1) [leaf] - 
+  fa942426a6fd-1 (2, 2, 1) [leaf] - 
+
--- a/tests/test-stablesort.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-stablesort.t	Fri Mar 31 15:44:10 2017 +0200
@@ -138,7 +138,7 @@
   updating to branch default
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg -R repo_B pull --rev 13
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -146,7 +146,7 @@
   added 4 changesets with 0 changes to 0 files (+1 heads)
   (run 'hg heads' to see heads, 'hg merge' to merge)
   $ hg -R repo_B pull --rev 14
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -154,7 +154,7 @@
   added 1 changesets with 0 changes to 0 files (+1 heads)
   (run 'hg heads .' to see heads, 'hg merge' to merge)
   $ hg -R repo_B pull
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -204,7 +204,7 @@
   updating to branch default
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg -R repo_C pull --rev 12
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -212,7 +212,7 @@
   added 2 changesets with 0 changes to 0 files (+1 heads)
   (run 'hg heads' to see heads, 'hg merge' to merge)
   $ hg -R repo_C pull --rev 15
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -220,7 +220,7 @@
   added 4 changesets with 0 changes to 0 files (+1 heads)
   (run 'hg heads .' to see heads, 'hg merge' to merge)
   $ hg -R repo_C pull
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -270,7 +270,7 @@
   updating to branch default
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg -R repo_D pull --rev 10
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -278,7 +278,7 @@
   added 5 changesets with 0 changes to 0 files
   (run 'hg update' to get a working copy)
   $ hg -R repo_D pull --rev 15
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -286,7 +286,7 @@
   added 4 changesets with 0 changes to 0 files (+1 heads)
   (run 'hg heads' to see heads, 'hg merge' to merge)
   $ hg -R repo_D pull
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -404,7 +404,7 @@
   updating to branch default
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg -R repo_E pull --rev e7d9710d9fc6
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -420,7 +420,7 @@
   updating to branch default
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg -R repo_F pull --rev d62d843c9a01
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -436,7 +436,7 @@
   updating to branch default
   0 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg -R repo_G pull --rev 43227190fef8
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -444,7 +444,7 @@
   added 1 changesets with 0 changes to 0 files (+1 heads)
   (run 'hg heads' to see heads, 'hg merge' to merge)
   $ hg -R repo_G pull --rev 2702dd0c91e7
-  pulling from $TESTTMP/repo_A
+  pulling from $TESTTMP/repo_A (glob)
   searching for changes
   adding changesets
   adding manifests
--- a/tests/test-topic-dest.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-topic-dest.t	Fri Mar 31 15:44:10 2017 +0200
@@ -276,7 +276,7 @@
   $ hg add other
   $ hg ci -m 'c_other'
   $ hg pull -r default --rebase
-  pulling from $TESTTMP/jungle
+  pulling from $TESTTMP/jungle (glob)
   searching for changes
   adding changesets
   adding manifests
--- a/tests/test-topic-push.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-topic-push.t	Fri Mar 31 15:44:10 2017 +0200
@@ -32,12 +32,12 @@
   $ hg add aaa
   $ hg commit -m 'CA'
   $ hg outgoing -G
-  comparing with $TESTTMP/main
+  comparing with $TESTTMP/main (glob)
   searching for changes
   @  0 default  draft CA
   
   $ hg push
-  pushing to $TESTTMP/main
+  pushing to $TESTTMP/main (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -56,25 +56,25 @@
   $ hg commit -m 'CC'
   created new head
   $ hg outgoing -G
-  comparing with $TESTTMP/main
+  comparing with $TESTTMP/main (glob)
   searching for changes
   @  2 default  draft CC
   
   o  1 default  draft CB
   
   $ hg push
-  pushing to $TESTTMP/main
+  pushing to $TESTTMP/main (glob)
   searching for changes
   abort: push creates new remote head 9fe81b7f425d!
   (merge or see "hg help push" for details about pushing new heads)
   [255]
   $ hg outgoing -r 'desc(CB)' -G
-  comparing with $TESTTMP/main
+  comparing with $TESTTMP/main (glob)
   searching for changes
   o  1 default  draft CB
   
   $ hg push -r 'desc(CB)'
-  pushing to $TESTTMP/main
+  pushing to $TESTTMP/main (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -88,18 +88,18 @@
   (branches are permanent and global, did you want a bookmark?)
   $ hg commit --amend
   $ hg outgoing -G
-  comparing with $TESTTMP/main
+  comparing with $TESTTMP/main (glob)
   searching for changes
   @  4 mountain  draft CC
   
   $ hg push 
-  pushing to $TESTTMP/main
+  pushing to $TESTTMP/main (glob)
   searching for changes
   abort: push creates new remote branches: mountain!
   (use 'hg push --new-branch' to create new remote branches)
   [255]
   $ hg push --new-branch
-  pushing to $TESTTMP/main
+  pushing to $TESTTMP/main (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -110,7 +110,7 @@
 Including on non-publishing
 
   $ hg push --new-branch draft
-  pushing to $TESTTMP/draft
+  pushing to $TESTTMP/draft (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -143,7 +143,7 @@
 Pushing a new topic to a non publishing server should not be seen as a new head
 
   $ hg push draft
-  pushing to $TESTTMP/draft
+  pushing to $TESTTMP/draft (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -162,7 +162,7 @@
 Pushing a new topic to a publishing server should be seen as a new head
 
   $ hg push
-  pushing to $TESTTMP/main
+  pushing to $TESTTMP/main (glob)
   searching for changes
   abort: push creates new remote head 67f579af159d!
   (merge or see "hg help push" for details about pushing new heads)
@@ -289,7 +289,7 @@
   
 
   $ hg push draft
-  pushing to $TESTTMP/draft
+  pushing to $TESTTMP/draft (glob)
   searching for changes
   abort: push creates new remote head f0bc62a661be on branch 'default:babar'!
   (merge or see "hg help push" for details about pushing new heads)
@@ -333,7 +333,7 @@
 Reject when pushing to draft
 
   $ hg push draft -r .
-  pushing to $TESTTMP/draft
+  pushing to $TESTTMP/draft (glob)
   searching for changes
   abort: push creates new remote head 4937c4cad39e!
   (merge or see "hg help push" for details about pushing new heads)
@@ -343,7 +343,7 @@
 Reject when pushing to publishing
 
   $ hg push -r .
-  pushing to $TESTTMP/main
+  pushing to $TESTTMP/main (glob)
   searching for changes
   adding changesets
   adding manifests
--- a/tests/test-topic-tutorial.t	Tue Mar 14 14:47:20 2017 -0700
+++ b/tests/test-topic-tutorial.t	Fri Mar 31 15:44:10 2017 +0200
@@ -193,7 +193,7 @@
 Topic will also affect rebase and merge destination. Let's pull the latest update from the main server::
 
   $ hg pull
-  pulling from $TESTTMP/server
+  pulling from $TESTTMP/server (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -276,7 +276,7 @@
   $ hg topic
      food
   $ hg push
-  pushing to $TESTTMP/server
+  pushing to $TESTTMP/server (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -392,7 +392,7 @@
 Lets see what other people did in the mean time::
 
   $ hg pull
-  pulling from $TESTTMP/server
+  pulling from $TESTTMP/server (glob)
   searching for changes
   adding changesets
   adding manifests
@@ -421,7 +421,7 @@
 Pushing that topic would create a new heads will be prevented::
 
   $ hg push --rev drinks
-  pushing to $TESTTMP/server
+  pushing to $TESTTMP/server (glob)
   searching for changes
   abort: push creates new remote head 70dfa201ed73!
   (merge or see "hg help push" for details about pushing new heads)
@@ -437,7 +437,7 @@
   merging shopping
   switching to topic tools
   $ hg push
-  pushing to $TESTTMP/server
+  pushing to $TESTTMP/server (glob)
   searching for changes
   abort: push creates new remote head 4cd7c1591a67!
   (merge or see "hg help push" for details about pushing new heads)
@@ -446,7 +446,7 @@
 Publishing only one of them is allowed (as long as it does not create a new branch head has we just saw in the previous case)::
 
   $ hg push -r drinks
-  pushing to $TESTTMP/server
+  pushing to $TESTTMP/server (glob)
   searching for changes
   adding changesets
   adding manifests
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/testlib/checkheads-util.sh	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,26 @@
+# common setup for head checking code
+
+. $TESTDIR/testlib/common.sh
+
+cat >> $HGRCPATH <<EOF
+[ui]
+logtemplate ="{node|short} ({phase}): {desc}\n"
+
+[phases]
+publish=False
+
+[extensions]
+strip=
+evolve=
+EOF
+
+setuprepos() {
+    echo creating basic server and client repo
+    hg init server
+    cd server
+    mkcommit root
+    hg phase --public .
+    mkcommit A0
+    cd .. 
+    hg clone server client
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/testlib/common.sh	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,16 @@
+. $TESTDIR/testlib/pythonpath.sh
+
+mkcommit() {
+   echo "$1" > "$1"
+   hg add "$1"
+   hg ci -m "$1"
+}
+
+getid() {
+   hg log --hidden --template '{node}\n' --rev "$1"
+}
+
+cat >> $HGRCPATH <<EOF
+[alias]
+debugobsolete=debugobsolete -d '0 0'
+EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/testlib/exchange-util.sh	Fri Mar 31 15:44:10 2017 +0200
@@ -0,0 +1,110 @@
+#!/bin/sh
+
+cat >> $HGRCPATH <<EOF
+[web]
+push_ssl = false
+allow_push = *
+
+[ui]
+logtemplate ="{node|short} ({phase}): {desc}\n"
+
+[phases]
+publish=False
+
+[experimental]
+verbose-obsolescence-exchange=false
+bundle2-exp=true
+bundle2-output-capture=True
+
+[alias]
+debugobsolete=debugobsolete -d '0 0'
+
+[extensions]
+hgext.strip=
+EOF
+echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH
+
+mkcommit() {
+   echo "$1" > "$1"
+   hg add "$1"
+   hg ci -m "$1"
+}
+getid() {
+   hg log --hidden --template '{node}\n' --rev "$1"
+}
+
+setuprepos() {
+    echo creating test repo for test case $1
+    mkdir $1
+    cd $1
+    echo - pulldest
+    hg init pushdest
+    cd pushdest
+    mkcommit O
+    hg phase --public .
+    cd ..
+    echo - main
+    hg clone -q pushdest main
+    echo - pushdest
+    hg clone -q main pulldest
+    echo 'cd into `main` and proceed with env setup'
+}
+
+dotest() {
+# dotest TESTNAME [TARGETNODE]
+
+    testcase=$1
+    shift
+    target="$1"
+    if [ $# -gt 0 ]; then
+        shift
+    fi
+    targetnode=""
+    desccall=""
+    cd $testcase
+    echo "## Running testcase $testcase"
+    if [ -n "$target" ]; then
+        desccall="desc("\'"$target"\'")"
+        targetnode="`hg -R main id -qr \"$desccall\"`"
+        echo "# testing echange of \"$target\" ($targetnode)"
+    fi
+    echo "## initial state"
+    echo "# obstore: main"
+    hg -R main     debugobsolete | sort
+    echo "# obstore: pushdest"
+    hg -R pushdest debugobsolete | sort
+    echo "# obstore: pulldest"
+    hg -R pulldest debugobsolete | sort
+
+    if [ -n "$target" ]; then
+        echo "## pushing \"$target\"" from main to pushdest
+        hg -R main push -r "$desccall" $@ pushdest
+    else
+        echo "## pushing from main to pushdest"
+        hg -R main push pushdest $@
+    fi
+    echo "## post push state"
+    echo "# obstore: main"
+    hg -R main     debugobsolete | sort
+    echo "# obstore: pushdest"
+    hg -R pushdest debugobsolete | sort
+    echo "# obstore: pulldest"
+    hg -R pulldest debugobsolete | sort
+    if [ -n "$target" ]; then
+        echo "## pulling \"$targetnode\"" from main into pulldest
+        hg -R pulldest pull -r $targetnode $@ main
+    else
+        echo "## pulling from main into pulldest"
+        hg -R pulldest pull main $@
+    fi
+    echo "## post pull state"
+    echo "# obstore: main"
+    hg -R main     debugobsolete | sort
+    echo "# obstore: pushdest"
+    hg -R pushdest debugobsolete | sort
+    echo "# obstore: pulldest"
+    hg -R pulldest debugobsolete | sort
+
+    cd ..
+
+}