Learn more  » Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

agriconnect / dulwich   python

Repository URL to install this package:

/ server.py

# server.py -- Implementation of the server side git protocols
# Copyright (C) 2008 John Carr <john.carr@unrouted.co.uk>
# Coprygith (C) 2011-2012 Jelmer Vernooij <jelmer@jelmer.uk>
#
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
# General Public License as public by the Free Software Foundation; version 2.0
# or (at your option) any later version. You can redistribute it and/or
# modify it under the terms of either of these two licenses.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# You should have received a copy of the licenses; if not, see
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
# License, Version 2.0.
#

"""Git smart network protocol server implementation.

For more detailed implementation on the network protocol, see the
Documentation/technical directory in the cgit distribution, and in particular:

* Documentation/technical/protocol-capabilities.txt
* Documentation/technical/pack-protocol.txt

Currently supported capabilities:

 * include-tag
 * thin-pack
 * multi_ack_detailed
 * multi_ack
 * side-band-64k
 * ofs-delta
 * no-progress
 * report-status
 * delete-refs
 * shallow
 * symref
"""

import collections
import os
import socket
import sys
import time
import zlib

try:
    import SocketServer
except ImportError:
    import socketserver as SocketServer

from dulwich.archive import tar_stream
from dulwich.errors import (
    ApplyDeltaError,
    ChecksumMismatch,
    GitProtocolError,
    NotGitRepository,
    UnexpectedCommandError,
    ObjectFormatException,
    )
from dulwich import log_utils
from dulwich.objects import (
    Commit,
    valid_hexsha,
    )
from dulwich.pack import (
    write_pack_objects,
    )
from dulwich.protocol import (  # noqa: F401
    BufferedPktLineWriter,
    capability_agent,
    CAPABILITIES_REF,
    CAPABILITY_DELETE_REFS,
    CAPABILITY_INCLUDE_TAG,
    CAPABILITY_MULTI_ACK_DETAILED,
    CAPABILITY_MULTI_ACK,
    CAPABILITY_NO_DONE,
    CAPABILITY_NO_PROGRESS,
    CAPABILITY_OFS_DELTA,
    CAPABILITY_QUIET,
    CAPABILITY_REPORT_STATUS,
    CAPABILITY_SHALLOW,
    CAPABILITY_SIDE_BAND_64K,
    CAPABILITY_THIN_PACK,
    COMMAND_DEEPEN,
    COMMAND_DONE,
    COMMAND_HAVE,
    COMMAND_SHALLOW,
    COMMAND_UNSHALLOW,
    COMMAND_WANT,
    MULTI_ACK,
    MULTI_ACK_DETAILED,
    Protocol,
    ProtocolFile,
    ReceivableProtocol,
    SIDE_BAND_CHANNEL_DATA,
    SIDE_BAND_CHANNEL_PROGRESS,
    SIDE_BAND_CHANNEL_FATAL,
    SINGLE_ACK,
    TCP_GIT_PORT,
    ZERO_SHA,
    ack_type,
    extract_capabilities,
    extract_want_line_capabilities,
    symref_capabilities,
    )
from dulwich.refs import (
    ANNOTATED_TAG_SUFFIX,
    write_info_refs,
    )
from dulwich.repo import (
    Repo,
    )


logger = log_utils.getLogger(__name__)


class Backend(object):
    """A backend for the Git smart server implementation."""

    def open_repository(self, path):
        """Open the repository at a path.

        :param path: Path to the repository
        :raise NotGitRepository: no git repository was found at path
        :return: Instance of BackendRepo
        """
        raise NotImplementedError(self.open_repository)


class BackendRepo(object):
    """Repository abstraction used by the Git server.

    The methods required here are a subset of those provided by
    dulwich.repo.Repo.
    """

    object_store = None
    refs = None

    def get_refs(self):
        """
        Get all the refs in the repository

        :return: dict of name -> sha
        """
        raise NotImplementedError

    def get_peeled(self, name):
        """Return the cached peeled value of a ref, if available.

        :param name: Name of the ref to peel
        :return: The peeled value of the ref. If the ref is known not point to
            a tag, this will be the SHA the ref refers to. If no cached
            information about a tag is available, this method may return None,
            but it should attempt to peel the tag if possible.
        """
        return None

    def fetch_objects(self, determine_wants, graph_walker, progress,
                      get_tagged=None):
        """
        Yield the objects required for a list of commits.

        :param progress: is a callback to send progress messages to the client
        :param get_tagged: Function that returns a dict of pointed-to sha ->
            tag sha for including tags.
        """
        raise NotImplementedError


class DictBackend(Backend):
    """Trivial backend that looks up Git repositories in a dictionary."""

    def __init__(self, repos):
        self.repos = repos

    def open_repository(self, path):
        logger.debug('Opening repository at %s', path)
        try:
            return self.repos[path]
        except KeyError:
            raise NotGitRepository(
                "No git repository was found at %(path)s" % dict(path=path)
            )


