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

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

2009 statements  

1# pack.py -- For dealing with packed git objects. 

2# Copyright (C) 2007 James Westby <jw+debian@jameswestby.net> 

3# Copyright (C) 2008-2013 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"""Classes for dealing with packed git objects. 

24 

25A pack is a compact representation of a bunch of objects, stored 

26using deltas where possible. 

27 

28They have two parts, the pack file, which stores the data, and an index 

29that tells you where the data is. 

30 

31To find an object you look in all of the index files 'til you find a 

32match for the object name. You then use the pointer got from this as 

33a pointer in to the corresponding packfile. 

34""" 

35 

36__all__ = [ 

37 "DEFAULT_PACK_DELTA_WINDOW_SIZE", 

38 "DEFAULT_PACK_INDEX_VERSION", 

39 "DELTA_TYPES", 

40 "OFS_DELTA", 

41 "PACK_SPOOL_FILE_MAX_SIZE", 

42 "REF_DELTA", 

43 "DeltaChainIterator", 

44 "FilePackIndex", 

45 "MemoryPackIndex", 

46 "ObjectContainer", 

47 "Pack", 

48 "PackChunkGenerator", 

49 "PackData", 

50 "PackFileDisappeared", 

51 "PackHint", 

52 "PackIndex", 

53 "PackIndex1", 

54 "PackIndex2", 

55 "PackIndex3", 

56 "PackIndexEntry", 

57 "PackIndexer", 

58 "PackInflater", 

59 "PackStreamCopier", 

60 "PackStreamReader", 

61 "PackedObjectContainer", 

62 "SHA1Reader", 

63 "SHA1Writer", 

64 "UnpackedObject", 

65 "UnpackedObjectIterator", 

66 "UnpackedObjectStream", 

67 "UnresolvedDeltas", 

68 "apply_delta", 

69 "bisect_find_sha", 

70 "chunks_length", 

71 "compute_buffer_sha", 

72 "compute_file_sha", 

73 "deltas_from_sorted_objects", 

74 "deltify_pack_objects", 

75 "extend_pack", 

76 "find_reusable_deltas", 

77 "full_unpacked_object", 

78 "generate_unpacked_objects", 

79 "iter_sha1", 

80 "load_pack_index", 

81 "load_pack_index_file", 

82 "obj_sha", 

83 "pack_header_chunks", 

84 "pack_object_chunks", 

85 "pack_object_header", 

86 "pack_objects_to_data", 

87 "read_pack_header", 

88 "read_pack_header_at", 

89 "read_zlib_chunks", 

90 "read_zlib_chunks_at", 

91 "sort_objects_for_delta", 

92 "take_msb_bytes", 

93 "take_msb_bytes_at", 

94 "unpack_object", 

95 "unpack_object_at", 

96 "verify_and_read", 

97 "write_pack", 

98 "write_pack_data", 

99 "write_pack_from_container", 

100 "write_pack_header", 

101 "write_pack_index", 

102 "write_pack_object", 

103 "write_pack_objects", 

104] 

105 

106import binascii 

107from collections import defaultdict, deque 

108from contextlib import suppress 

109from io import BytesIO, UnsupportedOperation 

110 

111try: 

112 from cdifflib import CSequenceMatcher as SequenceMatcher 

113except ModuleNotFoundError: 

114 from difflib import SequenceMatcher 

115 

116import logging 

117import os 

118import struct 

119import sys 

120import threading 

121import warnings 

122import zlib 

123from collections.abc import Callable, Iterable, Iterator, Sequence, Set 

124from hashlib import sha1, sha256 

125from itertools import chain 

126from os import SEEK_END 

127from struct import unpack_from 

128from types import TracebackType 

129from typing import ( 

130 IO, 

131 TYPE_CHECKING, 

132 Any, 

133 BinaryIO, 

134 Generic, 

135 Protocol, 

136 TypeVar, 

137) 

138 

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

140 from typing import Self 

141else: 

142 from typing_extensions import Self 

143 

144import mmap 

145 

146from .errors import ApplyDeltaError, ChecksumMismatch 

147from .file import GitFile, _GitFile 

148from .lru_cache import LRUSizeCache 

149from .object_format import OBJECT_FORMAT_TYPE_NUMS, SHA1, ObjectFormat 

150from .objects import ( 

151 ObjectID, 

152 RawObjectID, 

153 ShaFile, 

154 hex_to_sha, 

155 object_header, 

156 sha_to_hex, 

157) 

158 

159if TYPE_CHECKING: 

160 from _hashlib import HASH as HashObject 

161 

162 from .bitmap import PackBitmap 

163 from .commit_graph import CommitGraph 

164 from .object_store import BaseObjectStore 

165 from .refs import Ref 

166 

167logger = logging.getLogger(__name__) 

168 

169# Some platforms (e.g. plan9) don't support mmap properly 

170has_mmap = sys.platform != "Plan9" 

171 

172OFS_DELTA = 6 

173REF_DELTA = 7 

174 

175DELTA_TYPES = (OFS_DELTA, REF_DELTA) 

176 

177 

178DEFAULT_PACK_DELTA_WINDOW_SIZE = 10 

179 

180# Keep pack files under 16Mb in memory, otherwise write them out to disk 

181PACK_SPOOL_FILE_MAX_SIZE = 16 * 1024 * 1024 

182 

183# Default pack index version to use when none is specified 

184DEFAULT_PACK_INDEX_VERSION = 2 

185 

186 

187OldUnpackedObject = tuple[bytes | int, list[bytes]] | list[bytes] | bytes 

188ResolveExtRefFn = Callable[[RawObjectID | ObjectID], tuple[int, bytes | list[bytes]]] 

189ProgressFn = Callable[[int, str], None] 

190PackHint = tuple[int, bytes | None] 

191 

192 

193def verify_and_read( 

194 read_func: Callable[[int], bytes], 

195 expected_hash: bytes, 

196 hash_algo: str, 

197 progress: Callable[[bytes], None] | None = None, 

198) -> Iterator[bytes]: 

199 """Read from stream, verify hash, then yield verified chunks. 

200 

201 This function downloads data to a temporary file (in-memory for small files, 

202 on-disk for large ones) while computing its hash. Only after the hash is 

203 verified to match expected_hash will it yield any data. This prevents 

204 corrupted or malicious data from reaching the caller. 

205 

206 Args: 

207 read_func: Function to read bytes (like file.read or HTTP response reader) 

208 expected_hash: Expected hash as hex string bytes (e.g., b'a3b2c1...') 

209 hash_algo: Hash algorithm name ('sha1' or 'sha256') 

210 progress: Optional progress callback 

211 

212 Yields: 

213 Chunks of verified data (only after hash verification succeeds) 

214 

215 Raises: 

216 ValueError: If hash doesn't match or algorithm unsupported 

217 """ 

218 from tempfile import SpooledTemporaryFile 

219 

220 from .object_format import OBJECT_FORMATS 

221 

222 # Get the hash function for this algorithm 

223 obj_format = OBJECT_FORMATS.get(hash_algo) 

224 if obj_format is None: 

225 raise ValueError(f"Unsupported hash algorithm: {hash_algo}") 

226 

227 hasher = obj_format.new_hash() 

228 

229 # Download to temporary file (memory or disk) while computing hash 

230 with SpooledTemporaryFile( 

231 max_size=PACK_SPOOL_FILE_MAX_SIZE, prefix="dulwich-verify-" 

232 ) as temp_file: 

233 # Read data, hash it, and write to temp file 

234 while True: 

235 chunk = read_func(65536) # Read in 64KB chunks 

236 if not chunk: 

237 break 

238 hasher.update(chunk) 

239 temp_file.write(chunk) 

240 

241 # Verify hash BEFORE yielding any data 

242 computed_hash = hasher.hexdigest().encode("ascii") 

243 if computed_hash != expected_hash: 

244 raise ValueError( 

245 f"hash mismatch: expected {expected_hash.decode('ascii')}, " 

246 f"got {computed_hash.decode('ascii')}" 

247 ) 

248 

249 # Hash verified! Now read from temp file and yield chunks 

250 if progress: 

251 progress(b"Hash verified, processing data\n") 

252 

253 temp_file.seek(0) 

254 while True: 

255 chunk = temp_file.read(65536) 

256 if not chunk: 

257 break 

258 yield chunk 

259 

260 

261class UnresolvedDeltas(Exception): 

262 """Delta objects could not be resolved.""" 

263 

264 def __init__(self, shas: list[bytes]) -> None: 

265 """Initialize UnresolvedDeltas exception. 

266 

267 Args: 

268 shas: List of SHA hashes for unresolved delta objects 

269 """ 

270 self.shas = shas 

271 

272 

273class DeltaCycle(UnresolvedDeltas): 

274 """A pack's delta chain references itself and cannot be resolved.""" 

275 

276 

277class ObjectContainer(Protocol): 

278 """Protocol for objects that can contain git objects.""" 

279 

280 def add_object(self, obj: ShaFile) -> None: 

281 """Add a single object to this object store.""" 

282 

283 def add_objects( 

284 self, 

285 objects: Sequence[tuple[ShaFile, str | None]], 

286 progress: Callable[..., None] | None = None, 

287 ) -> "Pack | None": 

288 """Add a set of objects to this object store. 

289 

290 Args: 

291 objects: Iterable over a list of (object, path) tuples 

292 progress: Progress callback for object insertion 

293 Returns: Optional Pack object of the objects written. 

294 """ 

295 

296 def __contains__(self, sha1: "ObjectID") -> bool: 

297 """Check if a hex sha is present.""" 

298 ... 

299 

300 def __getitem__(self, sha1: "ObjectID | RawObjectID") -> ShaFile: 

301 """Retrieve an object.""" 

302 ... 

303 

304 def get_commit_graph(self) -> "CommitGraph | None": 

305 """Get the commit graph for this object store. 

306 

307 Returns: 

308 CommitGraph object if available, None otherwise 

309 """ 

310 return None 

311 

312 

313class PackedObjectContainer(ObjectContainer): 

314 """Container for objects packed in a pack file.""" 

315 

316 def get_unpacked_object( 

317 self, sha1: "ObjectID | RawObjectID", *, include_comp: bool = False 

318 ) -> "UnpackedObject": 

319 """Get a raw unresolved object. 

320 

321 Args: 

322 sha1: SHA-1 hash of the object 

323 include_comp: Whether to include compressed data 

324 

325 Returns: 

326 UnpackedObject instance 

327 """ 

328 raise NotImplementedError(self.get_unpacked_object) 

329 

330 def iterobjects_subset( 

331 self, shas: Iterable["ObjectID"], *, allow_missing: bool = False 

332 ) -> Iterator[ShaFile]: 

333 """Iterate over a subset of objects. 

334 

335 Args: 

336 shas: Iterable of object SHAs to retrieve 

337 allow_missing: If True, skip missing objects 

338 

339 Returns: 

340 Iterator of ShaFile objects 

341 """ 

342 raise NotImplementedError(self.iterobjects_subset) 

343 

344 def iter_unpacked_subset( 

345 self, 

346 shas: Iterable["ObjectID | RawObjectID"], 

347 *, 

348 include_comp: bool = False, 

349 allow_missing: bool = False, 

350 convert_ofs_delta: bool = True, 

351 ) -> Iterator["UnpackedObject"]: 

352 """Iterate over unpacked objects from a subset of SHAs. 

353 

354 Args: 

355 shas: Set of object SHAs to retrieve 

356 include_comp: Include compressed data if True 

357 allow_missing: If True, skip missing objects 

358 convert_ofs_delta: If True, convert offset deltas to ref deltas 

359 

360 Returns: 

361 Iterator of UnpackedObject instances 

362 """ 

363 raise NotImplementedError(self.iter_unpacked_subset) 

364 

365 

366class UnpackedObjectStream: 

367 """Abstract base class for a stream of unpacked objects.""" 

368 

369 def __iter__(self) -> Iterator["UnpackedObject"]: 

370 """Iterate over unpacked objects.""" 

371 raise NotImplementedError(self.__iter__) 

372 

373 def __len__(self) -> int: 

374 """Return the number of objects in the stream.""" 

375 raise NotImplementedError(self.__len__) 

376 

377 

378def take_msb_bytes( 

379 read: Callable[[int], bytes], crc32: int | None = None 

380) -> tuple[list[int], int | None]: 

381 """Read bytes marked with most significant bit. 

382 

383 Args: 

384 read: Read function 

385 crc32: Optional CRC32 checksum to update 

386 

387 Returns: 

388 Tuple of (list of bytes read, updated CRC32 or None) 

389 """ 

390 ret: list[int] = [] 

391 while len(ret) == 0 or ret[-1] & 0x80: 

392 b = read(1) 

393 if crc32 is not None: 

394 crc32 = binascii.crc32(b, crc32) 

395 ret.append(ord(b[:1])) 

396 return ret, crc32 

397 

398 

399def take_msb_bytes_at( 

400 contents: "bytes | mmap.mmap", offset: int, crc32: int | None = None 

401) -> tuple[list[int], int, int | None]: 

402 """Read bytes marked with most significant bit from a buffer at an offset. 

403 

404 Args: 

405 contents: Buffer to read from 

406 offset: Offset in contents to start reading at 

407 crc32: Optional CRC32 checksum to update 

408 

409 Returns: 

410 Tuple of (list of bytes read, offset just past them, updated CRC32 or None) 

411 """ 

412 ret: list[int] = [] 

413 pos = offset 

414 while len(ret) == 0 or ret[-1] & 0x80: 

415 b = contents[pos : pos + 1] 

416 if not b: 

417 raise AssertionError(f"unexpected end of pack data at {pos}") 

418 pos += 1 

419 if crc32 is not None: 

420 crc32 = binascii.crc32(b, crc32) 

421 ret.append(ord(b)) 

422 return ret, pos, crc32 

423 

424 

425class PackFileDisappeared(Exception): 

426 """Raised when a pack file unexpectedly disappears. 

427 

428 This typically happens when a concurrent operation (e.g. ``git repack`` 

429 or ``git gc --auto``) removes a pack file between the moment dulwich 

430 snapshots the pack directory and the moment it actually opens the 

431 pack's ``.idx`` or ``.pack`` file. 

432 

433 The ``obj`` attribute holds the :class:`Pack` (or :class:`FilePackIndex`) 

434 whose backing file vanished, so the caller can evict the stale object 

435 from its cache and rescan the pack directory. 

436 """ 

437 

438 obj: "Pack | FilePackIndex" 

439 

440 def __init__(self, obj: "Pack | FilePackIndex") -> None: 

441 """Initialize PackFileDisappeared exception. 

442 

443 Args: 

444 obj: The pack or pack index that disappeared. 

445 """ 

446 self.obj = obj 

447 

448 

449class UnpackedObject: 

450 """Class encapsulating an object unpacked from a pack file. 

451 

452 These objects should only be created from within unpack_object. Most 

453 members start out as empty and are filled in at various points by 

454 read_zlib_chunks, unpack_object, DeltaChainIterator, etc. 

455 

456 End users of this object should take care that the function they're getting 

457 this object from is guaranteed to set the members they need. 

458 """ 

459 

460 __slots__ = [ 

461 "_sha", # Cached binary SHA. 

462 "comp_chunks", # Compressed object chunks. 

463 "crc32", # CRC32. 

464 "decomp_chunks", # Decompressed object chunks. 

465 "decomp_len", # Decompressed length of this object. 

466 "delta_base", # Delta base offset or SHA. 

467 "hash_func", # Hash function to use for computing object IDs. 

468 "obj_chunks", # Decompressed and delta-resolved chunks. 

469 "obj_type_num", # Type of this object. 

470 "offset", # Offset in its pack. 

471 "pack_type_num", # Type of this object in the pack (may be a delta). 

472 ] 

473 

474 obj_type_num: int | None 

475 obj_chunks: list[bytes] | None 

476 delta_base: bytes | int | None 

477 decomp_chunks: list[bytes] 

478 comp_chunks: list[bytes] | None 

479 decomp_len: int | None 

480 crc32: int | None 

481 offset: int | None 

482 pack_type_num: int 

483 _sha: bytes | None 

484 hash_func: Callable[[], "HashObject"] 

485 

486 # TODO(dborowitz): read_zlib_chunks and unpack_object could very well be 

487 # methods of this object. 

488 def __init__( 

489 self, 

490 pack_type_num: int, 

491 *, 

492 delta_base: bytes | int | None = None, 

493 decomp_len: int | None = None, 

494 crc32: int | None = None, 

495 sha: bytes | None = None, 

496 decomp_chunks: list[bytes] | None = None, 

497 offset: int | None = None, 

498 hash_func: Callable[[], "HashObject"] = sha1, 

499 ) -> None: 

500 """Initialize an UnpackedObject. 

501 

502 Args: 

503 pack_type_num: Type number of this object in the pack 

504 delta_base: Delta base (offset or SHA) if this is a delta object 

505 decomp_len: Decompressed length of this object 

506 crc32: CRC32 checksum 

507 sha: SHA hash of the object 

508 decomp_chunks: Decompressed chunks 

509 offset: Offset in the pack file 

510 hash_func: Hash function to use (defaults to sha1) 

511 """ 

512 self.offset = offset 

513 self._sha = sha 

514 self.pack_type_num = pack_type_num 

515 self.delta_base = delta_base 

516 self.comp_chunks = None 

517 self.decomp_chunks: list[bytes] = decomp_chunks or [] 

518 if decomp_chunks is not None and decomp_len is None: 

519 self.decomp_len = sum(map(len, decomp_chunks)) 

520 else: 

521 self.decomp_len = decomp_len 

522 self.crc32 = crc32 

523 self.hash_func = hash_func 

524 

525 if pack_type_num in DELTA_TYPES: 

526 self.obj_type_num = None 

527 self.obj_chunks = None 

528 else: 

529 self.obj_type_num = pack_type_num 

530 self.obj_chunks = self.decomp_chunks 

531 self.delta_base = delta_base 

532 

533 def sha(self) -> RawObjectID: 

534 """Return the binary SHA of this object.""" 

535 if self._sha is None: 

536 assert self.obj_type_num is not None and self.obj_chunks is not None 

537 self._sha = obj_sha(self.obj_type_num, self.obj_chunks, self.hash_func) 

538 return RawObjectID(self._sha) 

539 

540 def sha_file(self) -> ShaFile: 

541 """Return a ShaFile from this object.""" 

542 assert self.obj_type_num is not None and self.obj_chunks is not None 

543 return ShaFile.from_raw_chunks(self.obj_type_num, self.obj_chunks) 

544 

545 # Only provided for backwards compatibility with code that expects either 

546 # chunks or a delta tuple. 

547 def _obj(self) -> OldUnpackedObject: 

548 """Return the decompressed chunks, or (delta base, delta chunks).""" 

549 if self.pack_type_num in DELTA_TYPES: 

550 assert isinstance(self.delta_base, bytes | int) 

551 return (self.delta_base, self.decomp_chunks) 

552 else: 

553 return self.decomp_chunks 

554 

555 def __eq__(self, other: object) -> bool: 

556 """Check equality with another UnpackedObject.""" 

557 if not isinstance(other, UnpackedObject): 

558 return False 

559 for slot in self.__slots__: 

560 if getattr(self, slot) != getattr(other, slot): 

561 return False 

562 return True 

563 

564 def __ne__(self, other: object) -> bool: 

565 """Check inequality with another UnpackedObject.""" 

566 return not (self == other) 

567 

568 def __repr__(self) -> str: 

569 """Return string representation of this UnpackedObject.""" 

570 data = [f"{s}={getattr(self, s)!r}" for s in self.__slots__] 

571 return "{}({})".format(self.__class__.__name__, ", ".join(data)) 

572 

573 

574_ZLIB_BUFSIZE = 65536 # 64KB buffer for better I/O performance 

575 

576# Default maximum memory for caching delta base objects (matches Git's default 

577# for core.deltaBaseCacheLimit). 

578DEFAULT_DELTA_BASE_CACHE_LIMIT = 96 * 1024 * 1024 # 96 MiB 

579 

580 

581def read_zlib_chunks( 

582 read_some: Callable[[int], bytes], 

583 unpacked: UnpackedObject, 

584 include_comp: bool = False, 

585 buffer_size: int = _ZLIB_BUFSIZE, 

586) -> bytes: 

587 """Read zlib data from a buffer. 

588 

589 This function requires that the buffer have additional data following the 

590 compressed data, which is guaranteed to be the case for git pack files. 

591 

592 Args: 

593 read_some: Read function that returns at least one byte, but may 

594 return less than the requested size. 

595 unpacked: An UnpackedObject to write result data to. If its crc32 

596 attr is not None, the CRC32 of the compressed bytes will be computed 

597 using this starting CRC32. 

598 After this function, will have the following attrs set: 

599 * comp_chunks (if include_comp is True) 

600 * decomp_chunks 

601 * decomp_len 

602 * crc32 

603 include_comp: If True, include compressed data in the result. 

604 buffer_size: Size of the read buffer. 

605 Returns: Leftover unused data from the decompression. 

606 

607 Raises: 

608 zlib.error: if a decompression error occurred. 

609 """ 

610 if unpacked.decomp_len is None or unpacked.decomp_len <= -1: 

611 raise ValueError("non-negative zlib data stream size expected") 

612 decomp_obj = zlib.decompressobj() 

613 

614 comp_chunks = [] 

615 decomp_chunks = unpacked.decomp_chunks 

616 decomp_len = 0 

617 crc32 = unpacked.crc32 

618 max_decomp = unpacked.decomp_len 

619 

620 while True: 

621 add = read_some(buffer_size) 

622 if not add: 

623 raise zlib.error("EOF before end of zlib stream") 

624 comp_chunks.append(add) 

625 # +1 so overrun surfaces as unconsumed_tail rather than being truncated. 

626 remaining = max_decomp - decomp_len + 1 

627 decomp = decomp_obj.decompress(add, remaining) 

628 if decomp_obj.unconsumed_tail: 

629 raise zlib.error("decompressed data exceeds expected size") 

630 decomp_len += len(decomp) 

631 decomp_chunks.append(decomp) 

632 unused = decomp_obj.unused_data 

633 if unused: 

634 left = len(unused) 

635 if crc32 is not None: 

636 crc32 = binascii.crc32(add[:-left], crc32) 

637 if include_comp: 

638 comp_chunks[-1] = add[:-left] 

639 break 

640 elif crc32 is not None: 

641 crc32 = binascii.crc32(add, crc32) 

642 if crc32 is not None: 

643 crc32 &= 0xFFFFFFFF 

644 

645 if decomp_len != unpacked.decomp_len: 

646 raise zlib.error("decompressed data does not match expected size") 

647 

648 unpacked.crc32 = crc32 

649 if include_comp: 

650 unpacked.comp_chunks = comp_chunks 

651 return unused 

652 

653 

654def read_zlib_chunks_at( 

655 contents: "bytes | mmap.mmap", 

656 offset: int, 

657 unpacked: UnpackedObject, 

658 include_comp: bool = False, 

659 buffer_size: int = _ZLIB_BUFSIZE, 

660) -> int: 

661 """Read zlib data from a buffer at a given offset. 

662 

663 Like :func:`read_zlib_chunks`, but indexes the buffer directly instead of 

664 consuming a read callable, so concurrent readers do not share a position. 

665 

666 The buffer is fed to zlib in ``buffer_size`` slices rather than in one 

667 piece. That bounds ``unused_data``, which zlib materialises as a copy of 

668 everything it was handed past the end of the stream: passing the whole 

669 mapping would copy the entire remainder of the pack for every object read. 

670 

671 Slices are taken as memoryviews, so the compressed data is decompressed 

672 straight out of the mapping. With ``include_comp`` the chunks are kept on 

673 ``unpacked`` and outlive the mapping, so those are copied. 

674 

675 Args: 

676 contents: Buffer holding the compressed data. 

677 offset: Offset in contents at which the zlib stream starts. 

678 unpacked: An UnpackedObject to write result data to; see 

679 :func:`read_zlib_chunks` for the attributes set on it. 

680 include_comp: If True, include compressed data in the result. 

681 buffer_size: Number of bytes to feed to zlib at a time. 

682 Returns: Offset in contents just past the end of the zlib stream. 

683 

684 Raises: 

685 zlib.error: if a decompression error occurred. 

686 """ 

