test-compat: merge mercurial-5.1 into mercurial-5.0 mercurial-5.0
authorAnton Shestakov <av6@dwimlabs.net>
Wed, 08 Apr 2020 01:16:54 +0800
branchmercurial-5.0
changeset 5280 90fbbc29b93a
parent 5279 35e769c9604f (diff)
parent 5159 e5fe3ca5e6ad (current diff)
child 5281 6a716085302f
child 5364 be5aa681c122
test-compat: merge mercurial-5.1 into mercurial-5.0
tests/test-discovery-obshashrange.t
tests/test-evolve-orphan-split.t
--- a/CHANGELOG	Fri Feb 28 23:18:48 2020 +0700
+++ b/CHANGELOG	Wed Apr 08 01:16:54 2020 +0800
@@ -1,6 +1,18 @@
 Changelog
 =========
 
+9.3.1 - in progress
+-------------------
+
+  * obsexchange: avoid sending too large request to http server
+  * obsdiscovery: server no longer aborts with a 500 error if client sends a
+    request without obscommon
+  * evolve: improved behavior when evolving above the result of a split
+  * topic: fix auto-publish=abort with server that auto-publishes bare branches
+  * evolve: checking for new head on push is no longer confused by mixed
+            branches(or topics)
+  * single-heads: ignore obsolete section when enforcing one head per branch
+
 9.3.0 -- 2020-03-04
 -------------------
 
--- a/hgext3rd/evolve/__init__.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/evolve/__init__.py	Wed Apr 08 01:16:54 2020 +0800
@@ -292,6 +292,7 @@
     state,
     evolvecmd,
     exthelper,
+    headchecking,
     metadata,
     obscache,
     obsexchange,
@@ -346,6 +347,7 @@
 eh.merge(compat.eh)
 eh.merge(cmdrewrite.eh)
 eh.merge(rewind.eh)
+eh.merge(headchecking.eh)
 uisetup = eh.finaluisetup
 extsetup = eh.finalextsetup
 reposetup = eh.finalreposetup
--- a/hgext3rd/evolve/cmdrewrite.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/evolve/cmdrewrite.py	Wed Apr 08 01:16:54 2020 +0800
@@ -1407,7 +1407,7 @@
     for r in revs:
         ctx = repo[r]
         extra = ctx.extra().copy()
-        extra[b'__touch-noise__'] = random.randint(0, 0xffffffff)
+        extra[b'__touch-noise__'] = b'%d' % random.randint(0, 0xffffffff)
         # search for touched parent
         p1 = ctx.p1().node()
         p2 = ctx.p2().node()
--- a/hgext3rd/evolve/evolvecmd.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/evolve/evolvecmd.py	Wed Apr 08 01:16:54 2020 +0800
@@ -1618,9 +1618,9 @@
     Interrupted
     ===========
 
-    The `hg evolve` command is an all purpose tool that solve all kind of
+    The `hg evolve` command is an all purpose tool that solves all kinds of
     instabilities in your repository. Sometimes, instability resolution will lead
-    to merge conflict that cannot be solved without a human intervention (same as
+    to merge conflicts that cannot be solved without human intervention (same as
     `hg merge`). This can lead to an "interrupted state" where human assistance is
     requested. There are three things which you can do when you face a similar
     situation:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/headchecking.py	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,189 @@
+from __future__ import absolute_import
+
+import functools
+
+from mercurial import (
+    discovery,
+    error,
+    extensions,
+    phases,
+    scmutil,
+    util,
+)
+
+from mercurial.i18n import _
+
+
+from . import (
+    compat,
+    exthelper,
+)
+
+eh = exthelper.exthelper()
+
+
+@eh.uisetup
+def uisetup(ui):
+    extensions.wrapfunction(discovery, '_postprocessobsolete', _postprocessobsolete)
+    extensions.wrapfunction(scmutil, 'enforcesinglehead', enforcesinglehead)
+
+def branchinfo(pushop, repo, node):
+    return repo[node].branch()
+
+# taken from 7d5455b988ec + branchinfo abstraction.
+def _postprocessobsolete(orig, pushop, futurecommon, candidate_newhs):
+    """post process the list of new heads with obsolescence information
+
+    Exists as a sub-function to contain the complexity and allow extensions to
+    experiment with smarter logic.
+
+    Returns (newheads, discarded_heads) tuple
+    """
+    pushingmarkerfor = discovery.pushingmarkerfor
+    # 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
+
+    # define various utilities and containers
+    repo = pushop.repo
+    unfi = repo.unfiltered()
+    torev = compat.getgetrev(unfi.changelog)
+    public = phases.public
+    getphase = unfi._phasecache.phase
+    ispublic = lambda r: getphase(unfi, r) == public
+    ispushed = lambda n: torev(n) in futurecommon
+    hasoutmarker = functools.partial(pushingmarkerfor, unfi.obsstore, ispushed)
+    successorsmarkers = unfi.obsstore.successors
+    newhs = set()  # final set of new heads
+    discarded = set()  # new head of fully replaced branch
+
+    localcandidate = set()  # candidate heads known locally
+    unknownheads = set()  # candidate heads unknown locally
+    for h in candidate_newhs:
+        if h in unfi:
+            localcandidate.add(h)
+        else:
+            if successorsmarkers.get(h) is not None:
+                msg = (
+                    b'checkheads: remote head unknown locally has'
+                    b' local marker: %s\n'
+                )
+                repo.ui.debug(msg % hex(h))
+            unknownheads.add(h)
+
+    # fast path the simple case
+    if len(localcandidate) == 1:
+        return unknownheads | set(candidate_newhs), set()
+
+    # actually process branch replacement
+    while localcandidate:
+        nh = localcandidate.pop()
+        current_branch = branchinfo(pushop, unfi, nh)
+        # run this check early to skip the evaluation of the whole branch
+        if ispublic(torev(nh)) or not unfi[nh].obsolete():
+            newhs.add(nh)
+            continue
+
+        # Get all revs/nodes on the branch exclusive to this head
+        # (already filtered heads are "ignored"))
+        branchrevs = unfi.revs(
+            b'only(%n, (%ln+%ln))', nh, localcandidate, newhs
+        )
+
+        branchnodes = []
+        for r in branchrevs:
+            ctx = unfi[r]
+            if ctx.branch() == current_branch:
+                branchnodes.append(ctx.node())
+
+        # The branch won't be hidden 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(torev(n)) for n in branchnodes)
+            or (any(torev(n) in futurecommon and not unfi[n].obsolete() for n in branchnodes))
+            # XXX `hasoutmarker` does not guarantee the changeset to be
+            # obsolete, nor obsoleted by the push.
+            or any(not hasoutmarker(n) for n in branchnodes)
+        ):
+            newhs.add(nh)
+        else:
+            # note: 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,
+            discarded.add(nh)
+    newhs |= unknownheads
+    return newhs, discarded
+
+def _get_branch_name(ctx):
+    # make it easy for extension with the branch logic there
+    branch = ctx.branch()
+    if util.safehasattr(ctx, 'topic'):
+        topic = ctx.topic()
+        if topic:
+            branch = "%s:%s" % (branch, topic)
+    return branch
+
+def _filter_obsolete_heads(repo, heads):
+    """filter heads to return non-obsolete ones
+
+    Given a list of heads (on the same named branch) return a new list of heads
+    where the obsolete part have been skimmed out.
+    """
+    new_heads = []
+    old_heads = heads[:]
+    while old_heads:
+        rh = old_heads.pop()
+        ctx = repo[rh]
+        current_name = _get_branch_name(ctx)
+        # run this check early to skip the evaluation of the whole branch
+        if not ctx.obsolete():
+            new_heads.append(rh)
+            continue
+
+        # Get all revs/nodes on the branch exclusive to this head
+        # (already filtered heads are "ignored"))
+        sections_revs = repo.revs(
+            b'only(%d, (%ld+%ld))', rh, old_heads, new_heads,
+        )
+        keep_revs = []
+        for r in sections_revs:
+            ctx = repo[r]
+            if ctx.obsolete():
+                continue
+            if _get_branch_name(ctx) != current_name:
+                continue
+            keep_revs.append(r)
+        for h in repo.revs(b'heads(%ld and (::%ld))', sections_revs, keep_revs):
+            new_heads.append(h)
+    new_heads.sort()
+    return new_heads
+
+def enforcesinglehead(orig, repo, tr, desc, accountclosed=False):
+    """check that no named branch has multiple heads"""
+    nodesummaries = scmutil.nodesummaries
+    if desc in (b'strip', b'repair'):
+        # skip the logic during strip
+        return
+    visible = repo.filtered(b'visible')
+    # possible improvement: we could restrict the check to affected branch
+    bm = visible.branchmap()
+    cl = repo.changelog
+    to_rev = cl.rev
+    to_node = cl.node
+    for name in bm:
+        all_heads = bm.branchheads(name, closed=accountclosed)
+        all_heads = [to_rev(n) for n in all_heads]
+        heads = _filter_obsolete_heads(repo, all_heads)
+        heads = [to_node(r) for r in heads]
+        if len(heads) > 1:
+            msg = _(b'rejecting multiple heads on branch "%s"')
+            msg %= name
+            hint = _(b'%d heads: %s')
+            hint %= (len(heads), nodesummaries(repo, heads))
+            raise error.Abort(msg, hint=hint)
--- a/hgext3rd/evolve/obsdiscovery.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/evolve/obsdiscovery.py	Wed Apr 08 01:16:54 2020 +0800
@@ -844,7 +844,12 @@
     repo = pullop.repo
     remote = pullop.remote
     unfi = repo.unfiltered()
-    revs = unfi.revs(b'::(%ln - null)', pullop.common)
+    # Also exclude filtered revisions. Working on unfiltered repository can
+    # give a bit more precise view of the repository. However it makes the
+    # overall operation more complicated.
+    filteredrevs = repo.changelog.filteredrevs
+    # XXX probably not very efficient
+    revs = unfi.revs(b'::(%ln - null) - %ld', pullop.common, filteredrevs)
     boundaries = {b'heads': pullop.pulledsubset}
     if not revs: # nothing common
         boundaries[b'common'] = [node.nullid]
@@ -859,8 +864,14 @@
     if bundle2 and _canobshashrange(repo, remote):
         obsexcmsg(repo.ui, b"looking for common markers in %i nodes\n"
                   % len(revs))
