Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dulwich/protocol.py: 32%

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

387 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 

25__all__ = [ 

26 "CAPABILITIES_REF", 

27 "CAPABILITY_AGENT", 

28 "CAPABILITY_ALLOW_REACHABLE_SHA1_IN_WANT", 

29 "CAPABILITY_ALLOW_TIP_SHA1_IN_WANT", 

30 "CAPABILITY_ATOMIC", 

31 "CAPABILITY_DEEPEN_NOT", 

32 "CAPABILITY_DEEPEN_RELATIVE", 

33 "CAPABILITY_DEEPEN_SINCE", 

34 "CAPABILITY_DELETE_REFS", 

35 "CAPABILITY_FETCH", 

36 "CAPABILITY_FILTER", 

37 "CAPABILITY_INCLUDE_TAG", 

38 "CAPABILITY_MULTI_ACK", 

39 "CAPABILITY_MULTI_ACK_DETAILED", 

40 "CAPABILITY_NO_DONE", 

41 "CAPABILITY_NO_PROGRESS", 

42 "CAPABILITY_OBJECT_FORMAT", 

43 "CAPABILITY_OFS_DELTA", 

44 "CAPABILITY_QUIET", 

45 "CAPABILITY_REPORT_STATUS", 

46 "CAPABILITY_SHALLOW", 

47 "CAPABILITY_SIDE_BAND", 

48 "CAPABILITY_SIDE_BAND_64K", 

49 "CAPABILITY_SYMREF", 

50 "CAPABILITY_THIN_PACK", 

51 "COMMAND_DEEPEN", 

52 "COMMAND_DEEPEN_NOT", 

53 "COMMAND_DEEPEN_SINCE", 

54 "COMMAND_DONE", 

55 "COMMAND_FILTER", 

56 "COMMAND_HAVE", 

57 "COMMAND_SHALLOW", 

58 "COMMAND_UNSHALLOW", 

59 "COMMAND_WANT", 

60 "COMMON_CAPABILITIES", 

61 "DEFAULT_GIT_PROTOCOL_VERSION_FETCH", 

62 "DEFAULT_GIT_PROTOCOL_VERSION_SEND", 

63 "DEPTH_INFINITE", 

64 "GIT_PROTOCOL_VERSIONS", 

65 "KNOWN_RECEIVE_CAPABILITIES", 

66 "KNOWN_UPLOAD_CAPABILITIES", 

67 "MULTI_ACK", 

68 "MULTI_ACK_DETAILED", 

69 "NAK_LINE", 

70 "PEELED_TAG_SUFFIX", 

71 "SIDE_BAND_CHANNEL_DATA", 

72 "SIDE_BAND_CHANNEL_FATAL", 

73 "SIDE_BAND_CHANNEL_PROGRESS", 

74 "SINGLE_ACK", 

75 "TCP_GIT_PORT", 

76 "BufferedPktLineWriter", 

77 "PktLineParser", 

78 "Protocol", 

79 "ReceivableProtocol", 

80 "ack_type", 

81 "agent_string", 

82 "capability_agent", 

83 "capability_object_format", 

84 "capability_symref", 

85 "extract_capabilities", 

86 "extract_capability_names", 

87 "extract_want_line_capabilities", 

88 "find_capability", 

89 "format_ack_line", 

90 "format_capability_line", 

91 "format_cmd_pkt", 

92 "format_ref_line", 

93 "format_shallow_line", 

94 "format_unshallow_line", 

95 "parse_capability", 

96 "parse_cmd_pkt", 

97 "pkt_line", 

98 "pkt_seq", 

99 "serialize_refs", 

100 "split_peeled_refs", 

101 "strip_peeled_refs", 

102 "symref_capabilities", 

103 "write_info_refs", 

104] 

105 

106import logging 

107import sys 

108import types 

109from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence 

110from io import BytesIO 

111from os import SEEK_END 

