# client.py -- Implementation of the client side git protocols
# Copyright (C) 2008-2013 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.
#
"""Client side support for the Git protocol.
The Dulwich client supports the following capabilities:
* thin-pack
* multi_ack_detailed
* multi_ack
* side-band-64k
* ofs-delta
* quiet
* report-status
* delete-refs
Known capabilities that are not supported:
* shallow
* no-progress
* include-tag
"""
from contextlib import closing
from io import BytesIO, BufferedReader
import select
import socket
import subprocess
import sys
try:
from urllib import quote as urlquote
from urllib import unquote as urlunquote
except ImportError:
from urllib.parse import quote as urlquote
from urllib.parse import unquote as urlunquote
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
import dulwich
from dulwich.errors import (
GitProtocolError,
NotGitRepository,
SendPackError,
UpdateRefsError,
)
from dulwich.protocol import (
HangupException,
_RBUFSIZE,
agent_string,
capability_agent,
extract_capability_names,
CAPABILITY_AGENT,
CAPABILITY_DELETE_REFS,
CAPABILITY_MULTI_ACK,
CAPABILITY_MULTI_ACK_DETAILED,
CAPABILITY_OFS_DELTA,
CAPABILITY_QUIET,
CAPABILITY_REPORT_STATUS,
CAPABILITY_SHALLOW,
CAPABILITY_SYMREF,
CAPABILITY_SIDE_BAND_64K,
CAPABILITY_THIN_PACK,
CAPABILITIES_REF,
KNOWN_RECEIVE_CAPABILITIES,
KNOWN_UPLOAD_CAPABILITIES,
COMMAND_DEEPEN,
COMMAND_SHALLOW,
COMMAND_UNSHALLOW,
COMMAND_DONE,
COMMAND_HAVE,
COMMAND_WANT,
SIDE_BAND_CHANNEL_DATA,
SIDE_BAND_CHANNEL_PROGRESS,
SIDE_BAND_CHANNEL_FATAL,
PktLineParser,
Protocol,
ProtocolFile,
TCP_GIT_PORT,
ZERO_SHA,
extract_capabilities,
parse_capability,
)
from dulwich.pack import (
write_pack_data,
write_pack_objects,
)
from dulwich.refs import (
read_info_refs,
ANNOTATED_TAG_SUFFIX,
)
class InvalidWants(Exception):
"""Invalid wants."""
def __init__(self, wants):
Exception.__init__(
self,
"requested wants not in server provided refs: %r" % wants)
def _fileno_can_read(fileno):
"""Check if a file descriptor is readable."""
return len(select.select([fileno], [], [], 0)[0]) > 0
def _win32_peek_avail(handle):
"""Wrapper around PeekNamedPipe to check how many bytes are available."""
from ctypes import byref, wintypes, windll
c_avail = wintypes.DWORD()
c_message = wintypes.DWORD()
success = windll.kernel32.PeekNamedPipe(
handle, None, 0, None, byref(c_avail),
byref(c_message))
if not success:
raise OSError(wintypes.GetLastError())
return c_avail.value
COMMON_CAPABILITIES = [CAPABILITY_OFS_DELTA, CAPABILITY_SIDE_BAND_64K]
UPLOAD_CAPABILITIES = ([CAPABILITY_THIN_PACK, CAPABILITY_MULTI_ACK,
CAPABILITY_MULTI_ACK_DETAILED, CAPABILITY_SHALLOW]
+ COMMON_CAPABILITIES)
RECEIVE_CAPABILITIES = [CAPABILITY_REPORT_STATUS] + COMMON_CAPABILITIES
class ReportStatusParser(object):
"""Handle status as reported by servers with 'report-status' capability.
"""
def __init__(self):
self._done = False
self._pack_status = None
self._ref_status_ok = True
self._ref_statuses = []
def check(self):
"""Check if there were any errors and, if so, raise exceptions.
:raise SendPackError: Raised when the server could not unpack
:raise UpdateRefsError: Raised when refs could not be updated
"""
if self._pack_status not in (b'unpack ok', None):
raise SendPackError(self._pack_status)
if not self._ref_status_ok:
ref_status = {}
ok = set()
for status in self._ref_statuses:
if b' ' not in status:
# malformed response, move on to the next one
continue
status, ref = status.split(b' ', 1)
if status == b'ng':
if b' ' in ref:
ref, status = ref.split(b' ', 1)
else:
ok.add(ref)
ref_status[ref] = status
# TODO(jelmer): don't assume encoding of refs is ascii.
raise UpdateRefsError(', '.join([
refname.decode('ascii') for refname in ref_status
if refname not in ok]) +
' failed to update', ref_status=ref_status)
def handle_packet(self, pkt):
"""Handle a packet.
:raise GitProtocolError: Raised when packets are received after a
flush packet.
"""
if self._done:
raise GitProtocolError("received more data after status report")
if pkt is None:
self._done = True
return
if self._pack_status is None:
self._pack_status = pkt.strip()
else:
ref_status = pkt.strip()
self._ref_statuses.append(ref_status)
if not ref_status.startswith(b'ok '):
self._ref_status_ok = False
def read_pkt_refs(proto):
server_capabilities = None
refs = {}
# Receive refs from server
for pkt in proto.read_pkt_seq():
(sha, ref) = pkt.rstrip(b'\n').split(None, 1)
if sha == b'ERR':
raise GitProtocolError(ref.decode('utf-8', 'replace'))
if server_capabilities is None:
(ref, server_capabilities) = extract_capabilities(ref)
refs[ref] = sha
if len(refs) == 0:
return {}, set([])
if refs == {CAPABILITIES_REF: ZERO_SHA}:
refs = {}
return refs, set(server_capabilities)
class FetchPackResult(object):
"""Result of a fetch-pack operation.
:var refs: Dictionary with all remote refs
:var symrefs: Dictionary with remote symrefs
:var agent: User agent string
"""
_FORWARDED_ATTRS = [
'clear', 'copy', 'fromkeys', 'get', 'has_key', 'items',
'iteritems', 'iterkeys', 'itervalues', 'keys', 'pop', 'popitem',
'setdefault', 'update', 'values', 'viewitems', 'viewkeys',
'viewvalues']
def __init__(self, refs, symrefs, agent, new_shallow=None,
new_unshallow=None):
self.refs = refs
self.symrefs = symrefs
self.agent = agent
self.new_shallow = new_shallow
self.new_unshallow = new_unshallow
def _warn_deprecated(self):
import warnings
warnings.warn(
"Use FetchPackResult.refs instead.",
DeprecationWarning, stacklevel=3)
def __eq__(self, other):
if isinstance(other, dict):
self._warn_deprecated()
return (self.refs == other)
return (self.refs == other.refs and
self.symrefs == other.symrefs and
self.agent == other.agent)
def __contains__(self, name):
self._warn_deprecated()
return name in self.refs
def __getitem__(self, name):
self._warn_deprecated()
return self.refs[name]
def __len__(self):
self._warn_deprecated()
return len(self.refs)
def __iter__(self):
self._warn_deprecated()
return iter(self.refs)
def __getattribute__(self, name):
if name in type(self)._FORWARDED_ATTRS:
self._warn_deprecated()
return getattr(self.refs, name)
return super(FetchPackResult, self).__getattribute__(name)
def __repr__(self):
return "%s(%r, %r, %r)" % (
self.__class__.__name__, self.refs, self.symrefs, self.agent)
def _read_shallow_updates(proto):
new_shallow = set()
new_unshallow = set()
for pkt in proto.read_pkt_seq():
cmd, sha = pkt.split(b' ', 1)
if cmd == COMMAND_SHALLOW:
new_shallow.add(sha.strip())
elif cmd == COMMAND_UNSHALLOW:
new_unshallow.add(sha.strip())
else:
raise GitProtocolError('unknown command %s' % pkt)
return (new_shallow, new_unshallow)
# TODO(durin42): this doesn't correctly degrade if the server doesn't
# support some capabilities. This should work properly with servers
# that don't support multi_ack.
class GitClient(object):
"""Git smart server client.
"""
def __init__(self, thin_packs=True, report_activity=None, quiet=False):
"""Create a new GitClient instance.
:param thin_packs: Whether or not thin packs should be retrieved
:param report_activity: Optional callback for reporting transport
activity.
"""
self._report_activity = report_activity
self._report_status_parser = None
self._fetch_capabilities = set(UPLOAD_CAPABILITIES)
self._fetch_capabilities.add(capability_agent())
self._send_capabilities = set(RECEIVE_CAPABILITIES)
self._send_capabilities.add(capability_agent())
if quiet:
self._send_capabilities.add(CAPABILITY_QUIET)
if not thin_packs:
self._fetch_capabilities.remove(CAPABILITY_THIN_PACK)
def get_url(self, path):
"""Retrieves full url to given path.
:param path: Repository path (as string)
:return: Url to path (as string)
"""
raise NotImplementedError(self.get_url)
@classmethod
def from_parsedurl(cls, parsedurl, **kwargs):
"""Create an instance of this client from a urlparse.parsed object.
:param parsedurl: Result of urlparse.urlparse()
:return: A `GitClient` object
"""
raise NotImplementedError(cls.from_parsedurl)
Loading ...