-        boundaries[b'missing'] = findmissingrange(repo.ui, unfi, pullop.remote,
-                                                  revs)
+        missing = findmissingrange(repo.ui, repo, pullop.remote, revs)
+        boundaries[b'missing'] = missing
+        # using getattr since `limitedarguments` is missing
+        # hg <= 5.0 (69921d02daaf)
+        if getattr(pullop.remote, 'limitedarguments', False):
+            # prepare for a possible fallback to common
+            common = repo.set("heads(only(%ld, %ln))", revs, missing)
+            boundaries[b'common'] = [c.node() for c in common]
     else:
         boundaries[b'common'] = [node.nullid]
     return boundaries
--- a/hgext3rd/evolve/obsexchange.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/evolve/obsexchange.py	Wed Apr 08 01:16:54 2020 +0800
@@ -54,6 +54,18 @@
     gboptsmap[b'evo_obscommon'] = b'nodes'
     gboptsmap[b'evo_missing_nodes'] = b'nodes'
 
+ARGUMENTS_LIMIT = 200
+
+OVERFLOW_MSG = """obsmarkers differ for %d common nodes
+|
+| This might be too much for the remote HTTP server that doesn't accept
+| arguments through POST request. (config: experimental.httppostargs=yes)
+|
+| Falling back to a less efficient fetching method.
+|
+| More efficient fetching method is possible and will be used in the future.
+"""
+
 @eh.wrapfunction(exchange, '_pullbundle2extraprepare')
 def _addobscommontob2pull(orig, pullop, kwargs):
     ret = orig(pullop, kwargs)
@@ -61,18 +73,27 @@
     if (b'obsmarkers' in kwargs
         and pullop.remote.capable(b'_evoext_getbundle_obscommon')):
         boundaries = obsdiscovery.buildpullobsmarkersboundaries(pullop)
-        if b'common' in boundaries:
+        use_common = True
+        if b'missing' in boundaries:
+            use_common = False
+            missing = boundaries[b'missing']
+            # using getattr since `limitedarguments` is missing
+            # hg <= 5.0 (69921d02daaf)
+            limitedarguments = getattr(pullop.remote, 'limitedarguments', False)
+            if limitedarguments and len(missing) > ARGUMENTS_LIMIT:
+                obsexcmsg(ui, OVERFLOW_MSG % len(missing))
+                use_common = True
+            else:
+                if missing:
+                    obsexcmsg(ui, b'request obsmarkers for %d common nodes\n'
+                              % len(missing))
+                kwargs[b'evo_missing_nodes'] = missing
+        if use_common and b'common' in boundaries:
             common = boundaries[b'common']
             if common != pullop.common:
                 obsexcmsg(ui, b'request obsmarkers for some common nodes\n')
             if common != [node.nullid]:
                 kwargs[b'evo_obscommon'] = common
-        elif b'missing' in boundaries:
-            missing = boundaries[b'missing']
-            if missing:
-                obsexcmsg(ui, b'request obsmarkers for %d common nodes\n'
-                          % len(missing))
-            kwargs[b'evo_missing_nodes'] = missing
     return ret
 
 def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs):
@@ -85,8 +106,10 @@
             if heads is None:
                 heads = repo.heads()
             obscommon = kwargs.get('evo_obscommon', ())
-            assert obscommon
-            obsset = repo.unfiltered().set(b'::%ln - ::%ln', heads, obscommon)
+            if obscommon:
+                obsset = repo.unfiltered().set(b'::%ln - ::%ln', heads, obscommon)
+            else:
+                obsset = repo.unfiltered().set(b'::%ln', heads)
             subset = [c.node() for c in obsset]
         else:
             common = kwargs.get('common')
--- a/hgext3rd/evolve/rewind.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/evolve/rewind.py	Wed Apr 08 01:16:54 2020 +0800
@@ -196,7 +196,7 @@
     if date is None:
         date = compat.makedate()
     noise = b"%s\0%s\0%d\0%d" % (ctx.node(), user, date[0], date[1])
-    extra[b'__rewind-hash__'] = hashlib.sha256(noise).hexdigest()
+    extra[b'__rewind-hash__'] = hashlib.sha256(noise).hexdigest().encode('ascii')
 
     p1 = ctx.p1().node()
     p1 = rewindmap.get(p1, p1)
--- a/hgext3rd/evolve/safeguard.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/evolve/safeguard.py	Wed Apr 08 01:16:54 2020 +0800
@@ -19,11 +19,35 @@
 
 eh = exthelper.exthelper()
 
-# hg <= 4.8
+# hg <= 4.8 (33d30fb1e4ae)
 if b'auto-publish' not in configitems.coreitems.get(b'experimental', {}):
 
     eh.configitem(b'experimental', b'auto-publish', b'publish')
 
+    def _checkpublish(pushop):
+        repo = pushop.repo
+        ui = repo.ui
+        behavior = ui.config(b'experimental', b'auto-publish')
+        nocheck = behavior not in (b'warn', b'abort')
+        if nocheck or getattr(pushop, 'publish', False):
+            return
+        remotephases = pushop.remote.listkeys(b'phases')
+        publishing = remotephases.get(b'publishing', False)
+        if publishing:
+            if pushop.revs is None:
+                published = repo.filtered(b'served').revs(b"not public()")
+            else:
+                published = repo.revs(b"::%ln - public()", pushop.revs)
+            if published:
+                if behavior == b'warn':
+                    ui.warn(_(b'%i changesets about to be published\n')
+                            % len(published))
+                elif behavior == b'abort':
+                    msg = _(b'push would publish 1 changesets')
+                    hint = _(b"behavior controlled by "
+                             b"'experimental.auto-publish' config")
+                    raise error.Abort(msg, hint=hint)
+
     @eh.reposetup
     def setuppublishprevention(ui, repo):
 
@@ -31,25 +55,6 @@
 
             def checkpush(self, pushop):
                 super(noautopublishrepo, self).checkpush(pushop)
-                behavior = self.ui.config(b'experimental', b'auto-publish')
-                nocheck = behavior not in (b'warn', b'abort')
-                if nocheck or getattr(pushop, 'publish', False):
-                    return
-                remotephases = pushop.remote.listkeys(b'phases')
-                publishing = remotephases.get(b'publishing', False)
-                if publishing:
-                    if pushop.revs is None:
-                        published = self.filtered(b'served').revs(b"not public()")
-                    else:
-                        published = self.revs(b"::%ln - public()", pushop.revs)
-                    if published:
-                        if behavior == b'warn':
-                            self.ui.warn(_(b'%i changesets about to be published\n')
-                                         % len(published))
-                        elif behavior == b'abort':
-                            msg = _(b'push would publish 1 changesets')
-                            hint = _(b"behavior controlled by "
-                                     b"'experimental.auto-publish' config")
-                            raise error.Abort(msg, hint=hint)
+                _checkpublish(pushop)
 
         repo.__class__ = noautopublishrepo
--- a/hgext3rd/evolve/stablerangecache.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/evolve/stablerangecache.py	Wed Apr 08 01:16:54 2020 +0800
@@ -46,20 +46,20 @@
 
 Your repository is probably big.
 
-Stable range are used for discovery missing osbsolescence markers during
+Stable ranges are used for discovery missing obsolescence markers during
 exchange. While the algorithm we use can scale well for large repositories, the
 naive python implementation that you are using is not very efficient, the
 storage backend for that cache neither.
 
 This computation will finish in a finite amount of time, even for repositories
-with millions of revision and many merges. However It might take multiple tens
+with millions of revisions and many merges. However it might take multiple tens
 of minutes to complete in such case.
 
 In the future, better implementation of the algorithm in a more appropriate
 language than Python will make it much faster. This data should also get
 exchanged between server and clients removing recomputation needs.
 
-In the mean time, got take a break while this cache is warming.
+In the meantime, go take a break while this cache is warming.
 
 See `hg help -e evolve` for details about how to control obsmarkers discovery and
 the update of related cache.
--- a/hgext3rd/evolve/utility.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/evolve/utility.py	Wed Apr 08 01:16:54 2020 +0800
@@ -139,10 +139,9 @@
     targets = obsutil.successorssets(repo, ctx.node())[0]
     assert targets
     targetrevs = [repo[r].rev() for r in targets]