112from typing import TYPE_CHECKING 

113 

114import dulwich 

115 

116from .errors import GitProtocolError, HangupException 

117from .objects import ObjectID, Tag 

118 

119if sys.version_info >= (3, 11): 

120 from typing import Self 

121else: 

122 from typing_extensions import Self 

123 

124logger = logging.getLogger(__name__) 

125 

126if TYPE_CHECKING: 

127 from .pack import ObjectContainer 

128 from .refs import Ref 

129 

130TCP_GIT_PORT = 9418 

131 

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

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

134# Williams in 2017. 

135# 

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

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

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

139# 

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

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

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

143# version 0 during 'git push'. 

144GIT_PROTOCOL_VERSIONS = [0, 1, 2] 

145DEFAULT_GIT_PROTOCOL_VERSION_FETCH = 2 

146DEFAULT_GIT_PROTOCOL_VERSION_SEND = 0 

147 

148# Suffix used in the Git protocol to indicate peeled tag references 

149PEELED_TAG_SUFFIX = b"^{}" 

150 

151ZERO_SHA: ObjectID = ObjectID(b"0" * 40) 

152 

153SINGLE_ACK = 0 

154MULTI_ACK = 1 

155MULTI_ACK_DETAILED = 2 

156 

157# pack data 

158SIDE_BAND_CHANNEL_DATA = 1 

159# progress messages 

160SIDE_BAND_CHANNEL_PROGRESS = 2 

161# fatal error message just before stream aborts 

162SIDE_BAND_CHANNEL_FATAL = 3 

163 

164CAPABILITY_ATOMIC = b"atomic" 

165CAPABILITY_DEEPEN_SINCE = b"deepen-since" 

166CAPABILITY_DEEPEN_NOT = b"deepen-not" 

167CAPABILITY_DEEPEN_RELATIVE = b"deepen-relative" 

168CAPABILITY_DELETE_REFS = b"delete-refs" 

169CAPABILITY_INCLUDE_TAG = b"include-tag" 

170CAPABILITY_MULTI_ACK = b"multi_ack" 

171CAPABILITY_MULTI_ACK_DETAILED = b"multi_ack_detailed" 

172CAPABILITY_NO_DONE = b"no-done" 

173CAPABILITY_NO_PROGRESS = b"no-progress" 

174CAPABILITY_OFS_DELTA = b"ofs-delta" 

175CAPABILITY_QUIET = b"quiet" 

176CAPABILITY_REPORT_STATUS = b"report-status" 

177CAPABILITY_SHALLOW = b"shallow" 

178CAPABILITY_SIDE_BAND = b"side-band" 

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

180CAPABILITY_THIN_PACK = b"thin-pack" 

181CAPABILITY_AGENT = b"agent" 

182CAPABILITY_SYMREF = b"symref" 

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

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

185CAPABILITY_FETCH = b"fetch" 

186CAPABILITY_FILTER = b"filter" 

187CAPABILITY_OBJECT_FORMAT = b"object-format" 

188CAPABILITY_PACKFILE_URIS = b"packfile-uris" 

189CAPABILITY_PUSH_OPTIONS = b"push-options" 

190 

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

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

193CAPABILITIES_REF = b"capabilities^{}" 

194 

195COMMON_CAPABILITIES = [ 

196 CAPABILITY_OFS_DELTA, 

197 CAPABILITY_SIDE_BAND, 

198 CAPABILITY_SIDE_BAND_64K, 

199 CAPABILITY_AGENT, 

200 CAPABILITY_NO_PROGRESS, 

201] 

