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#
22
23"""Generic functions for talking the git smart server protocol."""
24
25import types
26from collections.abc import Iterable
27from io import BytesIO
28from os import SEEK_END
29from typing import Callable, Optional
30
31import dulwich
32
33from .errors import GitProtocolError, HangupException
34
35TCP_GIT_PORT = 9418
36
37# Git protocol version 0 is the original Git protocol, which lacked a
38# version number until Git protocol version 1 was introduced by Brandon
39# Williams in 2017.
40#
41# Protocol version 1 is simply the original v0 protocol with the addition of
42# a single packet line, which precedes the ref advertisement, indicating the
43# protocol version being used. This was done in preparation for protocol v2.
44#
45# Git protocol version 2 was first introduced by Brandon Williams in 2018 and
46# adds many features. See the gitprotocol-v2(5) manual page for details.
47# As of 2024, Git only implements version 2 during 'git fetch' and still uses
48# version 0 during 'git push'.
49GIT_PROTOCOL_VERSIONS = [0, 1, 2]
50DEFAULT_GIT_PROTOCOL_VERSION_FETCH = 2
51DEFAULT_GIT_PROTOCOL_VERSION_SEND = 0
52
53ZERO_SHA = b"0" * 40
54
55SINGLE_ACK = 0
56MULTI_ACK = 1
57MULTI_ACK_DETAILED = 2
58
59# pack data
60SIDE_BAND_CHANNEL_DATA = 1
61# progress messages
62SIDE_BAND_CHANNEL_PROGRESS = 2
63# fatal error message just before stream aborts
64SIDE_BAND_CHANNEL_FATAL = 3
65
66CAPABILITY_ATOMIC = b"atomic"
67CAPABILITY_DEEPEN_SINCE = b"deepen-since"
68CAPABILITY_DEEPEN_NOT = b"deepen-not"
69CAPABILITY_DEEPEN_RELATIVE = b"deepen-relative"
70CAPABILITY_DELETE_REFS = b"delete-refs"
71CAPABILITY_INCLUDE_TAG = b"include-tag"
72CAPABILITY_MULTI_ACK = b"multi_ack"
73CAPABILITY_MULTI_ACK_DETAILED = b"multi_ack_detailed"
74CAPABILITY_NO_DONE = b"no-done"
75CAPABILITY_NO_PROGRESS = b"no-progress"
76CAPABILITY_OFS_DELTA = b"ofs-delta"
77CAPABILITY_QUIET = b"quiet"
78CAPABILITY_REPORT_STATUS = b"report-status"
79CAPABILITY_SHALLOW = b"shallow"
80CAPABILITY_SIDE_BAND = b"side-band"
81CAPABILITY_SIDE_BAND_64K = b"side-band-64k"
82CAPABILITY_THIN_PACK = b"thin-pack"
83CAPABILITY_AGENT = b"agent"
84CAPABILITY_SYMREF = b"symref"
85CAPABILITY_ALLOW_TIP_SHA1_IN_WANT = b"allow-tip-sha1-in-want"
86CAPABILITY_ALLOW_REACHABLE_SHA1_IN_WANT = b"allow-reachable-sha1-in-want"
87CAPABILITY_FETCH = b"fetch"
88CAPABILITY_FILTER = b"filter"
89
90# Magic ref that is used to attach capabilities to when
91# there are no refs. Should always be ste to ZERO_SHA.
92CAPABILITIES_REF = b"capabilities^{}"
93
94COMMON_CAPABILITIES = [
95 CAPABILITY_OFS_DELTA,
96 CAPABILITY_SIDE_BAND,
97 CAPABILITY_SIDE_BAND_64K,
98 CAPABILITY_AGENT,
99 CAPABILITY_NO_PROGRESS,
100]
101KNOWN_UPLOAD_CAPABILITIES = set(
102 [
103 *COMMON_CAPABILITIES,
104 CAPABILITY_THIN_PACK,
105 CAPABILITY_MULTI_ACK,
106 CAPABILITY_MULTI_ACK_DETAILED,
107 CAPABILITY_INCLUDE_TAG,
108 CAPABILITY_DEEPEN_SINCE,
109 CAPABILITY_SYMREF,
110 CAPABILITY_SHALLOW,
111 CAPABILITY_DEEPEN_NOT,
112 CAPABILITY_DEEPEN_RELATIVE,
113 CAPABILITY_ALLOW_TIP_SHA1_IN_WANT,
114 CAPABILITY_ALLOW_REACHABLE_SHA1_IN_WANT,
115 CAPABILITY_FETCH,
116 ]
117)
118KNOWN_RECEIVE_CAPABILITIES = set(
119 [
120 *COMMON_CAPABILITIES,
121 CAPABILITY_REPORT_STATUS,
122 CAPABILITY_DELETE_REFS,
123 CAPABILITY_QUIET,
124 CAPABILITY_ATOMIC,
125 ]
126)
127
128DEPTH_INFINITE = 0x7FFFFFFF
129
130NAK_LINE = b"NAK\n"
131
132
133def agent_string() -> bytes:
134 """Generate the agent string for dulwich.
135
136 Returns:
137 Agent string as bytes
138 """
139 return ("dulwich/" + ".".join(map(str, dulwich.__version__))).encode("ascii")
140
141
142def capability_agent() -> bytes:
143 """Generate the agent capability string.
144
145 Returns:
146 Agent capability with dulwich version
147 """
148 return CAPABILITY_AGENT + b"=" + agent_string()
149
150
151def capability_symref(from_ref: bytes, to_ref: bytes) -> bytes:
152 """Generate a symref capability string.
153
154 Args:
155 from_ref: Source reference name
156 to_ref: Target reference name
157
158 Returns:
159 Symref capability string
160 """
161 return CAPABILITY_SYMREF + b"=" + from_ref + b":" + to_ref
162
163
164def extract_capability_names(capabilities: Iterable[bytes]) -> set[bytes]:
165 """Extract capability names from a list of capabilities.
166
167 Args:
168 capabilities: List of capability strings
169
170 Returns:
171 Set of capability names
172 """
173 return {parse_capability(c)[0] for c in capabilities}
174
175
176def parse_capability(capability: bytes) -> tuple[bytes, Optional[bytes]]:
177 """Parse a capability string into name and value.
178
179 Args:
180 capability: Capability string
181
182 Returns:
183 Tuple of (capability_name, capability_value)
184 """
185 parts = capability.split(b"=", 1)
186 if len(parts) == 1:
187 return (parts[0], None)
188 return (parts[0], parts[1])
189
190
191def symref_capabilities(symrefs: Iterable[tuple[bytes, bytes]]) -> list[bytes]:
192 """Generate symref capability strings from symref pairs.
193
194 Args:
195 symrefs: Iterable of (from_ref, to_ref) tuples
196
197 Returns:
198 List of symref capability strings
199 """
200 return [capability_symref(*k) for k in symrefs]
201
202
203COMMAND_DEEPEN = b"deepen"
204COMMAND_SHALLOW = b"shallow"
205COMMAND_UNSHALLOW = b"unshallow"
206COMMAND_DONE = b"done"
207COMMAND_WANT = b"want"
208COMMAND_HAVE = b"have"
209
210
211def format_cmd_pkt(cmd: bytes, *args: bytes) -> bytes:
212 """Format a command packet.
213
214 Args:
215 cmd: Command name
216 *args: Command arguments
217
218 Returns:
219 Formatted command packet
220 """
221 return cmd + b" " + b"".join([(a + b"\0") for a in args])
222
223
224def parse_cmd_pkt(line: bytes) -> tuple[bytes, list[bytes]]:
225 """Parse a command packet.
226
227 Args:
228 line: Command line to parse
229
230 Returns:
231 Tuple of (command, [arguments])
232 """
233 splice_at = line.find(b" ")
234 cmd, args = line[:splice_at], line[splice_at + 1 :]
235 assert args[-1:] == b"\x00"
236 return cmd, args[:-1].split(b"\0")
237
238
239def pkt_line(data: Optional[bytes]) -> bytes:
240 """Wrap data in a pkt-line.
241
242 Args:
243 data: The data to wrap, as a str or None.
244 Returns: The data prefixed with its length in pkt-line format; if data was
245 None, returns the flush-pkt ('0000').
246 """
247 if data is None:
248 return b"0000"
249 return ("%04x" % (len(data) + 4)).encode("ascii") + data
250
251
252def pkt_seq(*seq: Optional[bytes]) -> bytes:
253 """Wrap a sequence of data in pkt-lines.
254
255 Args:
256 seq: An iterable of strings to wrap.
257 """
258 return b"".join([pkt_line(s) for s in seq]) + pkt_line(None)
259
260
261class Protocol:
262 """Class for interacting with a remote git process over the wire.
263
264 Parts of the git wire protocol use 'pkt-lines' to communicate. A pkt-line
265 consists of the length of the line as a 4-byte hex string, followed by the
266 payload data. The length includes the 4-byte header. The special line
267 '0000' indicates the end of a section of input and is called a 'flush-pkt'.
268
269 For details on the pkt-line format, see the cgit distribution:
270 Documentation/technical/protocol-common.txt
271 """
272
273 def __init__(
274 self,
275 read: Callable[[int], bytes],
276 write: Callable[[bytes], Optional[int]],
277 close: Optional[Callable[[], None]] = None,
278 report_activity: Optional[Callable[[int, str], None]] = None,
279 ) -> None:
280 """Initialize Protocol.
281
282 Args:
283 read: Function to read bytes from the transport
284 write: Function to write bytes to the transport
285 close: Optional function to close the transport
286 report_activity: Optional function to report activity
287 """
288 self.read = read
289 self.write = write
290 self._close = close
291 self.report_activity = report_activity
292 self._readahead: Optional[BytesIO] = None
293
294 def close(self) -> None:
295 """Close the underlying transport if a close function was provided."""
296 if self._close:
297 self._close()
298
299 def __enter__(self) -> "Protocol":
300 """Enter context manager."""
301 return self
302
303 def __exit__(
304 self,
305 exc_type: Optional[type[BaseException]],
306 exc_val: Optional[BaseException],
307 exc_tb: Optional[types.TracebackType],
308 ) -> None:
309 """Exit context manager and close transport."""
310 self.close()
311
312 def read_pkt_line(self) -> Optional[bytes]:
313 """Reads a pkt-line from the remote git process.
314
315 This method may read from the readahead buffer; see unread_pkt_line.
316
317 Returns: The next string from the stream, without the length prefix, or
318 None for a flush-pkt ('0000') or delim-pkt ('0001').
319 """
320 if self._readahead is None:
321 read = self.read
322 else:
323 read = self._readahead.read
324 self._readahead = None
325
326 try:
327 sizestr = read(4)
328 if not sizestr:
329 raise HangupException
330 size = int(sizestr, 16)
331 if size == 0 or size == 1: # flush-pkt or delim-pkt
332 if self.report_activity:
333 self.report_activity(4, "read")
334 return None
335 if self.report_activity:
336 self.report_activity(size, "read")
337 pkt_contents = read(size - 4)
338 except ConnectionResetError as exc:
339 raise HangupException from exc
340 except OSError as exc:
341 raise GitProtocolError(str(exc)) from exc
342 else:
343 if len(pkt_contents) + 4 != size:
344 raise GitProtocolError(
345 f"Length of pkt read {len(pkt_contents) + 4:04x} does not match length prefix {size:04x}"
346 )
347 return pkt_contents
348
349 def eof(self) -> bool:
350 """Test whether the protocol stream has reached EOF.
351
352 Note that this refers to the actual stream EOF and not just a
353 flush-pkt.
354
355 Returns: True if the stream is at EOF, False otherwise.
356 """
357 try:
358 next_line = self.read_pkt_line()
359 except HangupException:
360 return True
361 self.unread_pkt_line(next_line)
362 return False
363
364 def unread_pkt_line(self, data: Optional[bytes]) -> None:
365 """Unread a single line of data into the readahead buffer.
366
367 This method can be used to unread a single pkt-line into a fixed
368 readahead buffer.
369
370 Args:
371 data: The data to unread, without the length prefix.
372
373 Raises:
374 ValueError: If more than one pkt-line is unread.
375 """
376 if self._readahead is not None:
377 raise ValueError("Attempted to unread multiple pkt-lines.")
378 self._readahead = BytesIO(pkt_line(data))
379
380 def read_pkt_seq(self) -> Iterable[bytes]:
381 """Read a sequence of pkt-lines from the remote git process.
382
383 Returns: Yields each line of data up to but not including the next
384 flush-pkt.
385 """
386 pkt = self.read_pkt_line()
387 while pkt:
388 yield pkt
389 pkt = self.read_pkt_line()
390
391 def write_pkt_line(self, line: Optional[bytes]) -> None:
392 """Sends a pkt-line to the remote git process.
393
394 Args:
395 line: A string containing the data to send, without the length
396 prefix.
397 """
398 try:
399 line = pkt_line(line)
400 self.write(line)
401 if self.report_activity:
402 self.report_activity(len(line), "write")
403 except OSError as exc:
404 raise GitProtocolError(str(exc)) from exc
405
406 def write_sideband(self, channel: int, blob: bytes) -> None:
407 """Write multiplexed data to the sideband.
408
409 Args:
410 channel: An int specifying the channel to write to.
411 blob: A blob of data (as a string) to send on this channel.
412 """
413 # a pktline can be a max of 65520. a sideband line can therefore be
414 # 65520-5 = 65515
415 # WTF: Why have the len in ASCII, but the channel in binary.
416 while blob:
417 self.write_pkt_line(bytes(bytearray([channel])) + blob[:65515])
418 blob = blob[65515:]
419
420 def send_cmd(self, cmd: bytes, *args: bytes) -> None:
421 """Send a command and some arguments to a git server.
422
423 Only used for the TCP git protocol (git://).
424
425 Args:
426 cmd: The remote service to access.
427 args: List of arguments to send to remove service.
428 """
429 self.write_pkt_line(format_cmd_pkt(cmd, *args))
430
431 def read_cmd(self) -> tuple[bytes, list[bytes]]:
432 """Read a command and some arguments from the git client.
433
434 Only used for the TCP git protocol (git://).
435
436 Returns: A tuple of (command, [list of arguments]).
437 """
438 line = self.read_pkt_line()
439 if line is None:
440 raise GitProtocolError("Expected command, got flush packet")
441 return parse_cmd_pkt(line)
442
443
444_RBUFSIZE = 65536 # 64KB buffer for better network I/O performance
445
446
447class ReceivableProtocol(Protocol):
448 """Variant of Protocol that allows reading up to a size without blocking.
449
450 This class has a recv() method that behaves like socket.recv() in addition
451 to a read() method.
452
453 If you want to read n bytes from the wire and block until exactly n bytes
454 (or EOF) are read, use read(n). If you want to read at most n bytes from
455 the wire but don't care if you get less, use recv(n). Note that recv(n)
456 will still block until at least one byte is read.
457 """
458
459 def __init__(
460 self,
461 recv: Callable[[int], bytes],
462 write: Callable[[bytes], Optional[int]],
463 close: Optional[Callable[[], None]] = None,
464 report_activity: Optional[Callable[[int, str], None]] = None,
465 rbufsize: int = _RBUFSIZE,
466 ) -> None:
467 """Initialize ReceivableProtocol.
468
469 Args:
470 recv: Function to receive bytes from the transport
471 write: Function to write bytes to the transport
472 close: Optional function to close the transport
473 report_activity: Optional function to report activity
474 rbufsize: Read buffer size
475 """
476 super().__init__(self.read, write, close=close, report_activity=report_activity)
477 self._recv = recv
478 self._rbuf = BytesIO()
479 self._rbufsize = rbufsize
480
481 def read(self, size: int) -> bytes:
482 """Read bytes from the socket.
483
484 Args:
485 size: Number of bytes to read
486
487 Returns:
488 Bytes read from socket
489 """
490 # From _fileobj.read in socket.py in the Python 2.6.5 standard library,
491 # with the following modifications:
492 # - omit the size <= 0 branch
493 # - seek back to start rather than 0 in case some buffer has been
494 # consumed.
495 # - use SEEK_END instead of the magic number.
496 # Copyright (c) 2001-2010 Python Software Foundation; All Rights
497 # Reserved
498 # Licensed under the Python Software Foundation License.
499 # TODO: see if buffer is more efficient than cBytesIO.
500 assert size > 0
501
502 # Our use of BytesIO rather than lists of string objects returned by
503 # recv() minimizes memory usage and fragmentation that occurs when
504 # rbufsize is large compared to the typical return value of recv().
505 buf = self._rbuf
506 start = buf.tell()
507 buf.seek(0, SEEK_END)
508 # buffer may have been partially consumed by recv()
509 buf_len = buf.tell() - start
510 if buf_len >= size:
511 # Already have size bytes in our buffer? Extract and return.
512 buf.seek(start)
513 rv = buf.read(size)
514 self._rbuf = BytesIO()
515 self._rbuf.write(buf.read())
516 self._rbuf.seek(0)
517 return rv
518
519 self._rbuf = BytesIO() # reset _rbuf. we consume it via buf.
520 while True:
521 left = size - buf_len
522 # recv() will malloc the amount of memory given as its
523 # parameter even though it often returns much less data
524 # than that. The returned data string is short lived
525 # as we copy it into a BytesIO and free it. This avoids
526 # fragmentation issues on many platforms.
527 data = self._recv(left)
528 if not data:
529 break
530 n = len(data)
531 if n == size and not buf_len:
532 # Shortcut. Avoid buffer data copies when:
533 # - We have no data in our buffer.
534 # AND
535 # - Our call to recv returned exactly the
536 # number of bytes we were asked to read.
537 return data
538 if n == left:
539 buf.write(data)
540 del data # explicit free
541 break
542 assert n <= left, f"_recv({left}) returned {n} bytes"
543 buf.write(data)
544 buf_len += n
545 del data # explicit free
546 # assert buf_len == buf.tell()
547 buf.seek(start)
548 return buf.read()
549
550 def recv(self, size: int) -> bytes:
551 """Receive bytes from the socket with buffering.
552
553 Args:
554 size: Maximum number of bytes to receive
555
556 Returns:
557 Bytes received from socket
558 """
559 assert size > 0
560
561 buf = self._rbuf
562 start = buf.tell()
563 buf.seek(0, SEEK_END)
564 buf_len = buf.tell()
565 buf.seek(start)
566
567 left = buf_len - start
568 if not left:
569 # only read from the wire if our read buffer is exhausted
570 data = self._recv(self._rbufsize)
571 if len(data) == size:
572 # shortcut: skip the buffer if we read exactly size bytes
573 return data
574 buf = BytesIO()
575 buf.write(data)
576 buf.seek(0)
577 del data # explicit free
578 self._rbuf = buf
579 return buf.read(size)
580
581
582def extract_capabilities(text: bytes) -> tuple[bytes, list[bytes]]:
583 """Extract a capabilities list from a string, if present.
584
585 Args:
586 text: String to extract from
587 Returns: Tuple with text with capabilities removed and list of capabilities
588 """
589 if b"\0" not in text:
590 return text, []
591 text, capabilities = text.rstrip().split(b"\0")
592 return (text, capabilities.strip().split(b" "))
593
594
595def extract_want_line_capabilities(text: bytes) -> tuple[bytes, list[bytes]]:
596 """Extract a capabilities list from a want line, if present.
597
598 Note that want lines have capabilities separated from the rest of the line
599 by a space instead of a null byte. Thus want lines have the form:
600
601 want obj-id cap1 cap2 ...
602
603 Args:
604 text: Want line to extract from
605 Returns: Tuple with text with capabilities removed and list of capabilities
606 """
607 split_text = text.rstrip().split(b" ")
608 if len(split_text) < 3:
609 return text, []
610 return (b" ".join(split_text[:2]), split_text[2:])
611
612
613def ack_type(capabilities: Iterable[bytes]) -> int:
614 """Extract the ack type from a capabilities list."""
615 if b"multi_ack_detailed" in capabilities:
616 return MULTI_ACK_DETAILED
617 elif b"multi_ack" in capabilities:
618 return MULTI_ACK
619 return SINGLE_ACK
620
621
622class BufferedPktLineWriter:
623 """Writer that wraps its data in pkt-lines and has an independent buffer.
624
625 Consecutive calls to write() wrap the data in a pkt-line and then buffers
626 it until enough lines have been written such that their total length
627 (including length prefix) reach the buffer size.
628 """
629
630 def __init__(
631 self, write: Callable[[bytes], Optional[int]], bufsize: int = 65515
632 ) -> None:
633 """Initialize the BufferedPktLineWriter.
634
635 Args:
636 write: A write callback for the underlying writer.
637 bufsize: The internal buffer size, including length prefixes.
638 """
639 self._write = write
640 self._bufsize = bufsize
641 self._wbuf = BytesIO()
642 self._buflen = 0
643
644 def write(self, data: bytes) -> None:
645 """Write data, wrapping it in a pkt-line."""
646 line = pkt_line(data)
647 line_len = len(line)
648 over = self._buflen + line_len - self._bufsize
649 if over >= 0:
650 start = line_len - over
651 self._wbuf.write(line[:start])
652 self.flush()
653 else:
654 start = 0
655 saved = line[start:]
656 self._wbuf.write(saved)
657 self._buflen += len(saved)
658
659 def flush(self) -> None:
660 """Flush all data from the buffer."""
661 data = self._wbuf.getvalue()
662 if data:
663 self._write(data)
664 self._len = 0
665 self._wbuf = BytesIO()
666
667
668class PktLineParser:
669 """Packet line parser that hands completed packets off to a callback."""
670
671 def __init__(self, handle_pkt: Callable[[Optional[bytes]], None]) -> None:
672 """Initialize PktLineParser.
673
674 Args:
675 handle_pkt: Callback function to handle completed packets
676 """
677 self.handle_pkt = handle_pkt
678 self._readahead = BytesIO()
679
680 def parse(self, data: bytes) -> None:
681 """Parse a fragment of data and call back for any completed packets."""
682 self._readahead.write(data)
683 buf = self._readahead.getvalue()
684 if len(buf) < 4:
685 return
686 while len(buf) >= 4:
687 size = int(buf[:4], 16)
688 if size == 0:
689 self.handle_pkt(None)
690 buf = buf[4:]
691 elif size <= len(buf):
692 self.handle_pkt(buf[4:size])
693 buf = buf[size:]
694 else:
695 break
696 self._readahead = BytesIO()
697 self._readahead.write(buf)
698
699 def get_tail(self) -> bytes:
700 """Read back any unused data."""
701 return self._readahead.getvalue()
702
703
704def format_capability_line(capabilities: Iterable[bytes]) -> bytes:
705 """Format a capabilities list for the wire protocol.
706
707 Args:
708 capabilities: List of capability strings
709
710 Returns:
711 Space-separated capabilities as bytes
712 """
713 return b"".join([b" " + c for c in capabilities])
714
715
716def format_ref_line(
717 ref: bytes, sha: bytes, capabilities: Optional[list[bytes]] = None
718) -> bytes:
719 """Format a ref advertisement line.
720
721 Args:
722 ref: Reference name
723 sha: SHA hash
724 capabilities: Optional list of capabilities
725
726 Returns:
727 Formatted ref line
728 """
729 if capabilities is None:
730 return sha + b" " + ref + b"\n"
731 else:
732 return sha + b" " + ref + b"\0" + format_capability_line(capabilities) + b"\n"
733
734
735def format_shallow_line(sha: bytes) -> bytes:
736 """Format a shallow line.
737
738 Args:
739 sha: SHA to mark as shallow
740
741 Returns:
742 Formatted shallow line
743 """
744 return COMMAND_SHALLOW + b" " + sha
745
746
747def format_unshallow_line(sha: bytes) -> bytes:
748 """Format an unshallow line.
749
750 Args:
751 sha: SHA to unshallow
752
753 Returns:
754 Formatted unshallow line
755 """
756 return COMMAND_UNSHALLOW + b" " + sha
757
758
759def format_ack_line(sha: bytes, ack_type: bytes = b"") -> bytes:
760 """Format an ACK line.
761
762 Args:
763 sha: SHA to acknowledge
764 ack_type: Optional ACK type (e.g. b"continue")
765
766 Returns:
767 Formatted ACK line
768 """
769 if ack_type:
770 ack_type = b" " + ack_type
771 return b"ACK " + sha + ack_type + b"\n"