-    roots = repo.revs(b'roots(%ld)', targetrevs)
-    heads = repo.revs(b'heads(%ld)', targetrevs)
-    if len(roots) > 1 or len(heads) > 1:
-        cheader = (_(b"ancestor '%s' split over multiple topological"
+    heads = repo.revs(b'heads(%ld::%ld)', targetrevs, targetrevs)
+    if len(heads) > 1:
+        cheader = (_(b"ancestor of '%s' split over multiple topological"
                      b" branches.\nchoose an evolve destination:") %
                    evolvecand)
         selectedrev = revselectionprompt(ui, repo, list(heads), cheader)
--- a/hgext3rd/topic/__init__.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/topic/__init__.py	Wed Apr 08 01:16:54 2020 +0800
@@ -140,6 +140,7 @@
     commands,
     context,
     error,
+    exchange,
     extensions,
     hg,
     localrepo,
@@ -380,6 +381,18 @@
     extensions.wrapfunction(context.workingctx, '__init__', wrapinit)
     # Wrap changelog.add to drop empty topic
     extensions.wrapfunction(changelog.changelog, 'add', wrapadd)
+    # Make exchange._checkpublish handle experimental.topic.publish-bare-branch
+    if util.safehasattr(exchange, '_checkpublish'):
+        extensions.wrapfunction(exchange, '_checkpublish',
+                                flow.replacecheckpublish)
+    else:
+        # hg <= 4.8 (33d30fb1e4ae)
+        try:
+            evolve = extensions.find(b'evolve')
+            extensions.wrapfunction(evolve.safeguard, '_checkpublish',
+                                    flow.replacecheckpublish)
+        except (KeyError, AttributeError):
+            pass
 
     server.setupserver(ui)
 
@@ -401,6 +414,9 @@
         def _restrictcapabilities(self, caps):
             caps = super(topicrepo, self)._restrictcapabilities(caps)
             caps.add(b'topics')
+            if self.ui.configbool(b'experimental',
+                                  b'topic.publish-bare-branch'):
+                caps.add(b'ext-topics-publish=auto')
             return caps
 
         def commit(self, *args, **kwargs):
--- a/hgext3rd/topic/discovery.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/topic/discovery.py	Wed Apr 08 01:16:54 2020 +0800
@@ -139,6 +139,46 @@
         repo.__class__ = oldrepo
 
 
+def _get_branch_name(ctx):
+    # make it easy for extension with the branch logic there
+    return ctx.branch()
+
+
+def _filter_obsolete_heads(repo, heads):
+    """filter heads to return non-obsolete ones
+
+    Given a list of heads (on the same named branch) return a new list of heads
+    where the obsolete part have been skimmed out.
+    """
+    new_heads = []
+    old_heads = heads[:]
+    while old_heads:
+        rh = old_heads.pop()
+        ctx = repo[rh]
+        current_name = _get_branch_name(ctx)
+        # run this check early to skip the evaluation of the whole branch
+        if not ctx.obsolete():
+            new_heads.append(rh)
+            continue
+
+        # Get all revs/nodes on the branch exclusive to this head
+        # (already filtered heads are "ignored"))
+        sections_revs = repo.revs(
+            b'only(%d, (%ld+%ld))', rh, old_heads, new_heads,
+        )
+        keep_revs = []
+        for r in sections_revs:
+            ctx = repo[r]
+            if ctx.obsolete():
+                continue
+            if _get_branch_name(ctx) != current_name:
+                continue
+            keep_revs.append(r)
+        for h in repo.revs(b'heads(%ld and (::%ld))', sections_revs, keep_revs):
+            new_heads.append(h)
+    new_heads.sort()
+    return new_heads
+
 # Discovery have deficiency around phases, branch can get new heads with pure
 # phases change. This happened with a changeset was allowed to be pushed
 # because it had a topic, but it later become public and create a new branch
@@ -150,7 +190,9 @@
     for b in repo.branchmap().iterbranches():
         if b':' in b[0]:
             continue
-        data[b[0]] = len(b[1])
+        oldheads = [repo[n].rev() for n in b[1]]
+        newheads = _filter_obsolete_heads(repo, oldheads)
+        data[b[0]] = len(newheads)
     return data
 
 def handlecheckheads(orig, op, inpart):
--- a/hgext3rd/topic/flow.py	Fri Feb 28 23:18:48 2020 +0700
+++ b/hgext3rd/topic/flow.py	Wed Apr 08 01:16:54 2020 +0800
@@ -108,3 +108,57 @@
                             extendpushoperation)
     extensions.wrapfunction(exchange, '_pushdiscoveryphase', wrapphasediscovery)
     exchange.pushdiscoverymapping[b'phase'] = exchange._pushdiscoveryphase
+
+def replacecheckpublish(orig, pushop):
+    listkeys = exchange.listkeys
+    repo = pushop.repo
+    ui = repo.ui
+    behavior = ui.config(b'experimental', b'auto-publish')
+    if pushop.publish or behavior not in (b'warn', b'confirm', b'abort'):
+        return
+
+    # possible modes are:
+    #
+    # none -> nothing is published on push
+    # all  -> everything is published on push
+    # auto -> only changeset without topic are published on push
+    #
+    # Unknown mode is assumed "all" for safety.
+    #
+    # TODO: do a wider brain storming about mode names.
+
+    mode = b'all'
+    remotephases = listkeys(pushop.remote, b'phases')
+    if not remotephases.get(b'publishing', False):
+        mode = b'none'
+        for c in pushop.remote.capabilities():
+            if c.startswith(b'ext-topics-publish'):
+                mode = c.split(b'=', 1)[1]
+                break
+    if mode == b'none':
+        return
+
+    if pushop.revs is None:
+        published = repo.filtered(b'served').revs(b'not public()')
+    else:
+        published = repo.revs(b'::%ln - public()', pushop.revs)
+    if mode == b'auto':
+        published = repo.revs(b'%ld::(%ld - topic())', published, published)
+    if published:
+        if behavior == b'warn':
+            ui.warn(
+                _(b'%i changesets about to be published\n') % len(published)
+            )
+        elif behavior == b'confirm':
+            if ui.promptchoice(
+                _(b'push and publish %i changesets (yn)?$$ &Yes $$ &No')
+                % len(published)
+            ):
+                raise error.Abort(_(b'user quit'))
+        elif behavior == b'abort':
+            msg = _(b'push would publish %i changesets') % len(published)
+            hint = _(
+                b"use --publish or adjust 'experimental.auto-publish'"
+                b" config"
+            )
+            raise error.Abort(msg, hint=hint)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-discovery-hidden-common.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,101 @@
+test for discovery with some remote changesets hidden locally
+=============================================================
+
+  $ . $TESTDIR/testlib/common.sh
+
+  $ cat << EOF >> $HGRCPATH
+  > [phases]
+  > publish = false
+  > [extensions]
+  > evolve =
+  > [experimental]
+  > verbose-obsolescence-exchange = 1
+  > [ui]
+  > logtemplate = "{rev} {node|short} {desc} {tags}\n"
+  > ssh = "$PYTHON" "$RUNTESTDIR/dummyssh"
+  > EOF
+
+  $ hg init server
+  $ hg clone ssh://user@dummy/server client
+  no changes found
+  updating to branch default
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd server
+  $ mkcommit root
+  $ mkcommit A0
+
+second pull:
+
+  $ hg -R ../client pull
+  pulling from ssh://user@dummy/server
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files
+  new changesets 1e4be0697311:8aaa48160adc (2 drafts)
+  (run 'hg update' to get a working copy)
+  $ hg -R ../client log -G
+  o  1 8aaa48160adc A0 tip
+  |
+  o  0 1e4be0697311 root
+  
+
+more update
+
+  $ hg tag --local stay-visible
+  $ 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)'`
+  obsoleted 1 changesets
+
+second pull:
+
+  $ hg -R ../client pull
+  pulling from ssh://user@dummy/server
+  searching for changes
+  OBSEXC: looking for common markers in 2 nodes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  new changesets f6082bc4ffef (1 drafts)
+  (run 'hg heads' to see heads)
+  $ hg -R ../client log -G
+  o  2 f6082bc4ffef A1 tip
+  |
+  o  0 1e4be0697311 root
+  
+
+more update:
+
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit A2
+  created new head
+  $ hg debugobsolete `getid 'desc(A1)'` `getid 'desc(A2)'`
+  obsoleted 1 changesets
+
+third pull:
+
+  $ hg -R ../client pull
+  pulling from ssh://user@dummy/server
+  searching for changes
+  OBSEXC: looking for common markers in 1 nodes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  new changesets c1f8d089020f (1 drafts)
+  (run 'hg heads' to see heads)
+  $ hg -R ../client log -G
+  o  3 c1f8d089020f A2 tip
+  |
+  o  0 1e4be0697311 root
+  
--- a/tests/test-discovery-obshashrange-cache.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-discovery-obshashrange-cache.t	Wed Apr 08 01:16:54 2020 +0800
@@ -16,11 +16,9 @@
   > verbose-obsolescence-exchange=1
   > [ui]
   > logtemplate = "{rev} {node|short} {desc} {tags}\n"
-  > ssh=python "$RUNTESTDIR/dummyssh"
+  > ssh = "$PYTHON" "$RUNTESTDIR/dummyssh"
   > [alias]
   > debugobsolete=debugobsolete -d '0 0'
-  > [ui]
-  > ssh=$PYTHON "$RUNTESTDIR/dummyssh"
   > EOF
 
   $ hg init main
--- a/tests/test-discovery-obshashrange.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-discovery-obshashrange.t	Wed Apr 08 01:16:54 2020 +0800
@@ -18,7 +18,7 @@
   > verbose-obsolescence-exchange=1
   > [ui]
   > logtemplate = "{rev} {node|short} {desc} {tags}\n"
-  > ssh=python "$RUNTESTDIR/dummyssh"
+  > ssh = "$PYTHON" "$RUNTESTDIR/dummyssh"
   > [alias]
   > debugobsolete=debugobsolete -d '0 0'
   > EOF
@@ -187,7 +187,7 @@
   could not import hgext.hgext3rd.evolve (No module named hgext3rd.evolve): trying hgext3rd.hgext3rd.evolve (?)
   could not import hgext3rd.hgext3rd.evolve (No module named hgext3rd.evolve): trying hgext3rd.evolve (?)
   pushing to ssh://user@dummy/server
-  running python "*/dummyssh" *user@dummy* *hg -R server serve --stdio* (glob)
+  running "*python*" "*/dummyssh" *user@dummy* *hg -R server serve --stdio* (glob)
   sending hello command
   sending between command
   remote: * (glob)
@@ -316,7 +316,7 @@
   * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> updated evo-ext-obscache in *.???? seconds (0r, 1o) (glob)
   * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> debugobsolete ffffffffffffffffffffffffffffffffffffffff 45f8b879de922f6a6e620ba04205730335b6fc7e exited 0 after *.?? seconds (glob)
   * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> push -f --debug (glob)
-  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> running python "*/dummyssh" *user@dummy* *hg -R server serve --stdio* (glob)
+  * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> running "*python*" "*/dummyssh" *user@dummy* *hg -R server serve --stdio* (glob)
   * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> sending hello command (glob)
   * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> sending between command (glob)
   * @45f8b879de922f6a6e620ba04205730335b6fc7e (*)> remote: * (glob)
--- a/tests/test-evolve-issue5832.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-evolve-issue5832.t	Wed Apr 08 01:16:54 2020 +0800
@@ -117,7 +117,7 @@
   move:[2] added b
   atop:[5] added a
   move:[4] merge commit
-  ancestor '7235ef625ea3' split over multiple topological branches.
+  ancestor of '7235ef625ea3' split over multiple topological branches.
   choose an evolve destination:
   1: [62fb70414f99] added c
   2: [5841d7cf9893] added d
@@ -257,7 +257,7 @@
   move:[2] added b
   atop:[6] added a
   move:[4] merge commit
-  ancestor 'cdf2ea1b9312' split over multiple topological branches.
+  ancestor of 'cdf2ea1b9312' split over multiple topological branches.
   choose an evolve destination:
   1: [62fb70414f99] added c
   2: [5841d7cf9893] added d
@@ -402,7 +402,7 @@
   > EOF
   move:[2] added b
   atop:[6] added a
-  ancestor 'b9b387427a53' split over multiple topological branches.
+  ancestor of 'b9b387427a53' split over multiple topological branches.
   choose an evolve destination:
   1: [62fb70414f99] added c
   2: [5841d7cf9893] added d