687 if unpacked.decomp_len is None or unpacked.decomp_len <= -1: 

688 raise ValueError("non-negative zlib data stream size expected") 

689 decomp_obj = zlib.decompressobj() 

690 

691 comp_chunks = [] 

692 decomp_chunks = unpacked.decomp_chunks 

693 decomp_len = 0 

694 crc32 = unpacked.crc32 

695 max_decomp = unpacked.decomp_len 

696 pos = offset 

697 

698 with memoryview(contents) as view: 

699 while True: 

700 add = view[pos : pos + buffer_size] 

701 if not add: 

702 raise zlib.error("EOF before end of zlib stream") 

703 pos += len(add) 

704 # +1 so overrun surfaces as unconsumed_tail rather than being truncated. 

705 remaining = max_decomp - decomp_len + 1 

706 decomp = decomp_obj.decompress(add, remaining) 

707 if decomp_obj.unconsumed_tail: 

708 raise zlib.error("decompressed data exceeds expected size") 

709 decomp_len += len(decomp) 

710 decomp_chunks.append(decomp) 

711 unused = decomp_obj.unused_data 

712 if unused: 

713 left = len(unused) 

714 pos -= left 

715 add = add[:-left] 

716 if crc32 is not None: 

717 crc32 = binascii.crc32(add, crc32) 

718 if include_comp: 

719 comp_chunks.append(bytes(add)) 

720 if unused: 

721 break 

722 if crc32 is not None: 

723 crc32 &= 0xFFFFFFFF 

724 

725 if decomp_len != unpacked.decomp_len: 

726 raise zlib.error("decompressed data does not match expected size") 

727 

728 unpacked.crc32 = crc32 

729 if include_comp: 

730 unpacked.comp_chunks = comp_chunks 

731 return pos 

732 

733 

734def iter_sha1(iter: Iterable[bytes]) -> bytes: 

735 """Return the hexdigest of the SHA1 over a set of names. 

736 

737 Args: 

738 iter: Iterator over string objects 

739 Returns: 40-byte hex sha1 digest 

740 """ 

741 sha = sha1() 

742 for name in iter: 

743 sha.update(name) 

744 return sha.hexdigest().encode("ascii") 

745 

746 

747def load_pack_index( 

748 path: str | os.PathLike[str], object_format: ObjectFormat 

749) -> "PackIndex": 

750 """Load an index file by path. 

751 

752 Args: 

753 path: Path to the index file 

754 object_format: Hash algorithm used by the repository 

755 Returns: A PackIndex loaded from the given path 

756 """ 

757 # Ownership of the file is transferred to the returned index, which mmaps 

758 # it and closes it in PackIndex.close(). It must not be closed here: on 

759 # Windows an mmap keeps the file locked, so closing the handle out from 

760 # under a live mapping leaves the .idx undeletable until the index is GCed. 

761 f = GitFile(path, "rb") 

762 try: 

763 return load_pack_index_file(path, f, object_format) 

764 except BaseException: 

765 f.close() 

766 raise 

767 

768 

769def _load_file_contents( 

770 f: IO[bytes] | _GitFile, size: int | None = None 

771) -> tuple[bytes | Any, int]: 

772 """Load contents from a file, preferring mmap when possible. 

773 

774 Args: 

775 f: File-like object to load 

776 size: Expected size, or None to determine from file 

777 Returns: Tuple of (contents, size) 

778 """ 

779 # Avoid rolling a SpooledTemporaryFile to disk just to get a descriptor. 

780 if getattr(f, "_rolled", True) is False: 

781 fd = None 

782 else: 

783 try: 

784 fd = f.fileno() 

785 except (UnsupportedOperation, AttributeError): 

786 fd = None 

787 # Attempt to use mmap if possible 

788 if fd is not None: 

789 if size is None: 

790 size = os.fstat(fd).st_size 

791 if has_mmap: 

792 try: 

793 contents = mmap.mmap(fd, size, access=mmap.ACCESS_READ) 

794 except (OSError, ValueError): 

795 # Can't mmap - perhaps a socket or invalid file descriptor 

796 pass 

797 else: 

798 return contents, size 

799 contents_bytes = f.read() 

800 size = len(contents_bytes) 

801 return contents_bytes, size 

802 

803 

804def _close_file_contents(contents: "bytes | mmap.mmap | None") -> None: 

805 """Close contents returned by _load_file_contents, if closeable. 

806 

807 Callers must close the mapping before the file it maps: on Windows the 

808 mapping holds a lock on the file, so the handle cannot be released while 

809 it is alive. 

810 """ 

811 close_fn = getattr(contents, "close", None) 

812 if close_fn is not None: 

813 close_fn() 

814 

815 

816def load_pack_index_file( 

817 path: str | os.PathLike[str], 

818 f: IO[bytes] | _GitFile, 

819 object_format: ObjectFormat, 

820) -> "PackIndex": 

821 """Load an index file from a file-like object. 

822 

823 Args: 

824 path: Path for the index file 

825 f: File-like object 

826 object_format: Hash algorithm used by the repository 

827 Returns: A PackIndex loaded from the given file 

828 """ 

829 contents, size = _load_file_contents(f) 

830 if contents[:4] == b"\377tOc": 

831 version = struct.unpack(b">L", contents[4:8])[0] 

832 if version == 2: 

833 return PackIndex2( 

834 path, 

835 object_format, 

836 file=f, 

837 contents=contents, 

838 size=size, 

839 ) 

840 elif version == 3: 

841 return PackIndex3(path, object_format, file=f, contents=contents, size=size) 

842 else: 

843 raise KeyError(f"Unknown pack index format {version}") 

844 else: 

845 return PackIndex1(path, object_format, file=f, contents=contents, size=size) 

846 

847 

848def bisect_find_sha( 

849 start: int, end: int, sha: bytes, unpack_name: Callable[[int], bytes] 

850) -> int | None: 

851 """Find a SHA in a data blob with sorted SHAs. 

852 

853 Args: 

854 start: Start index of range to search 

855 end: End index of range to search 

856 sha: Sha to find 

857 unpack_name: Callback to retrieve SHA by index 

858 Returns: Index of the SHA, or None if it wasn't found 

859 """ 

860 assert start <= end 

861 while start <= end: 

862 i = (start + end) // 2 

863 file_sha = unpack_name(i) 

864 if file_sha < sha: 

865 start = i + 1 

866 elif file_sha > sha: 

867 end = i - 1 

868 else: 

869 return i 

870 return None 

871 

872 

873PackIndexEntry = tuple[RawObjectID, int, int | None] 

874 

875 

876class PackIndex: 

877 """An index in to a packfile. 

878 

879 Given a sha id of an object a pack index can tell you the location in the 

880 packfile of that object if it has it. 

881 """ 

882 

883 object_format: "ObjectFormat" 

884 

885 def __eq__(self, other: object) -> bool: 

886 """Check equality with another PackIndex.""" 

887 if not isinstance(other, PackIndex): 

888 return False 

889 

890 for (name1, _, _), (name2, _, _) in zip( 

891 self.iterentries(), other.iterentries() 

892 ): 

893 if name1 != name2: 

894 return False 

895 return True 

896 

897 def __ne__(self, other: object) -> bool: 

898 """Check if this pack index is not equal to another.""" 

899 return not self.__eq__(other) 

900 

901 def __len__(self) -> int: 

902 """Return the number of entries in this pack index.""" 

903 raise NotImplementedError(self.__len__) 

904 

905 def __iter__(self) -> Iterator[ObjectID]: 

906 """Iterate over the SHAs in this pack.""" 

907 return map(lambda sha: sha_to_hex(RawObjectID(sha)), self._itersha()) 

908 

909 def iterentries(self) -> Iterator[PackIndexEntry]: 

910 """Iterate over the entries in this pack index. 

911 

912 Returns: iterator over tuples with object name, offset in packfile and 

913 crc32 checksum. 

914 """ 

915 raise NotImplementedError(self.iterentries) 

916 

917 def get_pack_checksum(self) -> bytes | None: 

918 """Return the SHA1 checksum stored for the corresponding packfile. 

919 

920 Returns: 20-byte binary digest, or None if not available 

921 """ 

922 raise NotImplementedError(self.get_pack_checksum) 

923 

924 def object_offset(self, sha: ObjectID | RawObjectID) -> int: 

925 """Return the offset in to the corresponding packfile for the object. 

926 

927 Given the name of an object it will return the offset that object 

928 lives at within the corresponding pack file. If the pack file doesn't 

929 have the object then None will be returned. 

930 """ 

931 raise NotImplementedError(self.object_offset) 

932 

933 def object_sha1(self, index: int) -> bytes: 

934 """Return the SHA1 corresponding to the index in the pack file.""" 

935 for name, offset, _crc32 in self.iterentries(): 

936 if offset == index: 

937 return name 

938 else: 

939 raise KeyError(index) 

940 

941 def _object_offset(self, sha: bytes) -> int: 

942 """See object_offset. 

943 

944 Args: 

945 sha: A *binary* SHA string. (20 characters long)_ 

946 """ 

947 raise NotImplementedError(self._object_offset) 

948 

949 def objects_sha1(self) -> bytes: 

950 """Return the hex SHA1 over all the shas of all objects in this pack. 

951 

952 Note: This is used for the filename of the pack. 

953 """ 

954 return iter_sha1(self._itersha()) 

955 

956 def _itersha(self) -> Iterator[bytes]: 

957 """Yield all the SHA1's of the objects in the index, sorted.""" 

958 raise NotImplementedError(self._itersha) 

959 

960 def iter_prefix(self, prefix: bytes) -> Iterator[RawObjectID]: 

961 """Iterate over all SHA1s with the given prefix. 

962 

963 Args: 

964 prefix: Binary prefix to match 

965 Returns: Iterator of matching SHA1s 

966 """ 

967 # Default implementation for PackIndex classes that don't override 

968 for sha, _, _ in self.iterentries(): 

969 if sha.startswith(prefix): 

970 yield RawObjectID(sha) 

971 

972 def close(self) -> None: 

973 """Close any open files.""" 

974 

975 def check(self) -> None: 

976 """Check the consistency of this pack index.""" 

977 

978 

979class MemoryPackIndex(PackIndex): 

980 """Pack index that is stored entirely in memory.""" 

981 

982 def __init__( 

983 self, 

984 entries: list[PackIndexEntry], 

985 object_format: ObjectFormat, 

986 pack_checksum: bytes | None = None, 

987 ) -> None: 

988 """Create a new MemoryPackIndex. 

989 

990 Args: 

991 entries: Sequence of name, idx, crc32 (sorted) 

992 object_format: Object format used by this index 

993 pack_checksum: Optional pack checksum 

994 """ 

995 self._by_sha = {} 

996 self._by_offset = {} 

997 for name, offset, _crc32 in entries: 

998 self._by_sha[name] = offset 

999 self._by_offset[offset] = name 

1000 self._entries = entries 

1001 self._pack_checksum = pack_checksum 

1002 self.object_format = object_format 

1003 

1004 def get_pack_checksum(self) -> bytes | None: 

1005 """Return the SHA checksum stored for the corresponding packfile.""" 

1006 return self._pack_checksum 

1007 

1008 def __len__(self) -> int: 

1009 """Return the number of entries in this pack index.""" 

1010 return len(self._entries) 

1011 

1012 def object_offset(self, sha: ObjectID | RawObjectID) -> int: 

1013 """Return the offset for the given SHA. 

1014 

1015 Args: 

1016 sha: SHA to look up (binary or hex) 

1017 Returns: Offset in the pack file 

1018 """ 

1019 lookup_sha: RawObjectID 

1020 if len(sha) == self.object_format.hex_length: 

1021 lookup_sha = hex_to_sha(ObjectID(sha)) 

1022 else: 

1023 lookup_sha = RawObjectID(sha) 

1024 return self._by_sha[lookup_sha] 

1025 

1026 def object_sha1(self, index: int) -> bytes: 

1027 """Return the SHA1 for the object at the given offset.""" 

1028 return self._by_offset[index] 

1029 

1030 def _itersha(self) -> Iterator[bytes]: 

1031 """Iterate over all SHA1s in the index.""" 

1032 return iter(self._by_sha) 

1033 

1034 def iterentries(self) -> Iterator[PackIndexEntry]: 

1035 """Iterate over all index entries.""" 

1036 return iter(self._entries) 

1037 

1038 @classmethod 

1039 def for_pack(cls, pack_data: "PackData") -> "MemoryPackIndex": 

1040 """Create a MemoryPackIndex from a PackData object.""" 

1041 return MemoryPackIndex( 

1042 list(pack_data.sorted_entries()), 

1043 pack_checksum=pack_data.get_stored_checksum(), 

1044 object_format=pack_data.object_format, 

1045 ) 

1046 

1047 @classmethod 

1048 def clone(cls, other_index: "PackIndex") -> "MemoryPackIndex": 

1049 """Create a copy of another PackIndex in memory.""" 

1050 return cls( 

1051 list(other_index.iterentries()), 

1052 other_index.object_format, 

1053 other_index.get_pack_checksum(), 

1054 ) 

1055 

1056 

1057class FilePackIndex(PackIndex): 

1058 """Pack index that is based on a file. 

1059 

1060 To do the loop it opens the file, and indexes first 256 4 byte groups 

1061 with the first byte of the sha id. The value in the four byte group indexed 

1062 is the end of the group that shares the same starting byte. Subtract one 

1063 from the starting byte and index again to find the start of the group. 

1064 The values are sorted by sha id within the group, so do the math to find 

1065 the start and end offset and then bisect in to find if the value is 

1066 present. 

1067 """ 

1068 

1069 _fan_out_table: list[int] 

1070 _file: IO[bytes] | _GitFile 

1071 

1072 def __init__( 

1073 self, 

1074 filename: str | os.PathLike[str], 

1075 file: IO[bytes] | _GitFile | None = None, 

1076 contents: "bytes | mmap.mmap | None" = None, 

1077 size: int | None = None, 

1078 ) -> None: 

1079 """Create a pack index object. 

1080 

1081 Provide it with the name of the index file to consider, and it will map 

1082 it whenever required. 

1083 """ 

1084 self._filename = filename 

1085 # Take the size now, so it can be checked each time we map the file to 

1086 # ensure that it hasn't changed. 

1087 if file is None: 

1088 self._file = GitFile(filename, "rb") 

1089 else: 

1090 self._file = file 

1091 if contents is None: 

1092 self._contents, self._size = _load_file_contents(self._file, size) 

1093 else: 

1094 self._contents = contents 

1095 self._size = size if size is not None else len(contents) 

1096 

1097 @property 

1098 def path(self) -> str: 

1099 """Return the path to this index file.""" 

1100 return os.fspath(self._filename) 

1101 

1102 def __eq__(self, other: object) -> bool: 

1103 """Check equality with another FilePackIndex.""" 

1104 # Quick optimization: 

1105 if ( 

1106 isinstance(other, FilePackIndex) 

1107 and self._fan_out_table != other._fan_out_table 

1108 ): 

1109 return False 

1110 

1111 return super().__eq__(other) 

1112 

1113 def close(self) -> None: 

1114 """Close the underlying file and any mmap.""" 

1115 _close_file_contents(self._contents) 

1116 self._file.close() 

1117 

1118 def __del__(self) -> None: 

1119 """Ensure the file and mmap are closed when GCed.""" 

1120 if not getattr(self._file, "closed", True): 

1121 import warnings 

1122 

1123 warnings.warn( 

1124 f"unclosed pack index {self!r}", 

1125 ResourceWarning, 

1126 stacklevel=2, 

1127 source=self, 

1128 ) 

1129 try: 

1130 self.close() 

1131 except Exception: 

1132 # Ignore errors during cleanup 

1133 pass 

1134 

1135 def __enter__(self) -> Self: 

1136 """Enter context manager.""" 

1137 return self 

1138 

1139 def __exit__( 

1140 self, 

1141 type: type | None, 

1142 value: BaseException | None, 

1143 traceback: TracebackType | None, 

1144 ) -> None: 

1145 """Exit context manager.""" 

1146 self.close() 

1147 

1148 def __len__(self) -> int: 

1149 """Return the number of entries in this pack index.""" 

1150 return self._fan_out_table[-1] 

1151 

1152 def _unpack_entry(self, i: int) -> PackIndexEntry: 

1153 """Unpack the i-th entry in the index file. 

1154 

1155 Returns: Tuple with object name (SHA), offset in pack file and CRC32 

1156 checksum (if known). 

1157 """ 

1158 raise NotImplementedError(self._unpack_entry) 

1159 

1160 def _unpack_name(self, i: int) -> bytes: 

1161 """Unpack the i-th name from the index file.""" 

1162 raise NotImplementedError(self._unpack_name) 

1163 

1164 def _unpack_offset(self, i: int) -> int: 

1165 """Unpack the i-th object offset from the index file.""" 

1166 raise NotImplementedError(self._unpack_offset) 

1167 

1168 def _unpack_crc32_checksum(self, i: int) -> int | None: 

1169 """Unpack the crc32 checksum for the i-th object from the index file.""" 

1170 raise NotImplementedError(self._unpack_crc32_checksum) 

1171 

1172 def _itersha(self) -> Iterator[bytes]: 

1173 """Iterate over all SHA1s in the index.""" 

1174 for i in range(len(self)): 

1175 yield self._unpack_name(i) 

1176 

1177 def iterentries(self) -> Iterator[PackIndexEntry]: 

1178 """Iterate over the entries in this pack index. 

1179 

1180 Returns: iterator over tuples with object name, offset in packfile and 

1181 crc32 checksum. 

1182 """ 

1183 for i in range(len(self)): 

1184 yield self._unpack_entry(i) 

1185 

1186 def _read_fan_out_table(self, start_offset: int) -> list[int]: 

1187 """Read the fan-out table from the index. 

1188 

1189 The fan-out table contains 256 entries mapping first byte values 

1190 to the number of objects with SHA1s less than or equal to that byte. 

1191 

1192 Args: 

1193 start_offset: Offset in the file where the fan-out table starts 

1194 Returns: List of 256 integers 

1195 """ 

1196 ret = [] 

1197 for i in range(0x100): 

1198 fanout_entry = self._contents[ 

1199 start_offset + i * 4 : start_offset + (i + 1) * 4 

1200 ] 

1201 ret.append(struct.unpack(">L", fanout_entry)[0]) 

1202 return ret 

1203 

1204 def check(self) -> None: 

1205 """Check that the stored checksum matches the actual checksum.""" 

1206 actual = self.calculate_checksum() 

1207 stored = self.get_stored_checksum() 

1208 if actual != stored: 

1209 raise ChecksumMismatch(stored, actual) 

1210 

1211 def calculate_checksum(self) -> bytes: 

1212 """Calculate the SHA1 checksum over this pack index. 

1213 

1214 Returns: This is a 20-byte binary digest 

1215 """ 

1216 return sha1(self._contents[:-20]).digest() 

1217 

1218 def get_pack_checksum(self) -> bytes: 

1219 """Return the SHA1 checksum stored for the corresponding packfile. 

1220 

1221 Returns: 20-byte binary digest 

1222 """ 

1223 return bytes(self._contents[-40:-20]) 

1224 

1225 def get_stored_checksum(self) -> bytes: 

1226 """Return the SHA1 checksum stored for this index. 

1227 

1228 Returns: 20-byte binary digest 

1229 """ 

1230 return bytes(self._contents[-20:]) 

1231 

1232 def object_offset(self, sha: ObjectID | RawObjectID) -> int: 

1233 """Return the offset in to the corresponding packfile for the object. 

1234 

1235 Given the name of an object it will return the offset that object 

1236 lives at within the corresponding pack file. If the pack file doesn't 

1237 have the object then None will be returned. 

1238 """ 

1239 lookup_sha: RawObjectID 

1240 if len(sha) == self.object_format.hex_length: # hex string 

1241 lookup_sha = hex_to_sha(ObjectID(sha)) 

1242 else: 

1243 lookup_sha = RawObjectID(sha) 

1244 try: 

1245 return self._object_offset(lookup_sha) 

1246 except ValueError as exc: 

1247 closed = getattr(self._contents, "closed", None) 

1248 if closed in (None, True): 

1249 raise PackFileDisappeared(self) from exc 

1250 raise 

1251 

1252 def _object_offset(self, sha: bytes) -> int: 

1253 """See object_offset. 

1254 

1255 Args: 

1256 sha: A *binary* SHA string. (20 characters long)_ 

1257 """ 

1258 hash_size = getattr(self, "hash_size", 20) # Default to SHA1 for v1 

1259 assert len(sha) == hash_size 

1260 idx = ord(sha[:1]) 

1261 if idx == 0: 

1262 start = 0 

1263 else: 

1264 start = self._fan_out_table[idx - 1] 

1265 end = self._fan_out_table[idx] 

1266 i = bisect_find_sha(start, end, sha, self._unpack_name) 

1267 if i is None: 

1268 raise KeyError(sha) 

1269 return self._unpack_offset(i) 

1270 

1271 def iter_prefix(self, prefix: bytes) -> Iterator[RawObjectID]: 

1272 """Iterate over all SHA1s with the given prefix.""" 

1273 start = ord(prefix[:1]) 

1274 if start == 0: 

1275 start = 0 

1276 else: 

1277 start = self._fan_out_table[start - 1] 

1278 end = ord(prefix[:1]) + 1 

1279 if end == 0x100: 

1280 end = len(self) 

1281 else: 

1282 end = self._fan_out_table[end] 

1283 assert start <= end 

1284 started = False 

1285 for i in range(start, end): 

1286 name: bytes = self._unpack_name(i) 

1287 if name.startswith(prefix): 

1288 yield RawObjectID(name) 

1289 started = True 

1290 elif started: 

1291 break 

1292 

1293 

1294class PackIndex1(FilePackIndex): 

1295 """Version 1 Pack Index file.""" 

1296 

1297 object_format = SHA1 

1298 

1299 def __init__( 

1300 self, 

1301 filename: str | os.PathLike[str], 

1302 object_format: ObjectFormat, 

1303 file: IO[bytes] | _GitFile | None = None, 

1304 contents: bytes | None = None, 

1305 size: int | None = None, 

1306 ) -> None: 

1307 """Initialize a version 1 pack index. 

1308 

1309 Args: 

1310 filename: Path to the index file 

1311 object_format: Object format used by the repository 

1312 file: Optional file object 

1313 contents: Optional mmap'd contents 

1314 size: Optional size of the index 

1315 """ 

1316 super().__init__(filename, file, contents, size) 

1317 

1318 # PackIndex1 only supports SHA1 

1319 if object_format != SHA1: 

1320 raise AssertionError( 

1321 f"PackIndex1 only supports SHA1, not {object_format.name}" 

1322 ) 

1323 

1324 self.object_format = object_format 

1325 self.version = 1 

1326 self._fan_out_table = self._read_fan_out_table(0) 

1327 self.hash_size = self.object_format.oid_length 

1328 self._entry_size = 4 + self.hash_size 

1329 

1330 def _unpack_entry(self, i: int) -> tuple[RawObjectID, int, None]: 

1331 base_offset = (0x100 * 4) + (i * self._entry_size) 

