#!/usr/bin/python3
# Copyright (C) 2019 Jelmer Vernooij <jelmer@jelmer.uk>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""Manage VCS repositories."""

import aiozipkin
import asyncpg.pool
import asyncio
from io import BytesIO
import logging
import os
from typing import Optional
import warnings

from aiohttp import web
from aiohttp.web_middlewares import normalize_path_middleware
from aiohttp_openmetrics import metrics_middleware, metrics
from http.client import parse_headers  # type: ignore

from breezy import urlutils
from breezy.controldir import ControlDir, format_registry
from breezy.errors import NotBranchError
from breezy.bzr.smart import medium
from breezy.transport import get_transport_from_url
from dulwich.objects import valid_hexsha
from dulwich.web import HTTPGitApplication
from dulwich.protocol import ReceivableProtocol
from dulwich.server import (
    DEFAULT_HANDLERS as DULWICH_SERVICE_HANDLERS,
    DictBackend,
)
from . import (
    state,
)

from .compat import to_thread
from .config import read_config, get_campaign_config
from .site import is_worker, iter_accept, env as site_env
from .vcs import (
    VcsManager,
    LocalVcsManager,
)


GIT_BACKEND_CHUNK_SIZE = 4096


async def bzr_diff_helper(repo, old_revid, new_revid, path=None):
    if path:
        raise NotImplementedError
    args = [
        sys.executable,
        '-m',
        'breezy',
        "diff",
        '-rrevid:%s..revid:%s' % (
            old_revid.decode(),
            new_revid.decode(),
        ),
        urlutils.join(repo.user_url, path or '')
    ]

    p = await asyncio.create_subprocess_exec(
        *args,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        stdin=asyncio.subprocess.PIPE,
    )

    # TODO(jelmer): Stream this
    try:
        (stdout, stderr) = await asyncio.wait_for(p.communicate(b""), 30.0)
    except asyncio.TimeoutError:
        raise web.HTTPRequestTimeout(text='diff generation timed out')

    if p.returncode != 3:
        return web.Response(body=stdout, content_type="text/x-diff")
    logging.warning('bzr diff failed: %s', stderr.decode())
    raise web.HTTPInternalServerError(text='bzr diff failed: %s' % stderr)


async def bzr_diff_request(request):
    package = request.match_info["package"]
    old_revid = request.query.get('old')
    path = request.query.get('path')
    if old_revid is not None:
        old_revid = old_revid.encode('utf-8')
    new_revid = request.query.get('new')
    if new_revid is not None:
        new_revid = new_revid.encode('utf-8')
    try:
        repo = request.app.vcs_manager.get_repository(package)
    except NotBranchError:
        repo = None
    if repo is None:
        raise web.HTTPServiceUnavailable(
            text="Local VCS repository for %s temporarily inaccessible" %
            package)
    return await bzr_diff_helper(repo, old_revid, new_revid, path)


async def bzr_revision_info_request(request):
    package = request.match_info["package"]
    old_revid = request.query.get('old')
    if old_revid is not None:
        old_revid = old_revid.encode('utf-8')
    new_revid = request.query.get('new')
    if new_revid is not None:
        new_revid = new_revid.encode('utf-8')
    try:
        repo = request.app.vcs_manager.get_repository(package)
    except NotBranchError:
        repo = None
    if repo is None:
        raise web.HTTPServiceUnavailable(
            text="Local VCS repository for %s temporarily inaccessible" %
            package)
    ret = []
    with repo.lock_read():
        graph = repo.get_graph()
        for rev in repo.iter_revisions(graph.iter_lefthand_ancestry(new_revid, [old_revid])):
            ret.append({
                'revision-id': rev.revision_id.decode('utf-8'),
                'link': None,
                'message': rev.description})
    return web.json_response(ret)