--- a/tests/test-evolve-obshistory-amend-then-fold.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-evolve-obshistory-amend-then-fold.t	Wed Apr 08 01:16:54 2020 +0800
@@ -77,8 +77,8 @@
      date:        Thu Jan 01 00:00:00 1970 +0000
      summary:     ROOT
   
- Actual test
- -----------
+Actual test
+-----------
 
 Check that debugobshistory on head show a coherent graph
   $ hg obslog eb5a0daa2192 --patch
--- a/tests/test-evolve-obshistory.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-evolve-obshistory.t	Wed Apr 08 01:16:54 2020 +0800
@@ -90,8 +90,8 @@
      date:        Thu Jan 01 00:00:00 1970 +0000
      summary:     ROOT
   
- Actual test
- -----------
+Actual test
+-----------
 
   $ hg obslog 7a230b46bf61 --patch
   @  7a230b46bf61 (3) A2
--- a/tests/test-evolve-orphan-split.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-evolve-orphan-split.t	Wed Apr 08 01:16:54 2020 +0800
@@ -184,7 +184,7 @@
   $ hg evolve --dry-run <<EOF
   > 1
   > EOF
-  ancestor 'd48a30875f01' split over multiple topological branches.
+  ancestor of 'd48a30875f01' split over multiple topological branches.
   choose an evolve destination:
   1: [f2632392aefe] added a b c
   2: [7f87764e5b64] added a b c
@@ -197,7 +197,7 @@
   $ hg evolve --dry-run <<EOF
   > 2
   > EOF
-  ancestor 'd48a30875f01' split over multiple topological branches.
+  ancestor of 'd48a30875f01' split over multiple topological branches.
   choose an evolve destination:
   1: [f2632392aefe] added a b c
   2: [7f87764e5b64] added a b c
@@ -213,7 +213,7 @@
   $ hg evolve --all <<EOF
   > foo
   > EOF
-  ancestor 'd48a30875f01' split over multiple topological branches.
+  ancestor of 'd48a30875f01' split over multiple topological branches.
   choose an evolve destination:
   1: [f2632392aefe] added a b c
   2: [7f87764e5b64] added a b c
@@ -225,7 +225,7 @@
   $ hg evolve --all <<EOF
   > 4
   > EOF
-  ancestor 'd48a30875f01' split over multiple topological branches.
+  ancestor of 'd48a30875f01' split over multiple topological branches.
   choose an evolve destination:
   1: [f2632392aefe] added a b c
   2: [7f87764e5b64] added a b c
@@ -237,7 +237,7 @@
   $ hg evolve --all <<EOF
   > -1
   > EOF
-  ancestor 'd48a30875f01' split over multiple topological branches.
+  ancestor of 'd48a30875f01' split over multiple topological branches.
   choose an evolve destination:
   1: [f2632392aefe] added a b c
   2: [7f87764e5b64] added a b c
@@ -249,7 +249,7 @@
   $ hg evolve --all <<EOF
   > q
   > EOF
-  ancestor 'd48a30875f01' split over multiple topological branches.
+  ancestor of 'd48a30875f01' split over multiple topological branches.
   choose an evolve destination:
   1: [f2632392aefe] added a b c
   2: [7f87764e5b64] added a b c
@@ -262,7 +262,7 @@
   $ hg evolve --all <<EOF
   > 1
   > EOF
-  ancestor 'd48a30875f01' split over multiple topological branches.
+  ancestor of 'd48a30875f01' split over multiple topological branches.
   choose an evolve destination:
   1: [f2632392aefe] added a b c
   2: [7f87764e5b64] added a b c
--- a/tests/test-evolve-serveronly-bundle2.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-evolve-serveronly-bundle2.t	Wed Apr 08 01:16:54 2020 +0800
@@ -8,7 +8,7 @@
   > push_ssl = false
   > allow_push = *
   > [ui]
-  > ssh=python "$RUNTESTDIR/dummyssh"
+  > ssh = "$PYTHON" "$RUNTESTDIR/dummyssh"
   > [phases]
   > publish = False
   > [experimental]
--- a/tests/test-evolve-split.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-evolve-split.t	Wed Apr 08 01:16:54 2020 +0800
@@ -59,3 +59,56 @@
   $ hg evolve --rev "0::"
   move:[2] add uu
   atop:[4] _pp
