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

290 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 Callable, Iterable, Sequence 

27from io import BytesIO 

28from os import SEEK_END 

29 

30import dulwich 

31 

32from .errors import GitProtocolError, HangupException 

33 

34TCP_GIT_PORT = 9418 

35 

36# Git protocol version 0 is the original Git protocol, which lacked a 

37# version number until Git protocol version 1 was introduced by Brandon 

38# Williams in 2017. 

39# 

40# Protocol version 1 is simply the original v0 protocol with the addition of 

41# a single packet line, which precedes the ref advertisement, indicating the 

42# protocol version being used. This was done in preparation for protocol v2. 

43# 

44# Git protocol version 2 was first introduced by Brandon Williams in 2018 and 

45# adds many features. See the gitprotocol-v2(5) manual page for details. 

46# As of 2024, Git only implements version 2 during 'git fetch' and still uses 

47# version 0 during 'git push'. 

48GIT_PROTOCOL_VERSIONS = [0, 1, 2] 

49DEFAULT_GIT_PROTOCOL_VERSION_FETCH = 2 

50DEFAULT_GIT_PROTOCOL_VERSION_SEND = 0 

51 

52ZERO_SHA = b"0" * 40 

53 

54SINGLE_ACK = 0 

55MULTI_ACK = 1 

56MULTI_ACK_DETAILED = 2 

57 

58# pack data 

59SIDE_BAND_CHANNEL_DATA = 1 

60# progress messages 

61SIDE_BAND_CHANNEL_PROGRESS = 2 

62# fatal error message just before stream aborts 

63SIDE_BAND_CHANNEL_FATAL = 3 

64 

65CAPABILITY_ATOMIC = b"atomic" 

66CAPABILITY_DEEPEN_SINCE = b"deepen-since" 

67CAPABILITY_DEEPEN_NOT = b"deepen-not" 

68CAPABILITY_DEEPEN_RELATIVE = b"deepen-relative" 

69CAPABILITY_DELETE_REFS = b"delete-refs" 

70CAPABILITY_INCLUDE_TAG = b"include-tag" 

71CAPABILITY_MULTI_ACK = b"multi_ack" 

72CAPABILITY_MULTI_ACK_DETAILED = b"multi_ack_detailed" 

73CAPABILITY_NO_DONE = b"no-done" 

74CAPABILITY_NO_PROGRESS = b"no-progress" 

75CAPABILITY_OFS_DELTA = b"ofs-delta" 

76CAPABILITY_QUIET = b"quiet" 

77CAPABILITY_REPORT_STATUS = b"report-status" 

78CAPABILITY_SHALLOW = b"shallow" 

79CAPABILITY_SIDE_BAND = b"side-band" 

80CAPABILITY_SIDE_BAND_64K = b"side-band-64k" 

81CAPABILITY_THIN_PACK = b"thin-pack" 

82CAPABILITY_AGENT = b"agent" 

83CAPABILITY_SYMREF = b"symref" 

84CAPABILITY_ALLOW_TIP_SHA1_IN_WANT = b"allow-tip-sha1-in-want" 

85CAPABILITY_ALLOW_REACHABLE_SHA1_IN_WANT = b"allow-reachable-sha1-in-want" 

86CAPABILITY_FETCH = b"fetch" 

87CAPABILITY_FILTER = b"filter" 

88 

89# Magic ref that is used to attach capabilities to when 

90# there are no refs. Should always be ste to ZERO_SHA. 

91CAPABILITIES_REF = b"capabilities^{}" 

92 

93COMMON_CAPABILITIES = [ 

94 CAPABILITY_OFS_DELTA, 

95 CAPABILITY_SIDE_BAND, 

96 CAPABILITY_SIDE_BAND_64K, 

97 CAPABILITY_AGENT, 

98 CAPABILITY_NO_PROGRESS, 

99] 

100KNOWN_UPLOAD_CAPABILITIES = set( 

101 [ 

102 *COMMON_CAPABILITIES, 

103 CAPABILITY_THIN_PACK, 

104 CAPABILITY_MULTI_ACK, 

105 CAPABILITY_MULTI_ACK_DETAILED, 

106 CAPABILITY_INCLUDE_TAG, 

107 CAPABILITY_DEEPEN_SINCE, 

108 CAPABILITY_SYMREF, 

109 CAPABILITY_SHALLOW, 

110 CAPABILITY_DEEPEN_NOT, 

111 CAPABILITY_DEEPEN_RELATIVE, 

112 CAPABILITY_ALLOW_TIP_SHA1_IN_WANT, 

113 CAPABILITY_ALLOW_REACHABLE_SHA1_IN_WANT, 

114 CAPABILITY_FETCH, 

115 ] 

116) 