1332 offset = unpack_from(">L", self._contents, base_offset)[0] 

1333 name = self._contents[base_offset + 4 : base_offset + 4 + self.hash_size] 

1334 return (RawObjectID(name), offset, None) 

1335 

1336 def _unpack_name(self, i: int) -> bytes: 

1337 offset = (0x100 * 4) + (i * self._entry_size) + 4 

1338 return self._contents[offset : offset + self.hash_size] 

1339 

1340 def _unpack_offset(self, i: int) -> int: 

1341 offset = (0x100 * 4) + (i * self._entry_size) 

1342 return int(unpack_from(">L", self._contents, offset)[0]) 

1343 

1344 def _unpack_crc32_checksum(self, i: int) -> None: 

1345 # Not stored in v1 index files 

1346 return None 

1347 

1348 

1349class PackIndex2(FilePackIndex): 

1350 """Version 2 Pack Index file.""" 

1351 

1352 object_format = SHA1 

1353 

1354 def __init__( 

1355 self, 

1356 filename: str | os.PathLike[str], 

1357 object_format: ObjectFormat, 

1358 file: IO[bytes] | _GitFile | None = None, 

1359 contents: bytes | None = None, 

1360 size: int | None = None, 

1361 ) -> None: 

1362 """Initialize a version 2 pack index. 

1363 

1364 Args: 

1365 filename: Path to the index file 

1366 object_format: Object format used by the repository 

1367 file: Optional file object 

1368 contents: Optional mmap'd contents 

1369 size: Optional size of the index 

1370 """ 

1371 super().__init__(filename, file, contents, size) 

1372 self.object_format = object_format 

1373 if self._contents[:4] != b"\377tOc": 

1374 raise AssertionError("Not a v2 pack index file") 

1375 (self.version,) = unpack_from(b">L", self._contents, 4) 

1376 if self.version != 2: 

1377 raise AssertionError(f"Version was {self.version}") 

1378 self._fan_out_table = self._read_fan_out_table(8) 

1379 self.hash_size = self.object_format.oid_length 

1380 self._name_table_offset = 8 + 0x100 * 4 

1381 self._crc32_table_offset = self._name_table_offset + self.hash_size * len(self) 

1382 self._pack_offset_table_offset = self._crc32_table_offset + 4 * len(self) 

1383 self._pack_offset_largetable_offset = self._pack_offset_table_offset + 4 * len( 

1384 self 

1385 ) 

1386 

1387 def _unpack_entry(self, i: int) -> tuple[RawObjectID, int, int]: 

1388 return ( 

1389 RawObjectID(self._unpack_name(i)), 

1390 self._unpack_offset(i), 

1391 self._unpack_crc32_checksum(i), 

1392 ) 

1393 

1394 def _unpack_name(self, i: int) -> bytes: 

1395 offset = self._name_table_offset + i * self.hash_size 

1396 return self._contents[offset : offset + self.hash_size] 

1397 

1398 def _unpack_offset(self, i: int) -> int: 

1399 offset = self._pack_offset_table_offset + i * 4 

1400 offset_val = int(unpack_from(">L", self._contents, offset)[0]) 

1401 if offset_val & (2**31): 

1402 offset = ( 

1403 self._pack_offset_largetable_offset + (offset_val & (2**31 - 1)) * 8 

1404 ) 

1405 offset_val = int(unpack_from(">Q", self._contents, offset)[0]) 

1406 return offset_val 

1407 

1408 def _unpack_crc32_checksum(self, i: int) -> int: 

1409 return int( 

1410 unpack_from(">L", self._contents, self._crc32_table_offset + i * 4)[0] 

1411 ) 

1412 

1413 def get_pack_checksum(self) -> bytes: 

1414 """Return the checksum stored for the corresponding packfile. 

1415 

1416 Returns: binary digest (size depends on hash algorithm) 

1417 """ 

1418 # Index ends with: pack_checksum + index_checksum 

1419 # Each checksum is hash_size bytes 

1420 checksum_size = self.hash_size 

1421 return bytes(self._contents[-2 * checksum_size : -checksum_size]) 

1422 

1423 def get_stored_checksum(self) -> bytes: 

1424 """Return the checksum stored for this index. 

1425 

1426 Returns: binary digest (size depends on hash algorithm) 

1427 """ 

1428 checksum_size = self.hash_size 

1429 return bytes(self._contents[-checksum_size:]) 

1430 

1431 def calculate_checksum(self) -> bytes: 

1432 """Calculate the checksum over this pack index. 

1433 

1434 Returns: binary digest (size depends on hash algorithm) 

1435 """ 

1436 # Determine hash function based on hash_size 

1437 if self.hash_size == 20: 

1438 hash_func = sha1 

1439 elif self.hash_size == 32: 

1440 hash_func = sha256 

1441 else: 

1442 raise ValueError(f"Unsupported hash size: {self.hash_size}") 

1443 

1444 return hash_func(self._contents[: -self.hash_size]).digest() 

1445 

1446 

1447class PackIndex3(FilePackIndex): 

1448 """Version 3 Pack Index file. 

1449 

1450 Supports variable hash sizes for SHA-1 (20 bytes) and SHA-256 (32 bytes). 

1451 """ 

1452 

1453 def __init__( 

1454 self, 

1455 filename: str | os.PathLike[str], 

1456 object_format: ObjectFormat, 

1457 file: IO[bytes] | _GitFile | None = None, 

1458 contents: bytes | None = None, 

1459 size: int | None = None, 

1460 ) -> None: 

1461 """Initialize a version 3 pack index. 

1462 

1463 Args: 

1464 filename: Path to the index file 

1465 object_format: Object format used by the repository 

1466 file: Optional file object 

1467 contents: Optional mmap'd contents 

1468 size: Optional size of the index 

1469 """ 

1470 super().__init__(filename, file, contents, size) 

1471 if self._contents[:4] != b"\377tOc": 

1472 raise AssertionError("Not a v3 pack index file") 

1473 (self.version,) = unpack_from(b">L", self._contents, 4) 

1474 if self.version != 3: 

1475 raise AssertionError(f"Version was {self.version}") 

1476 

1477 # Read hash algorithm identifier (1 = SHA-1, 2 = SHA-256) 

1478 (self.hash_format,) = unpack_from(b">L", self._contents, 8) 

1479 file_object_format = OBJECT_FORMAT_TYPE_NUMS[self.hash_format] 

1480 

1481 # Verify provided object_format matches what's in the file 

1482 if object_format != file_object_format: 

1483 raise AssertionError( 

1484 f"Object format mismatch: provided {object_format.name}, " 

1485 f"but file contains {file_object_format.name}" 

1486 ) 

1487 

1488 self.object_format = object_format 

1489 self.hash_size = self.object_format.oid_length 

1490 

1491 # Read length of shortened object names 

1492 (self.shortened_oid_len,) = unpack_from(b">L", self._contents, 12) 

1493 

1494 # Calculate offsets based on variable hash size 

1495 self._fan_out_table = self._read_fan_out_table( 

1496 16 

1497 ) # After header (4 + 4 + 4 + 4) 

1498 self._name_table_offset = 16 + 0x100 * 4 

1499 self._crc32_table_offset = self._name_table_offset + self.hash_size * len(self) 

1500 self._pack_offset_table_offset = self._crc32_table_offset + 4 * len(self) 

1501 self._pack_offset_largetable_offset = self._pack_offset_table_offset + 4 * len( 

1502 self 

1503 ) 

1504 

1505 def _unpack_entry(self, i: int) -> tuple[RawObjectID, int, int]: 

1506 return ( 

1507 RawObjectID(self._unpack_name(i)), 

1508 self._unpack_offset(i), 

1509 self._unpack_crc32_checksum(i), 

1510 ) 

1511 

1512 def _unpack_name(self, i: int) -> bytes: 

1513 offset = self._name_table_offset + i * self.hash_size 

1514 return self._contents[offset : offset + self.hash_size] 

1515 

1516 def _unpack_offset(self, i: int) -> int: 

1517 offset_pos = self._pack_offset_table_offset + i * 4 

1518 offset = unpack_from(">L", self._contents, offset_pos)[0] 

1519 assert isinstance(offset, int) 

1520 if offset & (2**31): 

1521 large_offset_pos = ( 

1522 self._pack_offset_largetable_offset + (offset & (2**31 - 1)) * 8 

1523 ) 

1524 offset = unpack_from(">Q", self._contents, large_offset_pos)[0] 

1525 assert isinstance(offset, int) 

1526 return offset 

1527 

1528 def _unpack_crc32_checksum(self, i: int) -> int: 

1529 result = unpack_from(">L", self._contents, self._crc32_table_offset + i * 4)[0] 

1530 assert isinstance(result, int) 

1531 return result 

1532 

1533 

1534def read_pack_header_at( 

1535 contents: "bytes | mmap.mmap", offset: int = 0 

1536) -> tuple[int, int]: 

1537 """Read the header of a pack file from a buffer. 

1538 

1539 Args: 

1540 contents: Buffer holding the pack 

1541 offset: Offset in contents at which the header starts 

1542 Returns: Tuple of (pack version, number of objects). 

1543 """ 

1544 header = contents[offset : offset + 12] 

1545 if not header: 

1546 raise AssertionError("file too short to contain pack") 

1547 if header[:4] != b"PACK": 

1548 raise AssertionError(f"Invalid pack header {bytes(header)!r}") 

1549 (version,) = unpack_from(b">L", header, 4) 

1550 if version not in (2, 3): 

1551 raise AssertionError(f"Version was {version}") 

1552 (num_objects,) = unpack_from(b">L", header, 8) 

1553 return (version, num_objects) 

1554 

1555 

1556def read_pack_header(read: Callable[[int], bytes]) -> tuple[int, int]: 

1557 """Read the header of a pack file. 

1558 

1559 Args: 

1560 read: Read function 

1561 Returns: Tuple of (pack version, number of objects). 

1562 """ 

1563 return read_pack_header_at(read(12)) 

1564 

1565 

1566def chunks_length(chunks: bytes | Iterable[bytes]) -> int: 

1567 """Get the total length of a sequence of chunks. 

1568 

1569 Args: 

1570 chunks: Either a single bytes object or an iterable of bytes 

1571 Returns: Total length in bytes 

1572 """ 

1573 if isinstance(chunks, bytes): 

1574 return len(chunks) 

1575 else: 

1576 return sum(map(len, chunks)) 

1577 

1578 

1579def _decode_object_header(raw: list[int]) -> tuple[int, int]: 

1580 """Decode an object type and size from a pack object header.""" 

1581 type_num = (raw[0] >> 4) & 0x07 

1582 size = raw[0] & 0x0F 

1583 for i, byte in enumerate(raw[1:]): 

1584 size += (byte & 0x7F) << ((i * 7) + 4) 

1585 return type_num, size 

1586 

1587 

1588def _decode_delta_base_offset(raw: list[int]) -> int: 

1589 """Decode an OFS_DELTA base offset from its variable-length encoding.""" 

1590 if raw[-1] & 0x80: 

1591 raise AssertionError 

1592 delta_base_offset = raw[0] & 0x7F 

1593 for byte in raw[1:]: 

1594 delta_base_offset += 1 

1595 delta_base_offset <<= 7 

1596 delta_base_offset += byte & 0x7F 

1597 if delta_base_offset == 0: 

1598 # A zero offset makes the delta reference itself, which would 

1599 # loop forever in resolve_object. git's C client rejects this 

1600 # with "delta offset == 0 is invalid". 

1601 raise ApplyDeltaError("OFS_DELTA has delta_base_offset of 0") 

1602 return delta_base_offset 

1603 

1604 

1605def unpack_object( 

1606 read_all: Callable[[int], bytes], 

1607 hash_func: Callable[[], "HashObject"], 

1608 read_some: Callable[[int], bytes] | None = None, 

1609 compute_crc32: bool = False, 

1610 include_comp: bool = False, 

1611 zlib_bufsize: int = _ZLIB_BUFSIZE, 

1612) -> tuple[UnpackedObject, bytes]: 

1613 """Unpack a Git object. 

1614 

1615 Args: 

1616 read_all: Read function that blocks until the number of requested 

1617 bytes are read. 

1618 hash_func: Hash function to use for computing object IDs. 

1619 read_some: Read function that returns at least one byte, but may not 

1620 return the number of bytes requested. 

1621 compute_crc32: If True, compute the CRC32 of the compressed data. If 

1622 False, the returned CRC32 will be None. 

1623 include_comp: If True, include compressed data in the result. 

1624 zlib_bufsize: An optional buffer size for zlib operations. 

1625 Returns: A tuple of (unpacked, unused), where unused is the unused data 

1626 leftover from decompression, and unpacked in an UnpackedObject with 

1627 the following attrs set: 

1628 

1629 * obj_chunks (for non-delta types) 

1630 * pack_type_num 

1631 * delta_base (for delta types) 

1632 * comp_chunks (if include_comp is True) 

1633 * decomp_chunks 

1634 * decomp_len 

1635 * crc32 (if compute_crc32 is True) 

1636 """ 

1637 if read_some is None: 

1638 read_some = read_all 

1639 if compute_crc32: 

1640 crc32 = 0 

1641 else: 

1642 crc32 = None 

1643 

1644 raw, crc32 = take_msb_bytes(read_all, crc32=crc32) 

1645 type_num, size = _decode_object_header(raw) 

1646 

1647 delta_base: int | bytes | None 

1648 raw_base = len(raw) 

1649 if type_num == OFS_DELTA: 

1650 raw, crc32 = take_msb_bytes(read_all, crc32=crc32) 

1651 raw_base += len(raw) 

1652 delta_base = _decode_delta_base_offset(raw) 

1653 elif type_num == REF_DELTA: 

1654 # Determine hash size from hash_func 

1655 hash_size = len(hash_func().digest()) 

1656 delta_base_obj = read_all(hash_size) 

1657 if crc32 is not None: 

1658 crc32 = binascii.crc32(delta_base_obj, crc32) 

1659 delta_base = delta_base_obj 

1660 raw_base += hash_size 

1661 else: 

1662 delta_base = None 

1663 

1664 unpacked = UnpackedObject( 

1665 type_num, 

1666 delta_base=delta_base, 

1667 decomp_len=size, 

1668 crc32=crc32, 

1669 hash_func=hash_func, 

1670 ) 

1671 unused = read_zlib_chunks( 

1672 read_some, 

1673 unpacked, 

1674 buffer_size=zlib_bufsize, 

1675 include_comp=include_comp, 

1676 ) 

1677 return unpacked, unused 

1678 

1679 

1680def unpack_object_at( 

1681 contents: "bytes | mmap.mmap", 

1682 offset: int, 

1683 hash_func: Callable[[], "HashObject"], 

1684 compute_crc32: bool = False, 

1685 include_comp: bool = False, 

1686 zlib_bufsize: int = _ZLIB_BUFSIZE, 

1687) -> tuple[UnpackedObject, int]: 

1688 """Unpack a Git object from a buffer at a given offset. 

1689 

1690 Like :func:`unpack_object`, but indexes the buffer directly rather than 

1691 consuming a read callable, so any number of readers can work on the same 

1692 buffer concurrently. 

1693 

1694 Args: 

1695 contents: Buffer holding the pack. 

1696 offset: Offset in contents at which the object starts. 

1697 hash_func: Hash function to use for computing object IDs. 

1698 compute_crc32: If True, compute the CRC32 of the compressed data. 

1699 include_comp: If True, include compressed data in the result. 

1700 zlib_bufsize: An optional buffer size for zlib operations. 

1701 Returns: A tuple of (unpacked, end), where end is the offset just past 

1702 the object and unpacked is an UnpackedObject with its ``offset`` set; 

1703 see :func:`unpack_object` for the other attributes. 

1704 """ 

1705 crc32: int | None = 0 if compute_crc32 else None 

1706 

1707 raw, pos, crc32 = take_msb_bytes_at(contents, offset, crc32=crc32) 

1708 type_num, size = _decode_object_header(raw) 

1709 

1710 delta_base: int | bytes | None 

1711 if type_num == OFS_DELTA: 

1712 raw, pos, crc32 = take_msb_bytes_at(contents, pos, crc32=crc32) 

1713 delta_base = _decode_delta_base_offset(raw) 

1714 elif type_num == REF_DELTA: 

1715 hash_size = len(hash_func().digest()) 

1716 delta_base_obj = bytes(contents[pos : pos + hash_size]) 

1717 if len(delta_base_obj) != hash_size: 

1718 raise AssertionError(f"unexpected end of pack data at {pos}") 

1719 pos += hash_size 

1720 if crc32 is not None: 

1721 crc32 = binascii.crc32(delta_base_obj, crc32) 

1722 delta_base = delta_base_obj 

1723 else: 

1724 delta_base = None 

1725 

1726 unpacked = UnpackedObject( 

1727 type_num, 

1728 delta_base=delta_base, 

1729 decomp_len=size, 

1730 crc32=crc32, 

1731 hash_func=hash_func, 

1732 ) 

1733 unpacked.offset = offset 

1734 end = read_zlib_chunks_at( 

1735 contents, 

1736 pos, 

1737 unpacked, 

1738 buffer_size=zlib_bufsize, 

1739 include_comp=include_comp, 

1740 ) 

1741 return unpacked, end 

1742 

1743 

1744def _compute_object_size(value: tuple[int, Any]) -> int: 

1745 """Compute the size of a unresolved object for use with LRUSizeCache.""" 

1746 (num, obj) = value 

1747 if num in DELTA_TYPES: 

1748 return chunks_length(obj[1]) 

1749 return chunks_length(obj) 

1750 

1751 

1752class PackStreamReader: 

1753 """Class to read a pack stream. 

1754 

1755 The pack is read from a ReceivableProtocol using read() or recv() as 

1756 appropriate. 

1757 """ 

1758 

1759 def __init__( 

1760 self, 

1761 hash_func: Callable[[], "HashObject"], 

1762 read_all: Callable[[int], bytes], 

1763 read_some: Callable[[int], bytes] | None = None, 

1764 zlib_bufsize: int = _ZLIB_BUFSIZE, 

1765 ) -> None: 

1766 """Initialize pack stream reader. 

1767 

1768 Args: 

1769 hash_func: Hash function to use for computing object IDs 

1770 read_all: Function to read all requested bytes 

1771 read_some: Function to read some bytes (optional) 

1772 zlib_bufsize: Buffer size for zlib decompression 

1773 """ 

1774 self.read_all = read_all 

1775 if read_some is None: 

1776 self.read_some = read_all 

1777 else: 

1778 self.read_some = read_some 

1779 self.hash_func = hash_func 

1780 self.sha = hash_func() 

1781 self._hash_size = len(hash_func().digest()) 

1782 self._offset = 0 

1783 self._rbuf = BytesIO() 

1784 # trailer is a deque to avoid memory allocation on small reads 

1785 self._trailer: deque[int] = deque() 

1786 self._zlib_bufsize = zlib_bufsize 

1787 

1788 def _read(self, read: Callable[[int], bytes], size: int) -> bytes: 

1789 """Read up to size bytes using the given callback. 

1790 

1791 As a side effect, update the verifier's hash (excluding the last 

1792 hash_size bytes read, which is the pack checksum). 

1793 

1794 Args: 

1795 read: The read callback to read from. 

1796 size: The maximum number of bytes to read; the particular 

1797 behavior is callback-specific. 

1798 Returns: Bytes read 

1799 """ 

1800 data = read(size) 

1801 

1802 # maintain a trailer of the last hash_size bytes we've read 

1803 n = len(data) 

1804 self._offset += n 

1805 tn = len(self._trailer) 

1806 if n >= self._hash_size: 

1807 to_pop = tn 

1808 to_add = self._hash_size 

1809 else: 

1810 to_pop = max(n + tn - self._hash_size, 0) 

1811 to_add = n 

1812 self.sha.update( 

1813 bytes(bytearray([self._trailer.popleft() for _ in range(to_pop)])) 

1814 ) 

1815 self._trailer.extend(data[-to_add:]) 

1816 

1817 # hash everything but the trailer 

1818 self.sha.update(data[:-to_add]) 

1819 return data 

1820 

1821 def _buf_len(self) -> int: 

1822 buf = self._rbuf 

1823 start = buf.tell() 

1824 buf.seek(0, SEEK_END) 

1825 end = buf.tell() 

1826 buf.seek(start) 

1827 return end - start 

1828 

1829 @property 

1830 def offset(self) -> int: 

1831 """Return current offset in the stream.""" 

1832 return self._offset - self._buf_len() 

1833 

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

1835 """Read, blocking until size bytes are read.""" 

1836 buf_len = self._buf_len() 

1837 if buf_len >= size: 

1838 return self._rbuf.read(size) 

1839 buf_data = self._rbuf.read() 

1840 self._rbuf = BytesIO() 

1841 return buf_data + self._read(self.read_all, size - buf_len) 

1842 

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

1844 """Read up to size bytes, blocking until one byte is read.""" 

1845 buf_len = self._buf_len() 

1846 if buf_len: 

1847 data = self._rbuf.read(size) 

1848 if size >= buf_len: 

1849 self._rbuf = BytesIO() 

1850 return data 

1851 return self._read(self.read_some, size) 

1852 

1853 def __len__(self) -> int: 

1854 """Return the number of objects in this pack.""" 

1855 return self._num_objects 

1856 

1857 def read_objects(self, compute_crc32: bool = False) -> Iterator[UnpackedObject]: 

1858 """Read the objects in this pack file. 

1859 

1860 Args: 

1861 compute_crc32: If True, compute the CRC32 of the compressed 

1862 data. If False, the returned CRC32 will be None. 

1863 Returns: Iterator over UnpackedObjects with the following members set: 

1864 offset 

1865 obj_type_num 

1866 obj_chunks (for non-delta types) 

1867 delta_base (for delta types) 

1868 decomp_chunks 

1869 decomp_len 

1870 crc32 (if compute_crc32 is True) 

1871 

1872 Raises: 

1873 ChecksumMismatch: if the checksum of the pack contents does not 

1874 match the checksum in the pack trailer. 

1875 zlib.error: if an error occurred during zlib decompression. 

1876 IOError: if an error occurred writing to the output file. 

1877 """ 

1878 _pack_version, self._num_objects = read_pack_header(self.read) 

1879 

1880 for _ in range(self._num_objects): 

1881 offset = self.offset 

1882 unpacked, unused = unpack_object( 

1883 self.read, 

1884 self.hash_func, 

1885 read_some=self.recv, 

1886 compute_crc32=compute_crc32, 

1887 zlib_bufsize=self._zlib_bufsize, 

1888 ) 

1889 unpacked.offset = offset 

1890 

1891 # prepend any unused data to current read buffer 

1892 buf = BytesIO() 

1893 buf.write(unused) 

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

1895 buf.seek(0) 

1896 self._rbuf = buf 

1897 

1898 yield unpacked 

1899 

1900 if self._buf_len() < self._hash_size: 

1901 # If the read buffer is full, then the last read() got the whole 

1902 # trailer off the wire. If not, it means there is still some of the 

1903 # trailer to read. We need to read() all hash_size bytes; N come from the 

1904 # read buffer and (hash_size - N) come from the wire. 

1905 self.read(self._hash_size) 

1906 

1907 pack_sha = bytearray(self._trailer) 

1908 if pack_sha != self.sha.digest(): 

1909 raise ChecksumMismatch( 

1910 sha_to_hex(RawObjectID(bytes(pack_sha))), self.sha.hexdigest() 

1911 ) 

1912 

1913 

1914class PackStreamCopier(PackStreamReader): 

1915 """Class to verify a pack stream as it is being read. 

1916 

1917 The pack is read from a ReceivableProtocol using read() or recv() as 

1918 appropriate and written out to the given file-like object. 

1919 """ 

