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

291 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, Sequence 

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_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" 

211 

212 

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

214 """Format a command packet. 

215 

216 Args: 

217 cmd: Command name 

218 *args: Command arguments 

219 

220 Returns: 

221 Formatted command packet 

222 """ 

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

224 

225 

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

227 """Parse a command packet. 

228 

229 Args: 

230 line: Command line to parse 

231 

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") 

239 

240 

241def pkt_line(data: Optional[bytes]) -> bytes: 

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

243 

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 

252 

253 

254def pkt_seq(*seq: Optional[bytes]) -> bytes: 

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

256 

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) 

261 

262 

263class Protocol: 

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

265 

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'. 

270 

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

272 Documentation/technical/protocol-common.txt 

273 """ 

274 

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. 

283 

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 

295 

296 def close(self) -> None: 

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

298 if self._close: 

299 self._close() 

300 

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

302 """Enter context manager.""" 

303 return self 

304 

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() 

313 

314 def read_pkt_line(self) -> Optional[bytes]: 

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

316 

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

318 

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 

327 

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 

350 

351 def eof(self) -> bool: 

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

353 

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

355 flush-pkt. 

356 

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 

365 

366 def unread_pkt_line(self, data: Optional[bytes]) -> None: 

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

368 

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

370 readahead buffer. 

371 

372 Args: 

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

374 

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)) 

381 

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

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

384 

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() 

392 

393 def write_pkt_line(self, line: Optional[bytes]) -> None: 

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

395 

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 

407 

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

409 """Write multiplexed data to the sideband. 

410 

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:] 

421 

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

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

424 

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

426 

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)) 

432 

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

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

435 

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

437 

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) 

444 

445 

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

447 

448 

449class ReceivableProtocol(Protocol): 

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

451 

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

453 to a read() method. 

454 

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 """ 

460 

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. 

470 

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 

482 

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

484 """Read bytes from the socket. 

485 

486 Args: 

487 size: Number of bytes to read 

488 

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 

503 

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 

520 

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() 

551 

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

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

554 

555 Args: 

556 size: Maximum number of bytes to receive 

557 

558 Returns: 

559 Bytes received from socket 

560 """ 

561 assert size > 0 

562 

563 buf = self._rbuf 

564 start = buf.tell() 

565 buf.seek(0, SEEK_END) 

566 buf_len = buf.tell() 

567 buf.seek(start) 

568 

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) 

582 

583 

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

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

586 

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" ")) 

595 

596 

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

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

599 

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: 

602 

603 want obj-id cap1 cap2 ... 

604 

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:]) 

613 

614 

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 

622 

623 

624class BufferedPktLineWriter: 

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

626 

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 """ 

631 

632 def __init__( 

633 self, write: Callable[[bytes], Optional[int]], bufsize: int = 65515 

634 ) -> None: 

635 """Initialize the BufferedPktLineWriter. 

636 

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 

645 

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) 

660 

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() 

668 

669 

670class PktLineParser: 

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

672 

673 def __init__(self, handle_pkt: Callable[[Optional[bytes]], None]) -> None: 

674 """Initialize PktLineParser. 

675 

676 Args: 

677 handle_pkt: Callback function to handle completed packets 

678 """ 

679 self.handle_pkt = handle_pkt 

680 self._readahead = BytesIO() 

681 

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) 

700 

701 def get_tail(self) -> bytes: 

702 """Read back any unused data.""" 

703 return self._readahead.getvalue() 

704 

705 

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

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

708 

709 Args: 

710 capabilities: List of capability strings 

711 

712 Returns: 

713 Space-separated capabilities as bytes 

714 """ 

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

716 

717 

718def format_ref_line( 

719 ref: bytes, sha: bytes, capabilities: Optional[Sequence[bytes]] = None 

720) -> bytes: 

721 """Format a ref advertisement line. 

722 

723 Args: 

724 ref: Reference name 

725 sha: SHA hash 

726 capabilities: Optional list of capabilities 

727 

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" 

735 

736 

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

738 """Format a shallow line. 

739 

740 Args: 

741 sha: SHA to mark as shallow 

742 

743 Returns: 

744 Formatted shallow line 

745 """ 

746 return COMMAND_SHALLOW + b" " + sha 

747 

748 

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

750 """Format an unshallow line. 

751 

752 Args: 

753 sha: SHA to unshallow 

754 

755 Returns: 

756 Formatted unshallow line 

757 """ 

758 return COMMAND_UNSHALLOW + b" " + sha 

759 

760 

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

762 """Format an ACK line. 

763 

764 Args: 

765 sha: SHA to acknowledge 

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

767 

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"