117KNOWN_RECEIVE_CAPABILITIES = set( 

118 [ 

119 *COMMON_CAPABILITIES, 

120 CAPABILITY_REPORT_STATUS, 

121 CAPABILITY_DELETE_REFS, 

122 CAPABILITY_QUIET, 

123 CAPABILITY_ATOMIC, 

124 ] 

125) 

126 

127DEPTH_INFINITE = 0x7FFFFFFF 

128 

129NAK_LINE = b"NAK\n" 

130 

131 

132def agent_string() -> bytes: 

133 """Generate the agent string for dulwich. 

134 

135 Returns: 

136 Agent string as bytes 

137 """ 

138 return ("dulwich/" + ".".join(map(str, dulwich.__version__))).encode("ascii") 

139 

140 

141def capability_agent() -> bytes: 

142 """Generate the agent capability string. 

143 

144 Returns: 

145 Agent capability with dulwich version 

146 """ 

147 return CAPABILITY_AGENT + b"=" + agent_string() 

148 

149 

150def capability_symref(from_ref: bytes, to_ref: bytes) -> bytes: 

151 """Generate a symref capability string. 

152 

153 Args: 

154 from_ref: Source reference name 

155 to_ref: Target reference name 

156 

157 Returns: 

158 Symref capability string 

159 """ 

160 return CAPABILITY_SYMREF + b"=" + from_ref + b":" + to_ref 

161 

162 

163def extract_capability_names(capabilities: Iterable[bytes]) -> set[bytes]: 

164 """Extract capability names from a list of capabilities. 

165 

166 Args: 

167 capabilities: List of capability strings 

168 

169 Returns: 

170 Set of capability names 

171 """ 

172 return {parse_capability(c)[0] for c in capabilities} 

173 

174 

175def parse_capability(capability: bytes) -> tuple[bytes, bytes | None]: 

176 """Parse a capability string into name and value. 

177 

178 Args: 

179 capability: Capability string 

180 

181 Returns: 

182 Tuple of (capability_name, capability_value) 

183 """ 

184 parts = capability.split(b"=", 1) 

185 if len(parts) == 1: 

186 return (parts[0], None) 

187 return (parts[0], parts[1]) 

188 

189 

190def symref_capabilities(symrefs: Iterable[tuple[bytes, bytes]]) -> list[bytes]: 

191 """Generate symref capability strings from symref pairs. 

192 

193 Args: 

194 symrefs: Iterable of (from_ref, to_ref) tuples 

195 

196 Returns: 

197 List of symref capability strings 

198 """ 

199 return [capability_symref(*k) for k in symrefs] 

200 

201 

202COMMAND_DEEPEN = b"deepen" 

203COMMAND_DEEPEN_SINCE = b"deepen-since" 

204COMMAND_DEEPEN_NOT = b"deepen-not" 

205COMMAND_SHALLOW = b"shallow" 

206COMMAND_UNSHALLOW = b"unshallow" 

207COMMAND_DONE = b"done" 

208COMMAND_WANT = b"want" 

209COMMAND_HAVE = b"have" 

210 

211 

212def format_cmd_pkt(cmd: bytes, *args: bytes) -> bytes: 

213 """Format a command packet. 

214 

215 Args: 

216 cmd: Command name 

217 *args: Command arguments 

218 

219 Returns: 

220 Formatted command packet 

221 """ 

222 return cmd + b" " + b"".join([(a + b"\0") for a in args]) 

223 

224 

225def parse_cmd_pkt(line: bytes) -> tuple[bytes, list[bytes]]: 

226 """Parse a command packet. 

227 

228 Args: 

229 line: Command line to parse 

230 

231 Returns: 

232 Tuple of (command, [arguments]) 

233 """ 

234 splice_at = line.find(b" ") 

235 cmd, args = line[:splice_at], line[splice_at + 1 :] 

236 assert args[-1:] == b"\x00" 

237 return cmd, args[:-1].split(b"\0") 

238 

239 

240def pkt_line(data: bytes | None) -> bytes: 

241 """Wrap data in a pkt-line. 

242 

243 Args: 

244 data: The data to wrap, as a str or None. 

245 Returns: The data prefixed with its length in pkt-line format; if data was 

246 None, returns the flush-pkt ('0000'). 

247 """ 

248 if data is None: 

