Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dulwich/protocol.py: 32%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# protocol.py -- Shared parts of the git protocols
2# Copyright (C) 2008 John Carr <john.carr@unrouted.co.uk>
3# Copyright (C) 2008-2012 Jelmer Vernooij <jelmer@jelmer.uk>
4#
5# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
6# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
7# General Public License as published by the Free Software Foundation; version 2.0
8# or (at your option) any later version. You can redistribute it and/or
9# modify it under the terms of either of these two licenses.
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17# You should have received a copy of the licenses; if not, see
18# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
19# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
20# License, Version 2.0.
21#
23"""Generic functions for talking the git smart server protocol."""
25__all__ = [
26 "CAPABILITIES_REF",
27 "CAPABILITY_AGENT",
28 "CAPABILITY_ALLOW_REACHABLE_SHA1_IN_WANT",
29 "CAPABILITY_ALLOW_TIP_SHA1_IN_WANT",
30 "CAPABILITY_ATOMIC",
31 "CAPABILITY_DEEPEN_NOT",
32 "CAPABILITY_DEEPEN_RELATIVE",
33 "CAPABILITY_DEEPEN_SINCE",
34 "CAPABILITY_DELETE_REFS",
35 "CAPABILITY_FETCH",
36 "CAPABILITY_FILTER",
37 "CAPABILITY_INCLUDE_TAG",
38 "CAPABILITY_MULTI_ACK",
39 "CAPABILITY_MULTI_ACK_DETAILED",
40 "CAPABILITY_NO_DONE",
41 "CAPABILITY_NO_PROGRESS",
42 "CAPABILITY_OBJECT_FORMAT",
43 "CAPABILITY_OFS_DELTA",
44 "CAPABILITY_QUIET",
45 "CAPABILITY_REPORT_STATUS",
46 "CAPABILITY_SHALLOW",
47 "CAPABILITY_SIDE_BAND",
48 "CAPABILITY_SIDE_BAND_64K",
49 "CAPABILITY_SYMREF",
50 "CAPABILITY_THIN_PACK",
51 "COMMAND_DEEPEN",
52 "COMMAND_DEEPEN_NOT",
53 "COMMAND_DEEPEN_SINCE",
54 "COMMAND_DONE",
55 "COMMAND_FILTER",
56 "COMMAND_HAVE",
57 "COMMAND_SHALLOW",
58 "COMMAND_UNSHALLOW",
59 "COMMAND_WANT",
60 "COMMON_CAPABILITIES",
61 "DEFAULT_GIT_PROTOCOL_VERSION_FETCH",
62 "DEFAULT_GIT_PROTOCOL_VERSION_SEND",
63 "DEPTH_INFINITE",
64 "GIT_PROTOCOL_VERSIONS",
65 "KNOWN_RECEIVE_CAPABILITIES",
66 "KNOWN_UPLOAD_CAPABILITIES",
67 "MULTI_ACK",
68 "MULTI_ACK_DETAILED",
69 "NAK_LINE",
70 "PEELED_TAG_SUFFIX",
71 "SIDE_BAND_CHANNEL_DATA",
72 "SIDE_BAND_CHANNEL_FATAL",
73 "SIDE_BAND_CHANNEL_PROGRESS",
74 "SINGLE_ACK",
75 "TCP_GIT_PORT",
76 "BufferedPktLineWriter",
77 "PktLineParser",
78 "Protocol",
79 "ReceivableProtocol",
80 "ack_type",
81 "agent_string",
82 "capability_agent",
83 "capability_object_format",
84 "capability_symref",
85 "extract_capabilities",
86 "extract_capability_names",
87 "extract_want_line_capabilities",
88 "find_capability",
89 "format_ack_line",
90 "format_capability_line",
91 "format_cmd_pkt",
92 "format_ref_line",
93 "format_shallow_line",
94 "format_unshallow_line",
95 "parse_capability",
96 "parse_cmd_pkt",
97 "pkt_line",
98 "pkt_seq",
99 "serialize_refs",
100 "split_peeled_refs",
101 "strip_peeled_refs",
102 "symref_capabilities",
103 "write_info_refs",
104]
106import logging
107import sys
108import types
109from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
110from io import BytesIO
111from os import SEEK_END
112from typing import TYPE_CHECKING
114import dulwich
116from .errors import GitProtocolError, HangupException
117from .objects import ObjectID, Tag
119if sys.version_info >= (3, 11):
120 from typing import Self
121else:
122 from typing_extensions import Self
124logger = logging.getLogger(__name__)
126if TYPE_CHECKING:
127 from .pack import ObjectContainer
128 from .refs import Ref
130TCP_GIT_PORT = 9418
132# Git protocol version 0 is the original Git protocol, which lacked a
133# version number until Git protocol version 1 was introduced by Brandon
134# Williams in 2017.
135#
136# Protocol version 1 is simply the original v0 protocol with the addition of
137# a single packet line, which precedes the ref advertisement, indicating the
138# protocol version being used. This was done in preparation for protocol v2.
139#
140# Git protocol version 2 was first introduced by Brandon Williams in 2018 and
141# adds many features. See the gitprotocol-v2(5) manual page for details.
142# As of 2024, Git only implements version 2 during 'git fetch' and still uses
143# version 0 during 'git push'.
144GIT_PROTOCOL_VERSIONS = [0, 1, 2]
145DEFAULT_GIT_PROTOCOL_VERSION_FETCH = 2
146DEFAULT_GIT_PROTOCOL_VERSION_SEND = 0
148# Suffix used in the Git protocol to indicate peeled tag references
149PEELED_TAG_SUFFIX = b"^{}"
151ZERO_SHA: ObjectID = ObjectID(b"0" * 40)
153SINGLE_ACK = 0
154MULTI_ACK = 1
155MULTI_ACK_DETAILED = 2
157# pack data
158SIDE_BAND_CHANNEL_DATA = 1
159# progress messages
160SIDE_BAND_CHANNEL_PROGRESS = 2
161# fatal error message just before stream aborts
162SIDE_BAND_CHANNEL_FATAL = 3
164CAPABILITY_ATOMIC = b"atomic"
165CAPABILITY_DEEPEN_SINCE = b"deepen-since"
166CAPABILITY_DEEPEN_NOT = b"deepen-not"
167CAPABILITY_DEEPEN_RELATIVE = b"deepen-relative"
168CAPABILITY_DELETE_REFS = b"delete-refs"
169CAPABILITY_INCLUDE_TAG = b"include-tag"
170CAPABILITY_MULTI_ACK = b"multi_ack"
171CAPABILITY_MULTI_ACK_DETAILED = b"multi_ack_detailed"
172CAPABILITY_NO_DONE = b"no-done"
173CAPABILITY_NO_PROGRESS = b"no-progress"
174CAPABILITY_OFS_DELTA = b"ofs-delta"
175CAPABILITY_QUIET = b"quiet"
176CAPABILITY_REPORT_STATUS = b"report-status"
177CAPABILITY_SHALLOW = b"shallow"
178CAPABILITY_SIDE_BAND = b"side-band"
179CAPABILITY_SIDE_BAND_64K = b"side-band-64k"
180CAPABILITY_THIN_PACK = b"thin-pack"
181CAPABILITY_AGENT = b"agent"
182CAPABILITY_SYMREF = b"symref"
183CAPABILITY_ALLOW_TIP_SHA1_IN_WANT = b"allow-tip-sha1-in-want"
184CAPABILITY_ALLOW_REACHABLE_SHA1_IN_WANT = b"allow-reachable-sha1-in-want"
185CAPABILITY_FETCH = b"fetch"
186CAPABILITY_FILTER = b"filter"
187CAPABILITY_OBJECT_FORMAT = b"object-format"
188CAPABILITY_PACKFILE_URIS = b"packfile-uris"
189CAPABILITY_PUSH_OPTIONS = b"push-options"
191# Magic ref that is used to attach capabilities to when
192# there are no refs. Should always be ste to ZERO_SHA.
193CAPABILITIES_REF = b"capabilities^{}"
195COMMON_CAPABILITIES = [
196 CAPABILITY_OFS_DELTA,
197 CAPABILITY_SIDE_BAND,
198 CAPABILITY_SIDE_BAND_64K,
199 CAPABILITY_AGENT,
200 CAPABILITY_NO_PROGRESS,
201]
202KNOWN_UPLOAD_CAPABILITIES = set(
203 [
204 *COMMON_CAPABILITIES,
205 CAPABILITY_THIN_PACK,
206 CAPABILITY_MULTI_ACK,
207 CAPABILITY_MULTI_ACK_DETAILED,
208 CAPABILITY_INCLUDE_TAG,
209 CAPABILITY_DEEPEN_SINCE,
210 CAPABILITY_SYMREF,
211 CAPABILITY_SHALLOW,
212 CAPABILITY_DEEPEN_NOT,
213 CAPABILITY_DEEPEN_RELATIVE,
214 CAPABILITY_ALLOW_TIP_SHA1_IN_WANT,
215 CAPABILITY_ALLOW_REACHABLE_SHA1_IN_WANT,
216 CAPABILITY_FETCH,
217 CAPABILITY_FILTER,
218 CAPABILITY_PACKFILE_URIS,
219 ]
220)
221KNOWN_RECEIVE_CAPABILITIES = set(
222 [
223 *COMMON_CAPABILITIES,
224 CAPABILITY_REPORT_STATUS,
225 CAPABILITY_DELETE_REFS,
226 CAPABILITY_QUIET,
227 CAPABILITY_ATOMIC,
228 CAPABILITY_PUSH_OPTIONS,
229 ]
230)
232DEPTH_INFINITE = 0x7FFFFFFF
234NAK_LINE = b"NAK\n"
237def agent_string() -> bytes:
238 """Generate the agent string for dulwich.
240 Returns:
241 Agent string as bytes
242 """
243 return ("dulwich/" + ".".join(map(str, dulwich.__version__))).encode("ascii")
246def capability_agent() -> bytes:
247 """Generate the agent capability string.
249 Returns:
250 Agent capability with dulwich version
251 """
252 return CAPABILITY_AGENT + b"=" + agent_string()
255def capability_object_format(fmt: str) -> bytes:
256 """Generate the object-format capability string.
258 Args:
259 fmt: Object format name (e.g., "sha1" or "sha256")
261 Returns:
262 Object-format capability with format name
263 """
264 return CAPABILITY_OBJECT_FORMAT + b"=" + fmt.encode("ascii")
267def capability_symref(from_ref: bytes, to_ref: bytes) -> bytes:
268 """Generate a symref capability string.
270 Args:
271 from_ref: Source reference name
272 to_ref: Target reference name
274 Returns:
275 Symref capability string
276 """
277 return CAPABILITY_SYMREF + b"=" + from_ref + b":" + to_ref
280def extract_capability_names(capabilities: Iterable[bytes]) -> set[bytes]:
281 """Extract capability names from a list of capabilities.
283 Args:
284 capabilities: List of capability strings
286 Returns:
287 Set of capability names
288 """
289 return {parse_capability(c)[0] for c in capabilities}
292def parse_capability(capability: bytes) -> tuple[bytes, bytes | None]:
293 """Parse a capability string into name and value.
295 Args:
296 capability: Capability string
298 Returns:
299 Tuple of (capability_name, capability_value)
300 """
301 parts = capability.split(b"=", 1)
302 if len(parts) == 1:
303 return (parts[0], None)
304 return (parts[0], parts[1])
307def symref_capabilities(symrefs: Iterable[tuple[bytes, bytes]]) -> list[bytes]:
308 """Generate symref capability strings from symref pairs.
310 Args:
311 symrefs: Iterable of (from_ref, to_ref) tuples
313 Returns:
314 List of symref capability strings
315 """
316 return [capability_symref(*k) for k in symrefs]
319COMMAND_DEEPEN = b"deepen"
320COMMAND_DEEPEN_SINCE = b"deepen-since"
321COMMAND_DEEPEN_NOT = b"deepen-not"
322COMMAND_SHALLOW = b"shallow"
323COMMAND_UNSHALLOW = b"unshallow"
324COMMAND_DONE = b"done"
325COMMAND_WANT = b"want"
326COMMAND_HAVE = b"have"
327COMMAND_FILTER = b"filter"
330def format_cmd_pkt(cmd: bytes, *args: bytes) -> bytes:
331 """Format a command packet.
333 Args:
334 cmd: Command name
335 *args: Command arguments
337 Returns:
338 Formatted command packet
339 """
340 return cmd + b" " + b"".join([(a + b"\0") for a in args])
343def parse_cmd_pkt(line: bytes) -> tuple[bytes, list[bytes]]:
344 """Parse a command packet.
346 Args:
347 line: Command line to parse
349 Returns:
350 Tuple of (command, [arguments])
351 """
352 splice_at = line.find(b" ")
353 cmd, args = line[:splice_at], line[splice_at + 1 :]
354 assert args[-1:] == b"\x00"
355 return cmd, args[:-1].split(b"\0")
358def pkt_line(data: bytes | None) -> bytes:
359 """Wrap data in a pkt-line.
361 Args:
362 data: The data to wrap, as a str or None.
363 Returns: The data prefixed with its length in pkt-line format; if data was
364 None, returns the flush-pkt ('0000').
365 """
366 if data is None:
367 return b"0000"
368 return f"{len(data) + 4:04x}".encode("ascii") + data
371def pkt_seq(*seq: bytes | None) -> bytes:
372 """Wrap a sequence of data in pkt-lines.
374 Args:
375 seq: An iterable of strings to wrap.
376 """
377 return b"".join([pkt_line(s) for s in seq]) + pkt_line(None)
380_HEX_DIGITS = frozenset(b"0123456789abcdefABCDEF")
383def _parse_pkt_line_length(sizestr: bytes) -> int:
384 """Parse the four-byte length prefix of a pkt-line.
386 Mirrors git's ``packet_length()``: the prefix must be exactly four
387 hexadecimal digits. ``int(sizestr, 16)`` on its own also accepts a leading
388 ``+``/``-``, surrounding whitespace, an ``0x`` prefix and digit-group
389 underscores, none of which git allows. Accepting a ``-``-prefixed prefix is
390 the dangerous case: the resulting negative length turns the following
391 ``read(size - 4)`` into a ``read()`` with a negative argument (which reads
392 the rest of the stream into memory) and makes the incremental
393 :class:`PktLineParser` loop without ever consuming its buffer.
395 Args:
396 sizestr: The four raw length bytes read from the wire.
397 Returns: The length encoded by the prefix.
399 Raises:
400 GitProtocolError: if the prefix is not four hexadecimal digits.
401 """
402 if len(sizestr) != 4 or not _HEX_DIGITS.issuperset(sizestr):
403 raise GitProtocolError(f"Invalid pkt-line length prefix: {sizestr!r}")
404 return int(sizestr, 16)
407class Protocol:
408 """Class for interacting with a remote git process over the wire.
410 Parts of the git wire protocol use 'pkt-lines' to communicate. A pkt-line
411 consists of the length of the line as a 4-byte hex string, followed by the
412 payload data. The length includes the 4-byte header. The special line
413 '0000' indicates the end of a section of input and is called a 'flush-pkt'.
415 For details on the pkt-line format, see the cgit distribution:
416 Documentation/technical/protocol-common.txt
417 """
419 def __init__(
420 self,
421 read: Callable[[int], bytes],
422 write: Callable[[bytes], int | None],
423 close: Callable[[], None] | None = None,
424 report_activity: Callable[[int, str], None] | None = None,
425 shutdown_write: Callable[[], None] | None = None,
426 ) -> None:
427 """Initialize Protocol.
429 Args:
430 read: Function to read bytes from the transport
431 write: Function to write bytes to the transport
432 close: Optional function to close the transport
433 report_activity: Optional function to report activity
434 shutdown_write: Optional function to half-close the write side of
435 the transport once the request has been fully sent
436 """
437 self.read = read
438 self.write = write
439 self._close = close
440 self.report_activity = report_activity
441 self._shutdown_write = shutdown_write
442 self._readahead: BytesIO | None = None
444 def shutdown_write(self) -> None:
445 """Half-close the write side of the transport, if supported.
447 Signals to the peer that no more data will be sent. On Windows a
448 server that closes a socket with client data still unread aborts with
449 an RST rather than a clean FIN; letting it read to EOF avoids that.
450 """
451 if self._shutdown_write is not None:
452 self._shutdown_write()
453 self._shutdown_write = None
455 def close(self) -> None:
456 """Close the underlying transport if a close function was provided."""
457 if self._close:
458 self._close()
459 self._close = None # Prevent double-close
461 def __del__(self) -> None:
462 """Ensure transport is closed when Protocol is garbage collected."""
463 if self._close is not None:
464 import warnings
466 warnings.warn(
467 f"unclosed Protocol {self!r}",
468 ResourceWarning,
469 stacklevel=2,
470 source=self,
471 )
472 try:
473 self.close()
474 except Exception:
475 # Ignore errors during cleanup
476 pass
478 def __enter__(self) -> Self:
479 """Enter context manager."""
480 return self
482 def __exit__(
483 self,
484 exc_type: type[BaseException] | None,
485 exc_val: BaseException | None,
486 exc_tb: types.TracebackType | None,
487 ) -> None:
488 """Exit context manager and close transport."""
489 self.close()
491 def read_pkt_line(self) -> bytes | None:
492 """Reads a pkt-line from the remote git process.
494 This method may read from the readahead buffer; see unread_pkt_line.
496 Returns: The next string from the stream, without the length prefix, or
497 None for a flush-pkt ('0000') or delim-pkt ('0001').
498 """
499 if self._readahead is None:
500 read = self.read
501 else:
502 read = self._readahead.read
503 self._readahead = None
505 try:
506 sizestr = read(4)
507 if not sizestr:
508 raise HangupException
509 size = _parse_pkt_line_length(sizestr)
510 if size == 0 or size == 1: # flush-pkt or delim-pkt
511 if self.report_activity:
512 self.report_activity(4, "read")
513 logger.debug("git< %s", sizestr.decode("ascii"))
514 return None
515 if size < 4:
516 raise GitProtocolError(f"Invalid pkt-line length: {size:04x}")
517 if self.report_activity:
518 self.report_activity(size, "read")
519 pkt_contents = read(size - 4)
520 except ConnectionResetError as exc:
521 raise HangupException from exc
522 except OSError as exc:
523 raise GitProtocolError(str(exc)) from exc
524 else:
525 if len(pkt_contents) + 4 != size:
526 raise GitProtocolError(
527 f"Length of pkt read {len(pkt_contents) + 4:04x} does not match length prefix {size:04x}"
528 )
529 # Log the packet contents (truncate if too long for readability)
530 if len(pkt_contents) > 80:
531 logger.debug(
532 "git< %s... (%d bytes)", pkt_contents[:80], len(pkt_contents)
533 )
534 else:
535 logger.debug("git< %s", pkt_contents)
536 return pkt_contents
538 def eof(self) -> bool:
539 """Test whether the protocol stream has reached EOF.
541 Note that this refers to the actual stream EOF and not just a
542 flush-pkt.
544 Returns: True if the stream is at EOF, False otherwise.
545 """
546 try:
547 next_line = self.read_pkt_line()
548 except HangupException:
549 return True
550 self.unread_pkt_line(next_line)
551 return False
553 def unread_pkt_line(self, data: bytes | None) -> None:
554 """Unread a single line of data into the readahead buffer.
556 This method can be used to unread a single pkt-line into a fixed
557 readahead buffer.
559 Args:
560 data: The data to unread, without the length prefix.
562 Raises:
563 ValueError: If more than one pkt-line is unread.
564 """
565 if self._readahead is not None:
566 raise ValueError("Attempted to unread multiple pkt-lines.")
567 self._readahead = BytesIO(pkt_line(data))
569 def read_pkt_seq(self) -> Iterable[bytes]:
570 """Read a sequence of pkt-lines from the remote git process.
572 Returns: Yields each line of data up to but not including the next
573 flush-pkt.
574 """
575 pkt = self.read_pkt_line()
576 while pkt:
577 yield pkt
578 pkt = self.read_pkt_line()
580 def write_pkt_line(self, line: bytes | None) -> None:
581 """Sends a pkt-line to the remote git process.
583 Args:
584 line: A string containing the data to send, without the length
585 prefix.
586 """
587 try:
588 # Log before converting to pkt format
589 if line is None:
590 logger.debug("git> 0000")
591 elif len(line) > 80:
592 logger.debug("git> %s... (%d bytes)", line[:80], len(line))
593 else:
594 logger.debug("git> %s", line)
596 line = pkt_line(line)
597 self.write(line)
598 if self.report_activity:
599 self.report_activity(len(line), "write")
600 except OSError as exc:
601 raise GitProtocolError(str(exc)) from exc
603 def write_sideband(self, channel: int, blob: bytes) -> None:
604 """Write multiplexed data to the sideband.
606 Args:
607 channel: An int specifying the channel to write to.
608 blob: A blob of data (as a string) to send on this channel.
609 """
610 # a pktline can be a max of 65520. a sideband line can therefore be
611 # 65520-5 = 65515
612 # WTF: Why have the len in ASCII, but the channel in binary.
613 while blob:
614 self.write_pkt_line(bytes(bytearray([channel])) + blob[:65515])
615 blob = blob[65515:]
617 def send_cmd(self, cmd: bytes, *args: bytes) -> None:
618 """Send a command and some arguments to a git server.
620 Only used for the TCP git protocol (git://).
622 Args:
623 cmd: The remote service to access.
624 args: List of arguments to send to remove service.
625 """
626 self.write_pkt_line(format_cmd_pkt(cmd, *args))
628 def read_cmd(self) -> tuple[bytes, list[bytes]]:
629 """Read a command and some arguments from the git client.
631 Only used for the TCP git protocol (git://).
633 Returns: A tuple of (command, [list of arguments]).
634 """
635 line = self.read_pkt_line()
636 if line is None:
637 raise GitProtocolError("Expected command, got flush packet")
638 return parse_cmd_pkt(line)
641_RBUFSIZE = 65536 # 64KB buffer for better network I/O performance
644class ReceivableProtocol(Protocol):
645 """Variant of Protocol that allows reading up to a size without blocking.
647 This class has a recv() method that behaves like socket.recv() in addition
648 to a read() method.
650 If you want to read n bytes from the wire and block until exactly n bytes
651 (or EOF) are read, use read(n). If you want to read at most n bytes from
652 the wire but don't care if you get less, use recv(n). Note that recv(n)
653 will still block until at least one byte is read.
654 """
656 def __init__(
657 self,
658 recv: Callable[[int], bytes],
659 write: Callable[[bytes], int | None],
660 close: Callable[[], None] | None = None,
661 report_activity: Callable[[int, str], None] | None = None,
662 rbufsize: int = _RBUFSIZE,
663 ) -> None:
664 """Initialize ReceivableProtocol.
666 Args:
667 recv: Function to receive bytes from the transport
668 write: Function to write bytes to the transport
669 close: Optional function to close the transport
670 report_activity: Optional function to report activity
671 rbufsize: Read buffer size
672 """
673 super().__init__(self.read, write, close=close, report_activity=report_activity)
674 self._recv = recv
675 self._rbuf = BytesIO()
676 self._rbufsize = rbufsize
678 def read(self, size: int) -> bytes:
679 """Read bytes from the socket.
681 Args:
682 size: Number of bytes to read
684 Returns:
685 Bytes read from socket
686 """
687 # From _fileobj.read in socket.py in the Python 2.6.5 standard library,
688 # with the following modifications:
689 # - omit the size <= 0 branch
690 # - seek back to start rather than 0 in case some buffer has been
691 # consumed.
692 # - use SEEK_END instead of the magic number.
693 # Copyright (c) 2001-2010 Python Software Foundation; All Rights
694 # Reserved
695 # Licensed under the Python Software Foundation License.
696 # TODO: see if buffer is more efficient than cBytesIO.
697 assert size > 0
699 # Our use of BytesIO rather than lists of string objects returned by
700 # recv() minimizes memory usage and fragmentation that occurs when
701 # rbufsize is large compared to the typical return value of recv().
702 buf = self._rbuf
703 start = buf.tell()
704 buf.seek(0, SEEK_END)
705 # buffer may have been partially consumed by recv()
706 buf_len = buf.tell() - start
707 if buf_len >= size:
708 # Already have size bytes in our buffer? Extract and return.
709 buf.seek(start)
710 rv = buf.read(size)
711 self._rbuf = BytesIO()
712 self._rbuf.write(buf.read())
713 self._rbuf.seek(0)
714 return rv
716 self._rbuf = BytesIO() # reset _rbuf. we consume it via buf.
717 while True:
718 left = size - buf_len
719 # recv() will malloc the amount of memory given as its
720 # parameter even though it often returns much less data
721 # than that. The returned data string is short lived
722 # as we copy it into a BytesIO and free it. This avoids
723 # fragmentation issues on many platforms.
724 data = self._recv(left)
725 if not data:
726 break
727 n = len(data)
728 if n == size and not buf_len:
729 # Shortcut. Avoid buffer data copies when:
730 # - We have no data in our buffer.
731 # AND
732 # - Our call to recv returned exactly the
733 # number of bytes we were asked to read.
734 return data
735 if n == left:
736 buf.write(data)
737 del data # explicit free
738 break
739 assert n <= left, f"_recv({left}) returned {n} bytes"
740 buf.write(data)
741 buf_len += n
742 del data # explicit free
743 # assert buf_len == buf.tell()
744 buf.seek(start)
745 return buf.read()
747 def recv(self, size: int) -> bytes:
748 """Receive bytes from the socket with buffering.
750 Args:
751 size: Maximum number of bytes to receive
753 Returns:
754 Bytes received from socket
755 """
756 assert size > 0
758 buf = self._rbuf
759 start = buf.tell()
760 buf.seek(0, SEEK_END)
761 buf_len = buf.tell()
762 buf.seek(start)
764 left = buf_len - start
765 if not left:
766 # only read from the wire if our read buffer is exhausted
767 data = self._recv(self._rbufsize)
768 if len(data) == size:
769 # shortcut: skip the buffer if we read exactly size bytes
770 return data
771 buf = BytesIO()
772 buf.write(data)
773 buf.seek(0)
774 del data # explicit free
775 self._rbuf = buf
776 return buf.read(size)
779def extract_capabilities(text: bytes) -> tuple[bytes, list[bytes]]:
780 """Extract a capabilities list from a string, if present.
782 Args:
783 text: String to extract from
784 Returns: Tuple with text with capabilities removed and list of capabilities
785 """
786 if b"\0" not in text:
787 return text, []
788 text, capabilities = text.rstrip().split(b"\0")
789 return (text, capabilities.strip().split(b" "))
792def extract_want_line_capabilities(text: bytes) -> tuple[bytes, list[bytes]]:
793 """Extract a capabilities list from a want line, if present.
795 Note that want lines have capabilities separated from the rest of the line
796 by a space instead of a null byte. Thus want lines have the form:
798 want obj-id cap1 cap2 ...
800 Args:
801 text: Want line to extract from
802 Returns: Tuple with text with capabilities removed and list of capabilities
803 """
804 split_text = text.rstrip().split(b" ")
805 if len(split_text) < 3:
806 return text, []
807 return (b" ".join(split_text[:2]), split_text[2:])
810def ack_type(capabilities: Iterable[bytes]) -> int:
811 """Extract the ack type from a capabilities list."""
812 if b"multi_ack_detailed" in capabilities:
813 return MULTI_ACK_DETAILED
814 elif b"multi_ack" in capabilities:
815 return MULTI_ACK
816 return SINGLE_ACK
819def find_capability(
820 capabilities: Iterable[bytes], *capability_names: bytes
821) -> bytes | None:
822 """Find a capability value in a list of capabilities.
824 This function looks for capabilities that may include arguments after an equals sign
825 and returns only the value part (after the '='). For capabilities without values,
826 returns the capability name itself.
828 Args:
829 capabilities: List of capability strings
830 capability_names: Capability name(s) to search for
832 Returns:
833 The value after '=' if found, or the capability name if no '=', or None if not found
835 Example:
836 >>> caps = [b'filter=blob:none', b'agent=git/2.0', b'thin-pack']
837 >>> find_capability(caps, b'filter')
838 b'blob:none'
839 >>> find_capability(caps, b'thin-pack')
840 b'thin-pack'
841 >>> find_capability(caps, b'missing')
842 None
843 """
844 for cap in capabilities:
845 for name in capability_names:
846 if cap == name:
847 return cap
848 elif cap.startswith(name + b"="):
849 return cap[len(name) + 1 :]
850 return None
853class BufferedPktLineWriter:
854 """Writer that wraps its data in pkt-lines and has an independent buffer.
856 Consecutive calls to write() wrap the data in a pkt-line and then buffers
857 it until enough lines have been written such that their total length
858 (including length prefix) reach the buffer size.
859 """
861 def __init__(
862 self, write: Callable[[bytes], int | None], bufsize: int = 65515
863 ) -> None:
864 """Initialize the BufferedPktLineWriter.
866 Args:
867 write: A write callback for the underlying writer.
868 bufsize: The internal buffer size, including length prefixes.
869 """
870 self._write = write
871 self._bufsize = bufsize
872 self._wbuf = BytesIO()
873 self._buflen = 0
875 def write(self, data: bytes) -> None:
876 """Write data, wrapping it in a pkt-line."""
877 line = pkt_line(data)
878 line_len = len(line)
879 over = self._buflen + line_len - self._bufsize
880 if over >= 0:
881 start = line_len - over
882 self._wbuf.write(line[:start])
883 self.flush()
884 else:
885 start = 0
886 saved = line[start:]
887 self._wbuf.write(saved)
888 self._buflen += len(saved)
890 def flush(self) -> None:
891 """Flush all data from the buffer."""
892 data = self._wbuf.getvalue()
893 if data:
894 self._write(data)
895 self._len = 0
896 self._wbuf = BytesIO()
899class PktLineParser:
900 """Packet line parser that hands completed packets off to a callback."""
902 def __init__(self, handle_pkt: Callable[[bytes | None], None]) -> None:
903 """Initialize PktLineParser.
905 Args:
906 handle_pkt: Callback function to handle completed packets
907 """
908 self.handle_pkt = handle_pkt
909 self._readahead = BytesIO()
911 def parse(self, data: bytes) -> None:
912 """Parse a fragment of data and call back for any completed packets."""
913 self._readahead.write(data)
914 buf = self._readahead.getvalue()
915 if len(buf) < 4:
916 return
917 while len(buf) >= 4:
918 size = _parse_pkt_line_length(buf[:4])
919 if size == 0:
920 self.handle_pkt(None)
921 buf = buf[4:]
922 elif size < 4:
923 raise GitProtocolError(f"Invalid pkt-line length: {size:04x}")
924 elif size <= len(buf):
925 self.handle_pkt(buf[4:size])
926 buf = buf[size:]
927 else:
928 break
929 self._readahead = BytesIO()
930 self._readahead.write(buf)
932 def get_tail(self) -> bytes:
933 """Read back any unused data."""
934 return self._readahead.getvalue()
937def format_capability_line(capabilities: Iterable[bytes]) -> bytes:
938 """Format a capabilities list for the wire protocol.
940 Args:
941 capabilities: List of capability strings
943 Returns:
944 Space-separated capabilities as bytes
945 """
946 return b"".join([b" " + c for c in capabilities])
949def format_ref_line(
950 ref: bytes, sha: bytes, capabilities: Sequence[bytes] | None = None
951) -> bytes:
952 """Format a ref advertisement line.
954 Args:
955 ref: Reference name
956 sha: SHA hash
957 capabilities: Optional list of capabilities
959 Returns:
960 Formatted ref line
961 """
962 if capabilities is None:
963 return sha + b" " + ref + b"\n"
964 else:
965 return sha + b" " + ref + b"\0" + format_capability_line(capabilities) + b"\n"
968def format_shallow_line(sha: bytes) -> bytes:
969 """Format a shallow line.
971 Args:
972 sha: SHA to mark as shallow
974 Returns:
975 Formatted shallow line
976 """
977 return COMMAND_SHALLOW + b" " + sha
980def format_unshallow_line(sha: bytes) -> bytes:
981 """Format an unshallow line.
983 Args:
984 sha: SHA to unshallow
986 Returns:
987 Formatted unshallow line
988 """
989 return COMMAND_UNSHALLOW + b" " + sha
992def format_ack_line(sha: bytes, ack_type: bytes = b"") -> bytes:
993 """Format an ACK line.
995 Args:
996 sha: SHA to acknowledge
997 ack_type: Optional ACK type (e.g. b"continue")
999 Returns:
1000 Formatted ACK line
1001 """
1002 if ack_type:
1003 ack_type = b" " + ack_type
1004 return b"ACK " + sha + ack_type + b"\n"
1007def strip_peeled_refs(
1008 refs: "Mapping[Ref, ObjectID | None]",
1009) -> "dict[Ref, ObjectID | None]":
1010 """Remove all peeled refs from a refs dictionary.
1012 Args:
1013 refs: Dictionary of refs (may include peeled refs with ^{} suffix)
1015 Returns:
1016 Dictionary with peeled refs removed
1017 """
1018 return {
1019 ref: sha for (ref, sha) in refs.items() if not ref.endswith(PEELED_TAG_SUFFIX)
1020 }
1023def split_peeled_refs(
1024 refs: "Mapping[Ref, ObjectID]",
1025) -> "tuple[dict[Ref, ObjectID], dict[Ref, ObjectID]]":
1026 """Split peeled refs from regular refs.
1028 Args:
1029 refs: Dictionary of refs (may include peeled refs with ^{} suffix)
1031 Returns:
1032 Tuple of (regular_refs, peeled_refs) where peeled_refs keys have
1033 the ^{} suffix removed
1034 """
1035 from .refs import Ref
1037 peeled: dict[Ref, ObjectID] = {}
1038 regular = {k: v for k, v in refs.items() if not k.endswith(PEELED_TAG_SUFFIX)}
1040 for ref, sha in refs.items():
1041 if ref.endswith(PEELED_TAG_SUFFIX):
1042 # Peeled refs are always ObjectID values
1043 peeled[Ref(ref[: -len(PEELED_TAG_SUFFIX)])] = sha
1045 return regular, peeled
1048def write_info_refs(
1049 refs: "Mapping[Ref, ObjectID]", store: "ObjectContainer"
1050) -> "Iterator[bytes]":
1051 """Generate info refs in the format used by the dumb HTTP protocol.
1053 Args:
1054 refs: Dictionary of refs
1055 store: Object store to peel tags from
1057 Yields:
1058 Lines in info/refs format (sha + tab + refname)
1059 """
1060 from .object_store import peel_sha
1061 from .refs import HEADREF
1063 for name, sha in sorted(refs.items()):
1064 # get_refs() includes HEAD as a special case, but we don't want to
1065 # advertise it
1066 if name == HEADREF:
1067 continue
1068 try:
1069 o = store[sha]
1070 except KeyError:
1071 continue
1072 _unpeeled, peeled = peel_sha(store, sha)
1073 yield o.id + b"\t" + name + b"\n"
1074 if o.id != peeled.id:
1075 yield peeled.id + b"\t" + name + PEELED_TAG_SUFFIX + b"\n"
1078def serialize_refs(
1079 store: "ObjectContainer", refs: "Mapping[Ref, ObjectID]"
1080) -> "dict[bytes, ObjectID]":
1081 """Serialize refs with peeled refs for Git protocol v0/v1.
1083 This function is used to prepare refs for transmission over the Git protocol.
1084 For tags, it includes both the tag object and the dereferenced object.
1086 Args:
1087 store: Object store to peel refs from
1088 refs: Dictionary of ref names to SHAs
1090 Returns:
1091 Dictionary with refs and peeled refs (marked with ^{})
1092 """
1093 import warnings
1095 from .object_store import peel_sha
1097 ret: dict[bytes, ObjectID] = {}
1098 for ref, sha in refs.items():
1099 try:
1100 unpeeled, peeled = peel_sha(store, ObjectID(sha))
1101 except KeyError:
1102 warnings.warn(
1103 "ref {} points at non-present sha {}".format(
1104 ref.decode("utf-8", "replace"), sha.decode("ascii")
1105 ),
1106 UserWarning,
1107 )
1108 continue
1109 else:
1110 if isinstance(unpeeled, Tag):
1111 ret[ref + PEELED_TAG_SUFFIX] = peeled.id
1112 ret[ref] = unpeeled.id
1113 return ret