async def git_diff_request(request):
    package = request.match_info["package"]
    old_sha = request.query.get('old')
    if old_sha is not None:
        old_sha = old_sha.encode('utf-8')
    new_sha = request.query.get('new')
    if new_sha is not None:
        new_sha = new_sha.encode('utf-8')
    path = request.query.get('path')
    try:
        repo = request.app.vcs_manager.get_repository(package)
    except NotBranchError:
        repo = None
    if repo is None:
        raise web.HTTPServiceUnavailable(
            text="Local VCS repository for %s temporarily inaccessible" %
            package)
    if not valid_hexsha(old_sha) or not valid_hexsha(new_sha):
        raise web.HTTPBadRequest(text='invalid shas specified')
    return await git_diff_helper(repo, old_sha, new_sha, path)


async def git_revision_info_request(request):
    from dulwich.errors import MissingCommitError
    package = request.match_info["package"]
    old_sha = request.query.get('old')
    if old_sha is not None:
        old_sha = old_sha.encode('utf-8')
    new_sha = request.query.get('new')
    if new_sha is not None:
        new_sha = new_sha.encode('utf-8')
    try:
        repo = request.app.vcs_manager.get_repository(package)
    except NotBranchError:
        repo = None
    if repo is None:
        raise web.HTTPServiceUnavailable(
            text="Local VCS repository for %s temporarily inaccessible" %
            package)
    if not valid_hexsha(old_sha) or not valid_hexsha(new_sha):
        raise web.HTTPBadRequest(text='invalid shas specified')
    ret = []
    try:
        walker = repo._git.get_walker(include=[new_sha], exclude=[old_sha])
    except MissingCommitError:
        return web.json_response({}, status=404)
    for entry in walker:
        ret.append({
            'commit-id': entry.commit.id.decode('ascii'),
            'revision-id': 'git-v1:' + entry.commit.id.decode('ascii'),
            'link': '/git/%s/commit/%s/' % (package, entry.commit.id.decode('ascii')),
            'message': entry.commit.message.decode('utf-8', 'replace')})
    return web.json_response(ret)


async def git_diff_helper(repo, old_sha, new_sha, path=None):
    args = [
        "git",
        "diff",
        old_sha, new_sha
    ]
    if path:
        args.extend(['-', path])

    p = await asyncio.create_subprocess_exec(
        *args,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        stdin=asyncio.subprocess.PIPE,
        cwd=repo.user_transport.local_abspath('.'),
    )

    # TODO(jelmer): Stream this
    try:
        (stdout, stderr) = await asyncio.wait_for(p.communicate(b""), 30.0)
    except asyncio.TimeoutError:
        raise web.HTTPRequestTimeout(text='diff generation timed out')

    if p.returncode == 0:
        return web.Response(body=stdout, content_type="text/x-diff")
    logging.warning('git diff failed: %s', stderr.decode())
    raise web.HTTPInternalServerError(text='git diff failed: %s' % stderr)


async def diff_request(request):
    run_id = request.match_info["run_id"]
    path = request.query.get('path')
    role = request.match_info["role"]
    span = aiozipkin.request_span(request)
    with span.new_child('sql:run'):
        async with request.app.db.acquire() as conn:
            row = await conn.fetchrow("""\
SELECT
  package,
  new_result_branch.base_revision AS base_revision,
  new_result_branch.revision AS revision
FROM run
LEFT JOIN new_result_branch ON new_result_branch.run_id = run.id
WHERE id = $1 AND new_result_branch.role = $2
""", run_id, role)
            if not row:
                raise web.HTTPNotFound(text="No such run: %r" % run_id)
    try:
        repo = request.app.vcs_manager.get_repository(row['package'])
    except NotBranchError:
        repo = None
    if repo is None:
        raise web.HTTPServiceUnavailable(
            text="Local VCS repository for %s temporarily inaccessible" %
            row['package'])
    if row['revision'] is None:
        raise web.HTTPNotFound(text="No branch with role %s" % role)
    old_revid = row['base_revision'].encode('utf-8')
    new_revid = row['revision'].encode('utf-8')

    if (hasattr(repo, '_git') and
            old_revid.startswith(b'git-v1:') and
            new_revid.startswith(b'git-v1:')):

        with span.new_child('subprocess:git-diff'):
            return await git_diff_helper(
                repo, old_revid.decode()[len('git-v1:'):],
                new_revid.decode()[len('git-v1:'):],
                path)
    else:
        with span.new_child('subprocess:brz-diff'):
            return await bzr_diff_helper(repo, old_revid, new_revid, path)