249 return b"0000" 

250 return f"{len(data) + 4:04x}".encode("ascii") + data 

251 

252 

253def pkt_seq(*seq: bytes | None) -> bytes: 

254 """Wrap a sequence of data in pkt-lines. 

255 

256 Args: 

257 seq: An iterable of strings to wrap. 

258 """ 

259 return b"".join([pkt_line(s) for s in seq]) + pkt_line(None) 

260 

261 

262class Protocol: 

263 """Class for interacting with a remote git process over the wire. 

264 

265 Parts of the git wire protocol use 'pkt-lines' to communicate. A pkt-line 

266 consists of the length of the line as a 4-byte hex string, followed by the 

267 payload data. The length includes the 4-byte header. The special line 

268 '0000' indicates the end of a section of input and is called a 'flush-pkt'. 

269 

270 For details on the pkt-line format, see the cgit distribution: 

271 Documentation/technical/protocol-common.txt 

272 """ 

273 

274 def __init__( 

275 self, 

276 read: Callable[[int], bytes], 

277 write: Callable[[bytes], int | None], 

278 close: Callable[[], None] | None = None, 

279 report_activity: Callable[[int, str], None] | None = None, 

280 ) -> None: 

281 """Initialize Protocol. 

282 

283 Args: 

284 read: Function to read bytes from the transport 

285 write: Function to write bytes to the transport 

286 close: Optional function to close the transport 

287 report_activity: Optional function to report activity 

288 """ 

289 self.read = read 

290 self.write = write 

291 self._close = close 

292 self.report_activity = report_activity 

293 self._readahead: BytesIO | None = None 

294 

295 def close(self) -> None: 

296 """Close the underlying transport if a close function was provided.""" 

297 if self._close: 

298 self._close() 

299 

300 def __enter__(self) -> "Protocol": 

301 """Enter context manager.""" 

302 return self 

303 

304 def __exit__( 

305 self, 

306 exc_type: type[BaseException] | None, 

307 exc_val: BaseException | None, 

308 exc_tb: types.TracebackType | None, 

309 ) -> None: 

310 """Exit context manager and close transport.""" 

311 self.close() 

312 

313 def read_pkt_line(self) -> bytes | None: 

314 """Reads a pkt-line from the remote git process. 

315 

316 This method may read from the readahead buffer; see unread_pkt_line. 

317 

318 Returns: The next string from the stream, without the length prefix, or 

319 None for a flush-pkt ('0000') or delim-pkt ('0001'). 

320 """ 

321 if self._readahead is None: 

322 read = self.read 

323 else: 

324 read = self._readahead.read 

325 self._readahead = None 

326 

327 try: 

328 sizestr = read(4) 

329 if not sizestr: 

330 raise HangupException 

331 size = int(sizestr, 16) 

332 if size == 0 or size == 1: # flush-pkt or delim-pkt 

333 if self.report_activity: 

334 self.report_activity(4, "read") 

335 return None 

336 if self.report_activity: 

337 self.report_activity(size, "read") 

338 pkt_contents = read(size - 4) 

339 except ConnectionResetError as exc: 

340 raise HangupException from exc 

341 except OSError as exc: 

342 raise GitProtocolError(str(exc)) from exc 

343 else: 

344 if len(pkt_contents) + 4 != size: 

345 raise GitProtocolError( 

346 f"Length of pkt read {len(pkt_contents) + 4:04x} does not match length prefix {size:04x}" 

347 ) 

348 return pkt_contents 

349 

350 def eof(self) -> bool: 

351 """Test whether the protocol stream has reached EOF. 

352 

353 Note that this refers to the actual stream EOF and not just a 

354 flush-pkt. 

355 

356 Returns: True if the stream is at EOF, False otherwise. 

357 """ 

358 try: 

359 next_line = self.read_pkt_line() 

360 except HangupException: 

361 return True 

362 self.unread_pkt_line(next_line) 

363 return False 

364 

365 def unread_pkt_line(self, data: bytes | None) -> None: 

366 """Unread a single line of data into the readahead buffer. 

367 

368 This method can be used to unread a single pkt-line into a fixed 

369 readahead buffer. 

370 

371 Args: 

372 data: The data to unread, without the length prefix. 

373 

374 Raises: 

375 ValueError: If more than one pkt-line is unread. 

376 """ 

377 if self._readahead is not None: 

378 raise ValueError("Attempted to unread multiple pkt-lines.") 

379 self._readahead = BytesIO(pkt_line(data)) 

380 