class FileSystemBackend(Backend):
    """Simple backend looking up Git repositories in the local file system."""

    def __init__(self, root=os.sep):
        super(FileSystemBackend, self).__init__()
        self.root = (os.path.abspath(root) + os.sep).replace(
                os.sep * 2, os.sep)

    def open_repository(self, path):
        logger.debug('opening repository at %s', path)
        abspath = os.path.abspath(os.path.join(self.root, path)) + os.sep
        normcase_abspath = os.path.normcase(abspath)
        normcase_root = os.path.normcase(self.root)
        if not normcase_abspath.startswith(normcase_root):
            raise NotGitRepository(
                    "Path %r not inside root %r" %
                    (path, self.root))
        return Repo(abspath)


class Handler(object):
    """Smart protocol command handler base class."""

    def __init__(self, backend, proto, http_req=None):
        self.backend = backend
        self.proto = proto
        self.http_req = http_req

    def handle(self):
        raise NotImplementedError(self.handle)


class PackHandler(Handler):
    """Protocol handler for packs."""

    def __init__(self, backend, proto, http_req=None):
        super(PackHandler, self).__init__(backend, proto, http_req)
        self._client_capabilities = None
        # Flags needed for the no-done capability
        self._done_received = False

    @classmethod
    def capability_line(cls, capabilities):
        logger.info('Sending capabilities: %s', capabilities)
        return b"".join([b" " + c for c in capabilities])

    @classmethod
    def capabilities(cls):
        raise NotImplementedError(cls.capabilities)

    @classmethod
    def innocuous_capabilities(cls):
        return [CAPABILITY_INCLUDE_TAG, CAPABILITY_THIN_PACK,
                CAPABILITY_NO_PROGRESS, CAPABILITY_OFS_DELTA,
                capability_agent()]

    @classmethod
    def required_capabilities(cls):
        """Return a list of capabilities that we require the client to have."""
        return []

    def set_client_capabilities(self, caps):
        allowable_caps = set(self.innocuous_capabilities())
        allowable_caps.update(self.capabilities())
        for cap in caps:
            if cap not in allowable_caps:
                raise GitProtocolError('Client asked for capability %s that '
                                       'was not advertised.' % cap)
        for cap in self.required_capabilities():
            if cap not in caps:
                raise GitProtocolError('Client does not support required '
                                       'capability %s.' % cap)
        self._client_capabilities = set(caps)
        logger.info('Client capabilities: %s', caps)

    def has_capability(self, cap):
        if self._client_capabilities is None:
            raise GitProtocolError('Server attempted to access capability %s '
                                   'before asking client' % cap)
        return cap in self._client_capabilities

    def notify_done(self):
        self._done_received = True


class UploadPackHandler(PackHandler):
    """Protocol handler for uploading a pack to the client."""

    def __init__(self, backend, args, proto, http_req=None,
                 advertise_refs=False):
        super(UploadPackHandler, self).__init__(
                backend, proto, http_req=http_req)
        self.repo = backend.open_repository(args[0])
        self._graph_walker = None
        self.advertise_refs = advertise_refs
        # A state variable for denoting that the have list is still
        # being processed, and the client is not accepting any other
        # data (such as side-band, see the progress method here).
        self._processing_have_lines = False

    @classmethod
    def capabilities(cls):
        return [CAPABILITY_MULTI_ACK_DETAILED, CAPABILITY_MULTI_ACK,
                CAPABILITY_SIDE_BAND_64K, CAPABILITY_THIN_PACK,
                CAPABILITY_OFS_DELTA, CAPABILITY_NO_PROGRESS,
                CAPABILITY_INCLUDE_TAG, CAPABILITY_SHALLOW, CAPABILITY_NO_DONE]

    @classmethod
    def required_capabilities(cls):
        return (CAPABILITY_SIDE_BAND_64K, CAPABILITY_THIN_PACK,
                CAPABILITY_OFS_DELTA)

    def progress(self, message):
        if (self.has_capability(CAPABILITY_NO_PROGRESS) or
                self._processing_have_lines):
            return
        self.proto.write_sideband(SIDE_BAND_CHANNEL_PROGRESS, message)

    def get_tagged(self, refs=None, repo=None):
        """Get a dict of peeled values of tags to their original tag shas.

        :param refs: dict of refname -> sha of possible tags; defaults to all
            of the backend's refs.
        :param repo: optional Repo instance for getting peeled refs; defaults
            to the backend's repo, if available
        :return: dict of peeled_sha -> tag_sha, where tag_sha is the sha of a
            tag whose peeled value is peeled_sha.
        """
        if not self.has_capability(CAPABILITY_INCLUDE_TAG):
            return {}
        if refs is None:
            refs = self.repo.get_refs()
        if repo is None:
            repo = getattr(self.repo, "repo", None)
            if repo is None:
                # Bail if we don't have a Repo available; this is ok since
                # clients must be able to handle if the server doesn't include
                # all relevant tags.
                # TODO: fix behavior when missing
                return {}
        # TODO(jelmer): Integrate this with the refs logic in
        # Repo.fetch_objects
        tagged = {}
        for name, sha in refs.items():
            peeled_sha = repo.get_peeled(name)
            if peeled_sha != sha:
                tagged[peeled_sha] = sha
        return tagged

    def handle(self):
        def write(x):
            return self.proto.write_sideband(SIDE_BAND_CHANNEL_DATA, x)
Loading ...