# aclbranchpat.py - changeset access control with branch patterns
#
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

"""aclbranchpat is a hook to apply ACLs to patterns of branch names.

This serves as a stop-gap measure for:

issue6944: Globbed branch ACL
https://foss.heptapod.net/mercurial/mercurial-devel/-/issues/6944

Usage:
  [aclbranchpat.deny]
  ^vendor/openssl$ = !@security
  [aclbranchpat.allow]
  ^releng-.* = @releng
  ^vendor/.* = *
  ^default$ = *
"""

import re

from hgext import acl
from mercurial.i18n import _
from mercurial.utils import procutil
from mercurial import (
    error,
    pycompat,
    registrar,
    util,
)

urlreq = util.urlreq


configtable = {}
configitem = registrar.configitem(configtable)

# [aclbranchpat.allow]
# ^vendor/.* = *
configitem(
    b'aclbranchpat.allow',
    b'.*',
    default=None,
    generic=True,
)

# [aclbranchpat.deny]
# ^vendor/openssl$ = !@security
configitem(
    b'aclbranchpat.deny',
    b'.*',
    default=None,
    generic=True,
)


def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
    # Allow pretxnclose-phase.  See:
    # https://foss.heptapod.net/mercurial/mercurial-devel/-/issues/10036
    # https://foss.heptapod.net/mercurial/mercurial-devel/-/merge_requests/1666
    if hooktype != b'pretxnchangegroup' and \
       hooktype != b'pretxnclose-phase':
        raise error.Abort(
            _(b'Unsupported hook type %r') % pycompat.bytestr(hooktype)
        )

    user = None
    if source == b'serve' and 'url' in kwargs:
        url = kwargs['url'].split(b':')
        if url[0] == b'remote' and url[1].startswith(b'http'):
            user = urlreq.unquote(url[3])

    if user is None:
        user = procutil.getuser()

    ui.debug(b'aclbranchpat: checking access for user "%s"\n' % user)
    _txnhook(ui, repo, hooktype, node, source, user, **kwargs)


def _txnhook(ui, repo, hooktype, node, source, user, **kwargs):
    allowbranchpats = buildmatch(ui, user, b'aclbranchpat.allow')
    denybranchpats = buildmatch(ui, user, b'aclbranchpat.deny')

    def process_changectx(ctx):
        branch = ctx.branch()
        if denybranchpats and denybranchpats(branch):
            raise error.Abort(
                _(
                    b'aclbranchpat: user "%s" denied on branch "%s"'
                    b' (changeset "%s")'
                )
                % (user, branch, ctx)
            )
        if allowbranchpats and not allowbranchpats(branch):
            raise error.Abort(
                _(
                    b'aclbranchpat: user "%s" not allowed on branch "%s"'
                    b' (changeset "%s")'
                )
                % (user, branch, ctx)
            )
        ui.debug(
            b'aclbranchpat: branch access granted: "%s" on branch "%s"\n'
            % (ctx, branch)
        )

    if hooktype == b'pretxnclose-phase':
        process_changectx(repo[node])
    else:
        for rev in range(repo[node].rev(), len(repo)):
            process_changectx(repo[rev])


def buildmatch(ui, user, key):
    if not ui.has_section(key):
        ui.debug(b'acl: %s not enabled\n' % key)
        return None
    pats = [
        pat
        for pat, users in ui.configitems(key)
        if acl._usermatch(ui, user, users)
    ]
    ui.debug(
        b'aclbranchpat: %s enabled, %d entries for user %s\n'
        % (key, len(pats), user)
    )
    if not pats:
        return util.never
    r = re.compile(b'|'.join(b'(%s)' % (pat,) for pat in pats))
    return lambda b: r.search(b)