381 def read_pkt_seq(self) -> Iterable[bytes]: 

382 """Read a sequence of pkt-lines from the remote git process. 

383 

384 Returns: Yields each line of data up to but not including the next 

385 flush-pkt. 

386 """ 

387 pkt = self.read_pkt_line() 

388 while pkt: 

389 yield pkt 

390 pkt = self.read_pkt_line() 

391 

392 def write_pkt_line(self, line: bytes | None) -> None: 

393 """Sends a pkt-line to the remote git process. 

394 

395 Args: 

396 line: A string containing the data to send, without the length 

397 prefix. 

398 """ 

399 try: 

400 line = pkt_line(line) 

401 self.write(line) 

402 if self.report_activity: 

403 self.report_activity(len(line), "write") 

404 except OSError as exc: 

405 raise GitProtocolError(str(exc)) from exc 

406 

407 def write_sideband(self, channel: int, blob: bytes) -> None: 

408 """Write multiplexed data to the sideband. 

409 

410 Args: 

411 channel: An int specifying the channel to write to. 

412 blob: A blob of data (as a string) to send on this channel. 

413 """ 

414 # a pktline can be a max of 65520. a sideband line can therefore be 

415 # 65520-5 = 65515 

416 # WTF: Why have the len in ASCII, but the channel in binary. 

417 while blob: 

418 self.write_pkt_line(bytes(bytearray([channel])) + blob[:65515]) 

419 blob = blob[65515:] 

420 

421 def send_cmd(self, cmd: bytes, *args: bytes) -> None: 

422 """Send a command and some arguments to a git server. 

423 

424 Only used for the TCP git protocol (git://). 

425 

426 Args: 

427 cmd: The remote service to access. 

428 args: List of arguments to send to remove service. 

429 """ 

430 self.write_pkt_line(format_cmd_pkt(cmd, *args)) 

431 

432 def read_cmd(self) -> tuple[bytes, list[bytes]]: 

433 """Read a command and some arguments from the git client. 

434 

435 Only used for the TCP git protocol (git://). 

436 

437 Returns: A tuple of (command, [list of arguments]). 

438 """ 

439 line = self.read_pkt_line() 

440 if line is None: 

441 raise GitProtocolError("Expected command, got flush packet") 

442 return parse_cmd_pkt(line) 

443 

444 

445_RBUFSIZE = 65536 # 64KB buffer for better network I/O performance 

446 

447 

448class ReceivableProtocol(Protocol): 

449 """Variant of Protocol that allows reading up to a size without blocking. 

450 

451 This class has a recv() method that behaves like socket.recv() in addition 

452 to a read() method. 

453 

454 If you want to read n bytes from the wire and block until exactly n bytes 

455 (or EOF) are read, use read(n). If you want to read at most n bytes from 

456 the wire but don't care if you get less, use recv(n). Note that recv(n) 

457 will still block until at least one byte is read. 

458 """ 

459 

460 def __init__( 

461 self, 

462 recv: Callable[[int], bytes], 

463 write: Callable[[bytes], int | None], 

464 close: Callable[[], None] | None = None, 

465 report_activity: Callable[[int, str], None] | None = None, 

466 rbufsize: int = _RBUFSIZE, 

467 ) -> None: 

468 """Initialize ReceivableProtocol. 

469 

470 Args: 

471 recv: Function to receive bytes from the transport 

472 write: Function to write bytes to the transport 

473 close: Optional function to close the transport 

474 report_activity: Optional function to report activity 

475 rbufsize: Read buffer size 

476 """ 

477 super().__init__(self.read, write, close=close, report_activity=report_activity) 

478 self._recv = recv 

479 self._rbuf = BytesIO() 

480 self._rbufsize = rbufsize 

481 

482 def read(self, size: int) -> bytes: 

483 """Read bytes from the socket. 

484 

485 Args: 

486 size: Number of bytes to read 

487 

488 Returns: 

489 Bytes read from socket 

490 """ 

491 # From _fileobj.read in socket.py in the Python 2.6.5 standard library, 

492 # with the following modifications: 

493 # - omit the size <= 0 branch 

494 # - seek back to start rather than 0 in case some buffer has been 

495 # consumed. 

496 # - use SEEK_END instead of the magic number. 

497 # Copyright (c) 2001-2010 Python Software Foundation; All Rights 

498 # Reserved 

499 # Licensed under the Python Software Foundation License. 

500 # TODO: see if buffer is more efficient than cBytesIO. 

501 assert size > 0 

502 

