branching: merge with stable
authorPierre-Yves David <pierre-yves.david@octobus.net>
Tue, 07 Apr 2020 19:33:40 +0200
changeset 5276 8431bb224862
parent 5263 bf37ba1c80ee (current diff)
parent 5274 fa1324e58fcf (diff)
child 5288 5cfec61b872b
branching: merge with stable
CHANGELOG
hgext3rd/evolve/__init__.py
hgext3rd/evolve/stablerangecache.py
hgext3rd/topic/__init__.py
hgext3rd/topic/discovery.py
hgext3rd/topic/flow.py
--- a/CHANGELOG	Sat Mar 21 15:04:18 2020 +0100
+++ b/CHANGELOG	Tue Apr 07 19:33:40 2020 +0200
@@ -6,13 +6,17 @@
 
   * compat: clean up old compatibility code
 
-9.3.0 - in progress
+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	Sat Mar 21 15:04:18 2020 +0100
+++ b/hgext3rd/evolve/__init__.py	Tue Apr 07 19:33:40 2020 +0200
@@ -290,6 +290,7 @@
     state,
     evolvecmd,
     exthelper,
+    headchecking,
     metadata,
     obscache,
     obsexchange,
@@ -344,6 +345,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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/evolve/headchecking.py	Tue Apr 07 19:33:40 2020 +0200
@@ -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/stablerangecache.py	Sat Mar 21 15:04:18 2020 +0100
+++ b/hgext3rd/evolve/stablerangecache.py	Tue Apr 07 19:33:40 2020 +0200
@@ -48,7 +48,7 @@
 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
--- a/hgext3rd/topic/__init__.py	Sat Mar 21 15:04:18 2020 +0100
+++ b/hgext3rd/topic/__init__.py	Tue Apr 07 19:33:40 2020 +0200
@@ -140,6 +140,7 @@
     commands,
     context,
     error,
+    exchange,
     extensions,
     hg,
     localrepo,
@@ -372,6 +373,8 @@
     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
+    extensions.wrapfunction(exchange, '_checkpublish', flow.replacecheckpublish)
 
     server.setupserver(ui)
 
@@ -393,6 +396,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	Sat Mar 21 15:04:18 2020 +0100
+++ b/hgext3rd/topic/discovery.py	Tue Apr 07 19:33:40 2020 +0200
@@ -133,6 +133,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
@@ -144,7 +184,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	Sat Mar 21 15:04:18 2020 +0100
+++ b/hgext3rd/topic/flow.py	Tue Apr 07 19:33:40 2020 +0200
@@ -118,3 +118,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-push-checkheads-mixed-branch-topic-G1.t	Tue Apr 07 19:33:40 2020 +0200
@@ -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 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)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,108 @@
+====================================
+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)"`
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  2 new orphan changesets
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,97 @@
+====================================
+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)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,98 @@
+====================================
+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)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,112 @@
+====================================
+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)"`
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  2 new orphan changesets
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,101 @@
+====================================
+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)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,93 @@
+====================================
+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)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,108 @@
+====================================
+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)"`
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  2 new orphan changesets
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,97 @@
+====================================
+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)"`
+  1 new obsolescence markers
+  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 ../..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-single-head-obsolescence-named-branch-A1.t	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,108 @@
+=========================================
+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)"`
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  3 new orphan changesets
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,107 @@
+
+=========================================
+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)"`
+  1 new obsolescence markers
+  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
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,116 @@
+=========================================
+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)"`
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  3 new orphan changesets
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,113 @@
+=========================================
+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)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -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
+  1 new obsolescence markers
+  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
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,114 @@
+=========================================
+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)"`
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  3 new orphan changesets
+  $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,115 @@
+
+=========================================
+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)"`
+  1 new obsolescence markers
+  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
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,116 @@
+=========================================
+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)"`
+  1 new obsolescence markers
+  obsoleted 1 changesets
+  3 new orphan changesets
+  $ hg debugobsolete `getid "desc(C0)" ` `getid "desc(C1)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -0,0 +1,113 @@
+=========================================
+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)"`
+  1 new obsolescence markers
+  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	Tue Apr 07 19:33:40 2020 +0200
@@ -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
+  1 new obsolescence markers
+  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
+  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	Sat Mar 21 15:04:18 2020 +0100
+++ b/tests/test-topic-flow-publish-bare.t	Tue Apr 07 19:33:40 2020 +0200
@@ -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/testlib/push-checkheads-util.sh	Sat Mar 21 15:04:18 2020 +0100
+++ b/tests/testlib/push-checkheads-util.sh	Tue Apr 07 19:33:40 2020 +0200
@@ -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
 }