async def _git_open_repo(vcs_manager, db, package):
    repo = vcs_manager.get_repository(package, "git")

    if repo is None:
        async with db.acquire() as conn:
            if not await package_exists(conn, package):
                raise web.HTTPNotFound(text='no such package: %s' % package)
        controldir = ControlDir.create(
            vcs_manager.get_repository_url(package, "git"),
            format=format_registry.get("git-bare")(),
        )
        logging.info(
            "Created missing git repository for %s at %s", package, controldir.user_url
        )
        return controldir.open_repository()
    else:
        return repo


def _git_check_service(service: str, allow_writes: bool = False):
    if service == "git-upload-pack":
        return

    if service == "git-receive-pack":
        if not allow_writes:
            raise web.HTTPUnauthorized(
                text="git-receive-pack requires login",
                headers={"WWW-Authenticate": 'Basic Realm="Debian Janitor"'},
            )
        return

    raise web.HTTPForbidden(text="Unsupported service %s" % service)


async def handle_klaus(request):
    package = request.match_info["package"]

    span = aiozipkin.request_span(request)
    with span.new_child('open-repo'):
        repo = await _git_open_repo(request.app.vcs_manager, request.app.db, package)

    from klaus import views, utils, KLAUS_VERSION
    from flask import Flask
    from klaus.repo import FancyRepo

    class Klaus(Flask):
        def __init__(self, package, repo):
            super(Klaus, self).__init__("klaus")
            self.package = package
            self.valid_repos = {package: FancyRepo(repo._transport.local_abspath("."))}

        def should_use_ctags(self, git_repo, git_commit):
            return False

        def create_jinja_environment(self):
            """Called by Flask.__init__"""
            env = super(Klaus, self).create_jinja_environment()
            for func in [
                "force_unicode",
                "timesince",
                "shorten_sha1",
                "shorten_message",
                "extract_author_name",
                "formattimestamp",
            ]:
                env.filters[func] = getattr(utils, func)

            env.globals["KLAUS_VERSION"] = KLAUS_VERSION
            env.globals["USE_SMARTHTTP"] = False
            env.globals["SITE_NAME"] = "Package list"
            return env

    app = Klaus(package, repo)

    for endpoint, rule in [
        ("blob", "/blob/"),
        ("blob", "/blob/<rev>/<path:path>"),
        ("blame", "/blame/"),
        ("blame", "/blame/<rev>/<path:path>"),
        ("raw", "/raw/<path:path>/"),
        ("raw", "/raw/<rev>/<path:path>"),
        ("submodule", "/submodule/<rev>/"),
        ("submodule", "/submodule/<rev>/<path:path>"),
        ("commit", "/commit/<path:rev>/"),
        ("patch", "/commit/<path:rev>.diff"),
        ("patch", "/commit/<path:rev>.patch"),
        ("index", "/"),
        ("index", "/<path:rev>"),
        ("history", "/tree/<rev>/"),
        ("history", "/tree/<rev>/<path:path>"),
        ("download", "/tarball/<path:rev>/"),
        ("repo_list", "/.."),
    ]:
        app.add_url_rule(
            rule, view_func=getattr(views, endpoint), defaults={"repo": package}
        )

    from aiohttp_wsgi import WSGIHandler

    wsgi_handler = WSGIHandler(app)

    return await wsgi_handler(request)


async def handle_set_git_remote(request):
    package = request.match_info["package"]
    remote = request.match_info["remote"]

    span = aiozipkin.request_span(request)
    with span.new_child('open-repo'):
        repo = await _git_open_repo(request.app.vcs_manager, request.app.db, package)

    post = await request.post()
    r = repo._git
    c = r.get_config()
    section = ("remote", remote)
    c.set(section, "url", post["url"])
    c.set(section, "fetch", "+refs/heads/*:refs/remotes/%s/*" % remote)
    b = BytesIO()
    c.write_to_file(b)
    r._controltransport.put_bytes("config", b.getvalue())

    # TODO(jelmer): Run 'git fetch $remote'?

    return web.Response()