503 # Our use of BytesIO rather than lists of string objects returned by 

504 # recv() minimizes memory usage and fragmentation that occurs when 

505 # rbufsize is large compared to the typical return value of recv(). 

506 buf = self._rbuf 

507 start = buf.tell() 

508 buf.seek(0, SEEK_END) 

509 # buffer may have been partially consumed by recv() 

510 buf_len = buf.tell() - start 

511 if buf_len >= size: 

512 # Already have size bytes in our buffer? Extract and return. 

513 buf.seek(start) 

514 rv = buf.read(size) 

515 self._rbuf = BytesIO() 

516 self._rbuf.write(buf.read()) 

517 self._rbuf.seek(0) 

518 return rv 

519 

520 self._rbuf = BytesIO() # reset _rbuf. we consume it via buf. 

521 while True: 

522 left = size - buf_len 

523 # recv() will malloc the amount of memory given as its 

524 # parameter even though it often returns much less data 

525 # than that. The returned data string is short lived 

526 # as we copy it into a BytesIO and free it. This avoids 

527 # fragmentation issues on many platforms. 

528 data = self._recv(left) 

529 if not data: 

530 break 

531 n = len(data) 

532 if n == size and not buf_len: 

533 # Shortcut. Avoid buffer data copies when: 

534 # - We have no data in our buffer. 

535 # AND 

536 # - Our call to recv returned exactly the 

537 # number of bytes we were asked to read. 

538 return data 

539 if n == left: 

540 buf.write(data) 

541 del data # explicit free 

542 break 

543 assert n <= left, f"_recv({left}) returned {n} bytes" 

544 buf.write(data) 

545 buf_len += n 

546 del data # explicit free 

547 # assert buf_len == buf.tell() 

548 buf.seek(start) 

549 return buf.read() 

550 

551 def recv(self, size: int) -> bytes: 

552 """Receive bytes from the socket with buffering. 

553 

554 Args: 

555 size: Maximum number of bytes to receive 

556 

557 Returns: 

558 Bytes received from socket 

559 """ 

560 assert size > 0 

561 

562 buf = self._rbuf 

563 start = buf.tell() 

564 buf.seek(0, SEEK_END) 

565 buf_len = buf.tell() 

566 buf.seek(start) 

567 

568 left = buf_len - start 

569 if not left: 

570 # only read from the wire if our read buffer is exhausted 

571 data = self._recv(self._rbufsize) 

572 if len(data) == size: 

573 # shortcut: skip the buffer if we read exactly size bytes 

574 return data 

575 buf = BytesIO() 

576 buf.write(data) 

577 buf.seek(0) 

578 del data # explicit free 

579 self._rbuf = buf 

580 return buf.read(size) 

581 

582 

583def extract_capabilities(text: bytes) -> tuple[bytes, list[bytes]]: 

584 """Extract a capabilities list from a string, if present. 

585 

586 Args: 

587 text: String to extract from 

588 Returns: Tuple with text with capabilities removed and list of capabilities 

589 """ 

590 if b"\0" not in text: 

591 return text, [] 

592 text, capabilities = text.rstrip().split(b"\0") 

593 return (text, capabilities.strip().split(b" ")) 

594 

595 

596def extract_want_line_capabilities(text: bytes) -> tuple[bytes, list[bytes]]: 

597 """Extract a capabilities list from a want line, if present. 

598 

599 Note that want lines have capabilities separated from the rest of the line 

600 by a space instead of a null byte. Thus want lines have the form: 

601 

602 want obj-id cap1 cap2 ... 

603 

604 Args: 

605 text: Want line to extract from 

606 Returns: Tuple with text with capabilities removed and list of capabilities 

607 """ 

608 split_text = text.rstrip().split(b" ") 

609 if len(split_text) < 3: 

610 return text, [] 

611 return (b" ".join(split_text[:2]), split_text[2:]) 

612 

613 

614def ack_type(capabilities: Iterable[bytes]) -> int: 

615 """Extract the ack type from a capabilities list.""" 

616 if b"multi_ack_detailed" in capabilities: 

617 return MULTI_ACK_DETAILED 

618 elif b"multi_ack" in capabilities: 

619 return MULTI_ACK 

620 return SINGLE_ACK 

621 

622 

623class BufferedPktLineWriter: 

624 """Writer that wraps its data in pkt-lines and has an independent buffer. 

625 

626 Consecutive calls to write() wrap the data in a pkt-line and then buffers 

627 it until enough lines have been written such that their total length 

628 (including length prefix) reach the buffer size. 

629 """ 