202KNOWN_UPLOAD_CAPABILITIES = set( 

203 [ 

204 *COMMON_CAPABILITIES, 

205 CAPABILITY_THIN_PACK, 

206 CAPABILITY_MULTI_ACK, 

207 CAPABILITY_MULTI_ACK_DETAILED, 

208 CAPABILITY_INCLUDE_TAG, 

209 CAPABILITY_DEEPEN_SINCE, 

210 CAPABILITY_SYMREF, 

211 CAPABILITY_SHALLOW, 

212 CAPABILITY_DEEPEN_NOT, 

213 CAPABILITY_DEEPEN_RELATIVE, 

214 CAPABILITY_ALLOW_TIP_SHA1_IN_WANT, 

215 CAPABILITY_ALLOW_REACHABLE_SHA1_IN_WANT, 

216 CAPABILITY_FETCH, 

217 CAPABILITY_FILTER, 

218 CAPABILITY_PACKFILE_URIS, 

219 ] 

220) 

221KNOWN_RECEIVE_CAPABILITIES = set( 

222 [ 

223 *COMMON_CAPABILITIES, 

224 CAPABILITY_REPORT_STATUS, 

225 CAPABILITY_DELETE_REFS, 

226 CAPABILITY_QUIET, 

227 CAPABILITY_ATOMIC, 

228 CAPABILITY_PUSH_OPTIONS, 

229 ] 

230) 

231 

232DEPTH_INFINITE = 0x7FFFFFFF 

233 

234NAK_LINE = b"NAK\n" 

235 

236 

237def agent_string() -> bytes: 

238 """Generate the agent string for dulwich. 

239 

240 Returns: 

241 Agent string as bytes 

242 """ 

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

244 

245 

246def capability_agent() -> bytes: 

247 """Generate the agent capability string. 

248 

249 Returns: 

250 Agent capability with dulwich version 

251 """ 

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

253 

254 

255def capability_object_format(fmt: str) -> bytes: 

256 """Generate the object-format capability string. 

257 

258 Args: 

259 fmt: Object format name (e.g., "sha1" or "sha256") 

260 

261 Returns: 

262 Object-format capability with format name 

263 """ 

264 return CAPABILITY_OBJECT_FORMAT + b"=" + fmt.encode("ascii") 

265 

266 

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

268 """Generate a symref capability string. 

269 

270 Args: 

271 from_ref: Source reference name 

272 to_ref: Target reference name 

273 

274 Returns: 

275 Symref capability string 

276 """ 

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

278 

279 

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

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

282 

283 Args: 

284 capabilities: List of capability strings 

285 

286 Returns: 

287 Set of capability names 

288 """ 

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

290 

291 

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

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

294 

295 Args: 

296 capability: Capability string 

297 

298 Returns: 

299 Tuple of (capability_name, capability_value) 

300 """ 

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

302 if len(parts) == 1: 

303 return (parts[0], None) 

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

305 

306 

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

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

309 

310 Args: 

311 symrefs: Iterable of (from_ref, to_ref) tuples 

312 

313 Returns: 

314 List of symref capability strings 

315 """ 

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

317 

318 

319COMMAND_DEEPEN = b"deepen" 

320COMMAND_DEEPEN_SINCE = b"deepen-since" 

321COMMAND_DEEPEN_NOT = b"deepen-not" 

322COMMAND_SHALLOW = b"shallow" 

323COMMAND_UNSHALLOW = b"unshallow" 

324COMMAND_DONE = b"done" 

325COMMAND_WANT = b"want" 

326COMMAND_HAVE = b"have" 

327COMMAND_FILTER = b"filter" 

328 

329 

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

331 """Format a command packet. 

332 

333 Args: 

334 cmd: Command name 

335 *args: Command arguments 

336 

337 Returns: 

338 Formatted command packet 

339 """ 

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

341 

342 

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

344 """Parse a command packet. 

345 

346 Args: 

347 line: Command line to parse 

348 

349 Returns: 

350 Tuple of (command, [arguments]) 

351 """ 

352 splice_at = line.find(b" ") 

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

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

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

356 

357 

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

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

360 

361 Args: 

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

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

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

365 """ 

366 if data is None: 