async def handle_set_bzr_remote(request):
    package = request.match_info["package"]
    remote = request.match_info["remote"]
    post = await request.post()

    local_branch = request.app.vcs_manager.get_branch(package, remote)

    local_branch.set_parent(post["url"])

    # TODO(jelmer): Run 'bzr pull'?

    return web.Response()


async def cgit_backend(request):
    package = request.match_info["package"]
    subpath = request.match_info["subpath"]
    span = aiozipkin.request_span(request)

    allow_writes = request.app.allow_writes
    if allow_writes is None:
        allow_writes = await is_worker(request.app.db, request)
    service = request.query.get("service")
    if service is not None:
        _git_check_service(service, allow_writes)

    with span.new_child('open-repo'):
        repo = await _git_open_repo(request.app.vcs_manager, request.app.db, package)

    args = ["/usr/bin/git"]
    if allow_writes:
        args.extend(["-c", "http.receivepack=1"])
    args.append("http-backend")
    local_path = repo.user_transport.local_abspath(".")
    full_path = os.path.join(local_path, subpath.lstrip('/'))
    env = {
        "GIT_HTTP_EXPORT_ALL": "true",
        "REQUEST_METHOD": request.method,
        "REMOTE_ADDR": request.remote,
        "CONTENT_TYPE": request.content_type,
        "PATH_TRANSLATED": full_path,
        "QUERY_STRING": request.query_string,
        # REMOTE_USER is not set
    }

    if request.content_type is not None:
        env['CONTENT_TYPE'] = request.content_type

    for key, value in request.headers.items():
        env["HTTP_" + key.replace("-", "_").upper()] = value

    for name in ["HTTP_CONTENT_ENCODING", "HTTP_CONTENT_LENGTH", "HTTP_TRANSFER_ENCODING"]:
        try:
            del env[name]
        except KeyError:
            pass

    p = await asyncio.create_subprocess_exec(
        *args,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        env=env,
        stdin=asyncio.subprocess.PIPE,
    )

    async def feed_stdin(stream):
        async for chunk in request.content.iter_any():
            stream.write(chunk)
            await stream.drain()
        stream.close()

    async def read_stderr(stream):
        line = await stream.readline()
        while line:
            logging.warning("git: %s", line.decode().rstrip('\n'))
            line = await stream.readline()

    async def read_stdout(stream):
        b = BytesIO()
        line = await stream.readline()
        while line != b'\r\n':
            b.write(line)
            line = await stream.readline()
        b.seek(0)
        headers = parse_headers(b)
        status = headers.get("Status")
        if status:
            del headers["Status"]
            (status_code, status_reason) = status.split(" ", 1)
            status_code = int(status_code)
            status_reason = status_reason
        else:
            status_code = 200
            status_reason = "OK"

        if 'Content-Length' in headers:
            content_length = int(headers['Content-Length'])
            return web.Response(
                headers=headers, status=status_code, reason=status_reason,
                body=await p.stdout.read(content_length))
        else:
            response = web.StreamResponse(
                headers=headers,
                status=status_code, reason=status_reason,
            )

            if tuple(request.version) == (1, 1):
                response.enable_chunked_encoding()

            await response.prepare(request)

            chunk = await p.stdout.read(GIT_BACKEND_CHUNK_SIZE)
            while chunk:
                await response.write(chunk)
                chunk = await p.stdout.read(GIT_BACKEND_CHUNK_SIZE)

            await response.write_eof()

            return response

    with span.new_child('git-backend'):
        try:
            unused_stderr, response, unused_stdin = await asyncio.gather(*[
                read_stderr(p.stderr), read_stdout(p.stdout),
                feed_stdin(p.stdin),
                ], return_exceptions=False)
        except asyncio.CancelledError:
            p.terminate()
            await p.wait()
            raise

    return response


