|
@@ -45,6 +45,7 @@ import select
|
|
|
import socket
|
|
|
import subprocess
|
|
|
import sys
|
|
|
+from typing import Optional, Dict, Callable, Set
|
|
|
|
|
|
from urllib.parse import (
|
|
|
quote as urlquote,
|
|
@@ -61,7 +62,6 @@ from dulwich.errors import (
|
|
|
GitProtocolError,
|
|
|
NotGitRepository,
|
|
|
SendPackError,
|
|
|
- UpdateRefsError,
|
|
|
)
|
|
|
from dulwich.protocol import (
|
|
|
HangupException,
|
|
@@ -163,7 +163,6 @@ class ReportStatusParser(object):
|
|
|
def __init__(self):
|
|
|
self._done = False
|
|
|
self._pack_status = None
|
|
|
- self._ref_status_ok = True
|
|
|
self._ref_statuses = []
|
|
|
|
|
|
def check(self):
|
|
@@ -171,30 +170,24 @@ class ReportStatusParser(object):
|
|
|
|
|
|
Raises:
|
|
|
SendPackError: Raised when the server could not unpack
|
|
|
- UpdateRefsError: Raised when refs could not be updated
|
|
|
+ Returns:
|
|
|
+ iterator over refs
|
|
|
"""
|
|
|
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)
|
|
|
+ for status in self._ref_statuses:
|
|
|
+ try:
|
|
|
+ status, rest = status.split(b' ', 1)
|
|
|
+ except ValueError:
|
|
|
+ # malformed response, move on to the next one
|
|
|
+ continue
|
|
|
+ if status == b'ng':
|
|
|
+ ref, error = rest.split(b' ', 1)
|
|
|
+ yield ref, error.decode('utf-8')
|
|
|
+ elif status == b'ok':
|
|
|
+ yield rest, None
|
|
|
+ else:
|
|
|
+ raise GitProtocolError('invalid ref status %r' % status)
|
|
|
|
|
|
def handle_packet(self, pkt):
|
|
|
"""Handle a packet.
|
|
@@ -213,8 +206,6 @@ class ReportStatusParser(object):
|
|
|
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):
|
|
@@ -246,8 +237,8 @@ class FetchPackResult(object):
|
|
|
"""
|
|
|
|
|
|
_FORWARDED_ATTRS = [
|
|
|
- 'clear', 'copy', 'fromkeys', 'get', 'has_key', 'items',
|
|
|
- 'iteritems', 'iterkeys', 'itervalues', 'keys', 'pop', 'popitem',
|
|
|
+ 'clear', 'copy', 'fromkeys', 'get', 'items',
|
|
|
+ 'keys', 'pop', 'popitem',
|
|
|
'setdefault', 'update', 'values', 'viewitems', 'viewkeys',
|
|
|
'viewvalues']
|
|
|
|
|
@@ -300,6 +291,66 @@ class FetchPackResult(object):
|
|
|
self.__class__.__name__, self.refs, self.symrefs, self.agent)
|
|
|
|
|
|
|
|
|
+class SendPackResult(object):
|
|
|
+ """Result of a upload-pack operation.
|
|
|
+
|
|
|
+ Attributes:
|
|
|
+ refs: Dictionary with all remote refs
|
|
|
+ agent: User agent string
|
|
|
+ ref_status: Optional dictionary mapping ref name to error message (if it
|
|
|
+ failed to update), or None if it was updated successfully
|
|
|
+ """
|
|
|
+
|
|
|
+ _FORWARDED_ATTRS = [
|
|
|
+ 'clear', 'copy', 'fromkeys', 'get', 'items',
|
|
|
+ 'keys', 'pop', 'popitem',
|
|
|
+ 'setdefault', 'update', 'values', 'viewitems', 'viewkeys',
|
|
|
+ 'viewvalues']
|
|
|
+
|
|
|
+ def __init__(self, refs, agent=None, ref_status=None):
|
|
|
+ self.refs = refs
|
|
|
+ self.agent = agent
|
|
|
+ self.ref_status = ref_status
|
|
|
+
|
|
|
+ def _warn_deprecated(self):
|
|
|
+ import warnings
|
|
|
+ warnings.warn(
|
|
|
+ "Use SendPackResult.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.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(SendPackResult, self).__getattribute__(name)
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return "%s(%r, %r)" % (
|
|
|
+ self.__class__.__name__, self.refs, self.agent)
|
|
|
+
|
|
|
+
|
|
|
def _read_shallow_updates(proto):
|
|
|
new_shallow = set()
|
|
|
new_unshallow = set()
|
|
@@ -382,13 +433,10 @@ class GitClient(object):
|
|
|
progress: Optional progress function
|
|
|
|
|
|
Returns:
|
|
|
- new_refs dictionary containing the changes that were made
|
|
|
- {refname: new_ref}, including deleted refs.
|
|
|
+ SendPackResult object
|
|
|
|
|
|
Raises:
|
|
|
SendPackError: if server rejects the pack data
|
|
|
- UpdateRefsError: if the server supports report-status
|
|
|
- and rejects ref updates
|
|
|
|
|
|
"""
|
|
|
raise NotImplementedError(self.send_pack)
|
|
@@ -469,43 +517,6 @@ class GitClient(object):
|
|
|
"""
|
|
|
raise NotImplementedError(self.get_refs)
|
|
|
|
|
|
- def _parse_status_report(self, proto):
|
|
|
- unpack = proto.read_pkt_line().strip()
|
|
|
- if unpack != b'unpack ok':
|
|
|
- st = True
|
|
|
- # flush remaining error data
|
|
|
- while st is not None:
|
|
|
- st = proto.read_pkt_line()
|
|
|
- raise SendPackError(unpack)
|
|
|
- statuses = []
|
|
|
- errs = False
|
|
|
- ref_status = proto.read_pkt_line()
|
|
|
- while ref_status:
|
|
|
- ref_status = ref_status.strip()
|
|
|
- statuses.append(ref_status)
|
|
|
- if not ref_status.startswith(b'ok '):
|
|
|
- errs = True
|
|
|
- ref_status = proto.read_pkt_line()
|
|
|
-
|
|
|
- if errs:
|
|
|
- ref_status = {}
|
|
|
- ok = set()
|
|
|
- for status in 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
|
|
|
- raise UpdateRefsError(', '.join([
|
|
|
- refname for refname in ref_status if refname not in ok]) +
|
|
|
- b' failed to update', ref_status=ref_status)
|
|
|
-
|
|
|
def _read_side_band64k_data(self, proto, channel_callbacks):
|
|
|
"""Read per-channel data.
|
|
|
|
|
@@ -578,13 +589,21 @@ class GitClient(object):
|
|
|
def _negotiate_receive_pack_capabilities(self, server_capabilities):
|
|
|
negotiated_capabilities = (
|
|
|
self._send_capabilities & server_capabilities)
|
|
|
+ agent = None
|
|
|
+ for capability in server_capabilities:
|
|
|
+ k, v = parse_capability(capability)
|
|
|
+ if k == CAPABILITY_AGENT:
|
|
|
+ agent = v
|
|
|
unknown_capabilities = ( # noqa: F841
|
|
|
extract_capability_names(server_capabilities) -
|
|
|
KNOWN_RECEIVE_CAPABILITIES)
|
|
|
# TODO(jelmer): warn about unknown capabilities
|
|
|
- return negotiated_capabilities
|
|
|
+ return negotiated_capabilities, agent
|
|
|
|
|
|
- def _handle_receive_pack_tail(self, proto, capabilities, progress=None):
|
|
|
+ def _handle_receive_pack_tail(
|
|
|
+ self, proto: Protocol, capabilities: Set[bytes],
|
|
|
+ progress: Callable[[bytes], None] = None
|
|
|
+ ) -> Optional[Dict[bytes, Optional[str]]]:
|
|
|
"""Handle the tail of a 'git-receive-pack' request.
|
|
|
|
|
|
Args:
|
|
@@ -593,7 +612,9 @@ class GitClient(object):
|
|
|
progress: Optional progress reporting function
|
|
|
|
|
|
Returns:
|
|
|
-
|
|
|
+ dict mapping ref name to:
|
|
|
+ error message if the ref failed to update
|
|
|
+ None if it was updated successfully
|
|
|
"""
|
|
|
if CAPABILITY_SIDE_BAND_64K in capabilities:
|
|
|
if progress is None:
|
|
@@ -609,7 +630,9 @@ class GitClient(object):
|
|
|
for pkt in proto.read_pkt_seq():
|
|
|
self._report_status_parser.handle_packet(pkt)
|
|
|
if self._report_status_parser is not None:
|
|
|
- self._report_status_parser.check()
|
|
|
+ return dict(self._report_status_parser.check())
|
|
|
+
|
|
|
+ return None
|
|
|
|
|
|
def _negotiate_upload_pack_capabilities(self, server_capabilities):
|
|
|
unknown_capabilities = ( # noqa: F841
|
|
@@ -798,13 +821,10 @@ class TraditionalGitClient(GitClient):
|
|
|
progress: Optional callback called with progress updates
|
|
|
|
|
|
Returns:
|
|
|
- new_refs dictionary containing the changes that were made
|
|
|
- {refname: new_ref}, including deleted refs.
|
|
|
+ SendPackResult
|
|
|
|
|
|
Raises:
|
|
|
SendPackError: if server rejects the pack data
|
|
|
- UpdateRefsError: if the server supports report-status
|
|
|
- and rejects ref updates
|
|
|
|
|
|
"""
|
|
|
proto, unused_can_read, stderr = self._connect(b'receive-pack', path)
|
|
@@ -813,7 +833,7 @@ class TraditionalGitClient(GitClient):
|
|
|
old_refs, server_capabilities = read_pkt_refs(proto)
|
|
|
except HangupException:
|
|
|
_remote_error_from_stderr(stderr)
|
|
|
- negotiated_capabilities = \
|
|
|
+ negotiated_capabilities, agent = \
|
|
|
self._negotiate_receive_pack_capabilities(server_capabilities)
|
|
|
if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
|
|
|
self._report_status_parser = ReportStatusParser()
|
|
@@ -827,7 +847,7 @@ class TraditionalGitClient(GitClient):
|
|
|
|
|
|
if set(new_refs.items()).issubset(set(old_refs.items())):
|
|
|
proto.write_pkt_line(None)
|
|
|
- return new_refs
|
|
|
+ return SendPackResult(new_refs, agent=agent, ref_status={})
|
|
|
|
|
|
if CAPABILITY_DELETE_REFS not in server_capabilities:
|
|
|
# Server does not support deletions. Fail later.
|
|
@@ -836,21 +856,24 @@ class TraditionalGitClient(GitClient):
|
|
|
if sha == ZERO_SHA:
|
|
|
if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
|
|
|
report_status_parser._ref_statuses.append(
|
|
|
- b'ng ' + sha +
|
|
|
+ b'ng ' + ref +
|
|
|
b' remote does not support deleting refs')
|
|
|
report_status_parser._ref_status_ok = False
|
|
|
del new_refs[ref]
|
|
|
|
|
|
if new_refs is None:
|
|
|
proto.write_pkt_line(None)
|
|
|
- return old_refs
|
|
|
+ return SendPackResult(old_refs, agent=agent, ref_status={})
|
|
|
|
|
|
if len(new_refs) == 0 and len(orig_new_refs):
|
|
|
# NOOP - Original new refs filtered out by policy
|
|
|
proto.write_pkt_line(None)
|
|
|
if report_status_parser is not None:
|
|
|
- report_status_parser.check()
|
|
|
- return old_refs
|
|
|
+ ref_status = dict(report_status_parser.check())
|
|
|
+ else:
|
|
|
+ ref_status = None
|
|
|
+ return SendPackResult(
|
|
|
+ old_refs, agent=agent, ref_status=ref_status)
|
|
|
|
|
|
(have, want) = self._handle_receive_pack_head(
|
|
|
proto, negotiated_capabilities, old_refs, new_refs)
|
|
@@ -862,9 +885,9 @@ class TraditionalGitClient(GitClient):
|
|
|
if self._should_send_pack(new_refs):
|
|
|
write_pack_data(proto.write_file(), pack_data_count, pack_data)
|
|
|
|
|
|
- self._handle_receive_pack_tail(
|
|
|
+ ref_status = self._handle_receive_pack_tail(
|
|
|
proto, negotiated_capabilities, progress)
|
|
|
- return new_refs
|
|
|
+ return SendPackResult(new_refs, agent=agent, ref_status=ref_status)
|
|
|
|
|
|
def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
|
|
|
progress=None, depth=None):
|
|
@@ -1136,13 +1159,10 @@ class LocalGitClient(GitClient):
|
|
|
progress: Optional progress function
|
|
|
|
|
|
Returns:
|
|
|
- new_refs dictionary containing the changes that were made
|
|
|
- {refname: new_ref}, including deleted refs.
|
|
|
+ SendPackResult
|
|
|
|
|
|
Raises:
|
|
|
SendPackError: if server rejects the pack data
|
|
|
- UpdateRefsError: if the server supports report-status
|
|
|
- and rejects ref updates
|
|
|
|
|
|
"""
|
|
|
if not progress:
|
|
@@ -1163,23 +1183,27 @@ class LocalGitClient(GitClient):
|
|
|
|
|
|
if (not want and
|
|
|
set(new_refs.items()).issubset(set(old_refs.items()))):
|
|
|
- return new_refs
|
|
|
+ return SendPackResult(new_refs, ref_status={})
|
|
|
|
|
|
target.object_store.add_pack_data(
|
|
|
*generate_pack_data(have, want, ofs_delta=True))
|
|
|
|
|
|
+ ref_status = {}
|
|
|
+
|
|
|
for refname, new_sha1 in new_refs.items():
|
|
|
old_sha1 = old_refs.get(refname, ZERO_SHA)
|
|
|
if new_sha1 != ZERO_SHA:
|
|
|
if not target.refs.set_if_equals(
|
|
|
refname, old_sha1, new_sha1):
|
|
|
- progress('unable to set %s to %s' %
|
|
|
- (refname, new_sha1))
|
|
|
+ msg = 'unable to set %s to %s' % (refname, new_sha1)
|
|
|
+ progress(msg)
|
|
|
+ ref_status[refname] = msg
|
|
|
else:
|
|
|
if not target.refs.remove_if_equals(refname, old_sha1):
|
|
|
progress('unable to remove %s' % refname)
|
|
|
+ ref_status[refname] = 'unable to remove'
|
|
|
|
|
|
- return new_refs
|
|
|
+ return SendPackResult(new_refs, ref_status=ref_status)
|
|
|
|
|
|
def fetch(self, path, target, determine_wants=None, progress=None,
|
|
|
depth=None):
|
|
@@ -1696,20 +1720,17 @@ class HttpGitClient(GitClient):
|
|
|
progress: Optional progress function
|
|
|
|
|
|
Returns:
|
|
|
- new_refs dictionary containing the changes that were made
|
|
|
- {refname: new_ref}, including deleted refs.
|
|
|
+ SendPackResult
|
|
|
|
|
|
Raises:
|
|
|
SendPackError: if server rejects the pack data
|
|
|
- UpdateRefsError: if the server supports report-status
|
|
|
- and rejects ref updates
|
|
|
|
|
|
"""
|
|
|
url = self._get_url(path)
|
|
|
old_refs, server_capabilities, url = self._discover_references(
|
|
|
b"git-receive-pack", url)
|
|
|
- negotiated_capabilities = self._negotiate_receive_pack_capabilities(
|
|
|
- server_capabilities)
|
|
|
+ negotiated_capabilities, agent = (
|
|
|
+ self._negotiate_receive_pack_capabilities(server_capabilities))
|
|
|
negotiated_capabilities.add(capability_agent())
|
|
|
|
|
|
if CAPABILITY_REPORT_STATUS in negotiated_capabilities:
|
|
@@ -1718,9 +1739,9 @@ class HttpGitClient(GitClient):
|
|
|
new_refs = update_refs(dict(old_refs))
|
|
|
if new_refs is None:
|
|
|
# Determine wants function is aborting the push.
|
|
|
- return old_refs
|
|
|
+ return SendPackResult(old_refs, agent=agent, ref_status={})
|
|
|
if set(new_refs.items()).issubset(set(old_refs.items())):
|
|
|
- return new_refs
|
|
|
+ return SendPackResult(new_refs, agent=agent, ref_status={})
|
|
|
if self.dumb:
|
|
|
raise NotImplementedError(self.fetch_pack)
|
|
|
req_data = BytesIO()
|
|
@@ -1736,9 +1757,10 @@ class HttpGitClient(GitClient):
|
|
|
data=req_data.getvalue())
|
|
|
try:
|
|
|
resp_proto = Protocol(read, None)
|
|
|
- self._handle_receive_pack_tail(
|
|
|
+ ref_status = self._handle_receive_pack_tail(
|
|
|
resp_proto, negotiated_capabilities, progress)
|
|
|
- return new_refs
|
|
|
+ return SendPackResult(
|
|
|
+ new_refs, agent=agent, ref_status=ref_status)
|
|
|
finally:
|
|
|
resp.close()
|
|
|
|
|
@@ -1770,7 +1792,7 @@ class HttpGitClient(GitClient):
|
|
|
if not wants:
|
|
|
return FetchPackResult(refs, symrefs, agent)
|
|
|
if self.dumb:
|
|
|
- raise NotImplementedError(self.send_pack)
|
|
|
+ raise NotImplementedError(self.fetch_pack)
|
|
|
req_data = BytesIO()
|
|
|
req_proto = Protocol(None, req_data.write)
|
|
|
(new_shallow, new_unshallow) = self._handle_upload_pack_head(
|