# 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 ...