async def dulwich_refs(request):
    package = request.match_info["package"]

    allow_writes = request.app.allow_writes
    if allow_writes is None:
        allow_writes = await is_worker(request.app.db, request)

    span = aiozipkin.request_span(request)
    with span.new_child('open-repo'):
        repo = await _git_open_repo(request.app.vcs_manager, request.app.db, package)
    r = repo._git

    service = request.query.get("service")
    _git_check_service(service, allow_writes)

    headers = {
        "Expires": "Fri, 01 Jan 1980 00:00:00 GMT",
        "Pragma": "no-cache",
        "Cache-Control": "no-cache, max-age=0, must-revalidate",
    }

    handler_cls = DULWICH_SERVICE_HANDLERS[service.encode("ascii")]

    response = web.StreamResponse(status=200, headers=headers)
    response.content_type = "application/x-%s-advertisement" % service

    await response.prepare(request)

    out = BytesIO()
    proto = ReceivableProtocol(BytesIO().read, out.write)
    handler = handler_cls(
        DictBackend({".": r}), ["."], proto, stateless_rpc=True, advertise_refs=True
    )
    handler.proto.write_pkt_line(b"# service=" + service.encode("ascii") + b"\n")
    handler.proto.write_pkt_line(None)

    await to_thread(handler.handle)

    await response.write(out.getvalue())

    await response.write_eof()

    return response


async def dulwich_service(request):
    package = request.match_info["package"]
    service = request.match_info["service"]

    allow_writes = request.app.allow_writes
    if allow_writes is None:
        allow_writes = await is_worker(request.app.db, request)

    span = aiozipkin.request_span(request)
    with span.new_child('open-repo'):
        repo = await _git_open_repo(request.app.vcs_manager, request.app.db, package)

    _git_check_service(service, allow_writes)

    headers = {
        "Expires": "Fri, 01 Jan 1980 00:00:00 GMT",
        "Pragma": "no-cache",
        "Cache-Control": "no-cache, max-age=0, must-revalidate",
    }
    handler_cls = DULWICH_SERVICE_HANDLERS[service.encode("ascii")]

    response = web.StreamResponse(status=200, headers=headers)
    response.content_type = "application/x-%s-result" % service

    await response.prepare(request)

    inf = BytesIO(await request.read())
    outf = BytesIO()

    def handle():
        r = repo._git
        proto = ReceivableProtocol(inf.read, outf.write)
        handler = handler_cls(DictBackend({".": r}), ["."], proto, stateless_rpc=True)
        handler.handle()

    await to_thread(handle)

    await response.write(outf.getvalue())

    await response.write_eof()
    return response


async def package_exists(conn, package):
    return bool(await conn.fetchrow("SELECT 1 FROM package WHERE name = $1", package))


async def _bzr_open_repo(vcs_manager, db, package):
    async with db.acquire() as conn:
        if not await package_exists(conn, package):
            raise web.HTTPNotFound(text='no such package: %s' % package)
    repo = vcs_manager.get_repository(package, "bzr")
    if repo is None:
        controldir = ControlDir.create(
            vcs_manager.get_repository_url(package, "bzr")
        )
        repo = controldir.create_repository(shared=True)
    return repo


async def bzr_backend(request):
    vcs_manager = request.app.vcs_manager
    package = request.match_info["package"]
    branch_name = request.match_info.get("branch")
    repo = await _bzr_open_repo(vcs_manager, request.app.db, package)
    if branch_name:
        try:
            get_campaign_config(request.app.config, branch_name)
        except KeyError:
            raise web.HTTPNotFound(text='no such suite: %s' % branch_name)
        transport = repo.user_transport.clone(branch_name)
    else:
        transport = repo.user_transport
    transport.ensure_base()
    allow_writes = request.app.allow_writes
    if allow_writes is None:
        allow_writes = await is_worker(request.app.db, request)
    if allow_writes:
        backing_transport = transport
    else:
        backing_transport = get_transport_from_url("readonly+" + transport.base)
    out_buffer = BytesIO()
    request_data_bytes = await request.read()

    protocol_factory, unused_bytes = medium._get_protocol_factory_for_bytes(
        request_data_bytes
    )

    smart_protocol_request = protocol_factory(
        transport, out_buffer.write, ".", backing_transport
    )
    smart_protocol_request.accept_bytes(unused_bytes)
    if smart_protocol_request.next_read_size() != 0:
        # The request appears to be incomplete, or perhaps it's just a
        # newer version we don't understand.  Regardless, all we can do
        # is return an error response in the format of our version of the
        # protocol.
        response_data = b"error\x01incomplete request\n"
    else:
        response_data = out_buffer.getvalue()

    response = web.StreamResponse(status=200)
    response.content_type = "application/octet-stream"

    await response.prepare(request)

    await response.write(response_data)

    await response.write_eof()

    return response