367 return b"0000" 

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

369 

370 

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

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

373 

374 Args: 

375 seq: An iterable of strings to wrap. 

376 """ 

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

378 

379 

380_HEX_DIGITS = frozenset(b"0123456789abcdefABCDEF") 

381 

382 

383def _parse_pkt_line_length(sizestr: bytes) -> int: 

384 """Parse the four-byte length prefix of a pkt-line. 

385 

386 Mirrors git's ``packet_length()``: the prefix must be exactly four 

387 hexadecimal digits. ``int(sizestr, 16)`` on its own also accepts a leading 

388 ``+``/``-``, surrounding whitespace, an ``0x`` prefix and digit-group 

389 underscores, none of which git allows. Accepting a ``-``-prefixed prefix is 

390 the dangerous case: the resulting negative length turns the following 

391 ``read(size - 4)`` into a ``read()`` with a negative argument (which reads 

392 the rest of the stream into memory) and makes the incremental 

393 :class:`PktLineParser` loop without ever consuming its buffer. 

394 

395 Args: 

396 sizestr: The four raw length bytes read from the wire. 

397 Returns: The length encoded by the prefix. 

398 

399 Raises: 

400 GitProtocolError: if the prefix is not four hexadecimal digits. 

401 """ 

402 if len(sizestr) != 4 or not _HEX_DIGITS.issuperset(sizestr): 

403 raise GitProtocolError(f"Invalid pkt-line length prefix: {sizestr!r}") 

404 return int(sizestr, 16) 

405 

406 

407class Protocol: 

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

409 

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

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

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

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

414 

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

416 Documentation/technical/protocol-common.txt 

417 """ 

418 

419 def __init__( 

420 self, 

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

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

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

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

425 shutdown_write: Callable[[], None] | None = None, 

426 ) -> None: 

427 """Initialize Protocol. 

428 

429 Args: 

430 read: Function to read bytes from the transport 

431 write: Function to write bytes to the transport 

432 close: Optional function to close the transport 

433 report_activity: Optional function to report activity 

434 shutdown_write: Optional function to half-close the write side of 

435 the transport once the request has been fully sent 

436 """ 

437 self.read = read 

438 self.write = write 

439 self._close = close 

440 self.report_activity = report_activity 

441 self._shutdown_write = shutdown_write 

442 self._readahead: BytesIO | None = None 

443 

444 def shutdown_write(self) -> None: 

445 """Half-close the write side of the transport, if supported. 

446 

447 Signals to the peer that no more data will be sent. On Windows a 

448 server that closes a socket with client data still unread aborts with 

449 an RST rather than a clean FIN; letting it read to EOF avoids that. 

450 """ 

451 if self._shutdown_write is not None: 

452 self._shutdown_write() 

453 self._shutdown_write = None 

454 

455 def close(self) -> None: 

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

457 if self._close: 

458 self._close() 

459 self._close = None # Prevent double-close 

460 

461 def __del__(self) -> None: 

462 """Ensure transport is closed when Protocol is garbage collected.""" 

463 if self._close is not None: 

464 import warnings 

465 

466 warnings.warn( 

467 f"unclosed Protocol {self!r}", 

468 ResourceWarning, 

469 stacklevel=2, 

470 source=self, 

471 ) 

472 try: 

473 self.close() 

474 except Exception: 

475 # Ignore errors during cleanup 

476 pass 

477 

478 def __enter__(self) -> Self: 

479 """Enter context manager.""" 

480 return self 

481 

482 def __exit__( 

483 self, 

484 exc_type: type[BaseException] | None, 

485 exc_val: BaseException | None, 

486 exc_tb: types.TracebackType | None, 

487 ) -> None: 

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

489 self.close() 

490 

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

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

493 

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

495 

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

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

