Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dulwich/protocol.py: 34%
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."""
25import types
26from collections.abc import Iterable, Sequence
27from io import BytesIO
28from os import SEEK_END
29from typing import Callable, Optional
31import dulwich
33from .errors import GitProtocolError, HangupException
35TCP_GIT_PORT = 9418
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
53ZERO_SHA = b"0" * 40
55SINGLE_ACK = 0
56MULTI_ACK = 1
57MULTI_ACK_DETAILED = 2
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
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"
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^{}"
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)
128DEPTH_INFINITE = 0x7FFFFFFF
130NAK_LINE = b"NAK\n"
133def agent_string() -> bytes:
134 """Generate the agent string for dulwich.
136 Returns:
137 Agent string as bytes
138 """
139 return ("dulwich/" + ".".join(map(str, dulwich.__version__))).encode("ascii")
142def capability_agent() -> bytes:
143 """Generate the agent capability string.
145 Returns:
146 Agent capability with dulwich version
147 """
148 return CAPABILITY_AGENT + b"=" + agent_string()
151def capability_symref(from_ref: bytes, to_ref: bytes) -> bytes:
152 """Generate a symref capability string.
154 Args:
155 from_ref: Source reference name
156 to_ref: Target reference name
158 Returns:
159 Symref capability string
160 """
161 return CAPABILITY_SYMREF + b"=" + from_ref + b":" + to_ref
164def extract_capability_names(capabilities: Iterable[bytes]) -> set[bytes]:
165 """Extract capability names from a list of capabilities.
167 Args:
168 capabilities: List of capability strings
170 Returns:
171 Set of capability names
172 """
173 return {parse_capability(c)[0] for c in capabilities}
176def parse_capability(capability: bytes) -> tuple[bytes, Optional[bytes]]:
177 """Parse a capability string into name and value.
179 Args:
180 capability: Capability string
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])
191def symref_capabilities(symrefs: Iterable[tuple[bytes, bytes]]) -> list[bytes]:
192 """Generate symref capability strings from symref pairs.
194 Args:
195 symrefs: Iterable of (from_ref, to_ref) tuples
197 Returns:
198 List of symref capability strings
199 """
200 return [capability_symref(*k) for k in symrefs]
203COMMAND_DEEPEN = b"deepen"
204COMMAND_DEEPEN_SINCE = b"deepen-since"
205COMMAND_DEEPEN_NOT = b"deepen-not"
206COMMAND_SHALLOW = b"shallow"
207COMMAND_UNSHALLOW = b"unshallow"
208COMMAND_DONE = b"done"
209COMMAND_WANT = b"want"
210COMMAND_HAVE = b"have"
213def format_cmd_pkt(cmd: bytes, *args: bytes) -> bytes:
214 """Format a command packet.
216 Args:
217 cmd: Command name
218 *args: Command arguments
220 Returns:
221 Formatted command packet
222 """
223 return cmd + b" " + b"".join([(a + b"\0") for a in args])
226def parse_cmd_pkt(line: bytes) -> tuple[bytes, list[bytes]]:
227 """Parse a command packet.
229 Args:
230 line: Command line to parse
232 Returns:
233 Tuple of (command, [arguments])
234 """
235 splice_at = line.find(b" ")
236 cmd, args = line[:splice_at], line[splice_at + 1 :]
237 assert args[-1:] == b"\x00"
238 return cmd, args[:-1].split(b"\0")
241def pkt_line(data: Optional[bytes]) -> bytes:
242 """Wrap data in a pkt-line.
244 Args:
245 data: The data to wrap, as a str or None.
246 Returns: The data prefixed with its length in pkt-line format; if data was
247 None, returns the flush-pkt ('0000').
248 """
249 if data is None:
250 return b"0000"
251 return ("%04x" % (len(data) + 4)).encode("ascii") + data
254def pkt_seq(*seq: Optional[bytes]) -> bytes:
255 """Wrap a sequence of data in pkt-lines.
257 Args:
258 seq: An iterable of strings to wrap.
259 """
260 return b"".join([pkt_line(s) for s in seq]) + pkt_line(None)
263class Protocol:
264 """Class for interacting with a remote git process over the wire.
266 Parts of the git wire protocol use 'pkt-lines' to communicate. A pkt-line
267 consists of the length of the line as a 4-byte hex string, followed by the
268 payload data. The length includes the 4-byte header. The special line
269 '0000' indicates the end of a section of input and is called a 'flush-pkt'.
271 For details on the pkt-line format, see the cgit distribution:
272 Documentation/technical/protocol-common.txt
273 """
275 def __init__(
276 self,
277 read: Callable[[int], bytes],
278 write: Callable[[bytes], Optional[int]],
279 close: Optional[Callable[[], None]] = None,
280 report_activity: Optional[Callable[[int, str], None]] = None,
281 ) -> None:
282 """Initialize Protocol.
284 Args:
285 read: Function to read bytes from the transport
286 write: Function to write bytes to the transport
287 close: Optional function to close the transport
288 report_activity: Optional function to report activity
289 """
290 self.read = read
291 self.write = write
292 self._close = close
293 self.report_activity = report_activity
294 self._readahead: Optional[BytesIO] = None
296 def close(self) -> None:
297 """Close the underlying transport if a close function was provided."""
298 if self._close:
299 self._close()
301 def __enter__(self) -> "Protocol":
302 """Enter context manager."""
303 return self
305 def __exit__(
306 self,
307 exc_type: Optional[type[BaseException]],
308 exc_val: Optional[BaseException],
309 exc_tb: Optional[types.TracebackType],
310 ) -> None:
311 """Exit context manager and close transport."""
312 self.close()
314 def read_pkt_line(self) -> Optional[bytes]:
315 """Reads a pkt-line from the remote git process.
317 This method may read from the readahead buffer; see unread_pkt_line.
319 Returns: The next string from the stream, without the length prefix, or
320 None for a flush-pkt ('0000') or delim-pkt ('0001').
321 """
322 if self._readahead is None:
323 read = self.read
324 else:
325 read = self._readahead.read
326 self._readahead = None
328 try:
329 sizestr = read(4)
330 if not sizestr:
331 raise HangupException
332 size = int(sizestr, 16)
333 if size == 0 or size == 1: # flush-pkt or delim-pkt
334 if self.report_activity:
335 self.report_activity(4, "read")
336 return None
337 if self.report_activity:
338 self.report_activity(size, "read")
339 pkt_contents = read(size - 4)
340 except ConnectionResetError as exc:
341 raise HangupException from exc
342 except OSError as exc:
343 raise GitProtocolError(str(exc)) from exc
344 else:
345 if len(pkt_contents) + 4 != size:
346 raise GitProtocolError(
347 f"Length of pkt read {len(pkt_contents) + 4:04x} does not match length prefix {size:04x}"
348 )
349 return pkt_contents
351 def eof(self) -> bool:
352 """Test whether the protocol stream has reached EOF.
354 Note that this refers to the actual stream EOF and not just a
355 flush-pkt.
357 Returns: True if the stream is at EOF, False otherwise.
358 """
359 try:
360 next_line = self.read_pkt_line()
361 except HangupException:
362 return True
363 self.unread_pkt_line(next_line)
364 return False
366 def unread_pkt_line(self, data: Optional[bytes]) -> None:
367 """Unread a single line of data into the readahead buffer.
369 This method can be used to unread a single pkt-line into a fixed
370 readahead buffer.
372 Args:
373 data: The data to unread, without the length prefix.
375 Raises:
376 ValueError: If more than one pkt-line is unread.
377 """
378 if self._readahead is not None:
379 raise ValueError("Attempted to unread multiple pkt-lines.")
380 self._readahead = BytesIO(pkt_line(data))
382 def read_pkt_seq(self) -> Iterable[bytes]:
383 """Read a sequence of pkt-lines from the remote git process.
385 Returns: Yields each line of data up to but not including the next
386 flush-pkt.
387 """
388 pkt = self.read_pkt_line()
389 while pkt:
390 yield pkt
391 pkt = self.read_pkt_line()
393 def write_pkt_line(self, line: Optional[bytes]) -> None:
394 """Sends a pkt-line to the remote git process.
396 Args:
397 line: A string containing the data to send, without the length
398 prefix.
399 """
400 try:
401 line = pkt_line(line)
402 self.write(line)
403 if self.report_activity:
404 self.report_activity(len(line), "write")
405 except OSError as exc:
406 raise GitProtocolError(str(exc)) from exc
408 def write_sideband(self, channel: int, blob: bytes) -> None:
409 """Write multiplexed data to the sideband.
411 Args:
412 channel: An int specifying the channel to write to.
413 blob: A blob of data (as a string) to send on this channel.
414 """
415 # a pktline can be a max of 65520. a sideband line can therefore be
416 # 65520-5 = 65515
417 # WTF: Why have the len in ASCII, but the channel in binary.
418 while blob:
419 self.write_pkt_line(bytes(bytearray([channel])) + blob[:65515])
420 blob = blob[65515:]
422 def send_cmd(self, cmd: bytes, *args: bytes) -> None:
423 """Send a command and some arguments to a git server.
425 Only used for the TCP git protocol (git://).
427 Args:
428 cmd: The remote service to access.
429 args: List of arguments to send to remove service.
430 """
431 self.write_pkt_line(format_cmd_pkt(cmd, *args))
433 def read_cmd(self) -> tuple[bytes, list[bytes]]:
434 """Read a command and some arguments from the git client.
436 Only used for the TCP git protocol (git://).
438 Returns: A tuple of (command, [list of arguments]).
439 """
440 line = self.read_pkt_line()
441 if line is None:
442 raise GitProtocolError("Expected command, got flush packet")
443 return parse_cmd_pkt(line)
446_RBUFSIZE = 65536 # 64KB buffer for better network I/O performance
449class ReceivableProtocol(Protocol):
450 """Variant of Protocol that allows reading up to a size without blocking.
452 This class has a recv() method that behaves like socket.recv() in addition
453 to a read() method.
455 If you want to read n bytes from the wire and block until exactly n bytes
456 (or EOF) are read, use read(n). If you want to read at most n bytes from
457 the wire but don't care if you get less, use recv(n). Note that recv(n)
458 will still block until at least one byte is read.
459 """
461 def __init__(
462 self,
463 recv: Callable[[int], bytes],
464 write: Callable[[bytes], Optional[int]],
465 close: Optional[Callable[[], None]] = None,
466 report_activity: Optional[Callable[[int, str], None]] = None,
467 rbufsize: int = _RBUFSIZE,
468 ) -> None:
469 """Initialize ReceivableProtocol.
471 Args:
472 recv: Function to receive bytes from the transport
473 write: Function to write bytes to the transport
474 close: Optional function to close the transport
475 report_activity: Optional function to report activity
476 rbufsize: Read buffer size
477 """
478 super().__init__(self.read, write, close=close, report_activity=report_activity)
479 self._recv = recv
480 self._rbuf = BytesIO()
481 self._rbufsize = rbufsize
483 def read(self, size: int) -> bytes:
484 """Read bytes from the socket.
486 Args:
487 size: Number of bytes to read
489 Returns:
490 Bytes read from socket
491 """
492 # From _fileobj.read in socket.py in the Python 2.6.5 standard library,
493 # with the following modifications:
494 # - omit the size <= 0 branch
495 # - seek back to start rather than 0 in case some buffer has been
496 # consumed.
497 # - use SEEK_END instead of the magic number.
498 # Copyright (c) 2001-2010 Python Software Foundation; All Rights
499 # Reserved
500 # Licensed under the Python Software Foundation License.
501 # TODO: see if buffer is more efficient than cBytesIO.
502 assert size > 0
504 # Our use of BytesIO rather than lists of string objects returned by
505 # recv() minimizes memory usage and fragmentation that occurs when
506 # rbufsize is large compared to the typical return value of recv().
507 buf = self._rbuf
508 start = buf.tell()
509 buf.seek(0, SEEK_END)
510 # buffer may have been partially consumed by recv()
511 buf_len = buf.tell() - start
512 if buf_len >= size:
513 # Already have size bytes in our buffer? Extract and return.
514 buf.seek(start)
515 rv = buf.read(size)
516 self._rbuf = BytesIO()
517 self._rbuf.write(buf.read())
518 self._rbuf.seek(0)
519 return rv
521 self._rbuf = BytesIO() # reset _rbuf. we consume it via buf.
522 while True:
523 left = size - buf_len
524 # recv() will malloc the amount of memory given as its
525 # parameter even though it often returns much less data
526 # than that. The returned data string is short lived
527 # as we copy it into a BytesIO and free it. This avoids
528 # fragmentation issues on many platforms.
529 data = self._recv(left)
530 if not data:
531 break
532 n = len(data)
533 if n == size and not buf_len:
534 # Shortcut. Avoid buffer data copies when:
535 # - We have no data in our buffer.
536 # AND
537 # - Our call to recv returned exactly the
538 # number of bytes we were asked to read.
539 return data
540 if n == left:
541 buf.write(data)
542 del data # explicit free
543 break
544 assert n <= left, f"_recv({left}) returned {n} bytes"
545 buf.write(data)
546 buf_len += n
547 del data # explicit free
548 # assert buf_len == buf.tell()
549 buf.seek(start)
550 return buf.read()
552 def recv(self, size: int) -> bytes:
553 """Receive bytes from the socket with buffering.
555 Args:
556 size: Maximum number of bytes to receive
558 Returns:
559 Bytes received from socket
560 """
561 assert size > 0
563 buf = self._rbuf
564 start = buf.tell()
565 buf.seek(0, SEEK_END)
566 buf_len = buf.tell()
567 buf.seek(start)
569 left = buf_len - start
570 if not left:
571 # only read from the wire if our read buffer is exhausted
572 data = self._recv(self._rbufsize)
573 if len(data) == size:
574 # shortcut: skip the buffer if we read exactly size bytes
575 return data
576 buf = BytesIO()
577 buf.write(data)
578 buf.seek(0)
579 del data # explicit free
580 self._rbuf = buf
581 return buf.read(size)
584def extract_capabilities(text: bytes) -> tuple[bytes, list[bytes]]:
585 """Extract a capabilities list from a string, if present.
587 Args:
588 text: String to extract from
589 Returns: Tuple with text with capabilities removed and list of capabilities
590 """
591 if b"\0" not in text:
592 return text, []
593 text, capabilities = text.rstrip().split(b"\0")
594 return (text, capabilities.strip().split(b" "))
597def extract_want_line_capabilities(text: bytes) -> tuple[bytes, list[bytes]]:
598 """Extract a capabilities list from a want line, if present.
600 Note that want lines have capabilities separated from the rest of the line
601 by a space instead of a null byte. Thus want lines have the form:
603 want obj-id cap1 cap2 ...
605 Args:
606 text: Want line to extract from
607 Returns: Tuple with text with capabilities removed and list of capabilities
608 """
609 split_text = text.rstrip().split(b" ")
610 if len(split_text) < 3:
611 return text, []
612 return (b" ".join(split_text[:2]), split_text[2:])
615def ack_type(capabilities: Iterable[bytes]) -> int:
616 """Extract the ack type from a capabilities list."""
617 if b"multi_ack_detailed" in capabilities:
618 return MULTI_ACK_DETAILED
619 elif b"multi_ack" in capabilities:
620 return MULTI_ACK
621 return SINGLE_ACK
624class BufferedPktLineWriter:
625 """Writer that wraps its data in pkt-lines and has an independent buffer.
627 Consecutive calls to write() wrap the data in a pkt-line and then buffers
628 it until enough lines have been written such that their total length
629 (including length prefix) reach the buffer size.
630 """
632 def __init__(
633 self, write: Callable[[bytes], Optional[int]], bufsize: int = 65515
634 ) -> None:
635 """Initialize the BufferedPktLineWriter.
637 Args:
638 write: A write callback for the underlying writer.
639 bufsize: The internal buffer size, including length prefixes.
640 """
641 self._write = write
642 self._bufsize = bufsize
643 self._wbuf = BytesIO()
644 self._buflen = 0
646 def write(self, data: bytes) -> None:
647 """Write data, wrapping it in a pkt-line."""
648 line = pkt_line(data)
649 line_len = len(line)
650 over = self._buflen + line_len - self._bufsize
651 if over >= 0:
652 start = line_len - over
653 self._wbuf.write(line[:start])
654 self.flush()
655 else:
656 start = 0
657 saved = line[start:]
658 self._wbuf.write(saved)
659 self._buflen += len(saved)
661 def flush(self) -> None:
662 """Flush all data from the buffer."""
663 data = self._wbuf.getvalue()
664 if data:
665 self._write(data)
666 self._len = 0
667 self._wbuf = BytesIO()
670class PktLineParser:
671 """Packet line parser that hands completed packets off to a callback."""
673 def __init__(self, handle_pkt: Callable[[Optional[bytes]], None]) -> None:
674 """Initialize PktLineParser.
676 Args:
677 handle_pkt: Callback function to handle completed packets
678 """
679 self.handle_pkt = handle_pkt
680 self._readahead = BytesIO()
682 def parse(self, data: bytes) -> None:
683 """Parse a fragment of data and call back for any completed packets."""
684 self._readahead.write(data)
685 buf = self._readahead.getvalue()
686 if len(buf) < 4:
687 return
688 while len(buf) >= 4:
689 size = int(buf[:4], 16)
690 if size == 0:
691 self.handle_pkt(None)
692 buf = buf[4:]
693 elif size <= len(buf):
694 self.handle_pkt(buf[4:size])
695 buf = buf[size:]
696 else:
697 break
698 self._readahead = BytesIO()
699 self._readahead.write(buf)
701 def get_tail(self) -> bytes:
702 """Read back any unused data."""
703 return self._readahead.getvalue()
706def format_capability_line(capabilities: Iterable[bytes]) -> bytes:
707 """Format a capabilities list for the wire protocol.
709 Args:
710 capabilities: List of capability strings
712 Returns:
713 Space-separated capabilities as bytes
714 """
715 return b"".join([b" " + c for c in capabilities])
718def format_ref_line(
719 ref: bytes, sha: bytes, capabilities: Optional[Sequence[bytes]] = None
720) -> bytes:
721 """Format a ref advertisement line.
723 Args:
724 ref: Reference name
725 sha: SHA hash
726 capabilities: Optional list of capabilities
728 Returns:
729 Formatted ref line
730 """
731 if capabilities is None:
732 return sha + b" " + ref + b"\n"
733 else:
734 return sha + b" " + ref + b"\0" + format_capability_line(capabilities) + b"\n"
737def format_shallow_line(sha: bytes) -> bytes:
738 """Format a shallow line.
740 Args:
741 sha: SHA to mark as shallow
743 Returns:
744 Formatted shallow line
745 """
746 return COMMAND_SHALLOW + b" " + sha
749def format_unshallow_line(sha: bytes) -> bytes:
750 """Format an unshallow line.
752 Args:
753 sha: SHA to unshallow
755 Returns:
756 Formatted unshallow line
757 """
758 return COMMAND_UNSHALLOW + b" " + sha
761def format_ack_line(sha: bytes, ack_type: bytes = b"") -> bytes:
762 """Format an ACK line.
764 Args:
765 sha: SHA to acknowledge
766 ack_type: Optional ACK type (e.g. b"continue")
768 Returns:
769 Formatted ACK line
770 """
771 if ack_type:
772 ack_type = b" " + ack_type
773 return b"ACK " + sha + ack_type + b"\n"