async def handle_repo_list(request):
    vcs = request.match_info["vcs"]
    span = aiozipkin.request_span(request)
    with span.new_child('list-repositories'):
        names = list(request.app.vcs_manager.list_repositories(vcs))
        names.sort()
    for accept in iter_accept(request):
        if accept in ('application/json', ):
            return web.json_response(names)
        elif accept in ('text/plain', ):
            return web.Response(
                text=''.join([line + '\n' for line in names]),
                content_type='text/plain')
        elif accept in ('text/html', ):
            template = site_env.get_template('repo-list.html')
            text = await template.render_async(vcs=vcs, repositories=names)
            return web.Response(text=text, content_type='text/html')
    return web.json_response(names)


async def handle_health(request):
    return web.Response(text='ok')


async def handle_index(request):
    return web.Response(text='')


async def create_web_app(
    listen_addr: str,
    port: int,
    vcs_manager: VcsManager,
    db: asyncpg.pool.Pool,
    config,
    dulwich_server: bool = False,
    client_max_size: Optional[int] = None,
):
    trailing_slash_redirect = normalize_path_middleware(append_slash=True)
    app = web.Application(
        middlewares=[trailing_slash_redirect], client_max_size=(client_max_size or 0)
    )
    app.vcs_manager = vcs_manager
    app.db = db
    app.allow_writes = True
    app.config = config
    public_app = web.Application(
        middlewares=[trailing_slash_redirect], client_max_size=(client_max_size or 0)
    )
    public_app.vcs_manager = vcs_manager
    public_app.db = db
    public_app.allow_writes = None
    public_app.config = config
    public_app.middlewares.insert(0, metrics_middleware)
    app.middlewares.insert(0, metrics_middleware)
    app.router.add_get("/metrics", metrics, name="metrics")
    app.router.add_get("/diff/{run_id}/{role}", diff_request, name='diff')
    if dulwich_server:
        app.router.add_post(
            "/git/{package}/{service:git-receive-pack|git-upload-pack}",
            dulwich_service,
            name='dulwich-service'
        )
        public_app.router.add_post(
            "/git/{package}/{service:git-receive-pack|git-upload-pack}",
            dulwich_service,
            name='dulwich-service-public'
        )
        app.router.add_get(
            "/git/{package}/info/refs",
            dulwich_refs, name='dulwich-refs')
        public_app.router.add_get(
            "/git/{package}/info/refs",
            dulwich_refs, name='dulwich-refs-public')
    else:
        for (method, regex), fn in HTTPGitApplication.services.items():
            app.router.add_route(
                method, "/git/{package}{subpath:" + regex.pattern + "}",
                cgit_backend,
            )
            public_app.router.add_route(
                method, "/git/{package}{subpath:" + regex.pattern + "}",
                cgit_backend,
            )


    app.router.add_get("/", handle_index, name="index")
    public_app.router.add_get("/", handle_index, name="index")
    public_app.router.add_get("/{vcs:git|bzr}/", handle_repo_list, name='public-repo-list')
    app.router.add_get("/{vcs:git|bzr}/", handle_repo_list, name='repo-list')
    app.router.add_get("/health", handle_health, name='health')
    public_app.router.add_get("/health", handle_health, name='health')
    app.router.add_get("/git/{package}/diff", git_diff_request, name='git-diff')
    app.router.add_get("/git/{package}/revision-info", git_revision_info_request, name='git-revision-info')
    public_app.router.add_get("/git/{package}/{path_info:.*}", handle_klaus, name='klaus')
    app.router.add_get("/bzr/{package}/diff", bzr_diff_request, name='bzr-diff')
    app.router.add_get("/bzr/{package}/revision-info", bzr_revision_info_request, name='bzr-revision-info')
    public_app.router.add_post("/bzr/{package}/{branch}/.bzr/smart", bzr_backend, name='bzr-branch-public')
    public_app.router.add_post("/bzr/{package}/.bzr/smart", bzr_backend, name='bzr-repo-public')
    app.router.add_post("/bzr/{package}/.bzr/smart", bzr_backend, name='bzr-repo')
    app.router.add_post("/bzr/{package}/{branch}/.bzr/smart", bzr_backend, name='bzr-branch')
    app.router.add_post("/git/{package}/remotes/{remote}", handle_set_git_remote, name='git-remote')
    app.router.add_post("/bzr/{package}/remotes/{remote}", handle_set_bzr_remote, name='bzr-remote')
    endpoint = aiozipkin.create_endpoint("janitor.vcs_store", ipv4=listen_addr, port=port)
    if config.zipkin_address:
        tracer = await aiozipkin.create(config.zipkin_address, endpoint, sample_rate=1.0)
    else:
        tracer = await aiozipkin.create_custom(endpoint)
    aiozipkin.setup(app, tracer)
    aiozipkin.setup(public_app, tracer)
    return app, public_app