1920 

1921 def __init__( 

1922 self, 

1923 hash_func: Callable[[], "HashObject"], 

1924 read_all: Callable[[int], bytes], 

1925 read_some: Callable[[int], bytes] | None, 

1926 outfile: IO[bytes], 

1927 delta_iter: "DeltaChainIterator[UnpackedObject] | None" = None, 

1928 ) -> None: 

1929 """Initialize the copier. 

1930 

1931 Args: 

1932 hash_func: Hash function to use for computing object IDs 

1933 read_all: Read function that blocks until the number of 

1934 requested bytes are read. 

1935 read_some: Read function that returns at least one byte, but may 

1936 not return the number of bytes requested. 

1937 outfile: File-like object to write output through. 

1938 delta_iter: Optional DeltaChainIterator to record deltas as we 

1939 read them. 

1940 """ 

1941 super().__init__(hash_func, read_all, read_some=read_some) 

1942 self.outfile = outfile 

1943 self._delta_iter = delta_iter 

1944 

1945 def _read(self, read: Callable[[int], bytes], size: int) -> bytes: 

1946 """Read data from the read callback and write it to the file.""" 

1947 data = super()._read(read, size) 

1948 self.outfile.write(data) 

1949 return data 

1950 

1951 def verify(self, progress: Callable[..., None] | None = None) -> None: 

1952 """Verify a pack stream and write it to the output file. 

1953 

1954 See PackStreamReader.iterobjects for a list of exceptions this may 

1955 throw. 

1956 """ 

1957 i = 0 # default count of entries if read_objects() is empty 

1958 for i, unpacked in enumerate(self.read_objects()): 

1959 if self._delta_iter: 

1960 self._delta_iter.record(unpacked) 

1961 if progress is not None: 

1962 progress(f"copying pack entries: {i}/{len(self)}\r".encode("ascii")) 

1963 if progress is not None: 

1964 progress(f"copied {i} pack entries\n".encode("ascii")) 

1965 

1966 

1967def obj_sha( 

1968 type: int, 

1969 chunks: bytes | Iterable[bytes], 

1970 hash_func: Callable[[], "HashObject"] = sha1, 

1971) -> bytes: 

1972 """Compute the SHA for a numeric type and object chunks. 

1973 

1974 Args: 

1975 type: Object type number 

1976 chunks: Object data chunks 

1977 hash_func: Hash function to use (defaults to sha1) 

1978 

1979 Returns: 

1980 Binary hash digest 

1981 """ 

1982 sha = hash_func() 

1983 sha.update(object_header(type, chunks_length(chunks))) 

1984 if isinstance(chunks, bytes): 

1985 sha.update(chunks) 

1986 else: 

1987 for chunk in chunks: 

1988 sha.update(chunk) 

1989 return sha.digest() 

1990 

1991 

1992def compute_file_sha( 

1993 f: IO[bytes], 

1994 hash_func: Callable[[], "HashObject"], 

1995 start_ofs: int = 0, 

1996 end_ofs: int = 0, 

1997 buffer_size: int = 1 << 16, 

1998) -> "HashObject": 

1999 """Hash a portion of a file into a new SHA. 

2000 

2001 Args: 

2002 f: A file-like object to read from that supports seek(). 

2003 hash_func: A callable that returns a new HashObject. 

2004 start_ofs: The offset in the file to start reading at. 

2005 end_ofs: The offset in the file to end reading at, relative to the 

2006 end of the file. 

2007 buffer_size: A buffer size for reading. 

2008 Returns: A new SHA object updated with data read from the file. 

2009 """ 

2010 sha = hash_func() 

2011 f.seek(0, SEEK_END) 

2012 length = f.tell() 

2013 if start_ofs < 0: 

2014 raise AssertionError(f"start_ofs cannot be negative: {start_ofs}") 

2015 if (end_ofs < 0 and length + end_ofs < start_ofs) or end_ofs > length: 

2016 raise AssertionError( 

2017 f"Attempt to read beyond file length. start_ofs: {start_ofs}, end_ofs: {end_ofs}, file length: {length}" 

2018 ) 

2019 todo = length + end_ofs - start_ofs 

2020 f.seek(start_ofs) 

2021 while todo: 

2022 data = f.read(min(todo, buffer_size)) 

2023 sha.update(data) 

2024 todo -= len(data) 

2025 return sha 

2026 

2027 

2028def compute_buffer_sha( 

2029 contents: "bytes | mmap.mmap", 

2030 hash_func: Callable[[], "HashObject"], 

2031 start_ofs: int = 0, 

2032 end_ofs: int = 0, 

2033) -> "HashObject": 

2034 """Hash a portion of a buffer into a new SHA. 

2035 

2036 The region is hashed in one pass through a memoryview, so a mapped pack 

2037 is never copied. The view is released before returning rather than left 

2038 to the garbage collector, since ``mmap.close()`` raises BufferError while 

2039 an export is alive. 

2040 

2041 Args: 

2042 contents: Buffer to hash. 

2043 hash_func: A callable that returns a new HashObject. 

2044 start_ofs: The offset in the buffer to start hashing at. 

2045 end_ofs: The offset to end hashing at, relative to the end of the 

2046 buffer. 

2047 Returns: A new SHA object updated with data read from the buffer. 

2048 """ 

2049 sha = hash_func() 

2050 length = len(contents) 

2051 if start_ofs < 0: 

2052 raise AssertionError(f"start_ofs cannot be negative: {start_ofs}") 

2053 if (end_ofs < 0 and length + end_ofs < start_ofs) or end_ofs > length: 

2054 raise AssertionError( 

2055 f"Attempt to read beyond buffer length. start_ofs: {start_ofs}, end_ofs: {end_ofs}, buffer length: {length}" 

2056 ) 

2057 with memoryview(contents) as view: 

2058 sha.update(view[start_ofs : length + end_ofs]) 

2059 return sha 

2060 

2061 

2062class PackData: 

2063 """The data contained in a packfile. 

2064 

2065 Pack files can be accessed both sequentially for exploding a pack, and 

2066 directly with the help of an index to retrieve a specific object. 

2067 

2068 The objects within are either complete or a delta against another. 

2069 

2070 The header is variable length. If the MSB of each byte is set then it 

2071 indicates that the subsequent byte is still part of the header. 

2072 For the first byte the next MS bits are the type, which tells you the type 

2073 of object, and whether it is a delta. The LS byte is the lowest bits of the 

2074 size. For each subsequent byte the LS 7 bits are the next MS bits of the 

2075 size, i.e. the last byte of the header contains the MS bits of the size. 

2076 

2077 For the complete objects the data is stored as zlib deflated data. 

2078 The size in the header is the uncompressed object size, so to uncompress 

2079 you need to just keep feeding data to zlib until you get an object back, 

2080 or it errors on bad data. This is done here by reading from the mapped 

2081 pack contents starting at the deflated object. 

2082 

2083 Currently there are no integrity checks done. Also no attempt is made to 

2084 try and detect the delta case, or a request for an object at the wrong 

2085 position. It will all just throw a zlib or KeyError. 

2086 """ 

2087 

2088 def __init__( 

2089 self, 

2090 filename: str | os.PathLike[str], 

2091 object_format: ObjectFormat, 

2092 file: IO[bytes] | None = None, 

2093 size: int | None = None, 

2094 *, 

2095 delta_window_size: int | None = None, 

2096 window_memory: int | None = None, 

2097 delta_cache_size: int | None = None, 

2098 depth: int | None = None, 

2099 threads: int | None = None, 

2100 big_file_threshold: int | None = None, 

2101 delta_base_cache_limit: int | None = None, 

2102 ) -> None: 

2103 """Create a PackData object representing the pack in the given filename. 

2104 

2105 The file must exist and stay readable until the object is disposed of. 

2106 It must also stay the same size. It is mapped into memory on open, so 

2107 reads index the mapping directly rather than sharing a file position. 

2108 

2109 The size argument is not trusted for checksum offsets, which are 

2110 derived from the mapped length instead. When given it is checked 

2111 against that length, so a caller passing a stale size gets an error 

2112 rather than silently wrong offsets. 

2113 """ 

2114 self._filename = filename 

2115 self.object_format = object_format 

2116 self._header_size = 12 

2117 self.delta_window_size = delta_window_size 

2118 self.window_memory = window_memory 

2119 self.delta_cache_size = delta_cache_size 

2120 self.depth = depth 

2121 self.threads = threads 

2122 self.big_file_threshold = big_file_threshold 

2123 self.delta_base_cache_limit = delta_base_cache_limit 

2124 self._file: IO[bytes] 

2125 self._contents: bytes | mmap.mmap | None = None 

2126 

2127 if file is None: 

2128 self._file = GitFile(self._filename, "rb") 

2129 self._close_file = True 

2130 else: 

2131 # A caller-supplied file stays the caller's to close; it may well 

2132 # keep writing to it after we are done reading. 

2133 self._file = file 

2134 self._close_file = False 

2135 try: 

2136 # Map the pack once; every read indexes this buffer at an explicit 

2137 # offset, so concurrent reads never contend on a file position. 

2138 self._contents, self._size = _load_file_contents(self._file) 

2139 if size is not None and size != self._size: 

2140 raise AssertionError( 

2141 f"{self._filename} is {self._size} bytes, but caller said {size}" 

2142 ) 

2143 minimum_size = self._header_size + self.object_format.oid_length 

2144 if self._size < minimum_size: 

2145 raise AssertionError( 

2146 f"{self._filename} is too small for a packfile ({self._size} < {minimum_size})" 

2147 ) 

2148 (_version, self._num_objects) = read_pack_header_at(self._contents) 

2149 

2150 # Use delta_base_cache_limit, then delta_cache_size, then default 

2151 cache_size = ( 

2152 delta_base_cache_limit 

2153 or delta_cache_size 

2154 or DEFAULT_DELTA_BASE_CACHE_LIMIT 

2155 ) 

2156 self._init_offset_cache(cache_size) 

2157 except BaseException: 

2158 self.close() 

2159 raise 

2160 

2161 def _init_offset_cache(self, max_size: int) -> None: 

2162 """Initialize the resolved object cache.""" 

2163 self._offset_cache = LRUSizeCache[int, tuple[int, OldUnpackedObject]]( 

2164 max_size, compute_size=_compute_object_size 

2165 ) 

2166 # Cache hits update the LRU linked list, so reads need locking too. 

2167 self._offset_cache_lock = threading.Lock() 

2168 

2169 @property 

2170 def filename(self) -> str: 

2171 """Get the filename of the pack file. 

2172 

2173 Returns: 

2174 Base filename without directory path 

2175 """ 

2176 return os.path.basename(self._filename) 

2177 

2178 @property 

2179 def path(self) -> str | os.PathLike[str]: 

2180 """Get the full path of the pack file. 

2181 

2182 Returns: 

2183 Full path to the pack file 

2184 """ 

2185 return self._filename 

2186 

2187 @classmethod 

2188 def from_file( 

2189 cls, 

2190 file: IO[bytes], 

2191 object_format: ObjectFormat, 

2192 size: int | None = None, 

2193 ) -> "PackData": 

2194 """Create a PackData object from an open file. 

2195 

2196 Args: 

2197 file: Open file object 

2198 object_format: Object format 

2199 size: Optional expected file size, checked against the mapped length 

2200 

2201 Returns: 

2202 PackData instance 

2203 """ 

2204 return cls(str(file), object_format, file=file, size=size) 

2205 

2206 @classmethod 

2207 def from_path( 

2208 cls, 

2209 path: str | os.PathLike[str], 

2210 object_format: ObjectFormat, 

2211 ) -> "PackData": 

2212 """Create a PackData object from a file path. 

2213 

2214 Args: 

2215 path: Path to the pack file 

2216 object_format: Object format 

2217 

2218 Returns: 

2219 PackData instance 

2220 """ 

2221 return cls(filename=path, object_format=object_format) 

2222 

2223 def _buffer(self) -> "bytes | mmap.mmap": 

2224 """Return the mapped pack contents.""" 

2225 contents = self._contents 

2226 if contents is None: 

2227 raise ValueError(f"read from closed PackData: {self._filename}") 

2228 return contents 

2229 

2230 def close(self) -> None: 

2231 """Release the mapping, and the pack file if we opened it. 

2232 

2233 Callers must drop the mapping before writing to or renaming the pack: 

2234 on Windows a live mapping locks the file. 

2235 """ 

2236 contents = self._contents 

2237 self._contents = None 

2238 _close_file_contents(contents) 

2239 if self._file is not None: 

2240 if self._close_file: 

2241 self._file.close() 

2242 self._file = None # type: ignore 

2243 

2244 def __del__(self) -> None: 

2245 """Ensure pack file is closed when PackData is garbage collected.""" 

2246 if getattr(self, "_file", None) is not None: 

2247 import warnings 

2248 

2249 warnings.warn( 

2250 f"unclosed PackData {self!r}", 

2251 ResourceWarning, 

2252 stacklevel=2, 

2253 source=self, 

2254 ) 

2255 try: 

2256 self.close() 

2257 except Exception: 

2258 # Ignore errors during cleanup 

2259 pass 

2260 

2261 def __enter__(self) -> Self: 

2262 """Enter context manager.""" 

2263 return self 

2264 

2265 def __exit__( 

2266 self, 

2267 type: type | None, 

2268 value: BaseException | None, 

2269 traceback: TracebackType | None, 

2270 ) -> None: 

2271 """Exit context manager.""" 

2272 self.close() 

2273 

2274 def __eq__(self, other: object) -> bool: 

2275 """Check equality with another object.""" 

2276 if isinstance(other, PackData): 

2277 return self.get_stored_checksum() == other.get_stored_checksum() 

2278 return False 

2279 

2280 def __len__(self) -> int: 

2281 """Returns the number of objects in this pack.""" 

2282 return self._num_objects 

2283 

2284 def calculate_checksum(self) -> bytes: 

2285 """Calculate the checksum for this pack. 

2286 

2287 Returns: Binary digest (size depends on hash algorithm) 

2288 """ 

2289 return compute_buffer_sha( 

2290 self._buffer(), 

2291 hash_func=self.object_format.hash_func, 

2292 end_ofs=-self.object_format.oid_length, 

2293 ).digest() 

2294 

2295 def iter_unpacked(self, *, include_comp: bool = False) -> Iterator[UnpackedObject]: 

2296 """Iterate over unpacked objects in the pack.""" 

2297 if self._num_objects is None: 

2298 return 

2299 

2300 contents = self._buffer() 

2301 offset = self._header_size 

2302 for _ in range(self._num_objects): 

2303 unpacked, offset = unpack_object_at( 

2304 contents, 

2305 offset, 

2306 self.object_format.hash_func, 

2307 compute_crc32=False, 

2308 include_comp=include_comp, 

2309 ) 

2310 yield unpacked 

2311 

2312 def iterentries( 

2313 self, 

2314 progress: Callable[[int, int], None] | None = None, 

2315 resolve_ext_ref: ResolveExtRefFn | None = None, 

2316 ) -> Iterator[PackIndexEntry]: 

2317 """Yield entries summarizing the contents of this pack. 

2318 

2319 Args: 

2320 progress: Progress function, called with current and total 

2321 object count. 

2322 resolve_ext_ref: Optional function to resolve external references 

2323 Returns: iterator of tuples with (sha, offset, crc32) 

2324 """ 

2325 num_objects = self._num_objects 

2326 indexer = PackIndexer.for_pack_data(self, resolve_ext_ref=resolve_ext_ref) 

2327 for i, result in enumerate(indexer): 

2328 if progress is not None: 

2329 progress(i, num_objects) 

2330 yield result 

2331 

2332 def sorted_entries( 

2333 self, 

2334 progress: Callable[[int, int], None] | None = None, 

2335 resolve_ext_ref: ResolveExtRefFn | None = None, 

2336 ) -> list[tuple[RawObjectID, int, int]]: 

2337 """Return entries in this pack, sorted by SHA. 

2338 

2339 Args: 

2340 progress: Progress function, called with current and total 

2341 object count 

2342 resolve_ext_ref: Optional function to resolve external references 

2343 Returns: Iterator of tuples with (sha, offset, crc32) 

2344 """ 

2345 return sorted( 

2346 self.iterentries(progress=progress, resolve_ext_ref=resolve_ext_ref) # type: ignore 

2347 ) 

2348 

2349 def create_index_v1( 

2350 self, 

2351 filename: str, 

2352 progress: Callable[..., None] | None = None, 

2353 resolve_ext_ref: ResolveExtRefFn | None = None, 

2354 ) -> bytes: 

2355 """Create a version 1 file for this data file. 

2356 

2357 Args: 

2358 filename: Index filename. 

2359 progress: Progress report function 

2360 resolve_ext_ref: Optional function to resolve external references 

2361 Returns: Checksum of index file 

2362 """ 

2363 entries = self.sorted_entries( 

2364 progress=progress, resolve_ext_ref=resolve_ext_ref 

2365 ) 

2366 checksum = self.calculate_checksum() 

2367 with GitFile(filename, "wb") as f: 

2368 write_pack_index_v1( 

2369 f, 

2370 entries, 

2371 checksum, 

2372 ) 

2373 return checksum 

2374 

2375 def create_index_v2( 

2376 self, 

2377 filename: str, 

2378 progress: Callable[..., None] | None = None, 

2379 resolve_ext_ref: ResolveExtRefFn | None = None, 

2380 ) -> bytes: 

2381 """Create a version 2 index file for this data file. 

2382 

2383 Args: 

2384 filename: Index filename. 

2385 progress: Progress report function 

2386 resolve_ext_ref: Optional function to resolve external references 

2387 Returns: Checksum of index file 

2388 """ 

2389 entries = self.sorted_entries( 

2390 progress=progress, resolve_ext_ref=resolve_ext_ref 

2391 ) 

2392 with GitFile(filename, "wb") as f: 

2393 return write_pack_index_v2(f, entries, self.calculate_checksum()) 

2394 

2395 def create_index_v3( 

2396 self, 

2397 filename: str, 

2398 progress: Callable[..., None] | None = None, 

2399 resolve_ext_ref: ResolveExtRefFn | None = None, 

2400 hash_format: int | None = None, 

2401 ) -> bytes: 

2402 """Create a version 3 index file for this data file. 

2403 

2404 Args: 

2405 filename: Index filename. 

2406 progress: Progress report function 

2407 resolve_ext_ref: Function to resolve external references 

2408 hash_format: Hash algorithm identifier (1 = SHA-1, 2 = SHA-256) 

2409 Returns: Checksum of index file 

2410 """ 

2411 entries = self.sorted_entries( 

2412 progress=progress, resolve_ext_ref=resolve_ext_ref 

2413 ) 

2414 with GitFile(filename, "wb") as f: 

2415 if hash_format is None: 

2416 hash_format = 1 # Default to SHA-1 

2417 return write_pack_index_v3( 

2418 f, entries, self.calculate_checksum(), hash_format=hash_format 

2419 ) 

2420 

2421 def create_index( 

2422 self, 

2423 filename: str, 

2424 progress: Callable[..., None] | None = None, 

2425 version: int = 2, 

2426 resolve_ext_ref: ResolveExtRefFn | None = None, 

2427 hash_format: int | None = None, 

2428 ) -> bytes: 

2429 """Create an index file for this data file. 

2430 

2431 Args: 

2432 filename: Index filename. 

2433 progress: Progress report function 

2434 version: Index version (1, 2, or 3) 

2435 resolve_ext_ref: Function to resolve external references 

2436 hash_format: Hash algorithm identifier for v3 (1 = SHA-1, 2 = SHA-256) 

2437 Returns: Checksum of index file 

2438 """ 

2439 if version == 1: 

2440 return self.create_index_v1( 

2441 filename, progress, resolve_ext_ref=resolve_ext_ref 

2442 ) 

2443 elif version == 2: 

2444 return self.create_index_v2( 

2445 filename, progress, resolve_ext_ref=resolve_ext_ref 

2446 ) 

2447 elif version == 3: 

2448 return self.create_index_v3( 

2449 filename, 

2450 progress, 

2451 resolve_ext_ref=resolve_ext_ref, 

2452 hash_format=hash_format, 

2453 ) 

2454 else: 

2455 raise ValueError(f"unknown index format {version}") 

2456 

2457 def get_stored_checksum(self) -> bytes: 

2458 """Return the expected checksum stored in this pack.""" 

2459 checksum_size = self.object_format.oid_length 

2460 return bytes(self._buffer()[self._size - checksum_size :]) 

2461 

2462 def check(self) -> None: 

2463 """Check the consistency of this pack.""" 

2464 actual = self.calculate_checksum() 

2465 stored = self.get_stored_checksum() 

2466 if actual != stored: 

2467 raise ChecksumMismatch(stored, actual) 

2468 

2469 def get_unpacked_object_at( 

2470 self, offset: int, *, include_comp: bool = False 

2471 ) -> UnpackedObject: 

2472 """Given offset in the packfile return a UnpackedObject.""" 

2473 assert offset >= self._header_size 

2474 unpacked, _ = unpack_object_at( 

2475 self._buffer(), 

2476 offset, 

2477 self.object_format.hash_func, 

2478 include_comp=include_comp, 

2479 ) 

2480 return unpacked 

2481 

2482 def _get_cached_object_at(self, offset: int) -> tuple[int, OldUnpackedObject]: 

2483 """Return the cached object at offset, or raise KeyError.""" 

2484 # Hot path: acquire/release directly rather than using the context 

2485 # manager, which measurably speeds up cache hits on get_object_at. 

2486 self._offset_cache_lock.acquire() 

2487 try: 

2488 return self._offset_cache[offset] 

2489 finally: 

2490 self._offset_cache_lock.release() 

2491 

2492 def _cache_object_at( 

2493 self, offset: int, type_num: int, chunks: OldUnpackedObject 

2494 ) -> None: 

2495 """Cache a resolved object at offset.""" 

2496 with self._offset_cache_lock: 

2497 self._offset_cache[offset] = (type_num, chunks) 

2498 

2499 def get_object_at(self, offset: int) -> tuple[int, OldUnpackedObject]: 

2500 """Given an offset in to the packfile return the object that is there. 

2501 

2502 Using the associated index the location of an object can be looked up, 

2503 and then the packfile can be asked directly for that object using this 

2504 function. 

2505 """ 

2506 try: 

2507 return self._get_cached_object_at(offset) 

2508 except KeyError: 

2509 pass 

2510 unpacked = self.get_unpacked_object_at(offset, include_comp=False) 

2511 return (unpacked.pack_type_num, unpacked._obj()) 

2512 

2513 

2514T = TypeVar("T") 

2515 

2516 

2517class DeltaChainIterator(Generic[T]): 

2518 """Abstract iterator over pack data based on delta chains. 

2519 

2520 Each object in the pack is guaranteed to be inflated exactly once, 

2521 regardless of how many objects reference it as a delta base. As a result, 

2522 memory usage is proportional to the length of the longest delta chain. 

2523 

2524 Subclasses can override _result to define the result type of the iterator. 

2525 By default, results are UnpackedObjects with the following members set: 

2526 

2527 * offset 

2528 * obj_type_num 

2529 * obj_chunks 

2530 * pack_type_num 

2531 * delta_base (for delta types) 

2532 * comp_chunks (if _include_comp is True) 

2533 * decomp_chunks 

2534 * decomp_len 

2535 * crc32 (if _compute_crc32 is True) 

2536 """ 

2537 

2538 _compute_crc32 = False 

2539 _include_comp = False 

2540 

2541 def __init__( 

2542 self, 

2543 file_obj: IO[bytes] | None, 

2544 hash_func: Callable[[], "HashObject"], 

2545 *, 

2546 resolve_ext_ref: ResolveExtRefFn | None = None, 

2547 object_format: "ObjectFormat | None" = None, 

2548 ) -> None: 