498 """ 

499 if self._readahead is None: 

500 read = self.read 

501 else: 

502 read = self._readahead.read 

503 self._readahead = None 

504 

505 try: 

506 sizestr = read(4) 

507 if not sizestr: 

508 raise HangupException 

509 size = _parse_pkt_line_length(sizestr) 

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

511 if self.report_activity: 

512 self.report_activity(4, "read") 

513 logger.debug("git< %s", sizestr.decode("ascii")) 

514 return None 

515 if size < 4: 

516 raise GitProtocolError(f"Invalid pkt-line length: {size:04x}") 

517 if self.report_activity: 

518 self.report_activity(size, "read") 

519 pkt_contents = read(size - 4) 

520 except ConnectionResetError as exc: 

521 raise HangupException from exc 

522 except OSError as exc: 

523 raise GitProtocolError(str(exc)) from exc 

524 else: 

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

526 raise GitProtocolError( 

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

528 ) 

529 # Log the packet contents (truncate if too long for readability) 

530 if len(pkt_contents) > 80: 

531 logger.debug( 

532 "git< %s... (%d bytes)", pkt_contents[:80], len(pkt_contents) 

533 ) 

534 else: 

535 logger.debug("git< %s", pkt_contents) 

536 return pkt_contents 

537 

538 def eof(self) -> bool: 

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

540 

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

542 flush-pkt. 

543 

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

545 """ 

546 try: 

547 next_line = self.read_pkt_line() 

548 except HangupException: 

549 return True 

550 self.unread_pkt_line(next_line) 

551 return False 

552 

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

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

555 

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

557 readahead buffer. 

558 

559 Args: 

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

561 

562 Raises: 

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

564 """ 

565 if self._readahead is not None: 

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

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

568 

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

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

571 

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

573 flush-pkt. 

574 """ 

575 pkt = self.read_pkt_line() 

576 while pkt: 

577 yield pkt 

578 pkt = self.read_pkt_line() 

579 

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

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

582 

583 Args: 

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

585 prefix. 

586 """ 

587 try: 

588 # Log before converting to pkt format 

589 if line is None: 

590 logger.debug("git> 0000") 

591 elif len(line) > 80: 

592 logger.debug("git> %s... (%d bytes)", line[:80], len(line)) 

593 else: 

594 logger.debug("git> %s", line) 

595 

596 line = pkt_line(line) 

597 self.write(line) 

598 if self.report_activity: 

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

600 except OSError as exc: 

601 raise GitProtocolError(str(exc)) from exc 

602 

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

604 """Write multiplexed data to the sideband. 

605 

606 Args: 

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

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

609 """ 

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

611 # 65520-5 = 65515 

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

613 while blob: 

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

615 blob = blob[65515:] 

616 

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

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

619 

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

621 

622 Args: 

623 cmd: The remote service to access. 

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

625 """ 

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

627 

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

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

630 

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

632 

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

634 """ 

635 line = self.read_pkt_line() 

636 if line is None: 

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

638 return parse_cmd_pkt(line) 

639 

640 

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

642 

643 

644class ReceivableProtocol(Protocol): 

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

646 

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

648 to a read() method. 

649 

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

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

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

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

654 """ 

655 

656 def __init__( 

657 self, 

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

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

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

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

662 rbufsize: int = _RBUFSIZE, 

663 ) -> None: 

664 """Initialize ReceivableProtocol. 

665 

666 Args: 

667 recv: Function to receive bytes from the transport 

668 write: Function to write bytes to the transport 

669 close: Optional function to close the transport 

670 report_activity: Optional function to report activity 

671 rbufsize: Read buffer size 

672 """ 

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

674 self._recv = recv 

675 self._rbuf = BytesIO() 

676 self._rbufsize = rbufsize 

677 

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

679 """Read bytes from the socket. 

680 

681 Args: 

682 size: Number of bytes to read 

683 

684 Returns: 

685 Bytes read from socket 

686 """ 

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

688 # with the following modifications: 