+
+  $ cd ..
+  $ hg init split-merged
+  $ cd split-merged
+  $ mkcommit aa
+
+Split the changeset such that the successors don't have a single root and there's an unrelated changeset in between
+  $ printf "oo" > oo;
+  $ printf "pp" > pp;
+  $ printf "qq" > qq;
+  $ hg add oo pp qq
+  $ hg commit -m "oo+pp+qq"
+  $ mkcommit uu
+  $ hg up 0
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ printf "oo" > oo;
+  $ hg add oo
+  $ hg commit -m "_oo"
+  created new head
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ printf "pp" > pp;
+  $ hg add pp
+  $ hg commit -m "_pp"
+  created new head
+  $ hg merge 3
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg ci -m 'merge oo and pp'
+  $ printf "qq" > qq;
+  $ hg add qq
+  $ hg commit -m "_qq"
+  $ hg prune --successor "desc(_oo) + desc(_pp) + desc(_qq)" -r "desc('oo+pp+qq')" --split
+  1 changesets pruned
+  1 new orphan changesets
+  $ hg log -G
+  @  6:ea5b1e180c04@default(draft) _qq
+  |
+  o    5:bf7c32161b4b@default(draft) merge oo and pp
+  |\
+  | o  4:ece0aaa22eb7@default(draft) _pp
+  | |
+  o |  3:a7fdfda64c08@default(draft) _oo
+  |/
+  | *  2:cc56c47d84b3@default(draft) add uu
+  | |
+  | x  1:575a7380a87d@default(draft) oo+pp+qq
+  |/
+  o  0:58663bb03074@default(draft) add aa
+  
+  $ hg evolve --rev "0::"
+  move:[2] add uu
+  atop:[6] _qq
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-push-checkheads-mixed-branch-topic-G1.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,91 @@
+====================================
+Testing head checking code: Case E-1
+====================================
+
+Mercurial checks for the introduction of new heads on push. Evolution comes
+into play to detect if existing branches on the server are being replaced by
+some of the new one we push.
+
+This case is part of a series of tests checking this behavior.
+
+Category F: case involving changeset on multiple topic
+TestCase 1: moving a branch to another location
+
+.. old-state:
+..
+.. * 1-changeset on branch default
+.. * 1-changeset on topic Z (above Y)
+..
+.. new-state:
+..
+.. * 1-changeset on branch default
+.. * 1-changeset on topic Z (rebased away from A0)
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ø⇠◔ B' topic Z
+..     | |
+..   A ◔ |    branch default
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir E1
+  $ cd E1
+  $ 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 topic Z
+  marked working directory as topic: Z
+  $ mkcommit B0
+  active topic 'Z' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg push
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg topic Z
+  marked working directory as topic: Z
+  $ mkcommit B1
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  845eeb768064 (draft)[Z]: B1
+  |
+  | x  35d2f30a8ba4 (draft)[Z]: B0
+  | |
+  | o  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/E1/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
+  obsoleted 1 changesets
+
+  $ cd ../..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-push-checkheads-mixed-branch-topic-G2.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,106 @@
+====================================
+Testing head checking code: Case E-2
+====================================
+
+Mercurial checks for the introduction of new heads on push. Evolution comes
+into play to detect if existing branches on the server are being replaced by
+some of the new one we push.
+
+This case is part of a series of tests checking this behavior.
+
+Category F: case involving changeset on multiple branch
+TestCase 2: moving interleaved branch away from each other
+
+.. old-state:
+..
+.. * 2-changeset on branch default
+.. * 1-changeset on topic Z (between the two other)
+..
+.. new-state:
+..
+.. * 2-changeset on branch default, aligned
+.. * 1-changeset on topic Z (at the same location)
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   C ø⇠◔ C' branch default
+..     | |
+..   B ◔ |    topic Z
+..     | |
+..   A ø⇠◔ A' branch default
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir E1
+  $ cd E1
+  $ 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 topic Z
+  marked working directory as topic: Z
+  $ mkcommit B0
+  active topic 'Z' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg topic --clear
+  $ mkcommit C0
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg push
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files
+  $ hg up 0
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ mkcommit C1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  obsoleted 1 changesets
+  2 new orphan changesets
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  0c76bc104656 (draft): C1
+  |
+  o  f6082bc4ffef (draft): A1
+  |
+  | x  44759c6d327d (draft): C0
+  | |
+  | *  35d2f30a8ba4 (draft)[Z]: B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc("C1")'
+  pushing to $TESTTMP/E1/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
+  obsoleted 2 changesets
+  1 new orphan changesets
+
+  $ cd ../..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-push-checkheads-mixed-branch-topic-G3.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,96 @@
+====================================
+Testing head checking code: Case E-3
+====================================
+
+Mercurial checks for the introduction of new heads on push. Evolution comes
+into play to detect if existing branches on the server are being replaced by
+some of the new one we push.
+
+This case is part of a series of tests checking this behavior.
+
+Category E: case involving changeset on multiple branch
+TestCase 8: moving only part of the interleaved branch away, creating 2 heads
+
+.. old-state:
+..
+.. * 2-changeset on branch default
+.. * 1-changeset on topic Z (between the two other)
+..
+.. new-state:
+..
+.. * 2-changeset on branch default, on untouched, the other moved
+.. * 1-changeset on topic Z (at the same location)
+..
+.. expected-result:
+..
+.. * push rejected
+..
+.. graph-summary:
+..
+..   C ø⇠◔ C' branch default
+..     | |
+..   B ◔ |    topic Z
+..     | |
+..   A ◔ |    branch default
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir E1
+  $ cd E1
+  $ 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 topic Z
+  marked working directory as topic: Z
+  $ mkcommit B0
+  active topic 'Z' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg topic --clear
+  $ mkcommit C0
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg push
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files
+  $ hg up 0
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ mkcommit C1
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  dc44c53142f0 (draft): C1
+  |
+  | x  44759c6d327d (draft): C0
+  | |
+  | o  35d2f30a8ba4 (draft)[Z]: B0
+  | |
+  | o  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc("C1")'
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  abort: push creates new remote head dc44c53142f0!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
+  $ cd ../..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-push-checkheads-multi-topics-F1.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,97 @@
+====================================
+Testing head checking code: Case E-1
+====================================
+
+Mercurial checks for the introduction of new heads on push. Evolution comes
+into play to detect if existing branches on the server are being replaced by
+some of the new one we push.
+
+This case is part of a series of tests checking this behavior.
+
+Category F: case involving changeset on multiple topic
+TestCase 1: moving a branch to another location
+
+.. old-state:
+..
+.. * 1-changeset on topic Y
+.. * 1-changeset on topic Z (above Y)
+..
+.. new-state:
+..
+.. * 1-changeset on topic Y
+.. * 1-changeset on topic Z (rebased away from A0)
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ø⇠◔ B' topic Z
+..     | |
+..   A ◔ |    topic Y
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir E1
+  $ cd E1
+  $ 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 topic -r . Y
+  switching to topic Y
+  changed topic on 1 changesets to "Y"
+  $ hg topic Z
+  $ mkcommit B0
+  active topic 'Z' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg push
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 1 changes to 2 files (+1 heads)
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg topic Z
+  marked working directory as topic: Z
+  $ mkcommit B1
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  845eeb768064 (draft)[Z]: B1
+  |
+  | x  e1494106e1ca (draft)[Z]: B0
+  | |
+  | o  f5cd873e2965 (draft)[Y]: A0
+  |/
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/E1/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
+  obsoleted 1 changesets
+
+  $ cd ../..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-push-checkheads-multi-topics-F2.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,110 @@
+====================================
+Testing head checking code: Case E-2
+====================================
+
+Mercurial checks for the introduction of new heads on push. Evolution comes
+into play to detect if existing branches on the server are being replaced by
+some of the new one we push.
+
+This case is part of a series of tests checking this behavior.
+
+Category F: case involving changeset on multiple branch
+TestCase 2: moving interleaved branch away from each other
+
+.. old-state:
+..
+.. * 2-changeset on topic Y
+.. * 1-changeset on topic Z (between the two other)
+..
+.. new-state:
+..
+.. * 2-changeset on topic Y, aligned
+.. * 1-changeset on topic Z (at the same location)
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   C ø⇠◔ C' topic Y
+..     | |
+..   B ◔ |    topic Z
+..     | |
+..   A ø⇠◔ A' topic Y
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir E1
+  $ cd E1
+  $ 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 topic -r . Y
+  switching to topic Y
+  changed topic on 1 changesets to "Y"
+  $ hg strip --config extensions.strip= --hidden 'hidden()' # clean old A0
+  saved backup bundle to $TESTTMP/E1/client/.hg/strip-backup/8aaa48160adc-19166392-backup.hg
+  $ hg topic Z
+  $ mkcommit B0
+  active topic 'Z' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg topic Y
+  $ mkcommit C0
+  $ hg push
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 2 changes to 3 files (+1 heads)
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  $ hg up 0
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ hg topic Y
+  marked working directory as topic: Y
+  $ mkcommit A1
+  $ mkcommit C1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  obsoleted 1 changesets
+  2 new orphan changesets
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  0e26ba57d799 (draft)[Y]: C1
+  |
+  o  fb4a34222909 (draft)[Y]: A1
+  |
+  | x  345721b128e8 (draft)[Y]: C0
+  | |
+  | *  e1494106e1ca (draft)[Z]: B0
+  | |
+  | x  f5cd873e2965 (draft)[Y]: A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc("C1")'
+  pushing to $TESTTMP/E1/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
+  obsoleted 2 changesets
+  1 new orphan changesets
+
+  $ cd ../..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-push-checkheads-multi-topics-F3.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,100 @@
+====================================
+Testing head checking code: Case E-3
+====================================
+
+Mercurial checks for the introduction of new heads on push. Evolution comes
+into play to detect if existing branches on the server are being replaced by
+some of the new one we push.
+
+This case is part of a series of tests checking this behavior.
+
+Category E: case involving changeset on multiple branch
+TestCase 8: moving only part of the interleaved branch away, creating 2 heads
+
+.. old-state:
+..
+.. * 2-changeset on topic Y
+.. * 1-changeset on topic Z (between the two other)
+..
+.. new-state:
+..
+.. * 2-changeset on topic Y, on untouched, the other moved
+.. * 1-changeset on topic Z (at the same location)
+..
+.. expected-result:
+..
+.. * push rejected
+..
+.. graph-summary:
+..
+..   C ø⇠◔ C' topic Y
+..     | |
+..   B ◔ |    topic Z
+..     | |
+..   A ◔ |    topic Y
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir E1
+  $ cd E1
+  $ 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 topic -r . Y
+  switching to topic Y
+  changed topic on 1 changesets to "Y"
+  $ hg strip --config extensions.strip= --hidden 'hidden()' # clean old A0
+  saved backup bundle to $TESTTMP/E1/client/.hg/strip-backup/8aaa48160adc-19166392-backup.hg
+  $ hg topic Z
+  $ mkcommit B0
+  active topic 'Z' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg topic Y
+  $ mkcommit C0
+  $ hg push
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 2 changes to 3 files (+1 heads)
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  $ hg up 0
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ hg topic Y
+  marked working directory as topic: Y
+  $ mkcommit C1
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  57530ca5eb24 (draft)[Y]: C1
+  |
+  | x  345721b128e8 (draft)[Y]: C0
+  | |
+  | o  e1494106e1ca (draft)[Z]: B0
+  | |
+  | o  f5cd873e2965 (draft)[Y]: A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc("C1")'
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  abort: push creates new remote head 57530ca5eb24 on branch 'default:Y'!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
+  $ cd ../..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-push-checkheads-multibranches-E1.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,92 @@
+====================================
+Testing head checking code: Case E-1
+====================================
+
+Mercurial checks for the introduction of new heads on push. Evolution comes
+into play to detect if existing branches on the server are being replaced by
+some of the new one we push.
+
+This case is part of a series of tests checking this behavior.
+
+Category E: case involving changeset on multiple branch
+TestCase 8: moving a branch to another location
+
+.. old-state:
+..
+.. * 1-changeset on branch default
+.. * 1-changeset on branch Z (above default)
+..
+.. new-state:
+..
+.. * 1-changeset on branch default
+.. * 1-changeset on branch Z (rebased away from A0)
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   B ø⇠◔ B'
+..     | |
+..   A ◔ |
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir E1
+  $ cd E1
+  $ 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 branch Z
+  marked working directory as branch Z
+  (branches are permanent and global, did you want a bookmark?)
+  $ mkcommit B0
+  $ hg push --new-branch
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  $ hg up 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg branch --force Z
+  marked working directory as branch Z
+  $ mkcommit B1
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  c98b855401e7 (draft): B1
+  |
+  | x  93e5c1321ece (draft): B0
+  | |
+  | o  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push
+  pushing to $TESTTMP/E1/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
+  obsoleted 1 changesets
+
+  $ cd ../..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-push-checkheads-multibranches-E2.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,106 @@
+====================================
+Testing head checking code: Case E-2
+====================================
+
+Mercurial checks for the introduction of new heads on push. Evolution comes
+into play to detect if existing branches on the server are being replaced by
+some of the new one we push.
+
+This case is part of a series of tests checking this behavior.
+
+Category E: case involving changeset on multiple branch
+TestCase 8: moving interleaved branch away from each other
+
+.. old-state:
+..
+.. * 2-changeset on branch default
+.. * 1-changeset on branch Z (between the two other)
+..
+.. new-state:
+..
+.. * 2-changeset on branch default, aligned
+.. * 1-changeset on branch Z (at the same location)
+..
+.. expected-result:
+..
+.. * push allowed
+..
+.. graph-summary:
+..
+..   C ø⇠◔ C'
+..     | |
+..   B ◔ |
+..     | |
+..   A ø⇠◔ A'
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir E1
+  $ cd E1
+  $ 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 branch Z
+  marked working directory as branch Z
+  (branches are permanent and global, did you want a bookmark?)
+  $ mkcommit B0
+  $ hg branch default --force
+  marked working directory as branch default
+  $ mkcommit C0
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg push --new-branch
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files
+  $ hg up 0
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ mkcommit C1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  obsoleted 1 changesets
+  2 new orphan changesets
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  0c76bc104656 (draft): C1
+  |
+  o  f6082bc4ffef (draft): A1
+  |
+  | x  afc55ba2ce61 (draft): C0
+  | |
+  | *  93e5c1321ece (draft): B0
+  | |
+  | x  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc("C1")'
+  pushing to $TESTTMP/E1/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
+  obsoleted 2 changesets
+  1 new orphan changesets
+
+  $ cd ../..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-push-checkheads-multibranches-E3.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,96 @@
+====================================
+Testing head checking code: Case E-3
+====================================
+
+Mercurial checks for the introduction of new heads on push. Evolution comes
+into play to detect if existing branches on the server are being replaced by
+some of the new one we push.
+
+This case is part of a series of tests checking this behavior.
+
+Category E: case involving changeset on multiple branch
+TestCase 8: moving only part of the interleaved branch away, creating 2 heads
+
+.. old-state:
+..
+.. * 2-changeset on branch default
+.. * 1-changeset on branch Z (between the two other)
+..
+.. new-state:
+..
+.. * 2-changeset on branch default, on untouched, the other moved
+.. * 1-changeset on branch Z (at the same location)
+..
+.. expected-result:
+..
+.. * push rejected
+..
+.. graph-summary:
+..
+..   C ø⇠◔ C'
+..     | |
+..   B ◔ |
+..     | |
+..   A ◔ |
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir E1
+  $ cd E1
+  $ 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 branch Z
+  marked working directory as branch Z
+  (branches are permanent and global, did you want a bookmark?)
+  $ mkcommit B0
+  $ hg branch default --force
+  marked working directory as branch default
+  $ mkcommit C0
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg push --new-branch
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files
+  $ hg up 0
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ mkcommit C1
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  dc44c53142f0 (draft): C1
+  |
+  | x  afc55ba2ce61 (draft): C0
+  | |
+  | o  93e5c1321ece (draft): B0
+  | |
+  | o  8aaa48160adc (draft): A0
+  |/
+  o  1e4be0697311 (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc("C1")'
+  pushing to $TESTTMP/E1/server
+  searching for changes
+  abort: push creates new remote head dc44c53142f0!
+  (merge or see 'hg help push' for details about pushing new heads)
+  [255]
+
+  $ cd ../..
--- a/tests/test-push-checkheads-pruned-B5.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-push-checkheads-pruned-B5.t	Wed Apr 08 01:16:54 2020 +0800
@@ -27,11 +27,11 @@
 ..
 .. graph-summary:
 ..
-..   B ⊗
+..   C ⊗
 ..     |
-..   A ø⇠◔ A'
+..   B ø⇠◔ B'
 ..     | |
-..   B ⊗ |
+..   A ⊗ |
 ..     |/
 ..     ●
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-named-branch-A1.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,106 @@
+=========================================
+Testing single head enforcement: Case A-1
+=========================================
+
+A repository is set to only accept a single head per name (typically named
+branch). However, obsolete changesets can make this enforcement more
+complicated, because they can be kept visible by other changeset on other
+branch.
+
+This case is part of a series of tests checking this behavior.
+
+Category A: Involving obsolescence
+TestCase 1: A fully obsolete branch kept visible by another one.
+
+.. old-state:
+..
+.. * 2 changeset changeset on branch default
+.. * 2 changeset changeset on branch Z on top of them.
+..
+.. new-state:
+..
+.. * 2 changeset changeset on branch Z at the same location
+.. * 2 changeset changeset on branch default superceeding the other ones
+..
+.. expected-result:
+..
+.. * only one head detected
+..
+.. graph-summary:
+..
+..   D ●      (branch Z)
+..     |
+..   C ●      (branch Z)
+..     |
+..   B ø⇠◔ B'
+..     | |
+..   A ø⇠◔ A'
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir A1
+  $ cd A1
+  $ setuprepos single-head
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ mkcommit B0
+  $ hg branch Z
+  marked working directory as branch Z
+  (branches are permanent and global, did you want a bookmark?)
+  $ mkcommit C0
+  $ mkcommit D0
+  $ hg push --new-branch
+  pushing to $TESTTMP/A1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 3 files
+  $ hg up 0
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ mkcommit B1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  obsoleted 1 changesets
+  3 new orphan changesets
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  262c8c798096 [default] (draft): B1
+  |
+  o  f6082bc4ffef [default] (draft): A1
+  |
+  | *  cdf1dbb37a67 [Z] (draft): D0
+  | |
+  | *  3213e3e16c67 [Z] (draft): C0
+  | |
+  | x  d73caddc5533 [default] (draft): B0
+  | |
+  | x  8aaa48160adc [default] (draft): A0
+  |/
+  o  1e4be0697311 [default] (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc("B1")'
+  pushing to $TESTTMP/A1/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
+  obsoleted 2 changesets
+  2 new orphan changesets
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-named-branch-A2.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,108 @@
+
+=========================================
+Testing single head enforcement: Case A-2
+=========================================
+
+A repository is set to only accept a single head per name (typically named
+branch). However, obsolete changesets can make this enforcement more
+complicated, because they can be kept visible by other changeset on other
+branch.
+
+This case is part of a series of tests checking this behavior.
+
+Category A: Involving obsolescence
+TestCase 2: A branch is split in two, effectively creating two heads
+
+.. old-state:
+..
+.. * 2 changeset changeset on branch default
+.. * 2 changeset changeset on branch Z on top of them.
+..
+.. new-state:
+..
+.. * 2 changeset changeset on branch Z at the same location
+.. * 1 changeset changeset on branch default unchanged
+.. * 1 changeset changeset on branch default superceeding the other ones
+..
+.. expected-result:
+..
+.. * two heads detected
+..
+.. graph-summary:
+..
+..   D ●      (branch Z)
+..     |
+..   C ●      (branch Z)
+..     |
+..   B ø⇠◔ B'
+..     | |
+..   A ● |
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir A2
+  $ cd A2
+  $ setuprepos single-head
+  creating basic server and client repo
+  updating to branch default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client
+  $ mkcommit B0
+  $ hg branch Z
+  marked working directory as branch Z
+  (branches are permanent and global, did you want a bookmark?)
+  $ mkcommit C0
+  $ mkcommit D0
+  $ hg push --new-branch
+  pushing to $TESTTMP/A2/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 3 files
+  $ hg up 0
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ mkcommit B1
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  obsoleted 1 changesets
+  2 new orphan changesets
+  $ hg log -G --hidden
+  @  25c56d33e4c4 [default] (draft): B1
+  |
+  | *  cdf1dbb37a67 [Z] (draft): D0
+  | |
+  | *  3213e3e16c67 [Z] (draft): C0
+  | |
+  | x  d73caddc5533 [default] (draft): B0
+  | |
+  | o  8aaa48160adc [default] (draft): A0
+  |/
+  o  1e4be0697311 [default] (public): root
+  
+
+Actual testing
+--------------
+
+(force push to make sure we get the changeset on the remote)
+
+  $ hg push -r 'desc("B1")' --force
+  pushing to $TESTTMP/A2/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
+  transaction abort!
+  rollback completed
+  abort: rejecting multiple heads on branch "default"
+  (2 heads: 8aaa48160adc 25c56d33e4c4)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-named-branch-A3.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,114 @@
+=========================================
+Testing single head enforcement: Case A-3
+=========================================
+
+A repository is set to only accept a single head per name (typically named
+branch). However, obsolete changesets can make this enforcement more
+complicated, because they can be kept visible by other changeset on other
+branch.
+
+This case is part of a series of tests checking this behavior.
+
+Category A: Involving obsolescence
+TestCase 3: Full superceedig of a branch interleaved with another
+
+.. old-state:
+..
+.. * 2 changeset changeset on branch default
+.. * 2 changeset changeset on branch Z interleaved with the other
+..
+.. new-state:
+..
+.. * 2 changeset changeset on branch Z at the same location
+.. * 2 changeset changeset on branch default superceeding the other ones
+..
+.. expected-result:
+..
+.. * only one head detected
+..
+.. graph-summary:
+..
+..   D ●      (branch Z)
+..     |
+..   C ø⇠◔ C'
+..     | |
+..   B ● |    (branch Z)
+..     | |
+..   A ø⇠◔ A'
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir A3
+  $ cd A3
+  $ setuprepos single-head
+  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 branch Z
+  marked working directory as branch Z
+  (branches are permanent and global, did you want a bookmark?)
+  $ mkcommit B0
+  $ hg branch default --force
+  marked working directory as branch default
+  $ mkcommit C0
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg branch Z --force
+  marked working directory as branch Z
+  $ mkcommit D0
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg push --new-branch
+  pushing to $TESTTMP/A3/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 3 files
+  $ hg up 0
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ mkcommit C1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  obsoleted 1 changesets
+  3 new orphan changesets
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  0c76bc104656 [default] (draft): C1
+  |
+  o  f6082bc4ffef [default] (draft): A1
+  |
+  | *  78578c4306ce [Z] (draft): D0
+  | |
+  | x  afc55ba2ce61 [default] (draft): C0
+  | |
+  | *  93e5c1321ece [Z] (draft): B0
+  | |
+  | x  8aaa48160adc [default] (draft): A0
+  |/
+  o  1e4be0697311 [default] (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc("C1")'
+  pushing to $TESTTMP/A3/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
+  obsoleted 2 changesets
+  2 new orphan changesets
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-named-branch-A4.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,112 @@
+=========================================
+Testing single head enforcement: Case A-4
+=========================================
+
+A repository is set to only accept a single head per name (typically named
+branch). However, obsolete changesets can make this enforcement more
+complicated, because they can be kept visible by other changeset on other
+branch.
+
+This case is part of a series of tests checking this behavior.
+
+Category A: Involving obsolescence
+TestCase 4: Partial rewrite of a branch to dis-interleave it
+
+.. old-state:
+..
+.. * 2 changeset changeset on branch default
+.. * 2 changeset changeset on branch Z interleaved with the other one
+..
+.. new-state:
+..
+.. * 2 changeset changeset on branch Z at the same location
+.. * 1 changeset on default untouched (the lower one)
+.. * 1 changeset on default moved on the other one
+..
+.. expected-result:
+..
+.. * only one head detected
+..
+.. graph-summary:
+..
+..   D ●      (branch Z)
+..     |
+..   C ø⇠◔ C'
+..     | |
+..   B ● |    (branch Z)
+..     |/
+..   A ●
+..     |
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir A4
+  $ cd A4
+  $ setuprepos single-head
+  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 branch Z
+  marked working directory as branch Z
+  (branches are permanent and global, did you want a bookmark?)
+  $ mkcommit B0
+  $ hg branch default --force
+  marked working directory as branch default
+  $ mkcommit C0
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg branch Z --force
+  marked working directory as branch Z
+  $ mkcommit D0
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg push --new-branch
+  pushing to $TESTTMP/A4/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 3 changes to 3 files
+  $ hg up 'desc("A0")'
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ mkcommit C1
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  obsoleted 1 changesets
+  1 new orphan changesets
+  $ hg log -G --hidden
+  @  cfe9ed94fa4a [default] (draft): C1
+  |
+  | *  78578c4306ce [Z] (draft): D0
+  | |
+  | x  afc55ba2ce61 [default] (draft): C0
+  | |
+  | o  93e5c1321ece [Z] (draft): B0
+  |/
+  o  8aaa48160adc [default] (draft): A0
+  |
+  o  1e4be0697311 [default] (public): root
+  
+
+Actual testing
+--------------
+
+(force push to make sure we get the changeset on the remote)
+
+  $ hg push -r 'desc("C1")' --force
+  pushing to $TESTTMP/A4/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
+  obsoleted 1 changesets
+  1 new orphan changesets
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-named-branch-A5.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,113 @@
+=========================================
+Testing single head enforcement: Case A-1
+=========================================
+
+A repository is set to only accept a single head per name (typically named
+branch). However, obsolete changesets can make this enforcement more
+complicated, because they can be kept visible by other changeset on other
+branch.
+
+This case is part of a series of tests checking this behavior.
+
+Category A: Involving obsolescence
+TestCase 1: obsoleting a merge reveal two heads
+
+.. old-state:
+..
+.. * 3 changeset changeset on branch default (2 on their own branch + 1 merge)
+.. * 1 changeset on branch Z (children of the merge)
+..
+.. new-state:
+..
+.. * 2 changeset changeset on branch default (merge is obsolete) each a head
+.. * 1 changeset on branch Z keeping the merge visible
+..
+.. expected-result:
+..
+.. * 2 heads detected (because we skip the merge).
+..
+.. graph-summary:
+..
+..   D ●      (branch Z)
+..     |
+..   C ●      (branch Z)
+..     |
+..   M ⊗
+..     |\
+..   A ● ● B
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir A5
+  $ cd A5
+  $ setuprepos single-head
+  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
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ hg merge
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg ci -m 'M0'
+  $ hg branch Z
+  marked working directory as branch Z
+  (branches are permanent and global, did you want a bookmark?)
+  $ mkcommit C0
+  $ hg push --new-branch
+  pushing to $TESTTMP/A5/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 3 changesets with 2 changes to 2 files
+  $ hg up 0
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ mkcommit A1
+  created new head
+  (consider using topic for lightweight branches. See 'hg help topic')
+  $ mkcommit B1
+  $ hg debugobsolete `getid "desc(M0)"` --record-parents
+  obsoleted 1 changesets
+  1 new orphan changesets
+  $ hg log -G --hidden
+  @  262c8c798096 [default] (draft): B1
+  |
+  o  f6082bc4ffef [default] (draft): A1
+  |
+  | *  61c95483cc12 [Z] (draft): C0
+  | |
+  | x    14d3d4d41d1a [default] (draft): M0
+  | |\
+  +---o  74ff5441d343 [default] (draft): B0
+  | |
+  | o  8aaa48160adc [default] (draft): A0
+  |/
+  o  1e4be0697311 [default] (public): root
+  
+
+Actual testing
+--------------
+
+(force push to make sure we get the changeset on the remote)
+
+  $ hg push -r 'desc("C0")' --force
+  pushing to $TESTTMP/A5/server
+  searching for changes
+  no changes found
+  1 new obsolescence markers
+  transaction abort!
+  rollback completed
+  abort: rejecting multiple heads on branch "default"
+  (2 heads: 8aaa48160adc 74ff5441d343)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-topic-B1.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,112 @@
+=========================================
+Testing single head enforcement: Case A-1
+=========================================
+
+A repository is set to only accept a single head per name (typically named
+branch). However, obsolete changesets can make this enforcement more
+complicated, because they can be kept visible by other changeset on other
+branch.
+
+This case is part of a series of tests checking this behavior.
+
+Category B: Involving obsolescence and topic
+TestCase 1: A fully obsolete topic kept visible by another one.
+
+.. old-state:
+..
+.. * 2 changeset changeset on topic X
+.. * 2 changeset changeset on topic Y on top of them.
+..
+.. new-state:
+..
+.. * 2 changeset changeset on topic Y at the same location
+.. * 2 changeset changeset on topic X superceeding the other ones
+..
+.. expected-result:
+..
+.. * only one head detected
+..
+.. graph-summary:
+..
+..   D ●      (topic-Y)
+..     |
+..   C ●      (topic-Y)
+..     |
+..   B ø⇠◔ B' (topic-X)
+..     | |
+..   A ø⇠◔ A' (topic-X)
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir B1
+  $ cd B1
+  $ setuprepos single-head
+  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 topic -r . topic-X
+  switching to topic topic-X
+  changed topic on 1 changesets to "topic-X"
+  $ hg strip --config extensions.strip= --hidden 'hidden()' --no-backup # clean old A0
+  $ mkcommit B0
+  $ hg topic topic-Y
+  $ mkcommit C0
+  active topic 'topic-Y' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ mkcommit D0
+  $ hg push --new-branch
+  pushing to $TESTTMP/B1/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 4 changesets with 3 changes to 4 files (+1 heads)
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  $ hg up 0
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ hg topic topic-X
+  marked working directory as topic: topic-X
+  $ mkcommit A1
+  $ mkcommit B1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  obsoleted 1 changesets
+  3 new orphan changesets
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  f4ed6717fb66 [default:topic-X] (draft): B1
+  |
+  o  c1340bef453e [default:topic-X] (draft): A1
+  |
+  | *  618812b710f7 [default:topic-Y] (draft): D0
+  | |
+  | *  d1ad53773db2 [default:topic-Y] (draft): C0
+  | |
+  | x  1c1f62b56685 [default:topic-X] (draft): B0
+  | |
+  | x  5a47a98cd8e5 [default:topic-X] (draft): A0
+  |/
+  o  1e4be0697311 [default] (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc("B1")'
+  pushing to $TESTTMP/B1/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
+  obsoleted 2 changesets
+  2 new orphan changesets
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-topic-B2.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,116 @@
+
+=========================================
+Testing single head enforcement: Case A-2
+=========================================
+
+A repository is set to only accept a single head per name (typically named
+branch). However, obsolete changesets can make this enforcement more
+complicated, because they can be kept visible by other changeset on other
+branch.
+
+This case is part of a series of tests checking this behavior.
+
+Category B: Involving obsolescence with topic
+TestCase 2: A branch is split in two, effectively creating two heads
+
+.. old-state:
+..
+.. * 2 changeset changeset on topic X
+.. * 2 changeset changeset on topic Y on top of them.
+..
+.. new-state:
+..
+.. * 2 changeset changeset on topic Y at the same location
+.. * 1 changeset changeset on topic X unchanged
+.. * 1 changeset changeset on topic X superceeding the other ones
+..
+.. expected-result:
+..
+.. * two heads detected
+..
+.. graph-summary:
+..
+..   D ●      (topic-Y)
+..     |
+..   C ●      (topic-Y)
+..     |
+..   B ø⇠◔ B' (topic-X)
+..     | |
+..   A ● |    (topic-X)
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir B2
+  $ cd B2
+  $ setuprepos single-head
+  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 topic -r . topic-X
+  switching to topic topic-X
+  changed topic on 1 changesets to "topic-X"
+  $ hg strip --config extensions.strip= --hidden 'hidden()' --no-backup # clean old A0
+  $ mkcommit B0
+  $ hg branch Z
+  marked working directory as branch Z
+  $ hg topic topic-Y
+  $ mkcommit C0
+  active topic 'topic-Y' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ mkcommit D0
+  $ hg push --new-branch
+  pushing to $TESTTMP/B2/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 4 changesets with 3 changes to 4 files (+1 heads)
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  $ hg up 0
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ hg topic topic-X
+  marked working directory as topic: topic-X
+  $ mkcommit B1
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  obsoleted 1 changesets
+  2 new orphan changesets
+  $ hg log -G --hidden
+  @  5a4735b75167 [default:topic-X] (draft): B1
+  |
+  | *  02490b2dd1c5 [Z:topic-Y] (draft): D0
+  | |
+  | *  447ad8382abc [Z:topic-Y] (draft): C0
+  | |
+  | x  1c1f62b56685 [default:topic-X] (draft): B0
+  | |
+  | o  5a47a98cd8e5 [default:topic-X] (draft): A0
+  |/
+  o  1e4be0697311 [default] (public): root
+  
+
+Actual testing
+--------------
+
+(force push to make sure we get the changeset on the remote)
+
+  $ hg push -r 'desc("B1")' --force
+  pushing to $TESTTMP/B2/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
+  transaction abort!
+  rollback completed
+  abort: rejecting multiple heads on branch "default:topic-X"
+  (2 heads: 5a47a98cd8e5 5a4735b75167)
+  [255]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-topic-B3.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,114 @@
+=========================================
+Testing single head enforcement: Case A-3
+=========================================
+
+A repository is set to only accept a single head per name (typically named
+branch). However, obsolete changesets can make this enforcement more
+complicated, because they can be kept visible by other changeset on other
+branch.
+
+This case is part of a series of tests checking this behavior.
+
+Category B: Involving obsolescence with topic
+TestCase 3: Full superceedig of a branch interleaved with another
+
+.. old-state:
+..
+.. * 2 changeset changeset on topic Y
+.. * 2 changeset changeset on topic X interleaved with the other
+..
+.. new-state:
+..
+.. * 2 changeset changeset on topic X at the same location
+.. * 2 changeset changeset on topic Y superceeding the other ones
+..
+.. expected-result:
+..
+.. * only one head detected
+..
+.. graph-summary:
+..
+..   D ●      (topic-Y)
+..     |
+..   C ø⇠◔ C' (topix-X)
+..     | |
+..   B ● |    (topic-Y)
+..     | |
+..   A ø⇠◔ A' (topic-X)
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir B3
+  $ cd B3
+  $ setuprepos single-head
+  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 topic -r . topic-X
+  switching to topic topic-X
+  changed topic on 1 changesets to "topic-X"
+  $ hg strip --config extensions.strip= --hidden 'hidden()' --no-backup # clean old A0
+  $ hg topic topic-Y
+  $ mkcommit B0
+  active topic 'topic-Y' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg topic topic-X
+  $ mkcommit C0
+  $ hg topic topic-Y
+  $ mkcommit D0
+  $ hg push --new-branch
+  pushing to $TESTTMP/B3/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 4 changesets with 3 changes to 4 files (+1 heads)
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  $ hg up 0
+  0 files updated, 0 files merged, 4 files removed, 0 files unresolved
+  $ hg topic topic-X
+  marked working directory as topic: topic-X
+  $ mkcommit A1
+  $ mkcommit C1
+  $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"`
+  obsoleted 1 changesets
+  3 new orphan changesets
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  obsoleted 1 changesets
+  $ hg log -G --hidden
+  @  9f6e6381b9aa [default:topic-X] (draft): C1
+  |
+  o  c1340bef453e [default:topic-X] (draft): A1
+  |
+  | *  850d57e10bfe [default:topic-Y] (draft): D0
+  | |
+  | x  fcdd583577e8 [default:topic-X] (draft): C0
+  | |
+  | *  030eec7a0fe2 [default:topic-Y] (draft): B0
+  | |
+  | x  5a47a98cd8e5 [default:topic-X] (draft): A0
+  |/
+  o  1e4be0697311 [default] (public): root
+  
+
+Actual testing
+--------------
+
+  $ hg push -r 'desc("C1")'
+  pushing to $TESTTMP/B3/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
+  obsoleted 2 changesets
+  2 new orphan changesets
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-topic-B4.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,112 @@
+=========================================
+Testing single head enforcement: Case A-4
+=========================================
+
+A repository is set to only accept a single head per name (typically named
+branch). However, obsolete changesets can make this enforcement more
+complicated, because they can be kept visible by other changeset on other
+branch.
+
+This case is part of a series of tests checking this behavior.
+
+Category A: Involving obsolescence
+TestCase 4: Partial rewrite of a branch to dis-interleave it
+
+.. old-state:
+..
+.. * 2 changeset changeset on topic X
+.. * 2 changeset changeset on topic Y interleaved with the other one
+..
+.. new-state:
+..
+.. * 2 changeset changeset on topic Y at the same location
+.. * 1 changeset on topic X untouched (the lower one)
+.. * 1 changeset on topic X moved on the other one
+..
+.. expected-result:
+..
+.. * only one head detected
+..
+.. graph-summary:
+..
+..   D ●      (topic-Y)
+..     |
+..   C ø⇠◔ C' (topic-X)
+..     | |
+..   B ● |    (topic-Y)
+..     |/
+..   A ●      (topic-X)
+..     |
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir B4
+  $ cd B4
+  $ setuprepos single-head
+  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 topic -r . topic-X
+  switching to topic topic-X
+  changed topic on 1 changesets to "topic-X"
+  $ hg strip --config extensions.strip= --hidden 'hidden()' --no-backup # clean old A0
+  $ hg topic topic-Y
+  $ mkcommit B0
+  active topic 'topic-Y' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg topic topic-X
+  $ mkcommit C0
+  $ hg topic topic-Y
+  $ mkcommit D0
+  $ hg push --new-branch
+  pushing to $TESTTMP/B4/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 4 changesets with 3 changes to 4 files (+1 heads)
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  $ hg up 'desc("A0")'
+  switching to topic topic-X
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ hg topic topic-X
+  $ mkcommit C1
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  obsoleted 1 changesets
+  1 new orphan changesets
+  $ hg log -G --hidden
+  @  b98a8bd4ca39 [default:topic-X] (draft): C1
+  |
+  | *  850d57e10bfe [default:topic-Y] (draft): D0
+  | |
+  | x  fcdd583577e8 [default:topic-X] (draft): C0
+  | |
+  | o  030eec7a0fe2 [default:topic-Y] (draft): B0
+  |/
+  o  5a47a98cd8e5 [default:topic-X] (draft): A0
+  |
+  o  1e4be0697311 [default] (public): root
+  
+
+Actual testing
+--------------
+
+(force push to make sure we get the changeset on the remote)
+
+  $ hg push -r 'desc("C1")' --force
+  pushing to $TESTTMP/B4/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
+  obsoleted 1 changesets
+  1 new orphan changesets
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-topic-B5.t	Wed Apr 08 01:16:54 2020 +0800
@@ -0,0 +1,117 @@
+=========================================
+Testing single head enforcement: Case A-1
+=========================================
+
+A repository is set to only accept a single head per name (typically named
+branch). However, obsolete changesets can make this enforcement more
+complicated, because they can be kept visible by other changeset on other
+branch.
+
+This case is part of a series of tests checking this behavior.
+
+Category A: Involving obsolescence
+TestCase 1: obsoleting a merge reveal two heads
+
+.. old-state:
+..
+.. * 3 changeset changeset on topic X (2 on their own branch + 1 merge)
+.. * 1 changeset on topic Y (children of the merge)
+..
+.. new-state:
+..
+.. * 2 changeset changeset on topic X (merge is obsolete) each a head
+.. * 1 changeset on topic Y keeping the merge visible
+..
+.. expected-result:
+..
+.. * 2 heads detected (because we skip the merge).
+..
+.. graph-summary:
+..
+..   D ●      (topic-Y)
+..     |
+..   C ●      (topic-Y)
+..     |
+..   M ⊗      (topic-X)
+..     |\
+..   A ● ● B  (topic-X)
+..     |/
+..     ●
+
+  $ . $TESTDIR/testlib/topic_setup.sh
+  $ . $TESTDIR/testlib/push-checkheads-util.sh
+
+Test setup
+----------
+
+  $ mkdir B5
+  $ cd B5
+  $ setuprepos single-head
+  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 topic -r . topic-X
+  switching to topic topic-X
+  changed topic on 1 changesets to "topic-X"
+  $ hg strip --config extensions.strip= --hidden 'hidden()' --no-backup # clean old A0
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg topic topic-X
+  marked working directory as topic: topic-X
+  $ mkcommit B0
+  $ hg merge
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg ci -m 'M0'
+  $ hg topic topic-Y
+  $ mkcommit C0
+  active topic 'topic-Y' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg push --new-branch
+  pushing to $TESTTMP/B5/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 4 changesets with 2 changes to 3 files (+1 heads)
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  $ hg up 0
+  0 files updated, 0 files merged, 3 files removed, 0 files unresolved
+  $ mkcommit A1
+  $ mkcommit B1
+  $ hg debugobsolete `getid "desc(M0)"` --record-parents
+  obsoleted 1 changesets
+  1 new orphan changesets
+  $ hg log -G --hidden
+  @  262c8c798096 [default] (draft): B1
+  |
+  o  f6082bc4ffef [default] (draft): A1
+  |
+  | *  339fd31549ed [default:topic-Y] (draft): C0
+  | |
+  | x    33b3d4185449 [default:topic-X] (draft): M0
+  | |\
+  +---o  d3826ff42cf7 [default:topic-X] (draft): B0
+  | |
+  | o  5a47a98cd8e5 [default:topic-X] (draft): A0
+  |/
+  o  1e4be0697311 [default] (public): root
+  
+
+Actual testing
+--------------
+
+(force push to make sure we get the changeset on the remote)
+
+  $ hg push -r 'desc("C0")' --force
+  pushing to $TESTTMP/B5/server
+  searching for changes
+  no changes found
+  1 new obsolescence markers
+  transaction abort!
+  rollback completed
+  abort: rejecting multiple heads on branch "default:topic-X"
+  (2 heads: 5a47a98cd8e5 d3826ff42cf7)
+  [255]
--- a/tests/test-topic-flow-publish-bare.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-topic-flow-publish-bare.t	Wed Apr 08 01:16:54 2020 +0800
@@ -321,3 +321,43 @@
   $ hg phase --public -r 10 --config experimental.topic.allow-publish=no
   abort: rejecting publishing of changeset 858be9a8daaf and 1 others
   [255]
+
+Checking the option to prevent automatic publishing
+===================================================
+
+  $ hg up branchA
+  2 files updated, 0 files merged, 5 files removed, 0 files unresolved
+
+Trying to push changeset without topic (would publish them)
+
+  $ mkcommit c_aM0
+  $ hg debugcapabilities $TESTTMP/bare-branch-server | grep topics
+    ext-topics-publish=auto
+    topics
+  $ hg push --config experimental.auto-publish=abort -r .
+  pushing to $TESTTMP/bare-branch-server
+  abort: push would publish 1 changesets
+  (use --publish or adjust 'experimental.auto-publish' config)
+  [255]
+  $ hg push --config experimental.auto-publish=abort -r . --publish
+  pushing to $TESTTMP/bare-branch-server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+
+Pushing a changeset with topic (not publishing, no warning)
+
+  $ hg topic test-push-protection
+  marked working directory as topic: test-push-protection
+  $ mkcommit c_aL0
+  active topic 'test-push-protection' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg push --config experimental.auto-publish=abort -r .
+  pushing to $TESTTMP/bare-branch-server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
--- a/tests/test-topic-push-concurrent-on.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-topic-push-concurrent-on.t	Wed Apr 08 01:16:54 2020 +0800
@@ -5,7 +5,7 @@
   $ cat << EOF >> $HGRCPATH
   > [ui]
   > logtemplate = {rev} {branch} {get(namespaces, "topics")} {phase} {desc|firstline}\n
-  > ssh =python "$RUNTESTDIR/dummyssh"
+  > ssh = "$PYTHON" "$RUNTESTDIR/dummyssh"
   > [server]
   > concurrent-push-mode=check-related
   > EOF
--- a/tests/test-topic-push.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-topic-push.t	Wed Apr 08 01:16:54 2020 +0800
@@ -3,7 +3,7 @@
   $ cat << EOF >> $HGRCPATH
   > [ui]
   > logtemplate = {rev} {branch} {get(namespaces, "topics")} {phase} {desc|firstline}\n
-  > ssh =python "$RUNTESTDIR/dummyssh"
+  > ssh = "$PYTHON" "$RUNTESTDIR/dummyssh"
   > EOF
 
   $ hg init main
--- a/tests/test-wireproto-bundle1.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-wireproto-bundle1.t	Wed Apr 08 01:16:54 2020 +0800
@@ -3,7 +3,7 @@
   > [defaults]
   > amend=-d "0 0"
   > [ui]
-  > ssh=python "$RUNTESTDIR/dummyssh"
+  > ssh = "$PYTHON" "$RUNTESTDIR/dummyssh"
   > [phases]
   > publish = False
   > [extensions]
--- a/tests/test-wireproto.t	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/test-wireproto.t	Wed Apr 08 01:16:54 2020 +0800
@@ -6,7 +6,7 @@
   > obsmarkers-exchange-debug=true
   > bundle2-exp=true
   > [ui]
-  > ssh=python "$RUNTESTDIR/dummyssh"
+  > ssh = "$PYTHON" "$RUNTESTDIR/dummyssh"
   > [phases]
   > publish = False
   > [extensions]
--- a/tests/testlib/push-checkheads-util.sh	Fri Feb 28 23:18:48 2020 +0700
+++ b/tests/testlib/push-checkheads-util.sh	Wed Apr 08 01:16:54 2020 +0800
@@ -5,7 +5,7 @@
 cat >> $HGRCPATH <<EOF
 [ui]
 # simpler log output
-logtemplate ="{node|short} ({phase}): {desc}\n"
+logtemplate ="{node|short} ({phase}){if(topic, "[{topic}]")}: {desc}\n"
 
 [phases]
 # non publishing server
@@ -26,4 +26,14 @@
     mkcommit A0
     cd ..
     hg clone server client
+
+    if [ "$1" = "single-head" ]; then
+        echo >> "server/.hg/hgrc" "[experimental]"
+        echo >> "server/.hg/hgrc" "# enforce a single name per branch"
+        echo >> "server/.hg/hgrc" "single-head-per-branch = yes"
+
+        echo >> "client/.hg/hgrc" "[ui]"
+        echo >> "client/.hg/hgrc" "# simpler log output"
+        echo >> "client/.hg/hgrc" 'logtemplate = "{node|short} [{branch}{if(topic, ":{topic}")}] ({phase}): {desc}\\n"'
+    fi
 }