2549 """Initialize DeltaChainIterator. 

2550 

2551 Args: 

2552 file_obj: File object to read pack data from 

2553 hash_func: Hash function to use for computing object IDs 

2554 resolve_ext_ref: Optional function to resolve external references 

2555 object_format: Optional object format. Required by subclasses 

2556 that materialise objects (e.g. PackInflater) when iterating 

2557 packs in a non-default hash algorithm such as SHA-256. 

2558 """ 

2559 self._file = file_obj 

2560 self._contents: bytes | mmap.mmap | None = None 

2561 self.hash_func = hash_func 

2562 self._object_format = object_format 

2563 self._resolve_ext_ref = resolve_ext_ref 

2564 self._pending_ofs: dict[int, list[int]] = defaultdict(list) 

2565 self._pending_ref: dict[bytes, list[int]] = defaultdict(list) 

2566 self._full_ofs: list[tuple[int, int]] = [] 

2567 self._ext_refs: list[RawObjectID] = [] 

2568 

2569 @classmethod 

2570 def for_pack_data( 

2571 cls, pack_data: PackData, resolve_ext_ref: ResolveExtRefFn | None = None 

2572 ) -> "DeltaChainIterator[T]": 

2573 """Create a DeltaChainIterator from pack data. 

2574 

2575 Args: 

2576 pack_data: PackData object to iterate 

2577 resolve_ext_ref: Optional function to resolve external refs 

2578 

2579 Returns: 

2580 DeltaChainIterator instance 

2581 """ 

2582 walker = cls( 

2583 None, 

2584 pack_data.object_format.hash_func, 

2585 resolve_ext_ref=resolve_ext_ref, 

2586 object_format=pack_data.object_format, 

2587 ) 

2588 walker.set_pack_data(pack_data) 

2589 for unpacked in pack_data.iter_unpacked(include_comp=False): 

2590 walker.record(unpacked) 

2591 return walker 

2592 

2593 @classmethod 

2594 def for_pack_subset( 

2595 cls, 

2596 pack: "Pack", 

2597 shas: Iterable[ObjectID | RawObjectID], 

2598 *, 

2599 allow_missing: bool = False, 

2600 resolve_ext_ref: ResolveExtRefFn | None = None, 

2601 ) -> "DeltaChainIterator[T]": 

2602 """Create a DeltaChainIterator for a subset of objects. 

2603 

2604 Args: 

2605 pack: Pack object containing the data 

2606 shas: Iterable of object SHAs to include 

2607 allow_missing: If True, skip missing objects 

2608 resolve_ext_ref: Optional function to resolve external refs 

2609 

2610 Returns: 

2611 DeltaChainIterator instance 

2612 """ 

2613 walker = cls( 

2614 None, 

2615 pack.object_format.hash_func, 

2616 resolve_ext_ref=resolve_ext_ref, 

2617 object_format=pack.object_format, 

2618 ) 

2619 walker.set_pack_data(pack.data) 

2620 todo = set() 

2621 for sha in shas: 

2622 try: 

2623 off = pack.index.object_offset(sha) 

2624 except KeyError: 

2625 if not allow_missing: 

2626 raise 

2627 else: 

2628 todo.add(off) 

2629 done = set() 

2630 while todo: 

2631 off = todo.pop() 

2632 unpacked = pack.data.get_unpacked_object_at(off) 

2633 walker.record(unpacked) 

2634 done.add(off) 

2635 base_ofs = None 

2636 if unpacked.pack_type_num == OFS_DELTA: 

2637 assert unpacked.offset is not None 

2638 assert unpacked.delta_base is not None 

2639 assert isinstance(unpacked.delta_base, int) 

2640 base_ofs = unpacked.offset - unpacked.delta_base 

2641 elif unpacked.pack_type_num == REF_DELTA: 

2642 with suppress(KeyError): 

2643 assert isinstance(unpacked.delta_base, bytes) 

2644 base_ofs = pack.index.object_offset( 

2645 RawObjectID(unpacked.delta_base) 

2646 ) 

2647 if base_ofs is not None and base_ofs not in done: 

2648 todo.add(base_ofs) 

2649 return walker 

2650 

2651 def record(self, unpacked: UnpackedObject) -> None: 

2652 """Record an unpacked object for later processing. 

2653 

2654 Args: 

2655 unpacked: UnpackedObject to record 

2656 """ 

2657 type_num = unpacked.pack_type_num 

2658 offset = unpacked.offset 

2659 assert offset is not None 

2660 if type_num == OFS_DELTA: 

2661 assert unpacked.delta_base is not None 

2662 assert isinstance(unpacked.delta_base, int) 

2663 base_offset = offset - unpacked.delta_base 

2664 self._pending_ofs[base_offset].append(offset) 

2665 elif type_num == REF_DELTA: 

2666 assert isinstance(unpacked.delta_base, bytes) 

2667 self._pending_ref[unpacked.delta_base].append(offset) 

2668 else: 

2669 self._full_ofs.append((offset, type_num)) 

2670 

2671 def set_pack_data(self, pack_data: PackData) -> None: 

2672 """Set the pack data for iteration. 

2673 

2674 Args: 

2675 pack_data: PackData object to use 

2676 """ 

2677 self._file = None 

2678 self._contents = pack_data._buffer() 

2679 

2680 def _walk_all_chains(self) -> Iterator[T]: 

2681 for offset, type_num in self._full_ofs: 

2682 yield from self._follow_chain(offset, type_num, None) 

2683 yield from self._walk_ref_chains() 

2684 assert not self._pending_ofs, repr(self._pending_ofs) 

2685 

2686 def _ensure_no_pending(self) -> None: 

2687 if self._pending_ref: 

2688 raise UnresolvedDeltas( 

2689 [sha_to_hex(RawObjectID(s)) for s in self._pending_ref] 

2690 ) 

2691 

2692 def _walk_ref_chains(self) -> Iterator[T]: 

2693 if not self._resolve_ext_ref: 

2694 self._ensure_no_pending() 

2695 return 

2696 

2697 for base_sha, pending in sorted(self._pending_ref.items()): 

2698 if base_sha not in self._pending_ref: 

2699 continue 

2700 try: 

2701 type_num, chunks = self._resolve_ext_ref(RawObjectID(base_sha)) 

2702 except KeyError: 

2703 # Not an external ref, but may depend on one. Either it will 

2704 # get popped via a _follow_chain call, or we will raise an 

2705 # error below. 

2706 continue 

2707 self._ext_refs.append(RawObjectID(base_sha)) 

2708 self._pending_ref.pop(base_sha) 

2709 for new_offset in pending: 

2710 yield from self._follow_chain(new_offset, type_num, chunks) 

2711 

2712 self._ensure_no_pending() 

2713 

2714 def _result(self, unpacked: UnpackedObject) -> T: 

2715 raise NotImplementedError 

2716 

2717 def _resolve_object( 

2718 self, 

2719 offset: int, 

2720 obj_type_num: int, 

2721 base_chunks: bytes | list[bytes] | None, 

2722 ) -> UnpackedObject: 

2723 if self._contents is not None: 

2724 unpacked, _ = unpack_object_at( 

2725 self._contents, 

2726 offset, 

2727 self.hash_func, 

2728 compute_crc32=self._compute_crc32, 

2729 include_comp=self._include_comp, 

2730 ) 

2731 else: 

2732 # add_thin_pack may still be writing to this file, so it cannot be 

2733 # mapped up front; read through the file position instead. 

2734 assert self._file is not None 

2735 self._file.seek(offset) 

2736 unpacked, _ = unpack_object( 

2737 self._file.read, 

2738 self.hash_func, 

2739 read_some=None, 

2740 compute_crc32=self._compute_crc32, 

2741 include_comp=self._include_comp, 

2742 ) 

2743 unpacked.offset = offset 

2744 if base_chunks is None: 

2745 assert unpacked.pack_type_num == obj_type_num 

2746 else: 

2747 assert unpacked.pack_type_num in DELTA_TYPES 

2748 unpacked.obj_type_num = obj_type_num 

2749 unpacked.obj_chunks = apply_delta(base_chunks, unpacked.decomp_chunks) 

2750 # A delta that resolves to a zero-byte payload for a 

2751 # commit/tree/tag is malformed: ``_parse_message`` / 

2752 # ``parse_tree`` accept the empty input silently, so without 

2753 # this guard a too-short delta could materialise an 

2754 # otherwise-valid SHA pointing at an empty commit object 

2755 # (which ``git fsck`` rejects). Only blobs may legitimately 

2756 # be empty, and an empty blob would never be stored as a 

2757 # delta in practice. 

2758 # Blob.type_num == 3 (avoid the import cycle). 

2759 if obj_type_num != 3 and chunks_length(unpacked.obj_chunks) == 0: 

2760 raise ApplyDeltaError( 

2761 f"delta resolved to empty payload for type {obj_type_num}" 

2762 ) 

2763 return unpacked 

2764 

2765 def _follow_chain( 

2766 self, 

2767 offset: int, 

2768 obj_type_num: int, 

2769 base_chunks: bytes | list[bytes] | None, 

2770 ) -> Iterator[T]: 

2771 # Unlike PackData.get_object_at, there is no need to cache offsets as 

2772 # this approach by design inflates each object exactly once. 

2773 todo = [(offset, obj_type_num, base_chunks)] 

2774 while todo: 

2775 (offset, obj_type_num, base_chunks) = todo.pop() 

2776 unpacked = self._resolve_object(offset, obj_type_num, base_chunks) 

2777 yield self._result(unpacked) 

2778 

2779 assert unpacked.offset is not None 

2780 unblocked = chain( 

2781 self._pending_ofs.pop(unpacked.offset, []), 

2782 self._pending_ref.pop(unpacked.sha(), []), 

2783 ) 

2784 todo.extend( 

2785 (new_offset, unpacked.obj_type_num, unpacked.obj_chunks) # type: ignore 

2786 for new_offset in unblocked 

2787 ) 

2788 

2789 def __iter__(self) -> Iterator[T]: 

2790 """Iterate over objects in the pack.""" 

2791 return self._walk_all_chains() 

2792 

2793 def ext_refs(self) -> list[RawObjectID]: 

2794 """Return external references.""" 

2795 return self._ext_refs 

2796 

2797 

2798class UnpackedObjectIterator(DeltaChainIterator[UnpackedObject]): 

2799 """Delta chain iterator that yield unpacked objects.""" 

2800 

2801 def _result(self, unpacked: UnpackedObject) -> UnpackedObject: 

2802 """Return the unpacked object. 

2803 

2804 Args: 

2805 unpacked: The unpacked object 

2806 

2807 Returns: 

2808 The unpacked object unchanged 

2809 """ 

2810 return unpacked 

2811 

2812 

2813class PackIndexer(DeltaChainIterator[PackIndexEntry]): 

2814 """Delta chain iterator that yields index entries.""" 

2815 

2816 _compute_crc32 = True 

2817 

2818 def _result(self, unpacked: UnpackedObject) -> PackIndexEntry: 

2819 """Convert unpacked object to pack index entry. 

2820 

2821 Args: 

2822 unpacked: The unpacked object 

2823 

2824 Returns: 

2825 Tuple of (sha, offset, crc32) for index entry 

2826 """ 

2827 assert unpacked.offset is not None 

2828 return unpacked.sha(), unpacked.offset, unpacked.crc32 

2829 

2830 

2831class PackInflater(DeltaChainIterator[ShaFile]): 

2832 """Delta chain iterator that yields ShaFile objects.""" 

2833 

2834 def _result(self, unpacked: UnpackedObject) -> ShaFile: 

2835 """Convert unpacked object to ShaFile. 

2836 

2837 Args: 

2838 unpacked: The unpacked object 

2839 

2840 Returns: 

2841 ShaFile object from the unpacked data 

2842 """ 

2843 assert unpacked.obj_type_num is not None and unpacked.obj_chunks is not None 

2844 return ShaFile.from_raw_chunks( 

2845 unpacked.obj_type_num, 

2846 unpacked.obj_chunks, 

2847 object_format=self._object_format, 

2848 ) 

2849 

2850 

2851class SHA1Reader(BinaryIO): 

2852 """Wrapper for file-like object that remembers the SHA1 of its data.""" 

2853 

2854 def __init__(self, f: IO[bytes]) -> None: 

2855 """Initialize SHA1Reader. 

2856 

2857 Args: 

2858 f: File-like object to wrap 

2859 """ 

2860 self.f = f 

2861 self.sha1 = sha1(b"") 

2862 

2863 def read(self, size: int = -1) -> bytes: 

2864 """Read bytes and update SHA1. 

2865 

2866 Args: 

2867 size: Number of bytes to read, -1 for all 

2868 

2869 Returns: 

2870 Bytes read from file 

2871 """ 

2872 data = self.f.read(size) 

2873 self.sha1.update(data) 

2874 return data 

2875 

2876 def check_sha(self, allow_empty: bool = False) -> None: 

2877 """Check if the SHA1 matches the expected value. 

2878 

2879 Args: 

2880 allow_empty: Allow empty SHA1 hash 

2881 

2882 Raises: 

2883 ChecksumMismatch: If SHA1 doesn't match 

2884 """ 

2885 stored = self.f.read(20) 

2886 # If git option index.skipHash is set the index will be empty 

2887 if stored != self.sha1.digest() and ( 

2888 not allow_empty 

2889 or ( 

2890 len(stored) == 20 

2891 and sha_to_hex(RawObjectID(stored)) 

2892 != b"0000000000000000000000000000000000000000" 

2893 ) 

2894 ): 

2895 raise ChecksumMismatch( 

2896 self.sha1.hexdigest(), 

2897 sha_to_hex(RawObjectID(stored)) if stored else b"", 

2898 ) 

2899 

2900 def close(self) -> None: 

2901 """Close the underlying file.""" 

2902 return self.f.close() 

2903 

2904 def tell(self) -> int: 

2905 """Return current file position.""" 

2906 return self.f.tell() 

2907 

2908 # BinaryIO abstract methods 

2909 def readable(self) -> bool: 

2910 """Check if file is readable.""" 

2911 return True 

2912 

2913 def writable(self) -> bool: 

2914 """Check if file is writable.""" 

2915 return False 

2916 

2917 def seekable(self) -> bool: 

2918 """Check if file is seekable.""" 

2919 return getattr(self.f, "seekable", lambda: False)() 

2920 

2921 def seek(self, offset: int, whence: int = 0) -> int: 

2922 """Seek to position in file. 

2923 

2924 Args: 

2925 offset: Position offset 

2926 whence: Reference point (0=start, 1=current, 2=end) 

2927 

2928 Returns: 

2929 New file position 

2930 """ 

2931 return self.f.seek(offset, whence) 

2932 

2933 def flush(self) -> None: 

2934 """Flush the file buffer.""" 

2935 if hasattr(self.f, "flush"): 

2936 self.f.flush() 

2937 

2938 def readline(self, size: int = -1) -> bytes: 

2939 """Read a line from the file. 

2940 

2941 Args: 

2942 size: Maximum bytes to read 

2943 

2944 Returns: 

2945 Line read from file 

2946 """ 

2947 return self.f.readline(size) 

2948 

2949 def readlines(self, hint: int = -1) -> list[bytes]: 

2950 """Read all lines from the file. 

2951 

2952 Args: 

2953 hint: Approximate number of bytes to read 

2954 

2955 Returns: 

2956 List of lines 

2957 """ 

2958 return self.f.readlines(hint) 

2959 

2960 def writelines(self, lines: Iterable[bytes], /) -> None: # type: ignore[override] 

2961 """Write multiple lines to the file (not supported).""" 

2962 raise UnsupportedOperation("writelines") 

2963 

2964 def write(self, data: bytes, /) -> int: # type: ignore[override] 

2965 """Write data to the file (not supported).""" 

2966 raise UnsupportedOperation("write") 

2967 

2968 def __enter__(self) -> Self: 

2969 """Enter context manager.""" 

2970 return self 

2971 

2972 def __exit__( 

2973 self, 

2974 type: type | None, 

2975 value: BaseException | None, 

2976 traceback: TracebackType | None, 

2977 ) -> None: 

2978 """Exit context manager and close file.""" 

2979 self.close() 

2980 

2981 def __iter__(self) -> "SHA1Reader": 

2982 """Return iterator for reading file lines.""" 

2983 return self 

2984 

2985 def __next__(self) -> bytes: 

2986 """Get next line from file. 

2987 

2988 Returns: 

2989 Next line 

2990 

2991 Raises: 

2992 StopIteration: When no more lines 

2993 """ 

2994 line = self.readline() 

2995 if not line: 

2996 raise StopIteration 

2997 return line 

2998 

2999 def fileno(self) -> int: 

3000 """Return file descriptor number.""" 

3001 return self.f.fileno() 

3002 

3003 def isatty(self) -> bool: 

3004 """Check if file is a terminal.""" 

3005 return getattr(self.f, "isatty", lambda: False)() 

3006 

3007 def truncate(self, size: int | None = None) -> int: 

3008 """Not supported for read-only file. 

3009 

3010 Raises: 

3011 UnsupportedOperation: Always raised 

3012 """ 

3013 raise UnsupportedOperation("truncate") 

3014 

3015 

3016class SHA1Writer(BinaryIO): 

3017 """Wrapper for file-like object that remembers the SHA1 of its data.""" 

3018 

3019 def __init__(self, f: BinaryIO | IO[bytes]) -> None: 

3020 """Initialize SHA1Writer. 

3021 

3022 Args: 

3023 f: File-like object to wrap 

3024 """ 

3025 self.f = f 

3026 self.length = 0 

3027 self.sha1 = sha1(b"") 

3028 self.digest: bytes | None = None 

3029 

3030 def write(self, data: bytes | bytearray | memoryview, /) -> int: # type: ignore[override] 

3031 """Write data and update SHA1. 

3032 

3033 Args: 

3034 data: Data to write 

3035 

3036 Returns: 

3037 Number of bytes written 

3038 """ 

3039 self.sha1.update(data) 

3040 written = self.f.write(data) 

3041 self.length += written 

3042 return written 

3043 

3044 def write_sha(self) -> bytes: 

3045 """Write the SHA1 digest to the file. 

3046 

3047 Returns: 

3048 The SHA1 digest bytes 

3049 """ 

3050 sha = self.sha1.digest() 

3051 assert len(sha) == 20 

3052 self.f.write(sha) 

3053 self.length += len(sha) 

3054 return sha 

3055 

3056 def close(self) -> None: 

3057 """Close the pack file and finalize the SHA.""" 

3058 self.digest = self.write_sha() 

3059 self.f.close() 

3060 

3061 def offset(self) -> int: 

3062 """Get the total number of bytes written. 

3063 

3064 Returns: 

3065 Total bytes written 

3066 """ 

3067 return self.length 

3068 

3069 def tell(self) -> int: 

3070 """Return current file position.""" 

3071 return self.f.tell() 

3072 

3073 # BinaryIO abstract methods 

3074 def readable(self) -> bool: 

3075 """Check if file is readable.""" 

3076 return False 

3077 

3078 def writable(self) -> bool: 

3079 """Check if file is writable.""" 

3080 return True 

3081 

3082 def seekable(self) -> bool: 

3083 """Check if file is seekable.""" 

3084 return getattr(self.f, "seekable", lambda: False)() 

3085 

3086 def seek(self, offset: int, whence: int = 0) -> int: 

3087 """Seek to position in file. 

3088 

3089 Args: 

3090 offset: Position offset 

3091 whence: Reference point (0=start, 1=current, 2=end) 

3092 

3093 Returns: 

3094 New file position 

3095 """ 

3096 return self.f.seek(offset, whence) 

3097 

3098 def flush(self) -> None: 

3099 """Flush the file buffer.""" 

3100 if hasattr(self.f, "flush"): 

3101 self.f.flush() 

3102 

3103 def readline(self, size: int = -1) -> bytes: 

3104 """Not supported for write-only file. 

3105 

3106 Raises: 

3107 UnsupportedOperation: Always raised 

3108 """ 

3109 raise UnsupportedOperation("readline") 

3110 

3111 def readlines(self, hint: int = -1) -> list[bytes]: 

3112 """Not supported for write-only file. 

3113 

3114 Raises: 

3115 UnsupportedOperation: Always raised 

3116 """ 

3117 raise UnsupportedOperation("readlines") 

3118 

3119 def writelines(self, lines: Iterable[bytes], /) -> None: # type: ignore[override] 

3120 """Write multiple lines to the file. 

3121 

3122 Args: 

3123 lines: Iterable of lines to write 

3124 """ 

3125 for line in lines: 

3126 self.write(line) 

3127 

3128 def read(self, size: int = -1) -> bytes: 

3129 """Not supported for write-only file. 

3130 

3131 Raises: 

3132 UnsupportedOperation: Always raised 

3133 """ 

3134 raise UnsupportedOperation("read") 

3135 

3136 def __enter__(self) -> Self: 

3137 """Enter context manager.""" 

3138 return self 

3139 

3140 def __exit__( 

3141 self, 

3142 type: type | None, 

3143 value: BaseException | None, 

3144 traceback: TracebackType | None, 

3145 ) -> None: 

3146 """Exit context manager and close file.""" 

3147 self.f.close() 

3148 

3149 def __iter__(self) -> "SHA1Writer": 

3150 """Return iterator.""" 

3151 return self 

3152 

3153 def __next__(self) -> bytes: 

3154 """Not supported for write-only file. 

3155 

3156 Raises: 

3157 UnsupportedOperation: Always raised 

3158 """ 

3159 raise UnsupportedOperation("__next__") 

3160 

3161 def fileno(self) -> int: 

3162 """Return file descriptor number.""" 

3163 return self.f.fileno() 

3164 

3165 def isatty(self) -> bool: 

3166 """Check if file is a terminal.""" 

3167 return getattr(self.f, "isatty", lambda: False)() 

3168 

3169 def truncate(self, size: int | None = None) -> int: 

3170 """Not supported for write-only file. 

3171 

3172 Raises: 

3173 UnsupportedOperation: Always raised 

3174 """ 

3175 raise UnsupportedOperation("truncate") 

3176 

3177 

3178class HashWriter(BinaryIO): 

3179 """Wrapper for file-like object that computes hash of its data. 

3180 

3181 This is a generic version that works with any hash algorithm. 

3182 """ 

3183 

3184 def __init__( 

3185 self, f: BinaryIO | IO[bytes], hash_func: Callable[[], "HashObject"] 

3186 ) -> None: 

3187 """Initialize HashWriter. 

3188 

3189 Args: 

3190 f: File-like object to wrap 

3191 hash_func: Hash function (e.g., sha1, sha256) 

3192 """ 

3193 self.f = f 

3194 self.length = 0 

3195 self.hash_obj = hash_func() 

3196 self.digest: bytes | None = None 

3197 

3198 def write(self, data: bytes | bytearray | memoryview, /) -> int: # type: ignore[override] 

3199 """Write data and update hash. 

3200 

3201 Args: 

3202 data: Data to write 

3203 

3204 Returns: 

3205 Number of bytes written 

3206 """ 

3207 self.hash_obj.update(data) 

3208 written = self.f.write(data) 

3209 self.length += written 

3210 return written 

3211 

3212 def write_hash(self) -> bytes: 

3213 """Write the hash digest to the file. 

3214 

3215 Returns: 

3216 The hash digest bytes 

3217 """ 

3218 digest = self.hash_obj.digest() 

3219 self.f.write(digest) 

3220 self.length += len(digest) 

3221 return digest 

3222 

3223 def close(self) -> None: 

3224 """Close the pack file and finalize the hash.""" 

3225 self.digest = self.write_hash() 