689 # - omit the size <= 0 branch 

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

691 # consumed. 

692 # - use SEEK_END instead of the magic number. 

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

694 # Reserved 

695 # Licensed under the Python Software Foundation License. 

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

697 assert size > 0 

698 

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

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

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

702 buf = self._rbuf 

703 start = buf.tell() 

704 buf.seek(0, SEEK_END) 

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

706 buf_len = buf.tell() - start 

707 if buf_len >= size: 

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

709 buf.seek(start) 

710 rv = buf.read(size) 

711 self._rbuf = BytesIO() 

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

713 self._rbuf.seek(0) 

714 return rv 

715 

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

717 while True: 

718 left = size - buf_len 

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

720 # parameter even though it often returns much less data 

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

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

723 # fragmentation issues on many platforms. 

724 data = self._recv(left) 

725 if not data: 

726 break 

727 n = len(data) 

728 if n == size and not buf_len: 

729 # Shortcut. Avoid buffer data copies when: 

730 # - We have no data in our buffer. 

731 # AND 

732 # - Our call to recv returned exactly the 

733 # number of bytes we were asked to read. 

734 return data 

735 if n == left: 

736 buf.write(data) 

737 del data # explicit free 

738 break 

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

740 buf.write(data) 

741 buf_len += n 

742 del data # explicit free 

743 # assert buf_len == buf.tell() 

744 buf.seek(start) 

745 return buf.read() 

746 

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

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

749 

750 Args: 

751 size: Maximum number of bytes to receive 

752 

753 Returns: 

754 Bytes received from socket 

755 """ 

756 assert size > 0 

757 

758 buf = self._rbuf 

759 start = buf.tell() 

760 buf.seek(0, SEEK_END) 

761 buf_len = buf.tell() 

762 buf.seek(start) 

763 

764 left = buf_len - start 

765 if not left: 

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

767 data = self._recv(self._rbufsize) 

768 if len(data) == size: 

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

770 return data 

771 buf = BytesIO() 

772 buf.write(data) 

773 buf.seek(0) 

774 del data # explicit free 

775 self._rbuf = buf 

776 return buf.read(size) 

777 

778 

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

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

781 

782 Args: 

783 text: String to extract from 

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

785 """ 

786 if b"\0" not in text: 

787 return text, [] 

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

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

790 

791 

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

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

794 

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

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

797 

798 want obj-id cap1 cap2 ... 

799 

800 Args: 

801 text: Want line to extract from 

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

803 """ 

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

805 if len(split_text) < 3: 

806 return text, [] 

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

808 

809 

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

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

812 if b"multi_ack_detailed" in capabilities: 

813 return MULTI_ACK_DETAILED 

814 elif b"multi_ack" in capabilities: 

815 return MULTI_ACK 

816 return SINGLE_ACK 

817 

818 

819def find_capability( 

820 capabilities: Iterable[bytes], *capability_names: bytes 

821) -> bytes | None: 

822 """Find a capability value in a list of capabilities. 

823 

824 This function looks for capabilities that may include arguments after an equals sign 

825 and returns only the value part (after the '='). For capabilities without values, 

826 returns the capability name itself. 

827 

828 Args: 

829 capabilities: List of capability strings 

830 capability_names: Capability name(s) to search for 

831 

832 Returns: 

833 The value after '=' if found, or the capability name if no '=', or None if not found 

834 

835 Example: 

836 >>> caps = [b'filter=blob:none', b'agent=git/2.0', b'thin-pack'] 

837 >>> find_capability(caps, b'filter') 

838 b'blob:none' 

839 >>> find_capability(caps, b'thin-pack') 

840 b'thin-pack' 

841 >>> find_capability(caps, b'missing') 

842 None 

