view hgext3rd/topic/server.py @ 6612:94bf2f307b75 stable

topic: check that topic namespace names are human-readable like topics
author Anton Shestakov <av6@dwimlabs.net>
date Mon, 11 Dec 2023 16:51:27 -0300
parents d511eba4cdb0
children c7083ba82d5f 23cad1a872b6
line wrap: on
line source

# topic/server.py - server specific behavior with topic
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from mercurial.i18n import _

from mercurial import (
    branchmap,
    error,
    extensions,
    localrepo,
    repoview,
    util,
    wireprototypes,
    wireprotov1peer,
    wireprotov1server,
)

from . import (
    common,
    compat,
    constants,
)

### Visibility restriction
#
# Serving draft changesets with topics to clients without topic extension can
# confuse them, because they won't see the topic label and will consider them
# normal anonymous heads. Instead we have the option to not serve changesets
# with topics to clients without topic support.
#
# To achieve this, we alter the behavior of the standard `heads` commands and
# introduce a new `heads` command that only clients with topic will know about.

# compat version of the wireprotocommand decorator, taken from evolve compat

FILTERNAME = b'served-no-topic'

def computeunservedtopic(repo, visibilityexceptions=None):
    assert not repo.changelog.filteredrevs
    filteredrevs = repoview.filtertable[b'served'](repo, visibilityexceptions).copy()
    mutable = repoview.filtertable[b'immutable'](repo, visibilityexceptions)
    consider = mutable - filteredrevs
    cl = repo.changelog
    extrafiltered = set()
    for r in consider:
        if cl.changelogrevision(r).extra.get(constants.extrakey, b''):
            extrafiltered.add(r)
    if extrafiltered:
        extrafiltered = set(repo.revs('%ld::%ld', extrafiltered, consider))
        filteredrevs = frozenset(filteredrevs | extrafiltered)
    return filteredrevs

def wrapheads(orig, repo, proto):
    """wrap head to hide topic^W draft changeset to old client"""
    hidetopics = repo.ui.configbool(b'experimental', b'topic.server-gate-topic-changesets')
    if common.hastopicext(repo) and hidetopics:
        h = repo.filtered(FILTERNAME).heads()
        return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + b'\n')
    return orig(repo, proto)

def topicheads(repo, proto):
    """Same as the normal wireprotocol command, but accessing with a different end point."""
    h = repo.heads()
    return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + b'\n')

def tns_heads(repo, proto, namespaces):
    """wireprotocol command to filter heads based on topic namespaces"""
    if not common.hastopicext(repo):
        return topicheads(repo, proto)

    namespaces = wireprototypes.decodelist(namespaces)
    if b'*' in namespaces:
        # pulling all topic namespaces, all changesets are visible
        h = repo.heads()
    else:
        # only changesets in the selected topic namespaces are visible
        h = []
        entries = compat.bcentries(repo.branchmaptns())
        for branch, nodes in compat.branchmapitems(entries):
            namedbranch, tns, topic = common.parsefqbn(branch)
            if tns == b'none' or tns in namespaces:
                h.extend(nodes)
    return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + b'\n')

def wireprotocaps(orig, repo, proto):
    """advertise the new topic specific `head` command for client with topic"""
    caps = orig(repo, proto)
    if common.hastopicext(repo) and repo.peer().capable(b'topics'):
        caps.append(b'_exttopics_heads')
        if repo.ui.configbool(b'phases', b'publish'):
            mode = b'all'
        elif repo.ui.configbool(b'experimental', b'topic.publish-bare-branch'):
            mode = b'auto'
        else:
            mode = b'none'
        caps.append(b'ext-topics-publish=%s' % mode)
        caps.append(b'ext-topics-tns-heads')
    return caps

def setupserver(ui):
    extensions.wrapfunction(wireprotov1server, 'heads', wrapheads)
    wireprotov1server.commands.pop(b'heads')
    wireprotov1server.wireprotocommand(b'heads', permission=b'pull')(wireprotov1server.heads)
    wireprotov1server.wireprotocommand(b'_exttopics_heads', permission=b'pull')(topicheads)
    wireprotov1server.wireprotocommand(b'tns_heads', b'namespaces', permission=b'pull')(tns_heads)
    extensions.wrapfunction(wireprotov1server, '_capabilities', wireprotocaps)

    if util.safehasattr(wireprotov1peer, 'future'):
        # hg <= 5.9 (c424ff4807e6)
        class tnspeer(wireprotov1peer.wirepeer):
            """ wirepeer that uses `future` class from before c424ff4807e6 """
            @wireprotov1peer.batchable
            def tns_heads(self, namespaces):
                f = wireprotov1peer.future()
                yield {b'namespaces': wireprototypes.encodelist(namespaces)}, f
                d = f.value
                try:
                    yield wireprototypes.decodelist(d[:-1])
                except ValueError:
                    self._abort(error.ResponseError(_(b"unexpected response:"), d))
    else:
        class tnspeer(wireprotov1peer.wirepeer):
            """ wirepeer that uses newer batchable scheme from c424ff4807e6 """
            @wireprotov1peer.batchable
            def tns_heads(self, namespaces):
                def decode(d):
                    try:
                        return wireprototypes.decodelist(d[:-1])
                    except ValueError:
                        self._abort(error.ResponseError(_(b"unexpected response:"), d))

                return {b'namespaces': wireprototypes.encodelist(namespaces)}, decode

    wireprotov1peer.wirepeer = tnspeer

    class topicpeerexecutor(wireprotov1peer.peerexecutor):

        def callcommand(self, command, args):
            if command == b'heads':
                if self._peer.capable(b'ext-topics-tns-heads'):
                    command = b'tns_heads'
                    if self._peer.ui.configbool(b'_internal', b'tns-explicit-target', False):
                        args[b'namespaces'] = [b'*']
                    else:
                        args[b'namespaces'] = self._peer.ui.configlist(b'experimental', b'tns-default-pull-namespaces', [b'*'])
                elif self._peer.capable(b'_exttopics_heads'):
                    command = b'_exttopics_heads'
                    if getattr(self._peer, '_exttopics_heads', None) is None:
                        self._peer._exttopics_heads = self._peer.heads
            s = super(topicpeerexecutor, self)
            return s.callcommand(command, args)

    wireprotov1peer.peerexecutor = topicpeerexecutor

    class topiccommandexecutor(localrepo.localcommandexecutor):
        def callcommand(self, command, args):
            if command == b'heads':
                if self._peer.capable(b'ext-topics-tns-heads'):
                    command = b'tns_heads'
                    if self._peer.ui.configbool(b'_internal', b'tns-explicit-target', False):
                        args[b'namespaces'] = [b'*']
                    else:
                        args[b'namespaces'] = self._peer.ui.configlist(b'experimental', b'tns-default-pull-namespaces', [b'*'])
            s = super(topiccommandexecutor, self)
            return s.callcommand(command, args)

    localrepo.localcommandexecutor = topiccommandexecutor

    if FILTERNAME not in repoview.filtertable:
        repoview.filtertable[FILTERNAME] = computeunservedtopic
        # hg <= 4.9 (caebe5e7f4bd)
        branchmap.subsettable[FILTERNAME] = b'immutable'
        branchmap.subsettable[b'served'] = FILTERNAME