async def main(argv=None):
    import argparse

    parser = argparse.ArgumentParser(prog="janitor.vcs_store")
    parser.add_argument(
        "--prometheus", type=str, help="Prometheus push gateway to export to."
    )
    parser.add_argument(
        "--listen-address", type=str, help="Listen address", default="localhost"
    )
    parser.add_argument("--port", type=int, help="Listen port", default=9923)
    parser.add_argument(
        "--public-port", type=int, help="Public listen port", default=9924)
    parser.add_argument(
        "--dulwich-server",
        action="store_true",
        help="Use dulwich server implementation.",
    )
    parser.add_argument(
        "--config",
        type=str,
        default="janitor.conf",
        help="Path to load configuration from.",
    )
    parser.add_argument(
        "--client-max-size",
        type=int,
        default=1024 ** 3,
        help="Maximum client body size (0 for no limit)",
    )
    parser.add_argument("--debug", action="store_true", help="Show debug info")
    parser.add_argument("--vcs-path", default=None, type=str, help="Path to local vcs storage")
    parser.add_argument("--gcp-logging", action="store_true")

    args = parser.parse_args()

    if args.gcp_logging:
        import google.cloud.logging
        client = google.cloud.logging.Client()
        client.get_default_handler()
        client.setup_logging()
    elif args.debug:
        logging.basicConfig(level=logging.DEBUG)
    else:
        logging.basicConfig(level=logging.INFO)

    if args.debug:
        loop = asyncio.get_event_loop()
        loop.set_debug(True)
        loop.slow_callback_duration = 0.001
        warnings.simplefilter('always', ResourceWarning)

    with open(args.config, "r") as f:
        config = read_config(f)

    state.DEFAULT_URL = config.database_location

    vcs_manager = LocalVcsManager(args.vcs_path or config.vcs_location)
    db = await state.create_pool(config.database_location)
    app, public_app = await create_web_app(
        args.listen_address,
        args.port,
        vcs_manager,
        db,
        config,
        dulwich_server=args.dulwich_server,
        client_max_size=args.client_max_size,
    )

    runner = web.AppRunner(app)
    public_runner = web.AppRunner(public_app)
    await runner.setup()
    await public_runner.setup()
    site = web.TCPSite(runner, args.listen_address, port=args.port)
    await site.start()
    logging.info("Listening on %s:%s", args.listen_address, args.port)
    site = web.TCPSite(public_runner, args.listen_address, port=args.public_port)
    await site.start()
    logging.info("Listening on %s:%s", args.listen_address, args.public_port)
    while True:
        await asyncio.sleep(3600)


if __name__ == "__main__":
    import sys

    sys.exit(asyncio.run(main(sys.argv)))