3226 self.f.close() 

3227 

3228 def offset(self) -> int: 

3229 """Get the total number of bytes written. 

3230 

3231 Returns: 

3232 Total bytes written 

3233 """ 

3234 return self.length 

3235 

3236 def tell(self) -> int: 

3237 """Return current file position.""" 

3238 return self.f.tell() 

3239 

3240 # BinaryIO abstract methods 

3241 def readable(self) -> bool: 

3242 """Check if file is readable.""" 

3243 return False 

3244 

3245 def writable(self) -> bool: 

3246 """Check if file is writable.""" 

3247 return True 

3248 

3249 def seekable(self) -> bool: 

3250 """Check if file is seekable.""" 

3251 return getattr(self.f, "seekable", lambda: False)() 

3252 

3253 def seek(self, offset: int, whence: int = 0) -> int: 

3254 """Seek to position in file. 

3255 

3256 Args: 

3257 offset: Position offset 

3258 whence: Reference point (0=start, 1=current, 2=end) 

3259 

3260 Returns: 

3261 New file position 

3262 """ 

3263 return self.f.seek(offset, whence) 

3264 

3265 def flush(self) -> None: 

3266 """Flush the file buffer.""" 

3267 if hasattr(self.f, "flush"): 

3268 self.f.flush() 

3269 

3270 def readline(self, size: int = -1) -> bytes: 

3271 """Not supported for write-only file. 

3272 

3273 Raises: 

3274 UnsupportedOperation: Always raised 

3275 """ 

3276 raise UnsupportedOperation("readline") 

3277 

3278 def readlines(self, hint: int = -1) -> list[bytes]: 

3279 """Not supported for write-only file. 

3280 

3281 Raises: 

3282 UnsupportedOperation: Always raised 

3283 """ 

3284 raise UnsupportedOperation("readlines") 

3285 

3286 def writelines(self, lines: Iterable[bytes], /) -> None: # type: ignore[override] 

3287 """Write multiple lines to the file. 

3288 

3289 Args: 

3290 lines: Iterable of lines to write 

3291 """ 

3292 for line in lines: 

3293 self.write(line) 

3294 

3295 def read(self, size: int = -1) -> bytes: 

3296 """Not supported for write-only file. 

3297 

3298 Raises: 

3299 UnsupportedOperation: Always raised 

3300 """ 

3301 raise UnsupportedOperation("read") 

3302 

3303 def __enter__(self) -> Self: 

3304 """Enter context manager.""" 

3305 return self 

3306 

3307 def __exit__( 

3308 self, 

3309 type: type | None, 

3310 value: BaseException | None, 

3311 traceback: TracebackType | None, 

3312 ) -> None: 

3313 """Exit context manager and close file.""" 

3314 self.close() 

3315 

3316 def __iter__(self) -> "HashWriter": 

3317 """Return iterator.""" 

3318 return self 

3319 

3320 def __next__(self) -> bytes: 

3321 """Not supported for write-only file. 

3322 

3323 Raises: 

3324 UnsupportedOperation: Always raised 

3325 """ 

3326 raise UnsupportedOperation("__next__") 

3327 

3328 def fileno(self) -> int: 

3329 """Return file descriptor number.""" 

3330 return self.f.fileno() 

3331 

3332 def isatty(self) -> bool: 

3333 """Check if file is a terminal.""" 

3334 return getattr(self.f, "isatty", lambda: False)() 

3335 

3336 def truncate(self, size: int | None = None) -> int: 

3337 """Not supported for write-only file. 

3338 

3339 Raises: 

3340 UnsupportedOperation: Always raised 

3341 """ 

3342 raise UnsupportedOperation("truncate") 

3343 

3344 

3345def pack_object_header( 

3346 type_num: int, 

3347 delta_base: bytes | int | None, 

3348 size: int, 

3349 object_format: "ObjectFormat", 

3350) -> bytearray: 

3351 """Create a pack object header for the given object info. 

3352 

3353 Args: 

3354 type_num: Numeric type of the object. 

3355 delta_base: Delta base offset or ref, or None for whole objects. 

3356 size: Uncompressed object size. 

3357 object_format: Object format (hash algorithm) to use. 

3358 Returns: A header for a packed object. 

3359 """ 

3360 header = [] 

3361 c = (type_num << 4) | (size & 15) 

3362 size >>= 4 

3363 while size: 

3364 header.append(c | 0x80) 

3365 c = size & 0x7F 

3366 size >>= 7 

3367 header.append(c) 

3368 if type_num == OFS_DELTA: 

3369 assert isinstance(delta_base, int) 

3370 ret = [delta_base & 0x7F] 

3371 delta_base >>= 7 

3372 while delta_base: 

3373 delta_base -= 1 

3374 ret.insert(0, 0x80 | (delta_base & 0x7F)) 

3375 delta_base >>= 7 

3376 header.extend(ret) 

3377 elif type_num == REF_DELTA: 

3378 assert isinstance(delta_base, bytes) 

3379 assert len(delta_base) == object_format.oid_length 

3380 header += delta_base 

3381 return bytearray(header) 

3382 

3383 

3384def pack_object_chunks( 

3385 type: int, 

3386 object: list[bytes] | tuple[bytes | int, list[bytes]], 

3387 object_format: "ObjectFormat", 

3388 *, 

3389 compression_level: int = -1, 

3390) -> Iterator[bytes]: 

3391 """Generate chunks for a pack object. 

3392 

3393 Args: 

3394 type: Numeric type of the object 

3395 object: Object to write 

3396 object_format: Object format (hash algorithm) to use 

3397 compression_level: the zlib compression level 

3398 Returns: Chunks 

3399 """ 

3400 if type in DELTA_TYPES: 

3401 if isinstance(object, tuple): 

3402 delta_base, object = object 

3403 else: 

3404 raise TypeError("Delta types require a tuple of (delta_base, object)") 

3405 else: 

3406 delta_base = None 

3407 

3408 # Convert object to list of bytes chunks 

3409 if isinstance(object, bytes): 

3410 chunks = [object] 

3411 elif isinstance(object, list): 

3412 chunks = object 

3413 elif isinstance(object, ShaFile): 

3414 chunks = object.as_raw_chunks() 

3415 else: 

3416 # Shouldn't reach here with proper typing 

3417 raise TypeError(f"Unexpected object type: {object.__class__.__name__}") 

3418 

3419 yield bytes( 

3420 pack_object_header( 

3421 type, delta_base, sum(map(len, chunks)), object_format=object_format 

3422 ) 

3423 ) 

3424 compressor = zlib.compressobj(level=compression_level) 

3425 for data in chunks: 

3426 yield compressor.compress(data) 

3427 yield compressor.flush() 

3428 

3429 

3430def write_pack_object( 

3431 write: Callable[[bytes], int], 

3432 type: int, 

3433 object: list[bytes] | tuple[bytes | int, list[bytes]], 

3434 object_format: "ObjectFormat", 

3435 *, 

3436 sha: "HashObject | None" = None, 

3437 compression_level: int = -1, 

3438) -> int: 

3439 """Write pack object to a file. 

3440 

3441 Args: 

3442 write: Write function to use 

3443 type: Numeric type of the object 

3444 object: Object to write 

3445 object_format: Object format (hash algorithm) to use 

3446 sha: Optional SHA-1 hasher to update 

3447 compression_level: the zlib compression level 

3448 Returns: CRC32 checksum of the written object 

3449 """ 

3450 crc32 = 0 

3451 for chunk in pack_object_chunks( 

3452 type, object, compression_level=compression_level, object_format=object_format 

3453 ): 

3454 write(chunk) 

3455 if sha is not None: 

3456 sha.update(chunk) 

3457 crc32 = binascii.crc32(chunk, crc32) 

3458 return crc32 & 0xFFFFFFFF 

3459 

3460 

3461def write_pack( 

3462 filename: str, 

3463 objects: Sequence[ShaFile] | Sequence[tuple[ShaFile, bytes | None]], 

3464 object_format: "ObjectFormat", 

3465 *, 

3466 deltify: bool | None = None, 

3467 delta_window_size: int | None = None, 

3468 compression_level: int = -1, 

3469) -> tuple[bytes, bytes]: 

3470 """Write a new pack data file. 

3471 

3472 Args: 

3473 filename: Path to the new pack file (without .pack extension) 

3474 objects: Objects to write to the pack 

3475 object_format: Object format 

3476 delta_window_size: Delta window size 

3477 deltify: Whether to deltify pack objects 

3478 compression_level: the zlib compression level 

3479 Returns: Tuple with checksum of pack file and index file 

3480 """ 

3481 with GitFile(filename + ".pack", "wb") as f: 

3482 entries, data_sum = write_pack_objects( 

3483 f, 

3484 objects, 

3485 delta_window_size=delta_window_size, 

3486 deltify=deltify, 

3487 compression_level=compression_level, 

3488 object_format=object_format, 

3489 ) 

3490 entries_list = sorted([(k, v[0], v[1]) for (k, v) in entries.items()]) 

3491 with GitFile(filename + ".idx", "wb") as f: 

3492 idx_sha = write_pack_index(f, entries_list, data_sum) 

3493 return data_sum, idx_sha 

3494 

3495 

3496def pack_header_chunks(num_objects: int) -> Iterator[bytes]: 

3497 """Yield chunks for a pack header.""" 

3498 yield b"PACK" # Pack header 

3499 yield struct.pack(b">L", 2) # Pack version 

3500 yield struct.pack(b">L", num_objects) # Number of objects in pack 

3501 

3502 

3503def write_pack_header( 

3504 write: Callable[[bytes], int] | IO[bytes], num_objects: int 

3505) -> None: 

3506 """Write a pack header for the given number of objects.""" 

3507 if not callable(write): 

3508 write_fn: Callable[[bytes], int] = write.write 

3509 warnings.warn( 

3510 "write_pack_header() now takes a write rather than file argument", 

3511 DeprecationWarning, 

3512 stacklevel=2, 

3513 ) 

3514 else: 

3515 write_fn = write 

3516 for chunk in pack_header_chunks(num_objects): 

3517 write_fn(chunk) 

3518 

3519 

3520def find_reusable_deltas( 

3521 container: PackedObjectContainer, 

3522 object_ids: Set[ObjectID], 

3523 *, 

3524 other_haves: Set[ObjectID] | None = None, 

3525 progress: Callable[..., None] | None = None, 

3526) -> Iterator[UnpackedObject]: 

3527 """Find deltas in a pack that can be reused. 

3528 

3529 Args: 

3530 container: Pack container to search for deltas 

3531 object_ids: Set of object IDs to find deltas for 

3532 other_haves: Set of other object IDs we have 

3533 progress: Optional progress reporting callback 

3534 

3535 Returns: 

3536 Iterator of UnpackedObject entries that can be reused 

3537 """ 

3538 if other_haves is None: 

3539 other_haves = set() 

3540 reused = 0 

3541 for i, unpacked in enumerate( 

3542 container.iter_unpacked_subset( 

3543 object_ids, allow_missing=True, convert_ofs_delta=True 

3544 ) 

3545 ): 

3546 if progress is not None and i % 1000 == 0: 

3547 progress(f"checking for reusable deltas: {i}/{len(object_ids)}\r".encode()) 

3548 if unpacked.pack_type_num == REF_DELTA: 

3549 hexsha = sha_to_hex(unpacked.delta_base) # type: ignore 

3550 if hexsha in object_ids or hexsha in other_haves: 

3551 yield unpacked 

3552 reused += 1 

3553 if progress is not None: 

3554 progress((f"found {reused} deltas to reuse\n").encode()) 

3555 

3556 

3557def deltify_pack_objects( 

3558 objects: Iterator[ShaFile] | Iterator[tuple[ShaFile, bytes | None]], 

3559 *, 

3560 window_size: int | None = None, 

3561 progress: Callable[..., None] | None = None, 

3562) -> Iterator[UnpackedObject]: 

3563 """Generate deltas for pack objects. 

3564 

3565 Args: 

3566 objects: An iterable of (object, path) tuples to deltify. 

3567 window_size: Window size; None for default 

3568 progress: Optional progress reporting callback 

3569 Returns: Iterator over type_num, object id, delta_base, content 

3570 delta_base is None for full text entries 

3571 """ 

3572 

3573 def objects_with_hints() -> Iterator[tuple[ShaFile, tuple[int, bytes | None]]]: 

3574 for e in objects: 

3575 if isinstance(e, ShaFile): 

3576 yield (e, (e.type_num, None)) 

3577 else: 

3578 yield (e[0], (e[0].type_num, e[1])) 

3579 

3580 sorted_objs = sort_objects_for_delta(objects_with_hints()) 

3581 yield from deltas_from_sorted_objects( 

3582 sorted_objs, 

3583 window_size=window_size, 

3584 progress=progress, 

3585 ) 

3586 

3587 

3588def sort_objects_for_delta( 

3589 objects: Iterator[ShaFile] | Iterator[tuple[ShaFile, PackHint | None]], 

3590) -> Iterator[tuple[ShaFile, bytes | None]]: 

3591 """Sort objects for optimal delta compression. 

3592 

3593 Args: 

3594 objects: Iterator of objects or (object, hint) tuples 

3595 

3596 Returns: 

3597 Iterator of sorted (ShaFile, path) tuples 

3598 """ 

3599 magic = [] 

3600 for entry in objects: 

3601 if isinstance(entry, tuple): 

3602 obj, hint = entry 

3603 if hint is None: 

3604 type_num = None 

3605 path = None 

3606 else: 

3607 (type_num, path) = hint 

3608 else: 

3609 obj = entry 

3610 type_num = None 

3611 path = None 

3612 magic.append((type_num, path, -obj.raw_length(), obj)) 

3613 # Build a list of objects ordered by the magic Linus heuristic 

3614 # This helps us find good objects to diff against us 

3615 magic.sort() 

3616 return ((x[3], x[1]) for x in magic) 

3617 

3618 

3619def deltas_from_sorted_objects( 

3620 objects: Iterator[tuple[ShaFile, bytes | None]], 

3621 window_size: int | None = None, 

3622 progress: Callable[..., None] | None = None, 

3623) -> Iterator[UnpackedObject]: 

3624 """Create deltas from sorted objects. 

3625 

3626 Args: 

3627 objects: Iterator of sorted objects to deltify 

3628 window_size: Delta window size; None for default 

3629 progress: Optional progress reporting callback 

3630 

3631 Returns: 

3632 Iterator of UnpackedObject entries 

3633 """ 

3634 # TODO(jelmer): Use threads 

3635 if window_size is None: 

3636 window_size = DEFAULT_PACK_DELTA_WINDOW_SIZE 

3637 

3638 possible_bases: deque[tuple[bytes, int, bytes]] = deque() 

3639 for i, (o, path) in enumerate(objects): 

3640 if progress is not None and i % 1000 == 0: 

3641 progress((f"generating deltas: {i}\r").encode()) 

3642 raw = o.as_raw_chunks() 

3643 raw_bytes = b"".join(raw) # Join once for efficiency 

3644 winner = raw 

3645 winner_len = sum(map(len, winner)) 

3646 winner_base = None 

3647 for base_id, base_type_num, base_bytes in possible_bases: 

3648 if base_type_num != o.type_num: 

3649 continue 

3650 delta_len = 0 

3651 delta = [] 

3652 for chunk in create_delta(base_bytes, raw_bytes): 

3653 delta_len += len(chunk) 

3654 if delta_len >= winner_len: 

3655 break 

3656 delta.append(chunk) 

3657 else: 

3658 winner_base = base_id 

3659 winner = delta 

3660 winner_len = sum(map(len, winner)) 

3661 yield UnpackedObject( 

3662 o.type_num, 

3663 sha=o.sha().digest(), 

3664 delta_base=winner_base, 

3665 decomp_len=winner_len, 

3666 decomp_chunks=winner, 

3667 ) 

3668 possible_bases.appendleft((o.sha().digest(), o.type_num, raw_bytes)) 

3669 while len(possible_bases) > window_size: 

3670 possible_bases.pop() 

3671 

3672 

3673def pack_objects_to_data( 

3674 objects: Sequence[ShaFile] 

3675 | Sequence[tuple[ShaFile, bytes | None]] 

3676 | Sequence[tuple[ShaFile, PackHint | None]], 

3677 *, 

3678 deltify: bool | None = None, 

3679 delta_window_size: int | None = None, 

3680 ofs_delta: bool = True, 

3681 progress: Callable[..., None] | None = None, 

3682) -> tuple[int, Iterator[UnpackedObject]]: 

3683 """Create pack data from objects. 

3684 

3685 Args: 

3686 objects: Pack objects 

3687 deltify: Whether to deltify pack objects 

3688 delta_window_size: Delta window size 

3689 ofs_delta: Whether to use offset deltas 

3690 progress: Optional progress reporting callback 

3691 Returns: Tuples with (type_num, hexdigest, delta base, object chunks) 

3692 """ 

3693 count = len(objects) 

3694 if deltify is None: 

3695 # PERFORMANCE/TODO(jelmer): This should be enabled but the python 

3696 # implementation is *much* too slow at the moment. 

3697 # Maybe consider enabling it just if the rust extension is available? 

3698 deltify = False 

3699 if deltify: 

3700 return ( 

3701 count, 

3702 deltify_pack_objects( 

3703 iter(objects), # type: ignore 

3704 window_size=delta_window_size, 

3705 progress=progress, 

3706 ), 

3707 ) 

3708 else: 

3709 

3710 def iter_without_path() -> Iterator[UnpackedObject]: 

3711 for o in objects: 

3712 if isinstance(o, tuple): 

3713 yield full_unpacked_object(o[0]) 

3714 else: 

3715 yield full_unpacked_object(o) 

3716 

3717 return (count, iter_without_path()) 

3718 

3719 

3720def generate_unpacked_objects( 

3721 container: PackedObjectContainer, 

3722 object_ids: Sequence[tuple[ObjectID, PackHint | None]], 

3723 delta_window_size: int | None = None, 

3724 deltify: bool | None = None, 

3725 reuse_deltas: bool = True, 

3726 ofs_delta: bool = True, 

3727 other_haves: set[ObjectID] | None = None, 

3728 progress: Callable[..., None] | None = None, 

3729) -> Iterator[UnpackedObject]: 

3730 """Create pack data from objects. 

3731 

3732 Returns: Tuples with (type_num, hexdigest, delta base, object chunks) 

3733 """ 

3734 todo = dict(object_ids) 

3735 if reuse_deltas: 

3736 for unpack in find_reusable_deltas( 

3737 container, set(todo), other_haves=other_haves, progress=progress 

3738 ): 

3739 del todo[sha_to_hex(RawObjectID(unpack.sha()))] 

3740 yield unpack 

3741 if deltify is None: 

3742 # PERFORMANCE/TODO(jelmer): This should be enabled but is *much* too 

3743 # slow at the moment. 

3744 deltify = False 

3745 if deltify: 

3746 objects_to_delta = container.iterobjects_subset( 

3747 todo.keys(), allow_missing=False 

3748 ) 

3749 sorted_objs = sort_objects_for_delta((o, todo[o.id]) for o in objects_to_delta) 

3750 yield from deltas_from_sorted_objects( 

3751 sorted_objs, 

3752 window_size=delta_window_size, 

3753 progress=progress, 

3754 ) 

3755 else: 

3756 for oid in todo: 

3757 yield full_unpacked_object(container[oid]) 

3758 

3759 

3760def full_unpacked_object(o: ShaFile) -> UnpackedObject: 

3761 """Create an UnpackedObject from a ShaFile. 

3762 

3763 Args: 

3764 o: ShaFile object to convert 

3765 

3766 Returns: 

3767 UnpackedObject with full object data 

3768 """ 

3769 return UnpackedObject( 

3770 o.type_num, 

3771 delta_base=None, 

3772 crc32=None, 

3773 decomp_chunks=o.as_raw_chunks(), 

3774 sha=o.sha().digest(), 

3775 ) 

3776 

3777 

3778def write_pack_from_container( 

3779 write: Callable[[bytes], None] 

3780 | Callable[[bytes | bytearray | memoryview], int] 

3781 | IO[bytes], 

3782 container: PackedObjectContainer, 

3783 object_ids: Sequence[tuple[ObjectID, PackHint | None]], 

3784 object_format: "ObjectFormat", 

3785 *, 

3786 delta_window_size: int | None = None, 

3787 deltify: bool | None = None, 

3788 reuse_deltas: bool = True, 

3789 compression_level: int = -1, 

3790 other_haves: set[ObjectID] | None = None, 

3791) -> tuple[dict[bytes, tuple[int, int]], bytes]: 

3792 """Write a new pack data file. 

3793 

3794 Args: 

3795 write: write function to use 

3796 container: PackedObjectContainer 

3797 object_ids: Sequence of (object_id, hint) tuples to write 

3798 object_format: Object format (hash algorithm) to use 

3799 delta_window_size: Sliding window size for searching for deltas; 

3800 Set to None for default window size. 

3801 deltify: Whether to deltify objects 

3802 reuse_deltas: Whether to reuse existing deltas 

3803 compression_level: the zlib compression level to use 

3804 other_haves: Set of additional object IDs the receiver has 

3805 Returns: Dict mapping id -> (offset, crc32 checksum), pack checksum 

3806 """ 

3807 pack_contents_count = len(object_ids) 

3808 pack_contents = generate_unpacked_objects( 

3809 container, 

3810 object_ids, 

3811 delta_window_size=delta_window_size, 

3812 deltify=deltify, 

3813 reuse_deltas=reuse_deltas, 

3814 other_haves=other_haves, 

3815 ) 

3816 

3817 return write_pack_data( 

3818 write, 

3819 pack_contents, 

3820 num_records=pack_contents_count, 

3821 compression_level=compression_level, 

3822 object_format=object_format, 

3823 ) 

3824 

3825 

3826def write_pack_objects( 

3827 write: Callable[[bytes], None] | IO[bytes], 

3828 objects: Sequence[ShaFile] | Sequence[tuple[ShaFile, bytes | None]], 

3829 object_format: "ObjectFormat", 

3830 *, 

3831 delta_window_size: int | None = None, 

3832 deltify: bool | None = None, 

3833 compression_level: int = -1, 

3834) -> tuple[dict[bytes, tuple[int, int]], bytes]: 

3835 """Write a new pack data file. 

3836 

3837 Args: 

3838 write: write function to use 

3839 objects: Sequence of (object, path) tuples to write 

3840 object_format: Object format (hash algorithm) to use 

3841 delta_window_size: Sliding window size for searching for deltas; 

3842 Set to None for default window size. 

3843 deltify: Whether to deltify objects 

3844 compression_level: the zlib compression level to use 

3845 Returns: Dict mapping id -> (offset, crc32 checksum), pack checksum 

3846 """ 

3847 pack_contents_count, pack_contents = pack_objects_to_data(objects, deltify=deltify) 

3848 

3849 return write_pack_data( 

3850 write, 

3851 pack_contents, 

3852 num_records=pack_contents_count, 

3853 compression_level=compression_level, 

3854 object_format=object_format, 

3855 ) 

3856 

3857 

3858class PackChunkGenerator: 

3859 """Generator for pack data chunks.""" 

3860 

3861 def __init__( 

3862 self, 

3863 object_format: "ObjectFormat", 

3864 num_records: int | None = None, 

3865 records: Iterator[UnpackedObject] | None = None, 

3866 progress: Callable[..., None] | None = None, 

3867 compression_level: int = -1, 

3868 reuse_compressed: bool = True, 

3869 ) -> None: 

3870 """Initialize PackChunkGenerator. 

3871 

3872 Args: 

3873 num_records: Expected number of records 

3874 records: Iterator of pack records 

3875 progress: Optional progress callback 

3876 compression_level: Compression level (-1 for default) 

3877 reuse_compressed: Whether to reuse compressed chunks 

3878 object_format: Object format (hash algorithm) to use 

3879 """ 

