hgext3rd/topic/discovery.py
author Pierre-Yves David <pierre-yves.david@octobus.net>
Wed, 28 Jun 2017 17:28:56 +0200
changeset 2676 10dedac0d82e
parent 2675 304232cc14b6
child 2695 b4824e169f18
permissions -rw-r--r--
topic: also insert the extra head check with using the new head checking This will get the protection in place in all cases.

from __future__ import absolute_import

import weakref

from mercurial.i18n import _
from mercurial import (
    bundle2,
    discovery,
    error,
    exchange,
    extensions,
    util,
    wireproto,
)

def _headssummary(orig, *args):
    # In mercurial < 4.2, we receive repo, remote and outgoing as arguments
    if len(args) == 3:
        pushoparg = False
        repo, remote, outgoing = args

    # In mercurial > 4.3, we receive the pushop as arguments
    elif len(args) == 1:
        pushoparg = True
        pushop = args[0]
        repo = pushop.repo.unfiltered()
        remote = pushop.remote
    else:
        msg = 'topic-ext _headssummary() takes 1 or 3 arguments (%d given)'
        raise TypeError(msg % len(args))

    publishing = ('phases' not in remote.listkeys('namespaces')
                  or bool(remote.listkeys('phases').get('publishing', False)))
    if publishing or not remote.capable('topics'):
        return orig(*args)

    class repocls(repo.__class__):
        def __getitem__(self, key):
            ctx = super(repocls, self).__getitem__(key)
            oldbranch = ctx.branch

            def branch():
                branch = oldbranch()
                topic = ctx.topic()
                if topic:
                    branch = "%s:%s" % (branch, topic)
                return branch

            ctx.branch = branch
            return ctx

    oldrepo = repo.__class__
    try:
        repo.__class__ = repocls
        unxx = repo.filtered('unfiltered-topic')
        repo.unfiltered = lambda: unxx
        if pushoparg:
            try:
                pushop.repo = repo
                summary = orig(pushop)
            finally:
                pushop.repo = repo
        else:
            summary = orig(repo, remote, outgoing)
        for key, value in summary.iteritems():
            if ':' in key: # This is a topic
                if value[0] is None and value[1]:
                    summary[key] = ([value[1][0]], ) + value[1:]
        return summary
    finally:
        if 'unfiltered' in vars(repo):
            del repo.unfiltered
        repo.__class__ = oldrepo

def wireprotobranchmap(orig, repo, proto):
    oldrepo = repo.__class__
    try:
        class repocls(repo.__class__):
            def branchmap(self):
                usetopic = not self.publishing()
                return super(repocls, self).branchmap(topic=usetopic)
        repo.__class__ = repocls
        return orig(repo, proto)
    finally:
        repo.__class__ = oldrepo


# 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
# head.
#
# Handle this by doing an extra check for new head creation server side
def _nbheads(repo):
    data = {}
    for b in repo.branchmap().iterbranches():
        if ':' in b[0]:
            continue
        data[b[0]] = len(b[1])
    return data

def handlecheckheads(orig, op, inpart):
    """This is used to check for new heads when publishing changeset"""
    orig(op, inpart)
    if op.repo.publishing():
        return
    tr = op.gettransaction()
    if tr.hookargs['source'] not in ('push', 'serve'): # not a push
        return
    tr._prepushheads = _nbheads(op.repo)
    reporef = weakref.ref(op.repo)
    oldvalidator = tr.validator

    def validator(tr):
        repo = reporef()
        if repo is not None:
            repo.invalidatecaches()
            finalheads = _nbheads(repo)
            for branch, oldnb in tr._prepushheads.iteritems():
                newnb = finalheads.pop(branch, 0)
                if oldnb < newnb:
                    msg = _('push create a new head on branch "%s"' % branch)
                    raise error.Abort(msg)
            for branch, newnb in finalheads.iteritems():
                if 1 < newnb:
                    msg = _('push create more than 1 head on new branch "%s"'
                            % branch)
                    raise error.Abort(msg)
        return oldvalidator(tr)
    tr.validator = validator
handlecheckheads.params = frozenset()

def _pushb2phases(orig, pushop, bundler):
    checktypes = ('check:heads', 'check:updated-heads')
    hascheck = any(p.type in checktypes for p in bundler._parts)
    if not hascheck and pushop.outdatedphases:
        exchange._pushb2ctxcheckheads(pushop, bundler)
    return orig(pushop, bundler)

def wireprotocaps(orig, repo, proto):
    caps = orig(repo, proto)
    if repo.peer().capable('topics'):
        caps.append('topics')
    return caps

def modsetup(ui):
    """run at uisetup time to install all destinations wrapping"""
    extensions.wrapfunction(discovery, '_headssummary', _headssummary)
    extensions.wrapfunction(wireproto, 'branchmap', wireprotobranchmap)
    extensions.wrapfunction(wireproto, '_capabilities', wireprotocaps)
    # we need a proper wrap b2 part stuff
    extensions.wrapfunction(bundle2, 'handlecheckheads', handlecheckheads)
    bundle2.handlecheckheads.params = frozenset()
    bundle2.parthandlermapping['check:heads'] = bundle2.handlecheckheads
    if util.safehasattr(bundle2, 'handlecheckupdatedheads'):
        # we still need a proper wrap b2 part stuff
        extensions.wrapfunction(bundle2, 'handlecheckupdatedheads', handlecheckheads)
        bundle2.handlecheckupdatedheads.params = frozenset()
        bundle2.parthandlermapping['check:updated-heads'] = bundle2.handlecheckupdatedheads
    extensions.wrapfunction(exchange, '_pushb2phases', _pushb2phases)
    exchange.b2partsgenmapping['phase'] = exchange._pushb2phases