843 """ 

844 for cap in capabilities: 

845 for name in capability_names: 

846 if cap == name: 

847 return cap 

848 elif cap.startswith(name + b"="): 

849 return cap[len(name) + 1 :] 

850 return None 

851 

852 

853class BufferedPktLineWriter: 

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

855 

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

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

858 (including length prefix) reach the buffer size. 

859 """ 

860 

861 def __init__( 

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

863 ) -> None: 

864 """Initialize the BufferedPktLineWriter. 

865 

866 Args: 

867 write: A write callback for the underlying writer. 

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

869 """ 

870 self._write = write 

871 self._bufsize = bufsize 

872 self._wbuf = BytesIO() 

873 self._buflen = 0 

874 

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

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

877 line = pkt_line(data) 

878 line_len = len(line) 

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

880 if over >= 0: 

881 start = line_len - over 

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

883 self.flush() 

884 else: 

885 start = 0 

886 saved = line[start:] 

887 self._wbuf.write(saved) 

888 self._buflen += len(saved) 

889 

890 def flush(self) -> None: 

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

892 data = self._wbuf.getvalue() 

893 if data: 

894 self._write(data) 

895 self._len = 0 

896 self._wbuf = BytesIO() 

897 

898 

899class PktLineParser: 

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

901 

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

903 """Initialize PktLineParser. 

904 

905 Args: 

906 handle_pkt: Callback function to handle completed packets 

907 """ 

908 self.handle_pkt = handle_pkt 

909 self._readahead = BytesIO() 

910 

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

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

913 self._readahead.write(data) 

914 buf = self._readahead.getvalue() 

915 if len(buf) < 4: 

916 return 

917 while len(buf) >= 4: 

918 size = _parse_pkt_line_length(buf[:4]) 

919 if size == 0: 

920 self.handle_pkt(None) 

921 buf = buf[4:] 

922 elif size < 4: 

923 raise GitProtocolError(f"Invalid pkt-line length: {size:04x}") 

924 elif size <= len(buf): 

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

926 buf = buf[size:] 

927 else: 

928 break 

929 self._readahead = BytesIO() 

930 self._readahead.write(buf) 

931 

932 def get_tail(self) -> bytes: 

933 """Read back any unused data.""" 

934 return self._readahead.getvalue() 

935 

936 

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

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

939 

940 Args: 

941 capabilities: List of capability strings 

942 

943 Returns: 

944 Space-separated capabilities as bytes 

945 """ 

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

947 

948 

949def format_ref_line( 

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

951) -> bytes: 

952 """Format a ref advertisement line. 

953 

954 Args: 

955 ref: Reference name 

956 sha: SHA hash 

957 capabilities: Optional list of capabilities 

958 

959 Returns: 

960 Formatted ref line 

961 """ 

962 if capabilities is None: 

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

964 else: 

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

966 

967 

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

969 """Format a shallow line. 

970 

971 Args: 

972 sha: SHA to mark as shallow 

973 

974 Returns: 

975 Formatted shallow line 

976 """ 

977 return COMMAND_SHALLOW + b" " + sha 

978 

979 

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

981 """Format an unshallow line. 

982 

983 Args: 

984 sha: SHA to unshallow 

985 

986 Returns: 

987 Formatted unshallow line 

988 """ 

989 return COMMAND_UNSHALLOW + b" " + sha 

990 

991 

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

993 """Format an ACK line. 

994 

995 Args: 

996 sha: SHA to acknowledge 

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

998 

999 Returns: 

1000 Formatted ACK line 

1001 """ 

1002 if ack_type: 

1003 ack_type = b" " + ack_type 

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

1005 

1006 

1007def strip_peeled_refs( 

1008 refs: "Mapping[Ref, ObjectID | None]", 

1009) -> "dict[Ref, ObjectID | None]": 

1010 """Remove all peeled refs from a refs dictionary. 

1011 

1012 Args: 

1013 refs: Dictionary of refs (may include peeled refs with ^{} suffix) 

1014 

1015 Returns: 

1016 Dictionary with peeled refs removed 