630 

631 def __init__( 

632 self, write: Callable[[bytes], int | None], bufsize: int = 65515 

633 ) -> None: 

634 """Initialize the BufferedPktLineWriter. 

635 

636 Args: 

637 write: A write callback for the underlying writer. 

638 bufsize: The internal buffer size, including length prefixes. 

639 """ 

640 self._write = write 

641 self._bufsize = bufsize 

642 self._wbuf = BytesIO() 

643 self._buflen = 0 

644 

645 def write(self, data: bytes) -> None: 

646 """Write data, wrapping it in a pkt-line.""" 

647 line = pkt_line(data) 

648 line_len = len(line) 

649 over = self._buflen + line_len - self._bufsize 

650 if over >= 0: 

651 start = line_len - over 

652 self._wbuf.write(line[:start]) 

653 self.flush() 

654 else: 

655 start = 0 

656 saved = line[start:] 

657 self._wbuf.write(saved) 

658 self._buflen += len(saved) 

659 

660 def flush(self) -> None: 

661 """Flush all data from the buffer.""" 

662 data = self._wbuf.getvalue() 

663 if data: 

664 self._write(data) 

665 self._len = 0 

666 self._wbuf = BytesIO() 

667 

668 

669class PktLineParser: 

670 """Packet line parser that hands completed packets off to a callback.""" 

671 

672 def __init__(self, handle_pkt: Callable[[bytes | None], None]) -> None: 

673 """Initialize PktLineParser. 

674 

675 Args: 

676 handle_pkt: Callback function to handle completed packets 

677 """ 

678 self.handle_pkt = handle_pkt 

679 self._readahead = BytesIO() 

680 

681 def parse(self, data: bytes) -> None: 

682 """Parse a fragment of data and call back for any completed packets.""" 

683 self._readahead.write(data) 

684 buf = self._readahead.getvalue() 

685 if len(buf) < 4: 

686 return 

687 while len(buf) >= 4: 

688 size = int(buf[:4], 16) 

689 if size == 0: 

690 self.handle_pkt(None) 

691 buf = buf[4:] 

692 elif size <= len(buf): 

693 self.handle_pkt(buf[4:size]) 

694 buf = buf[size:] 

695 else: 

696 break 

697 self._readahead = BytesIO() 

698 self._readahead.write(buf) 

699 

700 def get_tail(self) -> bytes: 

701 """Read back any unused data.""" 

702 return self._readahead.getvalue() 

703 

704 

705def format_capability_line(capabilities: Iterable[bytes]) -> bytes: 

706 """Format a capabilities list for the wire protocol. 

707 

708 Args: 

709 capabilities: List of capability strings 

710 

711 Returns: 

712 Space-separated capabilities as bytes 

713 """ 

714 return b"".join([b" " + c for c in capabilities]) 

715 

716 

717def format_ref_line( 

718 ref: bytes, sha: bytes, capabilities: Sequence[bytes] | None = None 

719) -> bytes: 

720 """Format a ref advertisement line. 

721 

722 Args: 

723 ref: Reference name 

724 sha: SHA hash 

725 capabilities: Optional list of capabilities 

726 

727 Returns: 

728 Formatted ref line 

729 """ 

730 if capabilities is None: 

731 return sha + b" " + ref + b"\n" 

732 else: 

733 return sha + b" " + ref + b"\0" + format_capability_line(capabilities) + b"\n" 

734 

735 

736def format_shallow_line(sha: bytes) -> bytes: 

737 """Format a shallow line. 

738 

739 Args: 

740 sha: SHA to mark as shallow 

741 

742 Returns: 

743 Formatted shallow line 

744 """ 

745 return COMMAND_SHALLOW + b" " + sha 

746 

747 

748def format_unshallow_line(sha: bytes) -> bytes: 

749 """Format an unshallow line. 

750 

751 Args: 

752 sha: SHA to unshallow 

753 

754 Returns: 

755 Formatted unshallow line 

756 """ 

757 return COMMAND_UNSHALLOW + b" " + sha 

758 

759 

760def format_ack_line(sha: bytes, ack_type: bytes = b"") -> bytes: 

761 """Format an ACK line. 

762 

763 Args: 

764 sha: SHA to acknowledge 

765 ack_type: Optional ACK type (e.g. b"continue") 

766 

767 Returns: 

768 Formatted ACK line 

769 """ 

770 if ack_type: 

771 ack_type = b" " + ack_type 

772 return b"ACK " + sha + ack_type + b"\n"