changeset 6296:a2855aff1268

topic: call a different wire protocol command to get tns-aware branchmap To see if remote supports topic namespaces, we announce topics-namespaces capability and then check it. If it's supported, then we can use a different branchmap command that gives us branchmap keyed by fqbn. The support for old "branch:topic" format for the keys is continued through the already existing code.
author Anton Shestakov <av6@dwimlabs.net>
date Mon, 25 Jul 2022 12:30:12 +0400
parents e81b3516242f
children 0ef3c127cf28
files hgext3rd/topic/__init__.py hgext3rd/topic/common.py hgext3rd/topic/discovery.py tests/test-extension-isolation.t tests/test-topic-flow-publish-bare.t
diffstat 5 files changed, 105 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/hgext3rd/topic/__init__.py	Wed Jul 27 20:23:08 2022 +0400
+++ b/hgext3rd/topic/__init__.py	Mon Jul 25 12:30:12 2022 +0400
@@ -503,6 +503,7 @@
         def _restrictcapabilities(self, caps):
             caps = super(topicrepo, self)._restrictcapabilities(caps)
             caps.add(b'topics')
+            caps.add(b'topics-namespaces')
             if self.ui.configbool(b'phases', b'publish'):
                 mode = b'all'
             elif self.ui.configbool(b'experimental',
@@ -592,6 +593,15 @@
                         entries[key] = value
             return bm
 
+        def branchmaptns(self, topic=None):
+            """branchmap using fqbn as keys"""
+            if topic is None:
+                topic = getattr(self, '_autobranchmaptopic', False)
+            topicfilter = topicmap.topicfilter(self.filtername)
+            if not topic or topicfilter == self.filtername:
+                return super(topicrepo, self).branchmap()
+            return self.filtered(topicfilter).branchmap()
+
         def branchheads(self, branch=None, start=None, closed=False):
             if branch is None:
                 branch = self[None].branch()
@@ -613,6 +623,10 @@
                     def branchmap(self):
                         usetopic = not self._repo.publishing()
                         return self._repo.branchmap(topic=usetopic, convertbm=usetopic)
+
+                    def branchmaptns(self):
+                        usetopic = not self._repo.publishing()
+                        return self._repo.branchmaptns(topic=usetopic)
                 peer.__class__ = topicpeer
             return peer
 
--- a/hgext3rd/topic/common.py	Wed Jul 27 20:23:08 2022 +0400
+++ b/hgext3rd/topic/common.py	Mon Jul 25 12:30:12 2022 +0400
@@ -80,3 +80,26 @@
         result += namespace + b'/'
     result += topic
     return result
+
+def upgradeformat(branch):
+    """take branch and topic in ":" format and return fqbn in "//" format
+
+    This function can be used for transforming branchmap contents of peers that
+    don't support topic namespaces yet to work with peers with topic namespaces
+    support.
+
+    >>> upgradeformat(b'branch')
+    'branch'
+    >>> upgradeformat(b'branch:topic')
+    'branch//topic'
+    >>> upgradeformat(b'branch//')
+    'branch////'
+    >>> upgradeformat(b'branch//:topic')
+    'branch////topic'
+    """
+    if b':' not in branch:
+        # formatting anyway, because named branch could contain "//"
+        return formatfqbn(branch=branch)
+    # topic namespace cannot be extracted from ":" format
+    branch, topic = branch.split(b':', 1)
+    return formatfqbn(branch=branch, topic=topic)
--- a/hgext3rd/topic/discovery.py	Wed Jul 27 20:23:08 2022 +0400
+++ b/hgext3rd/topic/discovery.py	Mon Jul 25 12:30:12 2022 +0400
@@ -8,18 +8,22 @@
 from mercurial import (
     bundle2,
     discovery,
+    encoding,
     error,
     exchange,
     extensions,
     scmutil,
     util,
+    wireprototypes,
+    wireprotov1server,
 )
+from mercurial.wireprotov1peer import batchable, wirepeer
 from . import (
     common,
     compat,
 )
 
-from mercurial import wireprotov1server
+urlreq = util.urlreq
 
 @contextlib.contextmanager
 def override_context_branch(repo, publishedset=()):
@@ -102,7 +106,10 @@
 
     publishedset = ()
     remotebranchmap = None
-    origremotebranchmap = remote.branchmap
+    if remote.capable(b'topics-namespaces'):
+        origremotebranchmap = remote.branchmaptns
+    else:
+        origremotebranchmap = remote.branchmap
     publishednode = [c.node() for c in pushop.outdatedphases]
     publishedset = repo.revs(b'ancestors(%ln + %ln)',
                              publishednode,
@@ -113,19 +120,17 @@
     def remotebranchmap():
         # drop topic information from changeset about to be published
         result = collections.defaultdict(list)
-        for branch, heads in compat.branchmapitems(origremotebranchmap()):
-            if b':' not in branch:
-                result[branch].extend(heads)
-            else:
-                namedbranch, topic = branch.split(b':', 1)
-                # we lost namespace when converting to ":" format
-                branch = common.formatfqbn(namedbranch, topic=topic)
-                for h in heads:
-                    r = getrev(h)
-                    if r is not None and r in publishedset:
-                        result[namedbranch].append(h)
-                    else:
-                        result[branch].append(h)
+        items = list(compat.branchmapitems(origremotebranchmap()))
+        if not remote.capable(b'topics-namespaces'):
+            items = [(common.upgradeformat(branch), heads) for branch, heads in items]
+        for branch, heads in items:
+            namedbranch, tns, topic = common.parsefqbn(branch)
+            for h in heads:
+                r = getrev(h)
+                if r is not None and r in publishedset:
+                    result[namedbranch].append(h)
+                else:
+                    result[branch].append(h)
         for heads in result.values():
             heads.sort()
         return result
@@ -160,6 +165,12 @@
             def branchmap(self):
                 usetopic = not self.publishing()
                 return super(repocls, self).branchmap(topic=usetopic, convertbm=usetopic)
+
+            # Where is branchmaptns method, you might ask? The answer is that
+            # this repocls is only relevant when we're trying to use the old
+            # branchmap server command. If we use branchmaptns command that was
+            # introduced as a part of topic namespaces support, then this
+            # repocls shouldn't be used at all.
         unfi.__class__ = repocls
         if repo.filtername is not None:
             repo = unfi.filtered(repo.filtername)
@@ -169,6 +180,19 @@
     finally:
         unfi.__class__ = oldrepocls
 
+def wireprotobranchmaptns(repo, proto):
+    """copied from wireprotov1server.branchmap()"""
+    if not common.hastopicext(repo):
+        return wireprotov1server.branchmap(repo, proto)
+    branchmaptns = repo.branchmaptns()
+    heads = []
+    for branch, nodes in branchmaptns.items():
+        branchname = urlreq.quote(encoding.fromlocal(branch))
+        branchnodes = wireprototypes.encodelist(nodes)
+        heads.append(b'%s %s' % (branchname, branchnodes))
+
+    return wireprototypes.bytesresponse(b'\n'.join(heads))
+
 def _get_branch_name(ctx):
     # make it easy for extension with the branch logic there
     return ctx.branch()
@@ -301,8 +325,29 @@
     caps = orig(repo, proto)
     if common.hastopicext(repo) and repo.peer().capable(b'topics'):
         caps.append(b'topics')
+        caps.append(b'topics-namespaces')
     return caps
 
+def branchmaptns(self):
+    """copied from wirepeer.branchmap()
+
+    Client-side command for communicating with a peer repository. Calls wire
+    protocol command of the same name (thanks to the batchable decorator).
+    """
+    def decode(d):
+        try:
+            branchmap = {}
+            for branchpart in d.splitlines():
+                branchname, branchheads = branchpart.split(b' ', 1)
+                branchname = encoding.tolocal(urlreq.unquote(branchname))
+                branchheads = wireprototypes.decodelist(branchheads)
+                branchmap[branchname] = branchheads
+            return branchmap
+        except TypeError:
+            self._abort(error.ResponseError(_(b"unexpected response:"), d))
+
+    return {}, decode
+
 def modsetup(ui):
     """run at uisetup time to install all destinations wrapping"""
     extensions.wrapfunction(discovery, '_headssummary', _headssummary)
@@ -310,6 +355,8 @@
     wireprotov1server.commands.pop(b'branchmap')
     wireprotov1server.wireprotocommand(b'branchmap', permission=b'pull')(wireprotov1server.branchmap)
     extensions.wrapfunction(wireprotov1server, '_capabilities', wireprotocaps)
+    wirepeer.branchmaptns = batchable(branchmaptns)
+    wireprotov1server.wireprotocommand(b'branchmaptns', permission=b'pull')(wireprotobranchmaptns)
     # we need a proper wrap b2 part stuff
     extensions.wrapfunction(bundle2, 'handlecheckheads', handlecheckheads)
     bundle2.handlecheckheads.params = frozenset()
--- a/tests/test-extension-isolation.t	Wed Jul 27 20:23:08 2022 +0400
+++ b/tests/test-extension-isolation.t	Mon Jul 25 12:30:12 2022 +0400
@@ -134,6 +134,7 @@
     _exttopics_heads
     ext-topics-publish=all
     topics
+    topics-namespaces
   $ hg debugcapabilities http://$LOCALIP:$HGPORT/repo-no-ext | egrep 'topics|evoext'
   [1]
 
@@ -148,6 +149,7 @@
     _exttopics_heads
     ext-topics-publish=all
     topics
+    topics-namespaces
   $ hg debugcapabilities http://$LOCALIP:$HGPORT/repo-evo | egrep 'topics|evoext'
     _evoext_getbundle_obscommon
     _evoext_obshashrange_v1
@@ -155,6 +157,7 @@
     _exttopics_heads
     ext-topics-publish=all
     topics
+    topics-namespaces
   $ hg debugcapabilities http://$LOCALIP:$HGPORT/repo-evo | egrep 'topics|evoext'
     _evoext_getbundle_obscommon
     _evoext_obshashrange_v1
--- a/tests/test-topic-flow-publish-bare.t	Wed Jul 27 20:23:08 2022 +0400
+++ b/tests/test-topic-flow-publish-bare.t	Mon Jul 25 12:30:12 2022 +0400
@@ -350,16 +350,19 @@
   $ hg debugcapabilities $TESTTMP/bare-branch-server | grep topics
     ext-topics-publish=auto
     topics
+    topics-namespaces
   $ hg debugcapabilities ssh://user@dummy/bare-branch-server | grep topics
     _exttopics_heads
     ext-topics-publish=auto
     topics
+    topics-namespaces
   $ hg serve -R ../bare-branch-server -p $HGPORT -d --pid-file hg.pid
   $ cat hg.pid >> $DAEMON_PIDS
   $ hg debugcapabilities http://localhost:$HGPORT | grep topics
     _exttopics_heads
     ext-topics-publish=auto
     topics
+    topics-namespaces
   $ killdaemons.py
 
 Trying to push changeset without topic (would publish them)