1017 """ 

1018 return { 

1019 ref: sha for (ref, sha) in refs.items() if not ref.endswith(PEELED_TAG_SUFFIX) 

1020 } 

1021 

1022 

1023def split_peeled_refs( 

1024 refs: "Mapping[Ref, ObjectID]", 

1025) -> "tuple[dict[Ref, ObjectID], dict[Ref, ObjectID]]": 

1026 """Split peeled refs from regular refs. 

1027 

1028 Args: 

1029 refs: Dictionary of refs (may include peeled refs with ^{} suffix) 

1030 

1031 Returns: 

1032 Tuple of (regular_refs, peeled_refs) where peeled_refs keys have 

1033 the ^{} suffix removed 

1034 """ 

1035 from .refs import Ref 

1036 

1037 peeled: dict[Ref, ObjectID] = {} 

1038 regular = {k: v for k, v in refs.items() if not k.endswith(PEELED_TAG_SUFFIX)} 

1039 

1040 for ref, sha in refs.items(): 

1041 if ref.endswith(PEELED_TAG_SUFFIX): 

1042 # Peeled refs are always ObjectID values 

1043 peeled[Ref(ref[: -len(PEELED_TAG_SUFFIX)])] = sha 

1044 

1045 return regular, peeled 

1046 

1047 

1048def write_info_refs( 

1049 refs: "Mapping[Ref, ObjectID]", store: "ObjectContainer" 

1050) -> "Iterator[bytes]": 

1051 """Generate info refs in the format used by the dumb HTTP protocol. 

1052 

1053 Args: 

1054 refs: Dictionary of refs 

1055 store: Object store to peel tags from 

1056 

1057 Yields: 

1058 Lines in info/refs format (sha + tab + refname) 

1059 """ 

1060 from .object_store import peel_sha 

1061 from .refs import HEADREF 

1062 

1063 for name, sha in sorted(refs.items()): 

1064 # get_refs() includes HEAD as a special case, but we don't want to 

1065 # advertise it 

1066 if name == HEADREF: 

1067 continue 

1068 try: 

1069 o = store[sha] 

1070 except KeyError: 

1071 continue 

1072 _unpeeled, peeled = peel_sha(store, sha) 

1073 yield o.id + b"\t" + name + b"\n" 

1074 if o.id != peeled.id: 

1075 yield peeled.id + b"\t" + name + PEELED_TAG_SUFFIX + b"\n" 

1076 

1077 

1078def serialize_refs( 

1079 store: "ObjectContainer", refs: "Mapping[Ref, ObjectID]" 

1080) -> "dict[bytes, ObjectID]": 

1081 """Serialize refs with peeled refs for Git protocol v0/v1. 

1082 

1083 This function is used to prepare refs for transmission over the Git protocol. 

1084 For tags, it includes both the tag object and the dereferenced object. 

1085 

1086 Args: 

1087 store: Object store to peel refs from 

1088 refs: Dictionary of ref names to SHAs 

1089 

1090 Returns: 

1091 Dictionary with refs and peeled refs (marked with ^{}) 

1092 """ 

1093 import warnings 

1094 

1095 from .object_store import peel_sha 

1096 

1097 ret: dict[bytes, ObjectID] = {} 

1098 for ref, sha in refs.items(): 

1099 try: 

1100 unpeeled, peeled = peel_sha(store, ObjectID(sha)) 

1101 except KeyError: 

1102 warnings.warn( 

1103 "ref {} points at non-present sha {}".format( 

1104 ref.decode("utf-8", "replace"), sha.decode("ascii") 

1105 ), 

1106 UserWarning, 

1107 ) 

1108 continue 

1109 else: 

1110 if isinstance(unpeeled, Tag): 

1111 ret[ref + PEELED_TAG_SUFFIX] = peeled.id 

1112 ret[ref] = unpeeled.id 

1113 return ret