3880 self.object_format = object_format 

3881 self.cs = object_format.new_hash() 

3882 self.entries: dict[bytes, tuple[int, int]] = {} 

3883 if records is None: 

3884 records = iter([]) # Empty iterator if None 

3885 self._it = self._pack_data_chunks( 

3886 records=records, 

3887 num_records=num_records, 

3888 progress=progress, 

3889 compression_level=compression_level, 

3890 reuse_compressed=reuse_compressed, 

3891 ) 

3892 

3893 def sha1digest(self) -> bytes: 

3894 """Return the SHA1 digest of the pack data.""" 

3895 return self.cs.digest() 

3896 

3897 def __iter__(self) -> Iterator[bytes]: 

3898 """Iterate over pack data chunks.""" 

3899 return self._it 

3900 

3901 def _pack_data_chunks( 

3902 self, 

3903 records: Iterator[UnpackedObject], 

3904 *, 

3905 num_records: int | None = None, 

3906 progress: Callable[..., None] | None = None, 

3907 compression_level: int = -1, 

3908 reuse_compressed: bool = True, 

3909 ) -> Iterator[bytes]: 

3910 """Iterate pack data file chunks. 

3911 

3912 Args: 

3913 records: Iterator over UnpackedObject 

3914 num_records: Number of records (defaults to len(records) if not specified) 

3915 progress: Function to report progress to 

3916 compression_level: the zlib compression level 

3917 reuse_compressed: Whether to reuse compressed chunks 

3918 Returns: Dict mapping id -> (offset, crc32 checksum), pack checksum 

3919 """ 

3920 # Write the pack 

3921 if num_records is None: 

3922 num_records = len(records) # type: ignore 

3923 offset = 0 

3924 for chunk in pack_header_chunks(num_records): 

3925 yield chunk 

3926 self.cs.update(chunk) 

3927 offset += len(chunk) 

3928 actual_num_records = 0 

3929 for i, unpacked in enumerate(records): 

3930 type_num = unpacked.pack_type_num 

3931 if progress is not None and i % 1000 == 0: 

3932 progress((f"writing pack data: {i}/{num_records}\r").encode("ascii")) 

3933 raw: list[bytes] | tuple[int, list[bytes]] | tuple[bytes, list[bytes]] 

3934 if unpacked.delta_base is not None: 

3935 assert isinstance(unpacked.delta_base, bytes), ( 

3936 f"Expected bytes, got {type(unpacked.delta_base)}" 

3937 ) 

3938 try: 

3939 base_offset, _base_crc32 = self.entries[unpacked.delta_base] 

3940 except KeyError: 

3941 type_num = REF_DELTA 

3942 assert isinstance(unpacked.delta_base, bytes) 

3943 raw = (unpacked.delta_base, unpacked.decomp_chunks) 

3944 else: 

3945 type_num = OFS_DELTA 

3946 raw = (offset - base_offset, unpacked.decomp_chunks) 

3947 else: 

3948 raw = unpacked.decomp_chunks 

3949 chunks: list[bytes] | Iterator[bytes] 

3950 if unpacked.comp_chunks is not None and reuse_compressed: 

3951 chunks = unpacked.comp_chunks 

3952 else: 

3953 chunks = pack_object_chunks( 

3954 type_num, 

3955 raw, 

3956 compression_level=compression_level, 

3957 object_format=self.object_format, 

3958 ) 

3959 crc32 = 0 

3960 object_size = 0 

3961 for chunk in chunks: 

3962 yield chunk 

3963 crc32 = binascii.crc32(chunk, crc32) 

3964 self.cs.update(chunk) 

3965 object_size += len(chunk) 

3966 actual_num_records += 1 

3967 self.entries[unpacked.sha()] = (offset, crc32) 

3968 offset += object_size 

3969 if actual_num_records != num_records: 

3970 raise AssertionError( 

3971 f"actual records written differs: {actual_num_records} != {num_records}" 

3972 ) 

3973 

3974 yield self.cs.digest() 

3975 

3976 

3977def write_pack_data( 

3978 write: Callable[[bytes], None] 

3979 | Callable[[bytes | bytearray | memoryview], int] 

3980 | IO[bytes], 

3981 records: Iterator[UnpackedObject], 

3982 object_format: "ObjectFormat", 

3983 *, 

3984 num_records: int | None = None, 

3985 progress: Callable[..., None] | None = None, 

3986 compression_level: int = -1, 

3987) -> tuple[dict[bytes, tuple[int, int]], bytes]: 

3988 """Write a new pack data file. 

3989 

3990 Args: 

3991 write: Write function to use 

3992 num_records: Number of records (defaults to len(records) if None) 

3993 records: Iterator over type_num, object_id, delta_base, raw 

3994 object_format: Object format (hash algorithm) to use 

3995 progress: Function to report progress to 

3996 compression_level: the zlib compression level 

3997 Returns: Dict mapping id -> (offset, crc32 checksum), pack checksum 

3998 """ 

3999 chunk_generator = PackChunkGenerator( 

4000 num_records=num_records, 

4001 records=records, 

4002 progress=progress, 

4003 compression_level=compression_level, 

4004 object_format=object_format, 

4005 ) 

4006 for chunk in chunk_generator: 

4007 if callable(write): 

4008 write(chunk) 

4009 else: 

4010 write.write(chunk) 

4011 return chunk_generator.entries, chunk_generator.sha1digest() 

4012 

4013 

4014def write_pack_index_v1( 

4015 f: IO[bytes], 

4016 entries: Iterable[tuple[bytes, int, int | None]], 

4017 pack_checksum: bytes, 

4018) -> bytes: 

4019 """Write a new pack index file. 

4020 

4021 Args: 

4022 f: A file-like object to write to 

4023 entries: List of tuples with object name (sha), offset_in_pack, 

4024 and crc32_checksum. 

4025 pack_checksum: Checksum of the pack file. 

4026 Returns: The SHA of the written index file 

4027 """ 

4028 f = SHA1Writer(f) 

4029 fan_out_table: dict[int, int] = defaultdict(lambda: 0) 

4030 for name, _offset, _entry_checksum in entries: 

4031 fan_out_table[ord(name[:1])] += 1 

4032 # Fan-out table 

4033 for i in range(0x100): 

4034 f.write(struct.pack(">L", fan_out_table[i])) 

4035 fan_out_table[i + 1] += fan_out_table[i] 

4036 for name, offset, _entry_checksum in entries: 

4037 if len(name) != 20: 

4038 raise TypeError("pack index v1 only supports SHA-1 names") 

4039 if not (offset <= 0xFFFFFFFF): 

4040 raise TypeError("pack format 1 only supports offsets < 2Gb") 

4041 f.write(struct.pack(">L20s", offset, name)) 

4042 assert len(pack_checksum) == 20 

4043 f.write(pack_checksum) 

4044 return f.write_sha() 

4045 

4046 

4047def _delta_encode_size(size: int) -> bytes: 

4048 ret = bytearray() 

4049 c = size & 0x7F 

4050 size >>= 7 

4051 while size: 

4052 ret.append(c | 0x80) 

4053 c = size & 0x7F 

4054 size >>= 7 

4055 ret.append(c) 

4056 return bytes(ret) 

4057 

4058 

4059# The length of delta compression copy operations in version 2 packs is limited 

4060# to 64K. To copy more, we use several copy operations. Version 3 packs allow 

4061# 24-bit lengths in copy operations, but we always make version 2 packs. 

4062_MAX_COPY_LEN = 0xFFFF 

4063 

4064 

4065def _encode_copy_operation(start: int, length: int) -> bytes: 

4066 scratch = bytearray([0x80]) 

4067 for i in range(4): 

4068 if start & 0xFF << i * 8: 

4069 scratch.append((start >> i * 8) & 0xFF) 

4070 scratch[0] |= 1 << i 

4071 for i in range(2): 

4072 if length & 0xFF << i * 8: 

4073 scratch.append((length >> i * 8) & 0xFF) 

4074 scratch[0] |= 1 << (4 + i) 

4075 return bytes(scratch) 

4076 

4077 

4078def _create_delta_py( 

4079 base_buf: bytes | list[bytes], target_buf: bytes | list[bytes] 

4080) -> Iterator[bytes]: 

4081 """Use python difflib to work out how to transform base_buf to target_buf. 

4082 

4083 Args: 

4084 base_buf: Base buffer 

4085 target_buf: Target buffer 

4086 """ 

4087 if isinstance(base_buf, list): 

4088 base_buf = b"".join(base_buf) 

4089 if isinstance(target_buf, list): 

4090 target_buf = b"".join(target_buf) 

4091 # write delta header 

4092 yield _delta_encode_size(len(base_buf)) 

4093 yield _delta_encode_size(len(target_buf)) 

4094 # write out delta opcodes 

4095 seq = SequenceMatcher(isjunk=None, a=base_buf, b=target_buf) 

4096 for opcode, i1, i2, j1, j2 in seq.get_opcodes(): 

4097 # Git patch opcodes don't care about deletes! 

4098 # if opcode == 'replace' or opcode == 'delete': 

4099 # pass 

4100 if opcode == "equal": 

4101 # If they are equal, unpacker will use data from base_buf 

4102 # Write out an opcode that says what range to use 

4103 copy_start = i1 

4104 copy_len = i2 - i1 

4105 while copy_len > 0: 

4106 to_copy = min(copy_len, _MAX_COPY_LEN) 

4107 yield _encode_copy_operation(copy_start, to_copy) 

4108 copy_start += to_copy 

4109 copy_len -= to_copy 

4110 if opcode == "replace" or opcode == "insert": 

4111 # If we are replacing a range or adding one, then we just 

4112 # output it to the stream (prefixed by its size) 

4113 s = j2 - j1 

4114 o = j1 

4115 while s > 127: 

4116 yield bytes([127]) 

4117 yield bytes(memoryview(target_buf)[o : o + 127]) 

4118 s -= 127 

4119 o += 127 

4120 yield bytes([s]) 

4121 yield bytes(memoryview(target_buf)[o : o + s]) 

4122 

4123 

4124# Default to pure Python implementation 

4125create_delta = _create_delta_py 

4126 

4127 

4128def apply_delta( 

4129 src_buf: bytes | list[bytes], delta: bytes | list[bytes] 

4130) -> list[bytes]: 

4131 """Based on the similar function in git's patch-delta.c. 

4132 

4133 Args: 

4134 src_buf: Source buffer 

4135 delta: Delta instructions 

4136 """ 

4137 if not isinstance(src_buf, bytes): 

4138 src_buf = b"".join(src_buf) 

4139 if not isinstance(delta, bytes): 

4140 delta = b"".join(delta) 

4141 out = [] 

4142 index = 0 

4143 delta_length = len(delta) 

4144 

4145 def get_delta_header_size(delta: bytes, index: int) -> tuple[int, int]: 

4146 size = 0 

4147 i = 0 

4148 while True: 

4149 # Bound-check explicitly: ``delta[index:index+1]`` silently 

4150 # returns b"" past the end, which would crash with TypeError 

4151 # in ``ord`` and leave the caller unable to distinguish a 

4152 # truncated delta from a programming bug. 

4153 if index >= delta_length: 

4154 raise ApplyDeltaError("delta truncated in size header") 

4155 cmd = ord(delta[index : index + 1]) 

4156 index += 1 

4157 size |= (cmd & ~0x80) << i 

4158 i += 7 

4159 if not cmd & 0x80: 

4160 break 

4161 return size, index 

4162 

4163 def read_byte(delta: bytes) -> int: 

4164 nonlocal index 

4165 # Bound-check explicitly: ``delta[index:index+1]`` silently returns 

4166 # b"" past the end, which would crash with TypeError in ``ord`` and 

4167 # leave the caller unable to distinguish a truncated delta from a 

4168 # programming bug. 

4169 if index >= delta_length: 

4170 raise ApplyDeltaError("delta truncated in copy op") 

4171 index += 1 

4172 return ord(delta[index - 1 : index]) 

4173 

4174 src_size, index = get_delta_header_size(delta, index) 

4175 dest_size, index = get_delta_header_size(delta, index) 

4176 if src_size != len(src_buf): 

4177 raise ApplyDeltaError( 

4178 f"Unexpected source buffer size: {src_size} vs {len(src_buf)}" 

4179 ) 

4180 while index < delta_length: 

4181 cmd = ord(delta[index : index + 1]) 

4182 index += 1 

4183 if cmd & 0x80: 

4184 cp_off = 0 

4185 for i in range(4): 

4186 if cmd & (1 << i): 

4187 x = read_byte(delta) 

4188 cp_off |= x << (i * 8) 

4189 cp_size = 0 

4190 # Version 3 packs can contain copy sizes larger than 64K. 

4191 for i in range(3): 

4192 if cmd & (1 << (4 + i)): 

4193 x = read_byte(delta) 

4194 cp_size |= x << (i * 8) 

4195 if cp_size == 0: 

4196 cp_size = 0x10000 

4197 if ( 

4198 cp_off + cp_size < cp_size 

4199 or cp_off + cp_size > src_size 

4200 or cp_size > dest_size 

4201 ): 

4202 break 

4203 out.append(src_buf[cp_off : cp_off + cp_size]) 

4204 elif cmd != 0: 

4205 if index + cmd > delta_length: 

4206 raise ApplyDeltaError("delta truncated in insert op") 

4207 out.append(delta[index : index + cmd]) 

4208 index += cmd 

4209 else: 

4210 raise ApplyDeltaError("Invalid opcode 0") 

4211 

4212 if index != delta_length: 

4213 raise ApplyDeltaError(f"delta not empty: {delta[index:]!r}") 

4214 

4215 if dest_size != chunks_length(out): 

4216 raise ApplyDeltaError("dest size incorrect") 

4217 

4218 return out 

4219 

4220 

4221def write_pack_index_v2( 

4222 f: IO[bytes], 

4223 entries: Iterable[tuple[bytes, int, int | None]], 

4224 pack_checksum: bytes, 

4225) -> bytes: 

4226 """Write a new pack index file. 

4227 

4228 Args: 

4229 f: File-like object to write to 

4230 entries: List of tuples with object name (sha), offset_in_pack, and 

4231 crc32_checksum. 

4232 pack_checksum: Checksum of the pack file. 

4233 Returns: The checksum of the index file written 

4234 """ 

4235 # Determine hash algorithm from pack_checksum length 

4236 if len(pack_checksum) == 20: 

4237 hash_func = sha1 

4238 elif len(pack_checksum) == 32: 

4239 hash_func = sha256 

4240 else: 

4241 raise ValueError(f"Unsupported pack checksum length: {len(pack_checksum)}") 

4242 

4243 f_writer = HashWriter(f, hash_func) 

4244 f_writer.write(b"\377tOc") # Magic! 

4245 f_writer.write(struct.pack(">L", 2)) 

4246 

4247 # Convert to list to allow multiple iterations 

4248 entries_list = list(entries) 

4249 

4250 fan_out_table: dict[int, int] = defaultdict(lambda: 0) 

4251 for name, offset, entry_checksum in entries_list: 

4252 fan_out_table[ord(name[:1])] += 1 

4253 

4254 if entries_list: 

4255 hash_size = len(entries_list[0][0]) 

4256 else: 

4257 hash_size = len(pack_checksum) # Use pack_checksum length as hash size 

4258 

4259 # Fan-out table 

4260 largetable: list[int] = [] 

4261 for i in range(0x100): 

4262 f_writer.write(struct.pack(b">L", fan_out_table[i])) 

4263 fan_out_table[i + 1] += fan_out_table[i] 

4264 for name, offset, entry_checksum in entries_list: 

4265 if len(name) != hash_size: 

4266 raise TypeError( 

4267 f"Object name has wrong length: expected {hash_size}, got {len(name)}" 

4268 ) 

4269 f_writer.write(name) 

4270 for name, offset, entry_checksum in entries_list: 

4271 f_writer.write(struct.pack(b">L", entry_checksum)) 

4272 for name, offset, entry_checksum in entries_list: 

4273 if offset < 2**31: 

4274 f_writer.write(struct.pack(b">L", offset)) 

4275 else: 

4276 f_writer.write(struct.pack(b">L", 2**31 + len(largetable))) 

4277 largetable.append(offset) 

4278 for offset in largetable: 

4279 f_writer.write(struct.pack(b">Q", offset)) 

4280 f_writer.write(pack_checksum) 

4281 return f_writer.write_hash() 

4282 

4283 

4284def write_pack_index_v3( 

4285 f: IO[bytes], 

4286 entries: Iterable[tuple[bytes, int, int | None]], 

4287 pack_checksum: bytes, 

4288 hash_format: int = 1, 

4289) -> bytes: 

4290 """Write a new pack index file in v3 format. 

4291 

4292 Args: 

4293 f: File-like object to write to 

4294 entries: List of tuples with object name (sha), offset_in_pack, and 

4295 crc32_checksum. 

4296 pack_checksum: Checksum of the pack file. 

4297 hash_format: Hash algorithm identifier (1 = SHA-1, 2 = SHA-256) 

4298 Returns: The SHA of the index file written 

4299 """ 

4300 if hash_format == 1: 

4301 hash_size = 20 # SHA-1 

4302 writer_cls = SHA1Writer 

4303 elif hash_format == 2: 

4304 hash_size = 32 # SHA-256 

4305 # TODO: Add SHA256Writer when SHA-256 support is implemented 

4306 raise NotImplementedError("SHA-256 support not yet implemented") 

4307 else: 

4308 raise ValueError(f"Unknown hash algorithm {hash_format}") 

4309 

4310 # Convert entries to list to allow multiple iterations 

4311 entries_list = list(entries) 

4312 

4313 # Calculate shortest unambiguous prefix length for object names 

4314 # For now, use full hash size (this could be optimized) 

4315 shortened_oid_len = hash_size 

4316 

4317 f = writer_cls(f) 

4318 f.write(b"\377tOc") # Magic! 

4319 f.write(struct.pack(">L", 3)) # Version 3 

4320 f.write(struct.pack(">L", hash_format)) # Hash algorithm 

4321 f.write(struct.pack(">L", shortened_oid_len)) # Shortened OID length 

4322 

4323 fan_out_table: dict[int, int] = defaultdict(lambda: 0) 

4324 for name, offset, entry_checksum in entries_list: 

4325 if len(name) != hash_size: 

4326 raise ValueError( 

4327 f"Object name has wrong length: expected {hash_size}, got {len(name)}" 

4328 ) 

4329 fan_out_table[ord(name[:1])] += 1 

4330 

4331 # Fan-out table 

4332 largetable: list[int] = [] 

4333 for i in range(0x100): 

4334 f.write(struct.pack(b">L", fan_out_table[i])) 

4335 fan_out_table[i + 1] += fan_out_table[i] 

4336 

4337 # Object names table 

4338 for name, offset, entry_checksum in entries_list: 

4339 f.write(name) 

4340 

4341 # CRC32 checksums table 

4342 for name, offset, entry_checksum in entries_list: 

4343 f.write(struct.pack(b">L", entry_checksum)) 

4344 

4345 # Offset table 

4346 for name, offset, entry_checksum in entries_list: 

4347 if offset < 2**31: 

4348 f.write(struct.pack(b">L", offset)) 

4349 else: 

4350 f.write(struct.pack(b">L", 2**31 + len(largetable))) 

4351 largetable.append(offset) 

4352 

4353 # Large offset table 

4354 for offset in largetable: 

4355 f.write(struct.pack(b">Q", offset)) 

4356 

4357 assert len(pack_checksum) == hash_size, ( 

4358 f"Pack checksum has wrong length: expected {hash_size}, got {len(pack_checksum)}" 

4359 ) 

4360 f.write(pack_checksum) 

4361 return f.write_sha() 

4362 

4363 

4364def write_pack_index( 

4365 f: IO[bytes], 

4366 entries: Iterable[tuple[bytes, int, int | None]], 

4367 pack_checksum: bytes, 

4368 progress: Callable[..., None] | None = None, 

4369 version: int | None = None, 

4370) -> bytes: 

4371 """Write a pack index file. 

4372 

4373 Args: 

4374 f: File-like object to write to. 

4375 entries: List of (checksum, offset, crc32) tuples 

4376 pack_checksum: Checksum of the pack file. 

4377 progress: Progress function (not currently used) 

4378 version: Pack index version to use (1, 2, or 3). If None, defaults to DEFAULT_PACK_INDEX_VERSION. 

4379 

4380 Returns: 

4381 SHA of the written index file 

4382 

4383 Raises: 

4384 ValueError: If an unsupported version is specified 

4385 """ 

4386 if version is None: 

4387 version = DEFAULT_PACK_INDEX_VERSION 

4388 

4389 if version == 1: 

4390 return write_pack_index_v1(f, entries, pack_checksum) 

4391 elif version == 2: 

4392 return write_pack_index_v2(f, entries, pack_checksum) 

4393 elif version == 3: 

4394 return write_pack_index_v3(f, entries, pack_checksum) 

4395 else: 

4396 raise ValueError(f"Unsupported pack index version: {version}") 

4397 

4398 

4399class Pack: 

4400 """A Git pack object.""" 

4401 

4402 _data_load: Callable[[], PackData] | None 

4403 _idx_load: Callable[[], PackIndex] | None 

4404 

4405 _data: PackData | None 

4406 _idx: PackIndex | None 

4407 _bitmap: "PackBitmap | None" 

4408 

4409 def __init__( 

4410 self, 

4411 basename: str, 

4412 *, 

4413 object_format: ObjectFormat, 

4414 resolve_ext_ref: ResolveExtRefFn | None = None, 

4415 delta_window_size: int | None = None, 

4416 window_memory: int | None = None, 

4417 delta_cache_size: int | None = None, 

4418 depth: int | None = None, 

4419 threads: int | None = None, 

4420 big_file_threshold: int | None = None, 

4421 delta_base_cache_limit: int | None = None, 

4422 ) -> None: 

4423 """Initialize a Pack object. 

4424 

4425 Args: 

4426 basename: Base path for pack files (without .pack/.idx extension) 

4427 object_format: Hash algorithm used by the repository 

4428 resolve_ext_ref: Optional function to resolve external references 

4429 delta_window_size: Size of the delta compression window 

4430 window_memory: Memory limit for delta compression window 

4431 delta_cache_size: Size of the delta cache 

4432 depth: Maximum depth for delta chains 

4433 threads: Number of threads to use for operations 

4434 big_file_threshold: Size threshold for big file handling 

4435 delta_base_cache_limit: Maximum bytes for delta base object cache 

4436 """ 

4437 self._basename = basename 

4438 self.object_format = object_format 

4439 self._data = None 

4440 self._idx = None 

4441 self._bitmap = None 

4442 self._idx_path = self._basename + ".idx" 

4443 self._data_path = self._basename + ".pack" 

4444 self._bitmap_path = self._basename + ".bitmap" 

4445 self.delta_window_size = delta_window_size 

4446 self.window_memory = window_memory 

4447 self.delta_cache_size = delta_cache_size 

4448 self.depth = depth 

4449 self.threads = threads 

4450 self.big_file_threshold = big_file_threshold 

4451 self.delta_base_cache_limit = delta_base_cache_limit 

4452 self._idx_load = lambda: load_pack_index(self._idx_path, object_format) 

4453 self._data_load = lambda: PackData( 

4454 self._data_path, 

4455 delta_window_size=delta_window_size, 

4456 window_memory=window_memory, 

4457 delta_cache_size=delta_cache_size, 

4458 depth=depth, 

4459 threads=threads, 

4460 big_file_threshold=big_file_threshold, 

4461 delta_base_cache_limit=delta_base_cache_limit, 

4462 object_format=object_format, 

4463 ) 

