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

289 statements  

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"