4464 self.resolve_ext_ref = resolve_ext_ref 

4465 

4466 @classmethod 

4467 def from_lazy_objects( 

4468 cls, 

4469 data_fn: Callable[[], PackData], 

4470 idx_fn: Callable[[], PackIndex], 

4471 ) -> "Pack": 

4472 """Create a new pack object from callables to load pack data and index objects.""" 

4473 # Load index to get object format 

4474 idx = idx_fn() 

4475 ret = cls("", object_format=idx.object_format) 

4476 ret._data_load = data_fn 

4477 ret._idx = idx 

4478 ret._idx_load = None 

4479 return ret 

4480 

4481 @classmethod 

4482 def from_objects(cls, data: PackData, idx: PackIndex) -> "Pack": 

4483 """Create a new pack object from pack data and index objects.""" 

4484 ret = cls("", object_format=idx.object_format) 

4485 ret._data = data 

4486 ret._data_load = None 

4487 ret._idx = idx 

4488 ret._idx_load = None 

4489 ret.check_length_and_checksum() 

4490 return ret 

4491 

4492 def name(self) -> bytes: 

4493 """The SHA over the SHAs of the objects in this pack.""" 

4494 return self.index.objects_sha1() 

4495 

4496 @property 

4497 def data(self) -> PackData: 

4498 """The pack data object being used.""" 

4499 if self._data is None: 

4500 assert self._data_load 

4501 try: 

4502 self._data = self._data_load() 

4503 except FileNotFoundError as exc: 

4504 raise PackFileDisappeared(self) from exc 

4505 self.check_length_and_checksum() 

4506 return self._data 

4507 

4508 @property 

4509 def index(self) -> PackIndex: 

4510 """The index being used. 

4511 

4512 Note: This may be an in-memory index 

4513 """ 

4514 if self._idx is None: 

4515 assert self._idx_load 

4516 try: 

4517 self._idx = self._idx_load() 

4518 except FileNotFoundError as exc: 

4519 raise PackFileDisappeared(self) from exc 

4520 return self._idx 

4521 

4522 @property 

4523 def bitmap(self) -> "PackBitmap | None": 

4524 """The bitmap being used, if available. 

4525 

4526 Returns: 

4527 PackBitmap instance, or None if no bitmap exists or the bitmap 

4528 was built for a different pack 

4529 

4530 Raises: 

4531 ValueError: If bitmap file is invalid or corrupt 

4532 """ 

4533 if self._bitmap is None: 

4534 from .bitmap import read_bitmap 

4535 

4536 try: 

4537 self._bitmap = read_bitmap( 

4538 self._bitmap_path, 

4539 pack_index=self.index, 

4540 pack_checksum=self.get_stored_checksum(), 

4541 ) 

4542 except ChecksumMismatch: 

4543 # The bitmap records the checksum of the pack it was built for. 

4544 # A mismatch means it is stale or was swapped in from another 

4545 # pack, so its positions no longer describe this pack's objects. 

4546 # Ignore it and let callers fall back to graph traversal, the 

4547 # same as git. 

4548 logger.warning( 

4549 "Ignoring bitmap %s: checksum does not match pack", 

4550 self._bitmap_path, 

4551 ) 

4552 return None 

4553 return self._bitmap 

4554 

4555 def ensure_bitmap( 

4556 self, 

4557 object_store: "BaseObjectStore", 

4558 refs: dict["Ref", "ObjectID"], 

4559 commit_interval: int | None = None, 

4560 progress: Callable[[str], None] | None = None, 

4561 ) -> "PackBitmap": 

4562 """Ensure a bitmap exists for this pack, generating one if needed. 

4563 

4564 Args: 

4565 object_store: Object store to read objects from 

4566 refs: Dictionary of ref names to commit SHAs 

4567 commit_interval: Include every Nth commit in bitmap index 

4568 progress: Optional progress reporting callback 

4569 

4570 Returns: 

4571 PackBitmap instance (either existing or newly generated) 

4572 """ 

4573 from .bitmap import generate_bitmap, write_bitmap 

4574 

4575 # Check if bitmap already exists 

4576 try: 

4577 existing = self.bitmap 

4578 if existing is not None: 

4579 return existing 

4580 except FileNotFoundError: 

4581 pass # No bitmap, we'll generate one 

4582 

4583 # Generate new bitmap 

4584 if progress: 

4585 progress(f"Generating bitmap for {self.name().decode('utf-8')}...\n") 

4586 

4587 pack_bitmap = generate_bitmap( 

4588 self.index, 

4589 object_store, 

4590 refs, 

4591 self.get_stored_checksum(), 

4592 commit_interval=commit_interval, 

4593 progress=progress, 

4594 ) 

4595 

4596 # Write bitmap file 

4597 write_bitmap(self._bitmap_path, pack_bitmap) 

4598 

4599 if progress: 

4600 progress(f"Wrote {self._bitmap_path}\n") 

4601 

4602 # Update cached bitmap 

4603 self._bitmap = pack_bitmap 

4604 

4605 return pack_bitmap 

4606 

4607 @property 

4608 def mmap_size(self) -> int: 

4609 """Return the total mmapped memory usage of this pack. 

4610 

4611 This includes the pack data file and index file sizes, 

4612 but only for components that have been loaded (and thus mmapped). 

4613 """ 

4614 total = 0 

4615 if self._data is not None: 

4616 total += self._data._size 

4617 if self._idx is not None and isinstance(self._idx, FilePackIndex): 

4618 total += self._idx._size 

4619 return total 

4620 

4621 def close(self) -> None: 

4622 """Close the pack file and index.""" 

4623 if self._data is not None: 

4624 self._data.close() 

4625 self._data = None 

4626 if self._idx is not None: 

4627 self._idx.close() 

4628 self._idx = None 

4629 

4630 def __del__(self) -> None: 

4631 """Ensure pack file is closed when Pack is garbage collected.""" 

4632 if self._data is not None or self._idx is not None: 

4633 import warnings 

4634 

4635 warnings.warn( 

4636 f"unclosed Pack {self!r}", ResourceWarning, stacklevel=2, source=self 

4637 ) 

4638 try: 

4639 self.close() 

4640 except Exception: 

4641 # Ignore errors during cleanup 

4642 pass 

4643 

4644 def __enter__(self) -> Self: 

4645 """Enter context manager.""" 

4646 return self 

4647 

4648 def __exit__( 

4649 self, 

4650 type: type | None, 

4651 value: BaseException | None, 

4652 traceback: TracebackType | None, 

4653 ) -> None: 

4654 """Exit context manager.""" 

4655 self.close() 

4656 

4657 def __eq__(self, other: object) -> bool: 

4658 """Check equality with another pack.""" 

4659 if not isinstance(other, Pack): 

4660 return False 

4661 return self.index == other.index 

4662 

4663 def __len__(self) -> int: 

4664 """Number of entries in this pack.""" 

4665 return len(self.index) 

4666 

4667 def __repr__(self) -> str: 

4668 """Return string representation of this pack.""" 

4669 return f"{self.__class__.__name__}({self._basename!r})" 

4670 

4671 def __iter__(self) -> Iterator[ObjectID]: 

4672 """Iterate over all the sha1s of the objects in this pack.""" 

4673 return iter(self.index) 

4674 

4675 def check_length_and_checksum(self) -> None: 

4676 """Sanity check the length and checksum of the pack index and data.""" 

4677 assert len(self.index) == len(self.data), ( 

4678 f"Length mismatch: {len(self.index)} (index) != {len(self.data)} (data)" 

4679 ) 

4680 idx_stored_checksum = self.index.get_pack_checksum() 

4681 data_stored_checksum = self.data.get_stored_checksum() 

4682 if ( 

4683 idx_stored_checksum is not None 

4684 and idx_stored_checksum != data_stored_checksum 

4685 ): 

4686 raise ChecksumMismatch( 

4687 sha_to_hex(RawObjectID(idx_stored_checksum)), 

4688 sha_to_hex(RawObjectID(data_stored_checksum)), 

4689 ) 

4690 

4691 def check(self) -> None: 

4692 """Check the integrity of this pack. 

4693 

4694 Raises: 

4695 ChecksumMismatch: if a checksum for the index or data is wrong 

4696 """ 

4697 self.index.check() 

4698 self.data.check() 

4699 for obj in self.iterobjects(): 

4700 obj.check() 

4701 # TODO: object connectivity checks 

4702 

4703 def get_stored_checksum(self) -> bytes: 

4704 """Return the stored checksum of the pack data.""" 

4705 return self.data.get_stored_checksum() 

4706 

4707 def pack_tuples(self) -> list[tuple[ShaFile, None]]: 

4708 """Return pack tuples for all objects in pack.""" 

4709 return [(o, None) for o in self.iterobjects()] 

4710 

4711 def __contains__(self, sha1: ObjectID | RawObjectID) -> bool: 

4712 """Check whether this pack contains a particular SHA1.""" 

4713 try: 

4714 self.index.object_offset(sha1) 

4715 return True 

4716 except KeyError: 

4717 return False 

4718 

4719 def get_raw(self, sha1: RawObjectID | ObjectID) -> tuple[int, bytes]: 

4720 """Get raw object data by SHA1.""" 

4721 offset = self.index.object_offset(sha1) 

4722 obj_type, obj = self.data.get_object_at(offset) 

4723 type_num, chunks = self.resolve_object(offset, obj_type, obj) 

4724 return type_num, b"".join(chunks) # type: ignore[arg-type] 

4725 

4726 def __getitem__(self, sha1: "ObjectID | RawObjectID") -> ShaFile: 

4727 """Retrieve the specified SHA1.""" 

4728 type, uncomp = self.get_raw(sha1) 

4729 return ShaFile.from_raw_string(type, uncomp, sha=sha1) 

4730 

4731 def iterobjects(self) -> Iterator[ShaFile]: 

4732 """Iterate over the objects in this pack.""" 

4733 return iter( 

4734 PackInflater.for_pack_data(self.data, resolve_ext_ref=self.resolve_ext_ref) 

4735 ) 

4736 

4737 def iterobjects_subset( 

4738 self, shas: Iterable[ObjectID], *, allow_missing: bool = False 

4739 ) -> Iterator[ShaFile]: 

4740 """Iterate over a subset of objects in this pack.""" 

4741 return ( 

4742 uo 

4743 for uo in PackInflater.for_pack_subset( 

4744 self, 

4745 shas, 

4746 allow_missing=allow_missing, 

4747 resolve_ext_ref=self.resolve_ext_ref, 

4748 ) 

4749 if uo.id in shas 

4750 ) 

4751 

4752 def iter_unpacked_subset( 

4753 self, 

4754 shas: Iterable[ObjectID | RawObjectID], 

4755 *, 

4756 include_comp: bool = False, 

4757 allow_missing: bool = False, 

4758 convert_ofs_delta: bool = False, 

4759 ) -> Iterator[UnpackedObject]: 

4760 """Iterate over unpacked objects in subset.""" 

4761 ofs_pending: dict[int, list[UnpackedObject]] = defaultdict(list) 

4762 ofs: dict[int, bytes] = {} 

4763 todo: set[ObjectID | RawObjectID] = set(shas) 

4764 for unpacked in self.iter_unpacked(include_comp=include_comp): 

4765 sha = unpacked.sha() 

4766 if unpacked.offset is not None: 

4767 ofs[unpacked.offset] = sha 

4768 hexsha = sha_to_hex(RawObjectID(sha)) 

4769 if hexsha in todo: 

4770 if unpacked.pack_type_num == OFS_DELTA: 

4771 assert isinstance(unpacked.delta_base, int) 

4772 assert unpacked.offset is not None 

4773 base_offset = unpacked.offset - unpacked.delta_base 

4774 try: 

4775 unpacked.delta_base = ofs[base_offset] 

4776 except KeyError: 

4777 ofs_pending[base_offset].append(unpacked) 

4778 continue 

4779 else: 

4780 unpacked.pack_type_num = REF_DELTA 

4781 yield unpacked 

4782 todo.remove(hexsha) 

4783 if unpacked.offset is not None: 

4784 for child in ofs_pending.pop(unpacked.offset, []): 

4785 child.pack_type_num = REF_DELTA 

4786 child.delta_base = sha 

4787 yield child 

4788 assert not ofs_pending 

4789 if not allow_missing and todo: 

4790 raise UnresolvedDeltas(list(todo)) 

4791 

4792 def iter_unpacked(self, include_comp: bool = False) -> Iterator[UnpackedObject]: 

4793 """Iterate over all unpacked objects in this pack.""" 

4794 ofs_to_entries = { 

4795 ofs: (sha, crc32) for (sha, ofs, crc32) in self.index.iterentries() 

4796 } 

4797 for unpacked in self.data.iter_unpacked(include_comp=include_comp): 

4798 assert unpacked.offset is not None 

4799 (sha, crc32) = ofs_to_entries[unpacked.offset] 

4800 unpacked._sha = sha 

4801 unpacked.crc32 = crc32 

4802 yield unpacked 

4803 

4804 def keep(self, msg: bytes | None = None) -> str: 

4805 """Add a .keep file for the pack, preventing git from garbage collecting it. 

4806 

4807 Args: 

4808 msg: A message written inside the .keep file; can be used later 

4809 to determine whether or not a .keep file is obsolete. 

4810 Returns: The path of the .keep file, as a string. 

4811 """ 

4812 keepfile_name = f"{self._basename}.keep" 

4813 with GitFile(keepfile_name, "wb") as keepfile: 

4814 if msg: 

4815 keepfile.write(msg) 

4816 keepfile.write(b"\n") 

4817 return keepfile_name 

4818 

4819 def unkeep(self) -> bool: 

4820 """Remove the .keep file for the pack, allowing git to garbage collect it. 

4821 

4822 This is the counterpart of :meth:`keep`. It is not an error to call 

4823 this on a pack that has no .keep file. 

4824 

4825 Returns: True if a .keep file was removed, False if there was none. 

4826 """ 

4827 try: 

4828 os.unlink(f"{self._basename}.keep") 

4829 except FileNotFoundError: 

4830 return False 

4831 return True 

4832 

4833 def get_ref( 

4834 self, sha: RawObjectID | ObjectID 

4835 ) -> tuple[int | None, int, OldUnpackedObject]: 

4836 """Get the object for a ref SHA, only looking in this pack.""" 

4837 # TODO: cache these results 

4838 try: 

4839 offset = self.index.object_offset(sha) 

4840 except KeyError: 

4841 offset = None 

4842 if offset: 

4843 type, obj = self.data.get_object_at(offset) 

4844 elif self.resolve_ext_ref: 

4845 type, obj = self.resolve_ext_ref(sha) 

4846 else: 

4847 raise KeyError(sha) 

4848 return offset, type, obj 

4849 

4850 def resolve_object( 

4851 self, 

4852 offset: int, 

4853 type: int, 

4854 obj: OldUnpackedObject, 

4855 get_ref: Callable[ 

4856 [RawObjectID | ObjectID], tuple[int | None, int, OldUnpackedObject] 

4857 ] 

4858 | None = None, 

4859 ) -> tuple[int, OldUnpackedObject]: 

4860 """Resolve an object, possibly resolving deltas when necessary. 

4861 

4862 Returns: Tuple with object type and contents. 

4863 """ 

4864 # Walk down the delta chain, building a stack of deltas to reach 

4865 # the requested object. 

4866 base_offset: int | None = offset 

4867 base_type = type 

4868 base_obj = obj 

4869 delta_stack = [] 

4870 seen_ref_offsets: set[int] = {offset} 

4871 while base_type in DELTA_TYPES: 

4872 prev_offset = base_offset 

4873 if get_ref is None: 

4874 get_ref = self.get_ref 

4875 assert isinstance(base_obj, tuple), ( 

4876 f"Expected delta tuple, got {base_obj.__class__.__name__}" 

4877 ) 

4878 if base_type == OFS_DELTA: 

4879 (delta_offset, delta) = base_obj 

4880 # TODO: clean up asserts and replace with nicer error messages 

4881 assert isinstance(delta_offset, int), ( 

4882 f"Expected int, got {delta_offset.__class__}" 

4883 ) 

4884 assert base_offset is not None 

4885 base_offset = base_offset - delta_offset 

4886 base_type, base_obj = self.data.get_object_at(base_offset) 

4887 assert isinstance(base_type, int) 

4888 elif base_type == REF_DELTA: 

4889 (basename, delta) = base_obj 

4890 assert ( 

4891 isinstance(basename, bytes) 

4892 and len(basename) == self.object_format.oid_length 

4893 ) 

4894 base_offset_temp, base_type, base_obj = get_ref(RawObjectID(basename)) 

4895 assert isinstance(base_type, int) 

4896 # base_offset_temp can be None for thin packs (external references) 

4897 base_offset = base_offset_temp 

4898 if base_offset == prev_offset: # object is based on itself 

4899 raise UnresolvedDeltas([basename]) 

4900 # A repeated REF_DELTA base offset means the chain cycles; 

4901 # without this check the loop would never terminate. 

4902 if base_offset is not None: 

4903 if base_offset in seen_ref_offsets: 

4904 raise DeltaCycle([basename]) 

4905 seen_ref_offsets.add(base_offset) 

4906 else: 

4907 raise AssertionError(f"Unexpected delta type: {base_type}") 

4908 delta_stack.append((prev_offset, base_type, delta)) 

4909 

4910 # Now grab the base object (mustn't be a delta) and apply the 

4911 # deltas all the way up the stack. 

4912 chunks = base_obj 

4913 for prev_offset, _delta_type, delta in reversed(delta_stack): 

4914 # Convert chunks to bytes for apply_delta if needed 

4915 if isinstance(chunks, list): 

4916 chunks_bytes = b"".join(chunks) 

4917 elif isinstance(chunks, tuple): 

4918 # For tuple type, second element is the actual data 

4919 _, chunk_data = chunks 

4920 if isinstance(chunk_data, list): 

4921 chunks_bytes = b"".join(chunk_data) 

4922 else: 

4923 chunks_bytes = chunk_data 

4924 else: 

4925 chunks_bytes = chunks 

4926 

4927 # Apply delta and get result as list 

4928 chunks = apply_delta(chunks_bytes, delta) 

4929 

4930 if prev_offset is not None: 

4931 self.data._cache_object_at(prev_offset, base_type, chunks) 

4932 return base_type, chunks 

4933 

4934 def entries( 

4935 self, progress: Callable[[int, int], None] | None = None 

4936 ) -> Iterator[PackIndexEntry]: 

4937 """Yield entries summarizing the contents of this pack. 

4938 

4939 Args: 

4940 progress: Progress function, called with current and total 

4941 object count. 

4942 Returns: iterator of tuples with (sha, offset, crc32) 

4943 """ 

4944 return self.data.iterentries( 

4945 progress=progress, resolve_ext_ref=self.resolve_ext_ref 

4946 ) 

4947 

4948 def sorted_entries( 

4949 self, progress: Callable[[int, int], None] | None = None 

4950 ) -> Iterator[PackIndexEntry]: 

4951 """Return entries in this pack, sorted by SHA. 

4952 

4953 Args: 

4954 progress: Progress function, called with current and total 

4955 object count 

4956 Returns: Iterator of tuples with (sha, offset, crc32) 

4957 """ 

4958 return iter( 

4959 self.data.sorted_entries( 

4960 progress=progress, resolve_ext_ref=self.resolve_ext_ref 

4961 ) 

4962 ) 

4963 

4964 def get_unpacked_object( 

4965 self, 

4966 sha: ObjectID | RawObjectID, 

4967 *, 

4968 include_comp: bool = False, 

4969 convert_ofs_delta: bool = True, 

4970 ) -> UnpackedObject: 

4971 """Get the unpacked object for a sha. 

4972 

4973 Args: 

4974 sha: SHA of object to fetch 

4975 include_comp: Whether to include compression data in UnpackedObject 

4976 convert_ofs_delta: Whether to convert offset deltas to ref deltas 

4977 """ 

4978 offset = self.index.object_offset(sha) 

4979 unpacked = self.data.get_unpacked_object_at(offset, include_comp=include_comp) 

4980 if unpacked.pack_type_num == OFS_DELTA and convert_ofs_delta: 

4981 assert isinstance(unpacked.delta_base, int) 

4982 unpacked.delta_base = self.index.object_sha1(offset - unpacked.delta_base) 

4983 unpacked.pack_type_num = REF_DELTA 

4984 return unpacked 

4985 

4986 

4987def extend_pack( 

4988 f: BinaryIO, 

4989 object_ids: Set["RawObjectID"], 

4990 get_raw: Callable[["RawObjectID | ObjectID"], tuple[int, bytes]], 

4991 object_format: "ObjectFormat", 

4992 *, 

4993 compression_level: int = -1, 

4994 progress: Callable[[bytes], None] | None = None, 

4995) -> tuple[bytes, list[tuple[RawObjectID, int, int]]]: 

4996 """Extend a pack file with more objects. 

4997 

4998 The caller should make sure that object_ids does not contain any objects 

4999 that are already in the pack 

5000 """ 

5001 # Update the header with the new number of objects. 

5002 f.seek(0) 

5003 _version, num_objects = read_pack_header(f.read) 

5004 

5005 if object_ids: 

5006 f.seek(0) 

5007 write_pack_header(f.write, num_objects + len(object_ids)) 

5008 

5009 # Must flush before reading (http://bugs.python.org/issue3207) 

5010 f.flush() 

5011 

5012 # Rescan the rest of the pack, computing the SHA with the new header. 

5013 new_sha = compute_file_sha( 

5014 f, hash_func=object_format.hash_func, end_ofs=-object_format.oid_length 

5015 ) 

5016 

5017 # Must reposition before writing (http://bugs.python.org/issue3207) 

5018 f.seek(0, os.SEEK_CUR) 

5019 

5020 extra_entries = [] 

5021 

5022 # Complete the pack. 

5023 for i, object_id in enumerate(object_ids): 

5024 if progress is not None: 

5025 progress( 

5026 (f"writing extra base objects: {i}/{len(object_ids)}\r").encode("ascii") 

5027 ) 

5028 assert len(object_id) == object_format.oid_length 

5029 type_num, data = get_raw(object_id) 

5030 offset = f.tell() 

5031 crc32 = write_pack_object( 

5032 f.write, 

5033 type_num, 

5034 [data], # Convert bytes to list[bytes] 

5035 sha=new_sha, 

5036 compression_level=compression_level, 

5037 object_format=object_format, 

5038 ) 

5039 extra_entries.append((object_id, offset, crc32)) 

5040 pack_sha = new_sha.digest() 

5041 f.write(pack_sha) 

5042 return pack_sha, extra_entries 

5043 

5044 

5045try: 

5046 from dulwich._pack import ( # type: ignore 

5047 apply_delta, 

5048 bisect_find_sha, 

5049 ) 

5050except ImportError: 

5051 pass 

5052 

5053# Try to import the Rust version of create_delta 

5054try: 

5055 from dulwich._pack import create_delta as _create_delta_rs 

5056except ImportError: 

5057 pass 

5058else: 

5059 # Wrap the Rust version to match the Python API (returns bytes instead of Iterator) 

5060 def _create_delta_rs_wrapper( 

5061 base_buf: bytes | list[bytes], target_buf: bytes | list[bytes] 

5062 ) -> Iterator[bytes]: 

5063 """Wrapper for Rust create_delta to match Python API.""" 

5064 if isinstance(base_buf, list): 

5065 base_buf = b"".join(base_buf) 

5066 if isinstance(target_buf, list): 

5067 target_buf = b"".join(target_buf) 

5068 yield _create_delta_rs(base_buf, target_buf) 

5069 

5070 create_delta = _create_delta_rs_wrapper