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

1878 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_file_sha", 

72 "deltas_from_sorted_objects", 

73 "deltify_pack_objects", 

74 "extend_pack", 

75 "find_reusable_deltas", 

76 "full_unpacked_object", 

77 "generate_unpacked_objects", 

78 "iter_sha1", 

79 "load_pack_index", 

80 "load_pack_index_file", 

81 "obj_sha", 

82 "pack_header_chunks", 

83 "pack_object_chunks", 

84 "pack_object_header", 

85 "pack_objects_to_data", 

86 "read_pack_header", 

87 "read_zlib_chunks", 

88 "sort_objects_for_delta", 

89 "take_msb_bytes", 

90 "unpack_object", 

91 "verify_and_read", 

92 "write_pack", 

93 "write_pack_data", 

94 "write_pack_from_container", 

95 "write_pack_header", 

96 "write_pack_index", 

97 "write_pack_object", 

98 "write_pack_objects", 

99] 

100 

101import binascii 

102from collections import defaultdict, deque 

103from contextlib import suppress 

104from io import BytesIO, UnsupportedOperation 

105 

106try: 

107 from cdifflib import CSequenceMatcher as SequenceMatcher 

108except ModuleNotFoundError: 

109 from difflib import SequenceMatcher 

110 

111import logging 

112import os 

113import struct 

114import sys 

115import warnings 

116import zlib 

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

118from hashlib import sha1, sha256 

119from itertools import chain 

120from os import SEEK_CUR, SEEK_END 

121from struct import unpack_from 

122from types import TracebackType 

123from typing import ( 

124 IO, 

125 TYPE_CHECKING, 

126 Any, 

127 BinaryIO, 

128 Generic, 

129 Protocol, 

130 TypeVar, 

131) 

132 

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

134 from typing import Self 

135else: 

136 from typing_extensions import Self 

137 

138import mmap 

139 

140from .errors import ApplyDeltaError, ChecksumMismatch 

141from .file import GitFile, _GitFile 

142from .lru_cache import LRUSizeCache 

143from .object_format import OBJECT_FORMAT_TYPE_NUMS, SHA1, ObjectFormat 

144from .objects import ( 

145 ObjectID, 

146 RawObjectID, 

147 ShaFile, 

148 hex_to_sha, 

149 object_header, 

150 sha_to_hex, 

151) 

152 

153if TYPE_CHECKING: 

154 from _hashlib import HASH as HashObject 

155 

156 from .bitmap import PackBitmap 

157 from .commit_graph import CommitGraph 

158 from .object_store import BaseObjectStore 

159 from .refs import Ref 

160 

161logger = logging.getLogger(__name__) 

162 

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

164has_mmap = sys.platform != "Plan9" 

165 

166OFS_DELTA = 6 

167REF_DELTA = 7 

168 

169DELTA_TYPES = (OFS_DELTA, REF_DELTA) 

170 

171 

172DEFAULT_PACK_DELTA_WINDOW_SIZE = 10 

173 

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

175PACK_SPOOL_FILE_MAX_SIZE = 16 * 1024 * 1024 

176 

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

178DEFAULT_PACK_INDEX_VERSION = 2 

179 

180 

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

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

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

184PackHint = tuple[int, bytes | None] 

185 

186 

187def verify_and_read( 

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

189 expected_hash: bytes, 

190 hash_algo: str, 

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

192) -> Iterator[bytes]: 

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

194 

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

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

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

198 corrupted or malicious data from reaching the caller. 

199 

200 Args: 

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

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

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

204 progress: Optional progress callback 

205 

206 Yields: 

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

208 

209 Raises: 

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

211 """ 

212 from tempfile import SpooledTemporaryFile 

213 

214 from .object_format import OBJECT_FORMATS 

215 

216 # Get the hash function for this algorithm 

217 obj_format = OBJECT_FORMATS.get(hash_algo) 

218 if obj_format is None: 

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

220 

221 hasher = obj_format.new_hash() 

222 

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

224 with SpooledTemporaryFile( 

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

226 ) as temp_file: 

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

228 while True: 

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

230 if not chunk: 

231 break 

232 hasher.update(chunk) 

233 temp_file.write(chunk) 

234 

235 # Verify hash BEFORE yielding any data 

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

237 if computed_hash != expected_hash: 

238 raise ValueError( 

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

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

241 ) 

242 

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

244 if progress: 

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

246 

247 temp_file.seek(0) 

248 while True: 

249 chunk = temp_file.read(65536) 

250 if not chunk: 

251 break 

252 yield chunk 

253 

254 

255class UnresolvedDeltas(Exception): 

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

257 

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

259 """Initialize UnresolvedDeltas exception. 

260 

261 Args: 

262 shas: List of SHA hashes for unresolved delta objects 

263 """ 

264 self.shas = shas 

265 

266 

267class ObjectContainer(Protocol): 

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

269 

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

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

272 

273 def add_objects( 

274 self, 

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

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

277 ) -> "Pack | None": 

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

279 

280 Args: 

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

282 progress: Progress callback for object insertion 

283 Returns: Optional Pack object of the objects written. 

284 """ 

285 

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

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

288 ... 

289 

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

291 """Retrieve an object.""" 

292 ... 

293 

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

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

296 

297 Returns: 

298 CommitGraph object if available, None otherwise 

299 """ 

300 return None 

301 

302 

303class PackedObjectContainer(ObjectContainer): 

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

305 

306 def get_unpacked_object( 

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

308 ) -> "UnpackedObject": 

309 """Get a raw unresolved object. 

310 

311 Args: 

312 sha1: SHA-1 hash of the object 

313 include_comp: Whether to include compressed data 

314 

315 Returns: 

316 UnpackedObject instance 

317 """ 

318 raise NotImplementedError(self.get_unpacked_object) 

319 

320 def iterobjects_subset( 

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

322 ) -> Iterator[ShaFile]: 

323 """Iterate over a subset of objects. 

324 

325 Args: 

326 shas: Iterable of object SHAs to retrieve 

327 allow_missing: If True, skip missing objects 

328 

329 Returns: 

330 Iterator of ShaFile objects 

331 """ 

332 raise NotImplementedError(self.iterobjects_subset) 

333 

334 def iter_unpacked_subset( 

335 self, 

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

337 *, 

338 include_comp: bool = False, 

339 allow_missing: bool = False, 

340 convert_ofs_delta: bool = True, 

341 ) -> Iterator["UnpackedObject"]: 

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

343 

344 Args: 

345 shas: Set of object SHAs to retrieve 

346 include_comp: Include compressed data if True 

347 allow_missing: If True, skip missing objects 

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

349 

350 Returns: 

351 Iterator of UnpackedObject instances 

352 """ 

353 raise NotImplementedError(self.iter_unpacked_subset) 

354 

355 

356class UnpackedObjectStream: 

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

358 

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

360 """Iterate over unpacked objects.""" 

361 raise NotImplementedError(self.__iter__) 

362 

363 def __len__(self) -> int: 

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

365 raise NotImplementedError(self.__len__) 

366 

367 

368def take_msb_bytes( 

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

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

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

372 

373 Args: 

374 read: Read function 

375 crc32: Optional CRC32 checksum to update 

376 

377 Returns: 

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

379 """ 

380 ret: list[int] = [] 

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

382 b = read(1) 

383 if crc32 is not None: 

384 crc32 = binascii.crc32(b, crc32) 

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

386 return ret, crc32 

387 

388 

389class PackFileDisappeared(Exception): 

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

391 

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

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

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

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

396 

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

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

399 from its cache and rescan the pack directory. 

400 """ 

401 

402 obj: "Pack | FilePackIndex" 

403 

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

405 """Initialize PackFileDisappeared exception. 

406 

407 Args: 

408 obj: The pack or pack index that disappeared. 

409 """ 

410 self.obj = obj 

411 

412 

413class UnpackedObject: 

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

415 

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

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

418 read_zlib_chunks, unpack_object, DeltaChainIterator, etc. 

419 

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

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

422 """ 

423 

424 __slots__ = [ 

425 "_sha", # Cached binary SHA. 

426 "comp_chunks", # Compressed object chunks. 

427 "crc32", # CRC32. 

428 "decomp_chunks", # Decompressed object chunks. 

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

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

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

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

433 "obj_type_num", # Type of this object. 

434 "offset", # Offset in its pack. 

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

436 ] 

437 

438 obj_type_num: int | None 

439 obj_chunks: list[bytes] | None 

440 delta_base: None | bytes | int 

441 decomp_chunks: list[bytes] 

442 comp_chunks: list[bytes] | None 

443 decomp_len: int | None 

444 crc32: int | None 

445 offset: int | None 

446 pack_type_num: int 

447 _sha: bytes | None 

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

449 

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

451 # methods of this object. 

452 def __init__( 

453 self, 

454 pack_type_num: int, 

455 *, 

456 delta_base: None | bytes | int = None, 

457 decomp_len: int | None = None, 

458 crc32: int | None = None, 

459 sha: bytes | None = None, 

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

461 offset: int | None = None, 

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

463 ) -> None: 

464 """Initialize an UnpackedObject. 

465 

466 Args: 

467 pack_type_num: Type number of this object in the pack 

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

469 decomp_len: Decompressed length of this object 

470 crc32: CRC32 checksum 

471 sha: SHA hash of the object 

472 decomp_chunks: Decompressed chunks 

473 offset: Offset in the pack file 

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

475 """ 

476 self.offset = offset 

477 self._sha = sha 

478 self.pack_type_num = pack_type_num 

479 self.delta_base = delta_base 

480 self.comp_chunks = None 

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

482 if decomp_chunks is not None and decomp_len is None: 

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

484 else: 

485 self.decomp_len = decomp_len 

486 self.crc32 = crc32 

487 self.hash_func = hash_func 

488 

489 if pack_type_num in DELTA_TYPES: 

490 self.obj_type_num = None 

491 self.obj_chunks = None 

492 else: 

493 self.obj_type_num = pack_type_num 

494 self.obj_chunks = self.decomp_chunks 

495 self.delta_base = delta_base 

496 

497 def sha(self) -> RawObjectID: 

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

499 if self._sha is None: 

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

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

502 return RawObjectID(self._sha) 

503 

504 def sha_file(self) -> ShaFile: 

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

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

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

508 

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

510 # chunks or a delta tuple. 

511 def _obj(self) -> OldUnpackedObject: 

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

513 if self.pack_type_num in DELTA_TYPES: 

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

515 return (self.delta_base, self.decomp_chunks) 

516 else: 

517 return self.decomp_chunks 

518 

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

520 """Check equality with another UnpackedObject.""" 

521 if not isinstance(other, UnpackedObject): 

522 return False 

523 for slot in self.__slots__: 

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

525 return False 

526 return True 

527 

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

529 """Check inequality with another UnpackedObject.""" 

530 return not (self == other) 

531 

532 def __repr__(self) -> str: 

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

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

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

536 

537 

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

539 

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

541# for core.deltaBaseCacheLimit). 

542DEFAULT_DELTA_BASE_CACHE_LIMIT = 96 * 1024 * 1024 # 96 MiB 

543 

544 

545def read_zlib_chunks( 

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

547 unpacked: UnpackedObject, 

548 include_comp: bool = False, 

549 buffer_size: int = _ZLIB_BUFSIZE, 

550) -> bytes: 

551 """Read zlib data from a buffer. 

552 

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

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

555 

556 Args: 

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

558 return less than the requested size. 

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

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

561 using this starting CRC32. 

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

563 * comp_chunks (if include_comp is True) 

564 * decomp_chunks 

565 * decomp_len 

566 * crc32 

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

568 buffer_size: Size of the read buffer. 

569 Returns: Leftover unused data from the decompression. 

570 

571 Raises: 

572 zlib.error: if a decompression error occurred. 

573 """ 

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

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

576 decomp_obj = zlib.decompressobj() 

577 

578 comp_chunks = [] 

579 decomp_chunks = unpacked.decomp_chunks 

580 decomp_len = 0 

581 crc32 = unpacked.crc32 

582 max_decomp = unpacked.decomp_len 

583 

584 while True: 

585 add = read_some(buffer_size) 

586 if not add: 

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

588 comp_chunks.append(add) 

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

590 remaining = max_decomp - decomp_len + 1 

591 decomp = decomp_obj.decompress(add, remaining) 

592 if decomp_obj.unconsumed_tail: 

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

594 decomp_len += len(decomp) 

595 decomp_chunks.append(decomp) 

596 unused = decomp_obj.unused_data 

597 if unused: 

598 left = len(unused) 

599 if crc32 is not None: 

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

601 if include_comp: 

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

603 break 

604 elif crc32 is not None: 

605 crc32 = binascii.crc32(add, crc32) 

606 if crc32 is not None: 

607 crc32 &= 0xFFFFFFFF 

608 

609 if decomp_len != unpacked.decomp_len: 

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

611 

612 unpacked.crc32 = crc32 

613 if include_comp: 

614 unpacked.comp_chunks = comp_chunks 

615 return unused 

616 

617 

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

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

620 

621 Args: 

622 iter: Iterator over string objects 

623 Returns: 40-byte hex sha1 digest 

624 """ 

625 sha = sha1() 

626 for name in iter: 

627 sha.update(name) 

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

629 

630 

631def load_pack_index( 

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

633) -> "PackIndex": 

634 """Load an index file by path. 

635 

636 Args: 

637 path: Path to the index file 

638 object_format: Hash algorithm used by the repository 

639 Returns: A PackIndex loaded from the given path 

640 """ 

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

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

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

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

645 f = GitFile(path, "rb") 

646 try: 

647 return load_pack_index_file(path, f, object_format) 

648 except BaseException: 

649 f.close() 

650 raise 

651 

652 

653def _load_file_contents( 

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

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

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

657 

658 Args: 

659 f: File-like object to load 

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

661 Returns: Tuple of (contents, size) 

662 """ 

663 try: 

664 fd = f.fileno() 

665 except (UnsupportedOperation, AttributeError): 

666 fd = None 

667 # Attempt to use mmap if possible 

668 if fd is not None: 

669 if size is None: 

670 size = os.fstat(fd).st_size 

671 if has_mmap: 

672 try: 

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

674 except (OSError, ValueError): 

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

676 pass 

677 else: 

678 return contents, size 

679 contents_bytes = f.read() 

680 size = len(contents_bytes) 

681 return contents_bytes, size 

682 

683 

684def load_pack_index_file( 

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

686 f: IO[bytes] | _GitFile, 

687 object_format: ObjectFormat, 

688) -> "PackIndex": 

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

690 

691 Args: 

692 path: Path for the index file 

693 f: File-like object 

694 object_format: Hash algorithm used by the repository 

695 Returns: A PackIndex loaded from the given file 

696 """ 

697 contents, size = _load_file_contents(f) 

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

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

700 if version == 2: 

701 return PackIndex2( 

702 path, 

703 object_format, 

704 file=f, 

705 contents=contents, 

706 size=size, 

707 ) 

708 elif version == 3: 

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

710 else: 

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

712 else: 

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

714 

715 

716def bisect_find_sha( 

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

718) -> int | None: 

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

720 

721 Args: 

722 start: Start index of range to search 

723 end: End index of range to search 

724 sha: Sha to find 

725 unpack_name: Callback to retrieve SHA by index 

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

727 """ 

728 assert start <= end 

729 while start <= end: 

730 i = (start + end) // 2 

731 file_sha = unpack_name(i) 

732 if file_sha < sha: 

733 start = i + 1 

734 elif file_sha > sha: 

735 end = i - 1 

736 else: 

737 return i 

738 return None 

739 

740 

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

742 

743 

744class PackIndex: 

745 """An index in to a packfile. 

746 

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

748 packfile of that object if it has it. 

749 """ 

750 

751 object_format: "ObjectFormat" 

752 

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

754 """Check equality with another PackIndex.""" 

755 if not isinstance(other, PackIndex): 

756 return False 

757 

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

759 self.iterentries(), other.iterentries() 

760 ): 

761 if name1 != name2: 

762 return False 

763 return True 

764 

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

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

767 return not self.__eq__(other) 

768 

769 def __len__(self) -> int: 

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

771 raise NotImplementedError(self.__len__) 

772 

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

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

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

776 

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

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

779 

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

781 crc32 checksum. 

782 """ 

783 raise NotImplementedError(self.iterentries) 

784 

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

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

787 

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

789 """ 

790 raise NotImplementedError(self.get_pack_checksum) 

791 

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

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

794 

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

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

797 have the object then None will be returned. 

798 """ 

799 raise NotImplementedError(self.object_offset) 

800 

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

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

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

804 if offset == index: 

805 return name 

806 else: 

807 raise KeyError(index) 

808 

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

810 """See object_offset. 

811 

812 Args: 

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

814 """ 

815 raise NotImplementedError(self._object_offset) 

816 

817 def objects_sha1(self) -> bytes: 

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

819 

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

821 """ 

822 return iter_sha1(self._itersha()) 

823 

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

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

826 raise NotImplementedError(self._itersha) 

827 

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

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

830 

831 Args: 

832 prefix: Binary prefix to match 

833 Returns: Iterator of matching SHA1s 

834 """ 

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

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

837 if sha.startswith(prefix): 

838 yield RawObjectID(sha) 

839 

840 def close(self) -> None: 

841 """Close any open files.""" 

842 

843 def check(self) -> None: 

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

845 

846 

847class MemoryPackIndex(PackIndex): 

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

849 

850 def __init__( 

851 self, 

852 entries: list[PackIndexEntry], 

853 object_format: ObjectFormat, 

854 pack_checksum: bytes | None = None, 

855 ) -> None: 

856 """Create a new MemoryPackIndex. 

857 

858 Args: 

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

860 object_format: Object format used by this index 

861 pack_checksum: Optional pack checksum 

862 """ 

863 self._by_sha = {} 

864 self._by_offset = {} 

865 for name, offset, _crc32 in entries: 

866 self._by_sha[name] = offset 

867 self._by_offset[offset] = name 

868 self._entries = entries 

869 self._pack_checksum = pack_checksum 

870 self.object_format = object_format 

871 

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

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

874 return self._pack_checksum 

875 

876 def __len__(self) -> int: 

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

878 return len(self._entries) 

879 

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

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

882 

883 Args: 

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

885 Returns: Offset in the pack file 

886 """ 

887 lookup_sha: RawObjectID 

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

889 lookup_sha = hex_to_sha(ObjectID(sha)) 

890 else: 

891 lookup_sha = RawObjectID(sha) 

892 return self._by_sha[lookup_sha] 

893 

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

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

896 return self._by_offset[index] 

897 

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

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

900 return iter(self._by_sha) 

901 

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

903 """Iterate over all index entries.""" 

904 return iter(self._entries) 

905 

906 @classmethod 

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

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

909 return MemoryPackIndex( 

910 list(pack_data.sorted_entries()), 

911 pack_checksum=pack_data.get_stored_checksum(), 

912 object_format=pack_data.object_format, 

913 ) 

914 

915 @classmethod 

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

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

918 return cls( 

919 list(other_index.iterentries()), 

920 other_index.object_format, 

921 other_index.get_pack_checksum(), 

922 ) 

923 

924 

925class FilePackIndex(PackIndex): 

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

927 

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

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

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

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

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

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

934 present. 

935 """ 

936 

937 _fan_out_table: list[int] 

938 _file: IO[bytes] | _GitFile 

939 

940 def __init__( 

941 self, 

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

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

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

945 size: int | None = None, 

946 ) -> None: 

947 """Create a pack index object. 

948 

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

950 it whenever required. 

951 """ 

952 self._filename = filename 

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

954 # ensure that it hasn't changed. 

955 if file is None: 

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

957 else: 

958 self._file = file 

959 if contents is None: 

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

961 else: 

962 self._contents = contents 

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

964 

965 @property 

966 def path(self) -> str: 

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

968 return os.fspath(self._filename) 

969 

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

971 """Check equality with another FilePackIndex.""" 

972 # Quick optimization: 

973 if ( 

974 isinstance(other, FilePackIndex) 

975 and self._fan_out_table != other._fan_out_table 

976 ): 

977 return False 

978 

979 return super().__eq__(other) 

980 

981 def close(self) -> None: 

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

983 # Close the mmap before the file: on Windows the mapping holds a lock 

984 # on the file, so the handle cannot be released while it is alive. 

985 close_fn = getattr(self._contents, "close", None) 

986 if close_fn is not None: 

987 close_fn() 

988 self._file.close() 

989 

990 def __del__(self) -> None: 

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

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

993 import warnings 

994 

995 warnings.warn( 

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

997 ResourceWarning, 

998 stacklevel=2, 

999 source=self, 

1000 ) 

1001 try: 

1002 self.close() 

1003 except Exception: 

1004 # Ignore errors during cleanup 

1005 pass 

1006 

1007 def __enter__(self) -> Self: 

1008 """Enter context manager.""" 

1009 return self 

1010 

1011 def __exit__( 

1012 self, 

1013 type: type | None, 

1014 value: BaseException | None, 

1015 traceback: TracebackType | None, 

1016 ) -> None: 

1017 """Exit context manager.""" 

1018 self.close() 

1019 

1020 def __len__(self) -> int: 

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

1022 return self._fan_out_table[-1] 

1023 

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

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

1026 

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

1028 checksum (if known). 

1029 """ 

1030 raise NotImplementedError(self._unpack_entry) 

1031 

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

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

1034 raise NotImplementedError(self._unpack_name) 

1035 

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

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

1038 raise NotImplementedError(self._unpack_offset) 

1039 

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

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

1042 raise NotImplementedError(self._unpack_crc32_checksum) 

1043 

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

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

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

1047 yield self._unpack_name(i) 

1048 

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

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

1051 

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

1053 crc32 checksum. 

1054 """ 

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

1056 yield self._unpack_entry(i) 

1057 

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

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

1060 

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

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

1063 

1064 Args: 

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

1066 Returns: List of 256 integers 

1067 """ 

1068 ret = [] 

1069 for i in range(0x100): 

1070 fanout_entry = self._contents[ 

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

1072 ] 

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

1074 return ret 

1075 

1076 def check(self) -> None: 

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

1078 actual = self.calculate_checksum() 

1079 stored = self.get_stored_checksum() 

1080 if actual != stored: 

1081 raise ChecksumMismatch(stored, actual) 

1082 

1083 def calculate_checksum(self) -> bytes: 

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

1085 

1086 Returns: This is a 20-byte binary digest 

1087 """ 

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

1089 

1090 def get_pack_checksum(self) -> bytes: 

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

1092 

1093 Returns: 20-byte binary digest 

1094 """ 

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

1096 

1097 def get_stored_checksum(self) -> bytes: 

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

1099 

1100 Returns: 20-byte binary digest 

1101 """ 

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

1103 

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

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

1106 

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

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

1109 have the object then None will be returned. 

1110 """ 

1111 lookup_sha: RawObjectID 

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

1113 lookup_sha = hex_to_sha(ObjectID(sha)) 

1114 else: 

1115 lookup_sha = RawObjectID(sha) 

1116 try: 

1117 return self._object_offset(lookup_sha) 

1118 except ValueError as exc: 

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

1120 if closed in (None, True): 

1121 raise PackFileDisappeared(self) from exc 

1122 raise 

1123 

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

1125 """See object_offset. 

1126 

1127 Args: 

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

1129 """ 

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

1131 assert len(sha) == hash_size 

1132 idx = ord(sha[:1]) 

1133 if idx == 0: 

1134 start = 0 

1135 else: 

1136 start = self._fan_out_table[idx - 1] 

1137 end = self._fan_out_table[idx] 

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

1139 if i is None: 

1140 raise KeyError(sha) 

1141 return self._unpack_offset(i) 

1142 

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

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

1145 start = ord(prefix[:1]) 

1146 if start == 0: 

1147 start = 0 

1148 else: 

1149 start = self._fan_out_table[start - 1] 

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

1151 if end == 0x100: 

1152 end = len(self) 

1153 else: 

1154 end = self._fan_out_table[end] 

1155 assert start <= end 

1156 started = False 

1157 for i in range(start, end): 

1158 name: bytes = self._unpack_name(i) 

1159 if name.startswith(prefix): 

1160 yield RawObjectID(name) 

1161 started = True 

1162 elif started: 

1163 break 

1164 

1165 

1166class PackIndex1(FilePackIndex): 

1167 """Version 1 Pack Index file.""" 

1168 

1169 object_format = SHA1 

1170 

1171 def __init__( 

1172 self, 

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

1174 object_format: ObjectFormat, 

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

1176 contents: bytes | None = None, 

1177 size: int | None = None, 

1178 ) -> None: 

1179 """Initialize a version 1 pack index. 

1180 

1181 Args: 

1182 filename: Path to the index file 

1183 object_format: Object format used by the repository 

1184 file: Optional file object 

1185 contents: Optional mmap'd contents 

1186 size: Optional size of the index 

1187 """ 

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

1189 

1190 # PackIndex1 only supports SHA1 

1191 if object_format != SHA1: 

1192 raise AssertionError( 

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

1194 ) 

1195 

1196 self.object_format = object_format 

1197 self.version = 1 

1198 self._fan_out_table = self._read_fan_out_table(0) 

1199 self.hash_size = self.object_format.oid_length 

1200 self._entry_size = 4 + self.hash_size 

1201 

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

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

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

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

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

1207 

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

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

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

1211 

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

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

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

1215 

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

1217 # Not stored in v1 index files 

1218 return None 

1219 

1220 

1221class PackIndex2(FilePackIndex): 

1222 """Version 2 Pack Index file.""" 

1223 

1224 object_format = SHA1 

1225 

1226 def __init__( 

1227 self, 

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

1229 object_format: ObjectFormat, 

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

1231 contents: bytes | None = None, 

1232 size: int | None = None, 

1233 ) -> None: 

1234 """Initialize a version 2 pack index. 

1235 

1236 Args: 

1237 filename: Path to the index file 

1238 object_format: Object format used by the repository 

1239 file: Optional file object 

1240 contents: Optional mmap'd contents 

1241 size: Optional size of the index 

1242 """ 

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

1244 self.object_format = object_format 

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

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

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

1248 if self.version != 2: 

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

1250 self._fan_out_table = self._read_fan_out_table(8) 

1251 self.hash_size = self.object_format.oid_length 

1252 self._name_table_offset = 8 + 0x100 * 4 

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

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

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

1256 self 

1257 ) 

1258 

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

1260 return ( 

1261 RawObjectID(self._unpack_name(i)), 

1262 self._unpack_offset(i), 

1263 self._unpack_crc32_checksum(i), 

1264 ) 

1265 

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

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

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

1269 

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

1271 offset = self._pack_offset_table_offset + i * 4 

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

1273 if offset_val & (2**31): 

1274 offset = ( 

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

1276 ) 

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

1278 return offset_val 

1279 

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

1281 return int( 

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

1283 ) 

1284 

1285 def get_pack_checksum(self) -> bytes: 

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

1287 

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

1289 """ 

1290 # Index ends with: pack_checksum + index_checksum 

1291 # Each checksum is hash_size bytes 

1292 checksum_size = self.hash_size 

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

1294 

1295 def get_stored_checksum(self) -> bytes: 

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

1297 

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

1299 """ 

1300 checksum_size = self.hash_size 

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

1302 

1303 def calculate_checksum(self) -> bytes: 

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

1305 

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

1307 """ 

1308 # Determine hash function based on hash_size 

1309 if self.hash_size == 20: 

1310 hash_func = sha1 

1311 elif self.hash_size == 32: 

1312 hash_func = sha256 

1313 else: 

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

1315 

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

1317 

1318 

1319class PackIndex3(FilePackIndex): 

1320 """Version 3 Pack Index file. 

1321 

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

1323 """ 

1324 

1325 def __init__( 

1326 self, 

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

1328 object_format: ObjectFormat, 

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

1330 contents: bytes | None = None, 

1331 size: int | None = None, 

1332 ) -> None: 

1333 """Initialize a version 3 pack index. 

1334 

1335 Args: 

1336 filename: Path to the index file 

1337 object_format: Object format used by the repository 

1338 file: Optional file object 

1339 contents: Optional mmap'd contents 

1340 size: Optional size of the index 

1341 """ 

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

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

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

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

1346 if self.version != 3: 

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

1348 

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

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

1351 file_object_format = OBJECT_FORMAT_TYPE_NUMS[self.hash_format] 

1352 

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

1354 if object_format != file_object_format: 

1355 raise AssertionError( 

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

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

1358 ) 

1359 

1360 self.object_format = object_format 

1361 self.hash_size = self.object_format.oid_length 

1362 

1363 # Read length of shortened object names 

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

1365 

1366 # Calculate offsets based on variable hash size 

1367 self._fan_out_table = self._read_fan_out_table( 

1368 16 

1369 ) # After header (4 + 4 + 4 + 4) 

1370 self._name_table_offset = 16 + 0x100 * 4 

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

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

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

1374 self 

1375 ) 

1376 

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

1378 return ( 

1379 RawObjectID(self._unpack_name(i)), 

1380 self._unpack_offset(i), 

1381 self._unpack_crc32_checksum(i), 

1382 ) 

1383 

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

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

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

1387 

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

1389 offset_pos = self._pack_offset_table_offset + i * 4 

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

1391 assert isinstance(offset, int) 

1392 if offset & (2**31): 

1393 large_offset_pos = ( 

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

1395 ) 

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

1397 assert isinstance(offset, int) 

1398 return offset 

1399 

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

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

1402 assert isinstance(result, int) 

1403 return result 

1404 

1405 

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

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

1408 

1409 Args: 

1410 read: Read function 

1411 Returns: Tuple of (pack version, number of objects). If no data is 

1412 available to read, returns (None, None). 

1413 """ 

1414 header = read(12) 

1415 if not header: 

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

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

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

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

1420 if version not in (2, 3): 

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

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

1423 return (version, num_objects) 

1424 

1425 

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

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

1428 

1429 Args: 

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

1431 Returns: Total length in bytes 

1432 """ 

1433 if isinstance(chunks, bytes): 

1434 return len(chunks) 

1435 else: 

1436 return sum(map(len, chunks)) 

1437 

1438 

1439def unpack_object( 

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

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

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

1443 compute_crc32: bool = False, 

1444 include_comp: bool = False, 

1445 zlib_bufsize: int = _ZLIB_BUFSIZE, 

1446) -> tuple[UnpackedObject, bytes]: 

1447 """Unpack a Git object. 

1448 

1449 Args: 

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

1451 bytes are read. 

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

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

1454 return the number of bytes requested. 

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

1456 False, the returned CRC32 will be None. 

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

1458 zlib_bufsize: An optional buffer size for zlib operations. 

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

1460 leftover from decompression, and unpacked in an UnpackedObject with 

1461 the following attrs set: 

1462 

1463 * obj_chunks (for non-delta types) 

1464 * pack_type_num 

1465 * delta_base (for delta types) 

1466 * comp_chunks (if include_comp is True) 

1467 * decomp_chunks 

1468 * decomp_len 

1469 * crc32 (if compute_crc32 is True) 

1470 """ 

1471 if read_some is None: 

1472 read_some = read_all 

1473 if compute_crc32: 

1474 crc32 = 0 

1475 else: 

1476 crc32 = None 

1477 

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

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

1480 size = raw[0] & 0x0F 

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

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

1483 

1484 delta_base: int | bytes | None 

1485 raw_base = len(raw) 

1486 if type_num == OFS_DELTA: 

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

1488 raw_base += len(raw) 

1489 if raw[-1] & 0x80: 

1490 raise AssertionError 

1491 delta_base_offset = raw[0] & 0x7F 

1492 for byte in raw[1:]: 

1493 delta_base_offset += 1 

1494 delta_base_offset <<= 7 

1495 delta_base_offset += byte & 0x7F 

1496 if delta_base_offset == 0: 

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

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

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

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

1501 delta_base = delta_base_offset 

1502 elif type_num == REF_DELTA: 

1503 # Determine hash size from hash_func 

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

1505 delta_base_obj = read_all(hash_size) 

1506 if crc32 is not None: 

1507 crc32 = binascii.crc32(delta_base_obj, crc32) 

1508 delta_base = delta_base_obj 

1509 raw_base += hash_size 

1510 else: 

1511 delta_base = None 

1512 

1513 unpacked = UnpackedObject( 

1514 type_num, 

1515 delta_base=delta_base, 

1516 decomp_len=size, 

1517 crc32=crc32, 

1518 hash_func=hash_func, 

1519 ) 

1520 unused = read_zlib_chunks( 

1521 read_some, 

1522 unpacked, 

1523 buffer_size=zlib_bufsize, 

1524 include_comp=include_comp, 

1525 ) 

1526 return unpacked, unused 

1527 

1528 

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

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

1531 (num, obj) = value 

1532 if num in DELTA_TYPES: 

1533 return chunks_length(obj[1]) 

1534 return chunks_length(obj) 

1535 

1536 

1537class PackStreamReader: 

1538 """Class to read a pack stream. 

1539 

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

1541 appropriate. 

1542 """ 

1543 

1544 def __init__( 

1545 self, 

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

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

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

1549 zlib_bufsize: int = _ZLIB_BUFSIZE, 

1550 ) -> None: 

1551 """Initialize pack stream reader. 

1552 

1553 Args: 

1554 hash_func: Hash function to use for computing object IDs 

1555 read_all: Function to read all requested bytes 

1556 read_some: Function to read some bytes (optional) 

1557 zlib_bufsize: Buffer size for zlib decompression 

1558 """ 

1559 self.read_all = read_all 

1560 if read_some is None: 

1561 self.read_some = read_all 

1562 else: 

1563 self.read_some = read_some 

1564 self.hash_func = hash_func 

1565 self.sha = hash_func() 

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

1567 self._offset = 0 

1568 self._rbuf = BytesIO() 

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

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

1571 self._zlib_bufsize = zlib_bufsize 

1572 

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

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

1575 

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

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

1578 

1579 Args: 

1580 read: The read callback to read from. 

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

1582 behavior is callback-specific. 

1583 Returns: Bytes read 

1584 """ 

1585 data = read(size) 

1586 

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

1588 n = len(data) 

1589 self._offset += n 

1590 tn = len(self._trailer) 

1591 if n >= self._hash_size: 

1592 to_pop = tn 

1593 to_add = self._hash_size 

1594 else: 

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

1596 to_add = n 

1597 self.sha.update( 

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

1599 ) 

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

1601 

1602 # hash everything but the trailer 

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

1604 return data 

1605 

1606 def _buf_len(self) -> int: 

1607 buf = self._rbuf 

1608 start = buf.tell() 

1609 buf.seek(0, SEEK_END) 

1610 end = buf.tell() 

1611 buf.seek(start) 

1612 return end - start 

1613 

1614 @property 

1615 def offset(self) -> int: 

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

1617 return self._offset - self._buf_len() 

1618 

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

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

1621 buf_len = self._buf_len() 

1622 if buf_len >= size: 

1623 return self._rbuf.read(size) 

1624 buf_data = self._rbuf.read() 

1625 self._rbuf = BytesIO() 

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

1627 

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

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

1630 buf_len = self._buf_len() 

1631 if buf_len: 

1632 data = self._rbuf.read(size) 

1633 if size >= buf_len: 

1634 self._rbuf = BytesIO() 

1635 return data 

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

1637 

1638 def __len__(self) -> int: 

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

1640 return self._num_objects 

1641 

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

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

1644 

1645 Args: 

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

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

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

1649 offset 

1650 obj_type_num 

1651 obj_chunks (for non-delta types) 

1652 delta_base (for delta types) 

1653 decomp_chunks 

1654 decomp_len 

1655 crc32 (if compute_crc32 is True) 

1656 

1657 Raises: 

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

1659 match the checksum in the pack trailer. 

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

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

1662 """ 

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

1664 

1665 for _ in range(self._num_objects): 

1666 offset = self.offset 

1667 unpacked, unused = unpack_object( 

1668 self.read, 

1669 self.hash_func, 

1670 read_some=self.recv, 

1671 compute_crc32=compute_crc32, 

1672 zlib_bufsize=self._zlib_bufsize, 

1673 ) 

1674 unpacked.offset = offset 

1675 

1676 # prepend any unused data to current read buffer 

1677 buf = BytesIO() 

1678 buf.write(unused) 

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

1680 buf.seek(0) 

1681 self._rbuf = buf 

1682 

1683 yield unpacked 

1684 

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

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

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

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

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

1690 self.read(self._hash_size) 

1691 

1692 pack_sha = bytearray(self._trailer) 

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

1694 raise ChecksumMismatch( 

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

1696 ) 

1697 

1698 

1699class PackStreamCopier(PackStreamReader): 

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

1701 

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

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

1704 """ 

1705 

1706 def __init__( 

1707 self, 

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

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

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

1711 outfile: IO[bytes], 

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

1713 ) -> None: 

1714 """Initialize the copier. 

1715 

1716 Args: 

1717 hash_func: Hash function to use for computing object IDs 

1718 read_all: Read function that blocks until the number of 

1719 requested bytes are read. 

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

1721 not return the number of bytes requested. 

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

1723 delta_iter: Optional DeltaChainIterator to record deltas as we 

1724 read them. 

1725 """ 

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

1727 self.outfile = outfile 

1728 self._delta_iter = delta_iter 

1729 

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

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

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

1733 self.outfile.write(data) 

1734 return data 

1735 

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

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

1738 

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

1740 throw. 

1741 """ 

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

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

1744 if self._delta_iter: 

1745 self._delta_iter.record(unpacked) 

1746 if progress is not None: 

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

1748 if progress is not None: 

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

1750 

1751 

1752def obj_sha( 

1753 type: int, 

1754 chunks: bytes | Iterable[bytes], 

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

1756) -> bytes: 

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

1758 

1759 Args: 

1760 type: Object type number 

1761 chunks: Object data chunks 

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

1763 

1764 Returns: 

1765 Binary hash digest 

1766 """ 

1767 sha = hash_func() 

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

1769 if isinstance(chunks, bytes): 

1770 sha.update(chunks) 

1771 else: 

1772 for chunk in chunks: 

1773 sha.update(chunk) 

1774 return sha.digest() 

1775 

1776 

1777def compute_file_sha( 

1778 f: IO[bytes], 

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

1780 start_ofs: int = 0, 

1781 end_ofs: int = 0, 

1782 buffer_size: int = 1 << 16, 

1783) -> "HashObject": 

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

1785 

1786 Args: 

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

1788 hash_func: A callable that returns a new HashObject. 

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

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

1791 end of the file. 

1792 buffer_size: A buffer size for reading. 

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

1794 """ 

1795 sha = hash_func() 

1796 f.seek(0, SEEK_END) 

1797 length = f.tell() 

1798 if start_ofs < 0: 

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

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

1801 raise AssertionError( 

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

1803 ) 

1804 todo = length + end_ofs - start_ofs 

1805 f.seek(start_ofs) 

1806 while todo: 

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

1808 sha.update(data) 

1809 todo -= len(data) 

1810 return sha 

1811 

1812 

1813class PackData: 

1814 """The data contained in a packfile. 

1815 

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

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

1818 

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

1820 

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

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

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

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

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

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

1827 

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

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

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

1831 or it errors on bad data. This is done here by just giving the complete 

1832 buffer from the start of the deflated object on. This is bad, but until I 

1833 get mmap sorted out it will have to do. 

1834 

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

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

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

1838 """ 

1839 

1840 def __init__( 

1841 self, 

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

1843 object_format: ObjectFormat, 

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

1845 size: int | None = None, 

1846 *, 

1847 delta_window_size: int | None = None, 

1848 window_memory: int | None = None, 

1849 delta_cache_size: int | None = None, 

1850 depth: int | None = None, 

1851 threads: int | None = None, 

1852 big_file_threshold: int | None = None, 

1853 delta_base_cache_limit: int | None = None, 

1854 ) -> None: 

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

1856 

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

1858 It must also stay the same size. It will be mapped whenever needed. 

1859 

1860 Currently there is a restriction on the size of the pack as the python 

1861 mmap implementation is flawed. 

1862 """ 

1863 self._filename = filename 

1864 self.object_format = object_format 

1865 self._size = size 

1866 self._header_size = 12 

1867 self.delta_window_size = delta_window_size 

1868 self.window_memory = window_memory 

1869 self.delta_cache_size = delta_cache_size 

1870 self.depth = depth 

1871 self.threads = threads 

1872 self.big_file_threshold = big_file_threshold 

1873 self.delta_base_cache_limit = delta_base_cache_limit 

1874 self._file: IO[bytes] 

1875 

1876 if file is None: 

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

1878 else: 

1879 self._file = file 

1880 (_version, self._num_objects) = read_pack_header(self._file.read) 

1881 

1882 # Use delta_base_cache_limit, then delta_cache_size, then default 

1883 cache_size = ( 

1884 delta_base_cache_limit or delta_cache_size or DEFAULT_DELTA_BASE_CACHE_LIMIT 

1885 ) 

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

1887 cache_size, compute_size=_compute_object_size 

1888 ) 

1889 

1890 @property 

1891 def filename(self) -> str: 

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

1893 

1894 Returns: 

1895 Base filename without directory path 

1896 """ 

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

1898 

1899 @property 

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

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

1902 

1903 Returns: 

1904 Full path to the pack file 

1905 """ 

1906 return self._filename 

1907 

1908 @classmethod 

1909 def from_file( 

1910 cls, 

1911 file: IO[bytes], 

1912 object_format: ObjectFormat, 

1913 size: int | None = None, 

1914 ) -> "PackData": 

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

1916 

1917 Args: 

1918 file: Open file object 

1919 object_format: Object format 

1920 size: Optional file size 

1921 

1922 Returns: 

1923 PackData instance 

1924 """ 

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

1926 

1927 @classmethod 

1928 def from_path( 

1929 cls, 

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

1931 object_format: ObjectFormat, 

1932 ) -> "PackData": 

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

1934 

1935 Args: 

1936 path: Path to the pack file 

1937 object_format: Object format 

1938 

1939 Returns: 

1940 PackData instance 

1941 """ 

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

1943 

1944 def close(self) -> None: 

1945 """Close the underlying pack file.""" 

1946 if self._file is not None: 

1947 self._file.close() 

1948 self._file = None # type: ignore 

1949 

1950 def __del__(self) -> None: 

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

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

1953 import warnings 

1954 

1955 warnings.warn( 

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

1957 ResourceWarning, 

1958 stacklevel=2, 

1959 source=self, 

1960 ) 

1961 try: 

1962 self.close() 

1963 except Exception: 

1964 # Ignore errors during cleanup 

1965 pass 

1966 

1967 def __enter__(self) -> Self: 

1968 """Enter context manager.""" 

1969 return self 

1970 

1971 def __exit__( 

1972 self, 

1973 type: type | None, 

1974 value: BaseException | None, 

1975 traceback: TracebackType | None, 

1976 ) -> None: 

1977 """Exit context manager.""" 

1978 self.close() 

1979 

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

1981 """Check equality with another object.""" 

1982 if isinstance(other, PackData): 

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

1984 return False 

1985 

1986 def _get_size(self) -> int: 

1987 if self._size is not None: 

1988 return self._size 

1989 self._size = os.path.getsize(self._filename) 

1990 if self._size < self._header_size: 

1991 errmsg = f"{self._filename} is too small for a packfile ({self._size} < {self._header_size})" 

1992 raise AssertionError(errmsg) 

1993 return self._size 

1994 

1995 def __len__(self) -> int: 

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

1997 return self._num_objects 

1998 

1999 def calculate_checksum(self) -> bytes: 

2000 """Calculate the checksum for this pack. 

2001 

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

2003 """ 

2004 return compute_file_sha( 

2005 self._file, 

2006 hash_func=self.object_format.hash_func, 

2007 end_ofs=-self.object_format.oid_length, 

2008 ).digest() 

2009 

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

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

2012 self._file.seek(self._header_size) 

2013 

2014 if self._num_objects is None: 

2015 return 

2016 

2017 for _ in range(self._num_objects): 

2018 offset = self._file.tell() 

2019 unpacked, unused = unpack_object( 

2020 self._file.read, 

2021 self.object_format.hash_func, 

2022 compute_crc32=False, 

2023 include_comp=include_comp, 

2024 ) 

2025 unpacked.offset = offset 

2026 yield unpacked 

2027 # Back up over unused data. 

2028 self._file.seek(-len(unused), SEEK_CUR) 

2029 

2030 def iterentries( 

2031 self, 

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

2033 resolve_ext_ref: ResolveExtRefFn | None = None, 

2034 ) -> Iterator[PackIndexEntry]: 

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

2036 

2037 Args: 

2038 progress: Progress function, called with current and total 

2039 object count. 

2040 resolve_ext_ref: Optional function to resolve external references 

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

2042 """ 

2043 num_objects = self._num_objects 

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

2045 for i, result in enumerate(indexer): 

2046 if progress is not None: 

2047 progress(i, num_objects) 

2048 yield result 

2049 

2050 def sorted_entries( 

2051 self, 

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

2053 resolve_ext_ref: ResolveExtRefFn | None = None, 

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

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

2056 

2057 Args: 

2058 progress: Progress function, called with current and total 

2059 object count 

2060 resolve_ext_ref: Optional function to resolve external references 

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

2062 """ 

2063 return sorted( 

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

2065 ) 

2066 

2067 def create_index_v1( 

2068 self, 

2069 filename: str, 

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

2071 resolve_ext_ref: ResolveExtRefFn | None = None, 

2072 ) -> bytes: 

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

2074 

2075 Args: 

2076 filename: Index filename. 

2077 progress: Progress report function 

2078 resolve_ext_ref: Optional function to resolve external references 

2079 Returns: Checksum of index file 

2080 """ 

2081 entries = self.sorted_entries( 

2082 progress=progress, resolve_ext_ref=resolve_ext_ref 

2083 ) 

2084 checksum = self.calculate_checksum() 

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

2086 write_pack_index_v1( 

2087 f, 

2088 entries, 

2089 checksum, 

2090 ) 

2091 return checksum 

2092 

2093 def create_index_v2( 

2094 self, 

2095 filename: str, 

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

2097 resolve_ext_ref: ResolveExtRefFn | None = None, 

2098 ) -> bytes: 

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

2100 

2101 Args: 

2102 filename: Index filename. 

2103 progress: Progress report function 

2104 resolve_ext_ref: Optional function to resolve external references 

2105 Returns: Checksum of index file 

2106 """ 

2107 entries = self.sorted_entries( 

2108 progress=progress, resolve_ext_ref=resolve_ext_ref 

2109 ) 

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

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

2112 

2113 def create_index_v3( 

2114 self, 

2115 filename: str, 

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

2117 resolve_ext_ref: ResolveExtRefFn | None = None, 

2118 hash_format: int | None = None, 

2119 ) -> bytes: 

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

2121 

2122 Args: 

2123 filename: Index filename. 

2124 progress: Progress report function 

2125 resolve_ext_ref: Function to resolve external references 

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

2127 Returns: Checksum of index file 

2128 """ 

2129 entries = self.sorted_entries( 

2130 progress=progress, resolve_ext_ref=resolve_ext_ref 

2131 ) 

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

2133 if hash_format is None: 

2134 hash_format = 1 # Default to SHA-1 

2135 return write_pack_index_v3( 

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

2137 ) 

2138 

2139 def create_index( 

2140 self, 

2141 filename: str, 

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

2143 version: int = 2, 

2144 resolve_ext_ref: ResolveExtRefFn | None = None, 

2145 hash_format: int | None = None, 

2146 ) -> bytes: 

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

2148 

2149 Args: 

2150 filename: Index filename. 

2151 progress: Progress report function 

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

2153 resolve_ext_ref: Function to resolve external references 

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

2155 Returns: Checksum of index file 

2156 """ 

2157 if version == 1: 

2158 return self.create_index_v1( 

2159 filename, progress, resolve_ext_ref=resolve_ext_ref 

2160 ) 

2161 elif version == 2: 

2162 return self.create_index_v2( 

2163 filename, progress, resolve_ext_ref=resolve_ext_ref 

2164 ) 

2165 elif version == 3: 

2166 return self.create_index_v3( 

2167 filename, 

2168 progress, 

2169 resolve_ext_ref=resolve_ext_ref, 

2170 hash_format=hash_format, 

2171 ) 

2172 else: 

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

2174 

2175 def get_stored_checksum(self) -> bytes: 

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

2177 checksum_size = self.object_format.oid_length 

2178 self._file.seek(-checksum_size, SEEK_END) 

2179 return self._file.read(checksum_size) 

2180 

2181 def check(self) -> None: 

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

2183 actual = self.calculate_checksum() 

2184 stored = self.get_stored_checksum() 

2185 if actual != stored: 

2186 raise ChecksumMismatch(stored, actual) 

2187 

2188 def get_unpacked_object_at( 

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

2190 ) -> UnpackedObject: 

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

2192 assert offset >= self._header_size 

2193 self._file.seek(offset) 

2194 unpacked, _ = unpack_object( 

2195 self._file.read, self.object_format.hash_func, include_comp=include_comp 

2196 ) 

2197 unpacked.offset = offset 

2198 return unpacked 

2199 

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

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

2202 

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

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

2205 function. 

2206 """ 

2207 try: 

2208 return self._offset_cache[offset] 

2209 except KeyError: 

2210 pass 

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

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

2213 

2214 

2215T = TypeVar("T") 

2216 

2217 

2218class DeltaChainIterator(Generic[T]): 

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

2220 

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

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

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

2224 

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

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

2227 

2228 * offset 

2229 * obj_type_num 

2230 * obj_chunks 

2231 * pack_type_num 

2232 * delta_base (for delta types) 

2233 * comp_chunks (if _include_comp is True) 

2234 * decomp_chunks 

2235 * decomp_len 

2236 * crc32 (if _compute_crc32 is True) 

2237 """ 

2238 

2239 _compute_crc32 = False 

2240 _include_comp = False 

2241 

2242 def __init__( 

2243 self, 

2244 file_obj: IO[bytes] | None, 

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

2246 *, 

2247 resolve_ext_ref: ResolveExtRefFn | None = None, 

2248 object_format: "ObjectFormat | None" = None, 

2249 ) -> None: 

2250 """Initialize DeltaChainIterator. 

2251 

2252 Args: 

2253 file_obj: File object to read pack data from 

2254 hash_func: Hash function to use for computing object IDs 

2255 resolve_ext_ref: Optional function to resolve external references 

2256 object_format: Optional object format. Required by subclasses 

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

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

2259 """ 

2260 self._file = file_obj 

2261 self.hash_func = hash_func 

2262 self._object_format = object_format 

2263 self._resolve_ext_ref = resolve_ext_ref 

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

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

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

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

2268 

2269 @classmethod 

2270 def for_pack_data( 

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

2272 ) -> "DeltaChainIterator[T]": 

2273 """Create a DeltaChainIterator from pack data. 

2274 

2275 Args: 

2276 pack_data: PackData object to iterate 

2277 resolve_ext_ref: Optional function to resolve external refs 

2278 

2279 Returns: 

2280 DeltaChainIterator instance 

2281 """ 

2282 walker = cls( 

2283 None, 

2284 pack_data.object_format.hash_func, 

2285 resolve_ext_ref=resolve_ext_ref, 

2286 object_format=pack_data.object_format, 

2287 ) 

2288 walker.set_pack_data(pack_data) 

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

2290 walker.record(unpacked) 

2291 return walker 

2292 

2293 @classmethod 

2294 def for_pack_subset( 

2295 cls, 

2296 pack: "Pack", 

2297 shas: Iterable[ObjectID | RawObjectID], 

2298 *, 

2299 allow_missing: bool = False, 

2300 resolve_ext_ref: ResolveExtRefFn | None = None, 

2301 ) -> "DeltaChainIterator[T]": 

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

2303 

2304 Args: 

2305 pack: Pack object containing the data 

2306 shas: Iterable of object SHAs to include 

2307 allow_missing: If True, skip missing objects 

2308 resolve_ext_ref: Optional function to resolve external refs 

2309 

2310 Returns: 

2311 DeltaChainIterator instance 

2312 """ 

2313 walker = cls( 

2314 None, 

2315 pack.object_format.hash_func, 

2316 resolve_ext_ref=resolve_ext_ref, 

2317 object_format=pack.object_format, 

2318 ) 

2319 walker.set_pack_data(pack.data) 

2320 todo = set() 

2321 for sha in shas: 

2322 try: 

2323 off = pack.index.object_offset(sha) 

2324 except KeyError: 

2325 if not allow_missing: 

2326 raise 

2327 else: 

2328 todo.add(off) 

2329 done = set() 

2330 while todo: 

2331 off = todo.pop() 

2332 unpacked = pack.data.get_unpacked_object_at(off) 

2333 walker.record(unpacked) 

2334 done.add(off) 

2335 base_ofs = None 

2336 if unpacked.pack_type_num == OFS_DELTA: 

2337 assert unpacked.offset is not None 

2338 assert unpacked.delta_base is not None 

2339 assert isinstance(unpacked.delta_base, int) 

2340 base_ofs = unpacked.offset - unpacked.delta_base 

2341 elif unpacked.pack_type_num == REF_DELTA: 

2342 with suppress(KeyError): 

2343 assert isinstance(unpacked.delta_base, bytes) 

2344 base_ofs = pack.index.object_offset( 

2345 RawObjectID(unpacked.delta_base) 

2346 ) 

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

2348 todo.add(base_ofs) 

2349 return walker 

2350 

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

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

2353 

2354 Args: 

2355 unpacked: UnpackedObject to record 

2356 """ 

2357 type_num = unpacked.pack_type_num 

2358 offset = unpacked.offset 

2359 assert offset is not None 

2360 if type_num == OFS_DELTA: 

2361 assert unpacked.delta_base is not None 

2362 assert isinstance(unpacked.delta_base, int) 

2363 base_offset = offset - unpacked.delta_base 

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

2365 elif type_num == REF_DELTA: 

2366 assert isinstance(unpacked.delta_base, bytes) 

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

2368 else: 

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

2370 

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

2372 """Set the pack data for iteration. 

2373 

2374 Args: 

2375 pack_data: PackData object to use 

2376 """ 

2377 self._file = pack_data._file 

2378 

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

2380 for offset, type_num in self._full_ofs: 

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

2382 yield from self._walk_ref_chains() 

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

2384 

2385 def _ensure_no_pending(self) -> None: 

2386 if self._pending_ref: 

2387 raise UnresolvedDeltas( 

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

2389 ) 

2390 

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

2392 if not self._resolve_ext_ref: 

2393 self._ensure_no_pending() 

2394 return 

2395 

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

2397 if base_sha not in self._pending_ref: 

2398 continue 

2399 try: 

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

2401 except KeyError: 

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

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

2404 # error below. 

2405 continue 

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

2407 self._pending_ref.pop(base_sha) 

2408 for new_offset in pending: 

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

2410 

2411 self._ensure_no_pending() 

2412 

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

2414 raise NotImplementedError 

2415 

2416 def _resolve_object( 

2417 self, 

2418 offset: int, 

2419 obj_type_num: int, 

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

2421 ) -> UnpackedObject: 

2422 assert self._file is not None 

2423 self._file.seek(offset) 

2424 unpacked, _ = unpack_object( 

2425 self._file.read, 

2426 self.hash_func, 

2427 read_some=None, 

2428 compute_crc32=self._compute_crc32, 

2429 include_comp=self._include_comp, 

2430 ) 

2431 unpacked.offset = offset 

2432 if base_chunks is None: 

2433 assert unpacked.pack_type_num == obj_type_num 

2434 else: 

2435 assert unpacked.pack_type_num in DELTA_TYPES 

2436 unpacked.obj_type_num = obj_type_num 

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

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

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

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

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

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

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

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

2445 # delta in practice. 

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

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

2448 raise ApplyDeltaError( 

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

2450 ) 

2451 return unpacked 

2452 

2453 def _follow_chain( 

2454 self, 

2455 offset: int, 

2456 obj_type_num: int, 

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

2458 ) -> Iterator[T]: 

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

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

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

2462 while todo: 

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

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

2465 yield self._result(unpacked) 

2466 

2467 assert unpacked.offset is not None 

2468 unblocked = chain( 

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

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

2471 ) 

2472 todo.extend( 

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

2474 for new_offset in unblocked 

2475 ) 

2476 

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

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

2479 return self._walk_all_chains() 

2480 

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

2482 """Return external references.""" 

2483 return self._ext_refs 

2484 

2485 

2486class UnpackedObjectIterator(DeltaChainIterator[UnpackedObject]): 

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

2488 

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

2490 """Return the unpacked object. 

2491 

2492 Args: 

2493 unpacked: The unpacked object 

2494 

2495 Returns: 

2496 The unpacked object unchanged 

2497 """ 

2498 return unpacked 

2499 

2500 

2501class PackIndexer(DeltaChainIterator[PackIndexEntry]): 

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

2503 

2504 _compute_crc32 = True 

2505 

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

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

2508 

2509 Args: 

2510 unpacked: The unpacked object 

2511 

2512 Returns: 

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

2514 """ 

2515 assert unpacked.offset is not None 

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

2517 

2518 

2519class PackInflater(DeltaChainIterator[ShaFile]): 

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

2521 

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

2523 """Convert unpacked object to ShaFile. 

2524 

2525 Args: 

2526 unpacked: The unpacked object 

2527 

2528 Returns: 

2529 ShaFile object from the unpacked data 

2530 """ 

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

2532 return ShaFile.from_raw_chunks( 

2533 unpacked.obj_type_num, 

2534 unpacked.obj_chunks, 

2535 object_format=self._object_format, 

2536 ) 

2537 

2538 

2539class SHA1Reader(BinaryIO): 

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

2541 

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

2543 """Initialize SHA1Reader. 

2544 

2545 Args: 

2546 f: File-like object to wrap 

2547 """ 

2548 self.f = f 

2549 self.sha1 = sha1(b"") 

2550 

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

2552 """Read bytes and update SHA1. 

2553 

2554 Args: 

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

2556 

2557 Returns: 

2558 Bytes read from file 

2559 """ 

2560 data = self.f.read(size) 

2561 self.sha1.update(data) 

2562 return data 

2563 

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

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

2566 

2567 Args: 

2568 allow_empty: Allow empty SHA1 hash 

2569 

2570 Raises: 

2571 ChecksumMismatch: If SHA1 doesn't match 

2572 """ 

2573 stored = self.f.read(20) 

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

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

2576 not allow_empty 

2577 or ( 

2578 len(stored) == 20 

2579 and sha_to_hex(RawObjectID(stored)) 

2580 != b"0000000000000000000000000000000000000000" 

2581 ) 

2582 ): 

2583 raise ChecksumMismatch( 

2584 self.sha1.hexdigest(), 

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

2586 ) 

2587 

2588 def close(self) -> None: 

2589 """Close the underlying file.""" 

2590 return self.f.close() 

2591 

2592 def tell(self) -> int: 

2593 """Return current file position.""" 

2594 return self.f.tell() 

2595 

2596 # BinaryIO abstract methods 

2597 def readable(self) -> bool: 

2598 """Check if file is readable.""" 

2599 return True 

2600 

2601 def writable(self) -> bool: 

2602 """Check if file is writable.""" 

2603 return False 

2604 

2605 def seekable(self) -> bool: 

2606 """Check if file is seekable.""" 

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

2608 

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

2610 """Seek to position in file. 

2611 

2612 Args: 

2613 offset: Position offset 

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

2615 

2616 Returns: 

2617 New file position 

2618 """ 

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

2620 

2621 def flush(self) -> None: 

2622 """Flush the file buffer.""" 

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

2624 self.f.flush() 

2625 

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

2627 """Read a line from the file. 

2628 

2629 Args: 

2630 size: Maximum bytes to read 

2631 

2632 Returns: 

2633 Line read from file 

2634 """ 

2635 return self.f.readline(size) 

2636 

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

2638 """Read all lines from the file. 

2639 

2640 Args: 

2641 hint: Approximate number of bytes to read 

2642 

2643 Returns: 

2644 List of lines 

2645 """ 

2646 return self.f.readlines(hint) 

2647 

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

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

2650 raise UnsupportedOperation("writelines") 

2651 

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

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

2654 raise UnsupportedOperation("write") 

2655 

2656 def __enter__(self) -> Self: 

2657 """Enter context manager.""" 

2658 return self 

2659 

2660 def __exit__( 

2661 self, 

2662 type: type | None, 

2663 value: BaseException | None, 

2664 traceback: TracebackType | None, 

2665 ) -> None: 

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

2667 self.close() 

2668 

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

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

2671 return self 

2672 

2673 def __next__(self) -> bytes: 

2674 """Get next line from file. 

2675 

2676 Returns: 

2677 Next line 

2678 

2679 Raises: 

2680 StopIteration: When no more lines 

2681 """ 

2682 line = self.readline() 

2683 if not line: 

2684 raise StopIteration 

2685 return line 

2686 

2687 def fileno(self) -> int: 

2688 """Return file descriptor number.""" 

2689 return self.f.fileno() 

2690 

2691 def isatty(self) -> bool: 

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

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

2694 

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

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

2697 

2698 Raises: 

2699 UnsupportedOperation: Always raised 

2700 """ 

2701 raise UnsupportedOperation("truncate") 

2702 

2703 

2704class SHA1Writer(BinaryIO): 

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

2706 

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

2708 """Initialize SHA1Writer. 

2709 

2710 Args: 

2711 f: File-like object to wrap 

2712 """ 

2713 self.f = f 

2714 self.length = 0 

2715 self.sha1 = sha1(b"") 

2716 self.digest: bytes | None = None 

2717 

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

2719 """Write data and update SHA1. 

2720 

2721 Args: 

2722 data: Data to write 

2723 

2724 Returns: 

2725 Number of bytes written 

2726 """ 

2727 self.sha1.update(data) 

2728 written = self.f.write(data) 

2729 self.length += written 

2730 return written 

2731 

2732 def write_sha(self) -> bytes: 

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

2734 

2735 Returns: 

2736 The SHA1 digest bytes 

2737 """ 

2738 sha = self.sha1.digest() 

2739 assert len(sha) == 20 

2740 self.f.write(sha) 

2741 self.length += len(sha) 

2742 return sha 

2743 

2744 def close(self) -> None: 

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

2746 self.digest = self.write_sha() 

2747 self.f.close() 

2748 

2749 def offset(self) -> int: 

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

2751 

2752 Returns: 

2753 Total bytes written 

2754 """ 

2755 return self.length 

2756 

2757 def tell(self) -> int: 

2758 """Return current file position.""" 

2759 return self.f.tell() 

2760 

2761 # BinaryIO abstract methods 

2762 def readable(self) -> bool: 

2763 """Check if file is readable.""" 

2764 return False 

2765 

2766 def writable(self) -> bool: 

2767 """Check if file is writable.""" 

2768 return True 

2769 

2770 def seekable(self) -> bool: 

2771 """Check if file is seekable.""" 

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

2773 

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

2775 """Seek to position in file. 

2776 

2777 Args: 

2778 offset: Position offset 

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

2780 

2781 Returns: 

2782 New file position 

2783 """ 

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

2785 

2786 def flush(self) -> None: 

2787 """Flush the file buffer.""" 

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

2789 self.f.flush() 

2790 

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

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

2793 

2794 Raises: 

2795 UnsupportedOperation: Always raised 

2796 """ 

2797 raise UnsupportedOperation("readline") 

2798 

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

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

2801 

2802 Raises: 

2803 UnsupportedOperation: Always raised 

2804 """ 

2805 raise UnsupportedOperation("readlines") 

2806 

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

2808 """Write multiple lines to the file. 

2809 

2810 Args: 

2811 lines: Iterable of lines to write 

2812 """ 

2813 for line in lines: 

2814 self.write(line) 

2815 

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

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

2818 

2819 Raises: 

2820 UnsupportedOperation: Always raised 

2821 """ 

2822 raise UnsupportedOperation("read") 

2823 

2824 def __enter__(self) -> Self: 

2825 """Enter context manager.""" 

2826 return self 

2827 

2828 def __exit__( 

2829 self, 

2830 type: type | None, 

2831 value: BaseException | None, 

2832 traceback: TracebackType | None, 

2833 ) -> None: 

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

2835 self.f.close() 

2836 

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

2838 """Return iterator.""" 

2839 return self 

2840 

2841 def __next__(self) -> bytes: 

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

2843 

2844 Raises: 

2845 UnsupportedOperation: Always raised 

2846 """ 

2847 raise UnsupportedOperation("__next__") 

2848 

2849 def fileno(self) -> int: 

2850 """Return file descriptor number.""" 

2851 return self.f.fileno() 

2852 

2853 def isatty(self) -> bool: 

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

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

2856 

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

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

2859 

2860 Raises: 

2861 UnsupportedOperation: Always raised 

2862 """ 

2863 raise UnsupportedOperation("truncate") 

2864 

2865 

2866class HashWriter(BinaryIO): 

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

2868 

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

2870 """ 

2871 

2872 def __init__( 

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

2874 ) -> None: 

2875 """Initialize HashWriter. 

2876 

2877 Args: 

2878 f: File-like object to wrap 

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

2880 """ 

2881 self.f = f 

2882 self.length = 0 

2883 self.hash_obj = hash_func() 

2884 self.digest: bytes | None = None 

2885 

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

2887 """Write data and update hash. 

2888 

2889 Args: 

2890 data: Data to write 

2891 

2892 Returns: 

2893 Number of bytes written 

2894 """ 

2895 self.hash_obj.update(data) 

2896 written = self.f.write(data) 

2897 self.length += written 

2898 return written 

2899 

2900 def write_hash(self) -> bytes: 

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

2902 

2903 Returns: 

2904 The hash digest bytes 

2905 """ 

2906 digest = self.hash_obj.digest() 

2907 self.f.write(digest) 

2908 self.length += len(digest) 

2909 return digest 

2910 

2911 def close(self) -> None: 

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

2913 self.digest = self.write_hash() 

2914 self.f.close() 

2915 

2916 def offset(self) -> int: 

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

2918 

2919 Returns: 

2920 Total bytes written 

2921 """ 

2922 return self.length 

2923 

2924 def tell(self) -> int: 

2925 """Return current file position.""" 

2926 return self.f.tell() 

2927 

2928 # BinaryIO abstract methods 

2929 def readable(self) -> bool: 

2930 """Check if file is readable.""" 

2931 return False 

2932 

2933 def writable(self) -> bool: 

2934 """Check if file is writable.""" 

2935 return True 

2936 

2937 def seekable(self) -> bool: 

2938 """Check if file is seekable.""" 

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

2940 

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

2942 """Seek to position in file. 

2943 

2944 Args: 

2945 offset: Position offset 

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

2947 

2948 Returns: 

2949 New file position 

2950 """ 

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

2952 

2953 def flush(self) -> None: 

2954 """Flush the file buffer.""" 

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

2956 self.f.flush() 

2957 

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

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

2960 

2961 Raises: 

2962 UnsupportedOperation: Always raised 

2963 """ 

2964 raise UnsupportedOperation("readline") 

2965 

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

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

2968 

2969 Raises: 

2970 UnsupportedOperation: Always raised 

2971 """ 

2972 raise UnsupportedOperation("readlines") 

2973 

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

2975 """Write multiple lines to the file. 

2976 

2977 Args: 

2978 lines: Iterable of lines to write 

2979 """ 

2980 for line in lines: 

2981 self.write(line) 

2982 

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

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

2985 

2986 Raises: 

2987 UnsupportedOperation: Always raised 

2988 """ 

2989 raise UnsupportedOperation("read") 

2990 

2991 def __enter__(self) -> Self: 

2992 """Enter context manager.""" 

2993 return self 

2994 

2995 def __exit__( 

2996 self, 

2997 type: type | None, 

2998 value: BaseException | None, 

2999 traceback: TracebackType | None, 

3000 ) -> None: 

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

3002 self.close() 

3003 

3004 def __iter__(self) -> "HashWriter": 

3005 """Return iterator.""" 

3006 return self 

3007 

3008 def __next__(self) -> bytes: 

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

3010 

3011 Raises: 

3012 UnsupportedOperation: Always raised 

3013 """ 

3014 raise UnsupportedOperation("__next__") 

3015 

3016 def fileno(self) -> int: 

3017 """Return file descriptor number.""" 

3018 return self.f.fileno() 

3019 

3020 def isatty(self) -> bool: 

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

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

3023 

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

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

3026 

3027 Raises: 

3028 UnsupportedOperation: Always raised 

3029 """ 

3030 raise UnsupportedOperation("truncate") 

3031 

3032 

3033def pack_object_header( 

3034 type_num: int, 

3035 delta_base: bytes | int | None, 

3036 size: int, 

3037 object_format: "ObjectFormat", 

3038) -> bytearray: 

3039 """Create a pack object header for the given object info. 

3040 

3041 Args: 

3042 type_num: Numeric type of the object. 

3043 delta_base: Delta base offset or ref, or None for whole objects. 

3044 size: Uncompressed object size. 

3045 object_format: Object format (hash algorithm) to use. 

3046 Returns: A header for a packed object. 

3047 """ 

3048 header = [] 

3049 c = (type_num << 4) | (size & 15) 

3050 size >>= 4 

3051 while size: 

3052 header.append(c | 0x80) 

3053 c = size & 0x7F 

3054 size >>= 7 

3055 header.append(c) 

3056 if type_num == OFS_DELTA: 

3057 assert isinstance(delta_base, int) 

3058 ret = [delta_base & 0x7F] 

3059 delta_base >>= 7 

3060 while delta_base: 

3061 delta_base -= 1 

3062 ret.insert(0, 0x80 | (delta_base & 0x7F)) 

3063 delta_base >>= 7 

3064 header.extend(ret) 

3065 elif type_num == REF_DELTA: 

3066 assert isinstance(delta_base, bytes) 

3067 assert len(delta_base) == object_format.oid_length 

3068 header += delta_base 

3069 return bytearray(header) 

3070 

3071 

3072def pack_object_chunks( 

3073 type: int, 

3074 object: list[bytes] | tuple[bytes | int, list[bytes]], 

3075 object_format: "ObjectFormat", 

3076 *, 

3077 compression_level: int = -1, 

3078) -> Iterator[bytes]: 

3079 """Generate chunks for a pack object. 

3080 

3081 Args: 

3082 type: Numeric type of the object 

3083 object: Object to write 

3084 object_format: Object format (hash algorithm) to use 

3085 compression_level: the zlib compression level 

3086 Returns: Chunks 

3087 """ 

3088 if type in DELTA_TYPES: 

3089 if isinstance(object, tuple): 

3090 delta_base, object = object 

3091 else: 

3092 raise TypeError("Delta types require a tuple of (delta_base, object)") 

3093 else: 

3094 delta_base = None 

3095 

3096 # Convert object to list of bytes chunks 

3097 if isinstance(object, bytes): 

3098 chunks = [object] 

3099 elif isinstance(object, list): 

3100 chunks = object 

3101 elif isinstance(object, ShaFile): 

3102 chunks = object.as_raw_chunks() 

3103 else: 

3104 # Shouldn't reach here with proper typing 

3105 raise TypeError(f"Unexpected object type: {object.__class__.__name__}") 

3106 

3107 yield bytes( 

3108 pack_object_header( 

3109 type, delta_base, sum(map(len, chunks)), object_format=object_format 

3110 ) 

3111 ) 

3112 compressor = zlib.compressobj(level=compression_level) 

3113 for data in chunks: 

3114 yield compressor.compress(data) 

3115 yield compressor.flush() 

3116 

3117 

3118def write_pack_object( 

3119 write: Callable[[bytes], int], 

3120 type: int, 

3121 object: list[bytes] | tuple[bytes | int, list[bytes]], 

3122 object_format: "ObjectFormat", 

3123 *, 

3124 sha: "HashObject | None" = None, 

3125 compression_level: int = -1, 

3126) -> int: 

3127 """Write pack object to a file. 

3128 

3129 Args: 

3130 write: Write function to use 

3131 type: Numeric type of the object 

3132 object: Object to write 

3133 object_format: Object format (hash algorithm) to use 

3134 sha: Optional SHA-1 hasher to update 

3135 compression_level: the zlib compression level 

3136 Returns: CRC32 checksum of the written object 

3137 """ 

3138 crc32 = 0 

3139 for chunk in pack_object_chunks( 

3140 type, object, compression_level=compression_level, object_format=object_format 

3141 ): 

3142 write(chunk) 

3143 if sha is not None: 

3144 sha.update(chunk) 

3145 crc32 = binascii.crc32(chunk, crc32) 

3146 return crc32 & 0xFFFFFFFF 

3147 

3148 

3149def write_pack( 

3150 filename: str, 

3151 objects: Sequence[ShaFile] | Sequence[tuple[ShaFile, bytes | None]], 

3152 object_format: "ObjectFormat", 

3153 *, 

3154 deltify: bool | None = None, 

3155 delta_window_size: int | None = None, 

3156 compression_level: int = -1, 

3157) -> tuple[bytes, bytes]: 

3158 """Write a new pack data file. 

3159 

3160 Args: 

3161 filename: Path to the new pack file (without .pack extension) 

3162 objects: Objects to write to the pack 

3163 object_format: Object format 

3164 delta_window_size: Delta window size 

3165 deltify: Whether to deltify pack objects 

3166 compression_level: the zlib compression level 

3167 Returns: Tuple with checksum of pack file and index file 

3168 """ 

3169 with GitFile(filename + ".pack", "wb") as f: 

3170 entries, data_sum = write_pack_objects( 

3171 f, 

3172 objects, 

3173 delta_window_size=delta_window_size, 

3174 deltify=deltify, 

3175 compression_level=compression_level, 

3176 object_format=object_format, 

3177 ) 

3178 entries_list = sorted([(k, v[0], v[1]) for (k, v) in entries.items()]) 

3179 with GitFile(filename + ".idx", "wb") as f: 

3180 idx_sha = write_pack_index(f, entries_list, data_sum) 

3181 return data_sum, idx_sha 

3182 

3183 

3184def pack_header_chunks(num_objects: int) -> Iterator[bytes]: 

3185 """Yield chunks for a pack header.""" 

3186 yield b"PACK" # Pack header 

3187 yield struct.pack(b">L", 2) # Pack version 

3188 yield struct.pack(b">L", num_objects) # Number of objects in pack 

3189 

3190 

3191def write_pack_header( 

3192 write: Callable[[bytes], int] | IO[bytes], num_objects: int 

3193) -> None: 

3194 """Write a pack header for the given number of objects.""" 

3195 if not callable(write): 

3196 write_fn: Callable[[bytes], int] = write.write 

3197 warnings.warn( 

3198 "write_pack_header() now takes a write rather than file argument", 

3199 DeprecationWarning, 

3200 stacklevel=2, 

3201 ) 

3202 else: 

3203 write_fn = write 

3204 for chunk in pack_header_chunks(num_objects): 

3205 write_fn(chunk) 

3206 

3207 

3208def find_reusable_deltas( 

3209 container: PackedObjectContainer, 

3210 object_ids: Set[ObjectID], 

3211 *, 

3212 other_haves: Set[ObjectID] | None = None, 

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

3214) -> Iterator[UnpackedObject]: 

3215 """Find deltas in a pack that can be reused. 

3216 

3217 Args: 

3218 container: Pack container to search for deltas 

3219 object_ids: Set of object IDs to find deltas for 

3220 other_haves: Set of other object IDs we have 

3221 progress: Optional progress reporting callback 

3222 

3223 Returns: 

3224 Iterator of UnpackedObject entries that can be reused 

3225 """ 

3226 if other_haves is None: 

3227 other_haves = set() 

3228 reused = 0 

3229 for i, unpacked in enumerate( 

3230 container.iter_unpacked_subset( 

3231 object_ids, allow_missing=True, convert_ofs_delta=True 

3232 ) 

3233 ): 

3234 if progress is not None and i % 1000 == 0: 

3235 progress(f"checking for reusable deltas: {i}/{len(object_ids)}\r".encode()) 

3236 if unpacked.pack_type_num == REF_DELTA: 

3237 hexsha = sha_to_hex(unpacked.delta_base) # type: ignore 

3238 if hexsha in object_ids or hexsha in other_haves: 

3239 yield unpacked 

3240 reused += 1 

3241 if progress is not None: 

3242 progress((f"found {reused} deltas to reuse\n").encode()) 

3243 

3244 

3245def deltify_pack_objects( 

3246 objects: Iterator[ShaFile] | Iterator[tuple[ShaFile, bytes | None]], 

3247 *, 

3248 window_size: int | None = None, 

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

3250) -> Iterator[UnpackedObject]: 

3251 """Generate deltas for pack objects. 

3252 

3253 Args: 

3254 objects: An iterable of (object, path) tuples to deltify. 

3255 window_size: Window size; None for default 

3256 progress: Optional progress reporting callback 

3257 Returns: Iterator over type_num, object id, delta_base, content 

3258 delta_base is None for full text entries 

3259 """ 

3260 

3261 def objects_with_hints() -> Iterator[tuple[ShaFile, tuple[int, bytes | None]]]: 

3262 for e in objects: 

3263 if isinstance(e, ShaFile): 

3264 yield (e, (e.type_num, None)) 

3265 else: 

3266 yield (e[0], (e[0].type_num, e[1])) 

3267 

3268 sorted_objs = sort_objects_for_delta(objects_with_hints()) 

3269 yield from deltas_from_sorted_objects( 

3270 sorted_objs, 

3271 window_size=window_size, 

3272 progress=progress, 

3273 ) 

3274 

3275 

3276def sort_objects_for_delta( 

3277 objects: Iterator[ShaFile] | Iterator[tuple[ShaFile, PackHint | None]], 

3278) -> Iterator[tuple[ShaFile, bytes | None]]: 

3279 """Sort objects for optimal delta compression. 

3280 

3281 Args: 

3282 objects: Iterator of objects or (object, hint) tuples 

3283 

3284 Returns: 

3285 Iterator of sorted (ShaFile, path) tuples 

3286 """ 

3287 magic = [] 

3288 for entry in objects: 

3289 if isinstance(entry, tuple): 

3290 obj, hint = entry 

3291 if hint is None: 

3292 type_num = None 

3293 path = None 

3294 else: 

3295 (type_num, path) = hint 

3296 else: 

3297 obj = entry 

3298 type_num = None 

3299 path = None 

3300 magic.append((type_num, path, -obj.raw_length(), obj)) 

3301 # Build a list of objects ordered by the magic Linus heuristic 

3302 # This helps us find good objects to diff against us 

3303 magic.sort() 

3304 return ((x[3], x[1]) for x in magic) 

3305 

3306 

3307def deltas_from_sorted_objects( 

3308 objects: Iterator[tuple[ShaFile, bytes | None]], 

3309 window_size: int | None = None, 

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

3311) -> Iterator[UnpackedObject]: 

3312 """Create deltas from sorted objects. 

3313 

3314 Args: 

3315 objects: Iterator of sorted objects to deltify 

3316 window_size: Delta window size; None for default 

3317 progress: Optional progress reporting callback 

3318 

3319 Returns: 

3320 Iterator of UnpackedObject entries 

3321 """ 

3322 # TODO(jelmer): Use threads 

3323 if window_size is None: 

3324 window_size = DEFAULT_PACK_DELTA_WINDOW_SIZE 

3325 

3326 possible_bases: deque[tuple[bytes, int, bytes]] = deque() 

3327 for i, (o, path) in enumerate(objects): 

3328 if progress is not None and i % 1000 == 0: 

3329 progress((f"generating deltas: {i}\r").encode()) 

3330 raw = o.as_raw_chunks() 

3331 raw_bytes = b"".join(raw) # Join once for efficiency 

3332 winner = raw 

3333 winner_len = sum(map(len, winner)) 

3334 winner_base = None 

3335 for base_id, base_type_num, base_bytes in possible_bases: 

3336 if base_type_num != o.type_num: 

3337 continue 

3338 delta_len = 0 

3339 delta = [] 

3340 for chunk in create_delta(base_bytes, raw_bytes): 

3341 delta_len += len(chunk) 

3342 if delta_len >= winner_len: 

3343 break 

3344 delta.append(chunk) 

3345 else: 

3346 winner_base = base_id 

3347 winner = delta 

3348 winner_len = sum(map(len, winner)) 

3349 yield UnpackedObject( 

3350 o.type_num, 

3351 sha=o.sha().digest(), 

3352 delta_base=winner_base, 

3353 decomp_len=winner_len, 

3354 decomp_chunks=winner, 

3355 ) 

3356 possible_bases.appendleft((o.sha().digest(), o.type_num, raw_bytes)) 

3357 while len(possible_bases) > window_size: 

3358 possible_bases.pop() 

3359 

3360 

3361def pack_objects_to_data( 

3362 objects: Sequence[ShaFile] 

3363 | Sequence[tuple[ShaFile, bytes | None]] 

3364 | Sequence[tuple[ShaFile, PackHint | None]], 

3365 *, 

3366 deltify: bool | None = None, 

3367 delta_window_size: int | None = None, 

3368 ofs_delta: bool = True, 

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

3370) -> tuple[int, Iterator[UnpackedObject]]: 

3371 """Create pack data from objects. 

3372 

3373 Args: 

3374 objects: Pack objects 

3375 deltify: Whether to deltify pack objects 

3376 delta_window_size: Delta window size 

3377 ofs_delta: Whether to use offset deltas 

3378 progress: Optional progress reporting callback 

3379 Returns: Tuples with (type_num, hexdigest, delta base, object chunks) 

3380 """ 

3381 count = len(objects) 

3382 if deltify is None: 

3383 # PERFORMANCE/TODO(jelmer): This should be enabled but the python 

3384 # implementation is *much* too slow at the moment. 

3385 # Maybe consider enabling it just if the rust extension is available? 

3386 deltify = False 

3387 if deltify: 

3388 return ( 

3389 count, 

3390 deltify_pack_objects( 

3391 iter(objects), # type: ignore 

3392 window_size=delta_window_size, 

3393 progress=progress, 

3394 ), 

3395 ) 

3396 else: 

3397 

3398 def iter_without_path() -> Iterator[UnpackedObject]: 

3399 for o in objects: 

3400 if isinstance(o, tuple): 

3401 yield full_unpacked_object(o[0]) 

3402 else: 

3403 yield full_unpacked_object(o) 

3404 

3405 return (count, iter_without_path()) 

3406 

3407 

3408def generate_unpacked_objects( 

3409 container: PackedObjectContainer, 

3410 object_ids: Sequence[tuple[ObjectID, PackHint | None]], 

3411 delta_window_size: int | None = None, 

3412 deltify: bool | None = None, 

3413 reuse_deltas: bool = True, 

3414 ofs_delta: bool = True, 

3415 other_haves: set[ObjectID] | None = None, 

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

3417) -> Iterator[UnpackedObject]: 

3418 """Create pack data from objects. 

3419 

3420 Returns: Tuples with (type_num, hexdigest, delta base, object chunks) 

3421 """ 

3422 todo = dict(object_ids) 

3423 if reuse_deltas: 

3424 for unpack in find_reusable_deltas( 

3425 container, set(todo), other_haves=other_haves, progress=progress 

3426 ): 

3427 del todo[sha_to_hex(RawObjectID(unpack.sha()))] 

3428 yield unpack 

3429 if deltify is None: 

3430 # PERFORMANCE/TODO(jelmer): This should be enabled but is *much* too 

3431 # slow at the moment. 

3432 deltify = False 

3433 if deltify: 

3434 objects_to_delta = container.iterobjects_subset( 

3435 todo.keys(), allow_missing=False 

3436 ) 

3437 sorted_objs = sort_objects_for_delta((o, todo[o.id]) for o in objects_to_delta) 

3438 yield from deltas_from_sorted_objects( 

3439 sorted_objs, 

3440 window_size=delta_window_size, 

3441 progress=progress, 

3442 ) 

3443 else: 

3444 for oid in todo: 

3445 yield full_unpacked_object(container[oid]) 

3446 

3447 

3448def full_unpacked_object(o: ShaFile) -> UnpackedObject: 

3449 """Create an UnpackedObject from a ShaFile. 

3450 

3451 Args: 

3452 o: ShaFile object to convert 

3453 

3454 Returns: 

3455 UnpackedObject with full object data 

3456 """ 

3457 return UnpackedObject( 

3458 o.type_num, 

3459 delta_base=None, 

3460 crc32=None, 

3461 decomp_chunks=o.as_raw_chunks(), 

3462 sha=o.sha().digest(), 

3463 ) 

3464 

3465 

3466def write_pack_from_container( 

3467 write: Callable[[bytes], None] 

3468 | Callable[[bytes | bytearray | memoryview], int] 

3469 | IO[bytes], 

3470 container: PackedObjectContainer, 

3471 object_ids: Sequence[tuple[ObjectID, PackHint | None]], 

3472 object_format: "ObjectFormat", 

3473 *, 

3474 delta_window_size: int | None = None, 

3475 deltify: bool | None = None, 

3476 reuse_deltas: bool = True, 

3477 compression_level: int = -1, 

3478 other_haves: set[ObjectID] | None = None, 

3479) -> tuple[dict[bytes, tuple[int, int]], bytes]: 

3480 """Write a new pack data file. 

3481 

3482 Args: 

3483 write: write function to use 

3484 container: PackedObjectContainer 

3485 object_ids: Sequence of (object_id, hint) tuples to write 

3486 object_format: Object format (hash algorithm) to use 

3487 delta_window_size: Sliding window size for searching for deltas; 

3488 Set to None for default window size. 

3489 deltify: Whether to deltify objects 

3490 reuse_deltas: Whether to reuse existing deltas 

3491 compression_level: the zlib compression level to use 

3492 other_haves: Set of additional object IDs the receiver has 

3493 Returns: Dict mapping id -> (offset, crc32 checksum), pack checksum 

3494 """ 

3495 pack_contents_count = len(object_ids) 

3496 pack_contents = generate_unpacked_objects( 

3497 container, 

3498 object_ids, 

3499 delta_window_size=delta_window_size, 

3500 deltify=deltify, 

3501 reuse_deltas=reuse_deltas, 

3502 other_haves=other_haves, 

3503 ) 

3504 

3505 return write_pack_data( 

3506 write, 

3507 pack_contents, 

3508 num_records=pack_contents_count, 

3509 compression_level=compression_level, 

3510 object_format=object_format, 

3511 ) 

3512 

3513 

3514def write_pack_objects( 

3515 write: Callable[[bytes], None] | IO[bytes], 

3516 objects: Sequence[ShaFile] | Sequence[tuple[ShaFile, bytes | None]], 

3517 object_format: "ObjectFormat", 

3518 *, 

3519 delta_window_size: int | None = None, 

3520 deltify: bool | None = None, 

3521 compression_level: int = -1, 

3522) -> tuple[dict[bytes, tuple[int, int]], bytes]: 

3523 """Write a new pack data file. 

3524 

3525 Args: 

3526 write: write function to use 

3527 objects: Sequence of (object, path) tuples to write 

3528 object_format: Object format (hash algorithm) to use 

3529 delta_window_size: Sliding window size for searching for deltas; 

3530 Set to None for default window size. 

3531 deltify: Whether to deltify objects 

3532 compression_level: the zlib compression level to use 

3533 Returns: Dict mapping id -> (offset, crc32 checksum), pack checksum 

3534 """ 

3535 pack_contents_count, pack_contents = pack_objects_to_data(objects, deltify=deltify) 

3536 

3537 return write_pack_data( 

3538 write, 

3539 pack_contents, 

3540 num_records=pack_contents_count, 

3541 compression_level=compression_level, 

3542 object_format=object_format, 

3543 ) 

3544 

3545 

3546class PackChunkGenerator: 

3547 """Generator for pack data chunks.""" 

3548 

3549 def __init__( 

3550 self, 

3551 object_format: "ObjectFormat", 

3552 num_records: int | None = None, 

3553 records: Iterator[UnpackedObject] | None = None, 

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

3555 compression_level: int = -1, 

3556 reuse_compressed: bool = True, 

3557 ) -> None: 

3558 """Initialize PackChunkGenerator. 

3559 

3560 Args: 

3561 num_records: Expected number of records 

3562 records: Iterator of pack records 

3563 progress: Optional progress callback 

3564 compression_level: Compression level (-1 for default) 

3565 reuse_compressed: Whether to reuse compressed chunks 

3566 object_format: Object format (hash algorithm) to use 

3567 """ 

3568 self.object_format = object_format 

3569 self.cs = object_format.new_hash() 

3570 self.entries: dict[bytes, tuple[int, int]] = {} 

3571 if records is None: 

3572 records = iter([]) # Empty iterator if None 

3573 self._it = self._pack_data_chunks( 

3574 records=records, 

3575 num_records=num_records, 

3576 progress=progress, 

3577 compression_level=compression_level, 

3578 reuse_compressed=reuse_compressed, 

3579 ) 

3580 

3581 def sha1digest(self) -> bytes: 

3582 """Return the SHA1 digest of the pack data.""" 

3583 return self.cs.digest() 

3584 

3585 def __iter__(self) -> Iterator[bytes]: 

3586 """Iterate over pack data chunks.""" 

3587 return self._it 

3588 

3589 def _pack_data_chunks( 

3590 self, 

3591 records: Iterator[UnpackedObject], 

3592 *, 

3593 num_records: int | None = None, 

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

3595 compression_level: int = -1, 

3596 reuse_compressed: bool = True, 

3597 ) -> Iterator[bytes]: 

3598 """Iterate pack data file chunks. 

3599 

3600 Args: 

3601 records: Iterator over UnpackedObject 

3602 num_records: Number of records (defaults to len(records) if not specified) 

3603 progress: Function to report progress to 

3604 compression_level: the zlib compression level 

3605 reuse_compressed: Whether to reuse compressed chunks 

3606 Returns: Dict mapping id -> (offset, crc32 checksum), pack checksum 

3607 """ 

3608 # Write the pack 

3609 if num_records is None: 

3610 num_records = len(records) # type: ignore 

3611 offset = 0 

3612 for chunk in pack_header_chunks(num_records): 

3613 yield chunk 

3614 self.cs.update(chunk) 

3615 offset += len(chunk) 

3616 actual_num_records = 0 

3617 for i, unpacked in enumerate(records): 

3618 type_num = unpacked.pack_type_num 

3619 if progress is not None and i % 1000 == 0: 

3620 progress((f"writing pack data: {i}/{num_records}\r").encode("ascii")) 

3621 raw: list[bytes] | tuple[int, list[bytes]] | tuple[bytes, list[bytes]] 

3622 if unpacked.delta_base is not None: 

3623 assert isinstance(unpacked.delta_base, bytes), ( 

3624 f"Expected bytes, got {type(unpacked.delta_base)}" 

3625 ) 

3626 try: 

3627 base_offset, _base_crc32 = self.entries[unpacked.delta_base] 

3628 except KeyError: 

3629 type_num = REF_DELTA 

3630 assert isinstance(unpacked.delta_base, bytes) 

3631 raw = (unpacked.delta_base, unpacked.decomp_chunks) 

3632 else: 

3633 type_num = OFS_DELTA 

3634 raw = (offset - base_offset, unpacked.decomp_chunks) 

3635 else: 

3636 raw = unpacked.decomp_chunks 

3637 chunks: list[bytes] | Iterator[bytes] 

3638 if unpacked.comp_chunks is not None and reuse_compressed: 

3639 chunks = unpacked.comp_chunks 

3640 else: 

3641 chunks = pack_object_chunks( 

3642 type_num, 

3643 raw, 

3644 compression_level=compression_level, 

3645 object_format=self.object_format, 

3646 ) 

3647 crc32 = 0 

3648 object_size = 0 

3649 for chunk in chunks: 

3650 yield chunk 

3651 crc32 = binascii.crc32(chunk, crc32) 

3652 self.cs.update(chunk) 

3653 object_size += len(chunk) 

3654 actual_num_records += 1 

3655 self.entries[unpacked.sha()] = (offset, crc32) 

3656 offset += object_size 

3657 if actual_num_records != num_records: 

3658 raise AssertionError( 

3659 f"actual records written differs: {actual_num_records} != {num_records}" 

3660 ) 

3661 

3662 yield self.cs.digest() 

3663 

3664 

3665def write_pack_data( 

3666 write: Callable[[bytes], None] 

3667 | Callable[[bytes | bytearray | memoryview], int] 

3668 | IO[bytes], 

3669 records: Iterator[UnpackedObject], 

3670 object_format: "ObjectFormat", 

3671 *, 

3672 num_records: int | None = None, 

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

3674 compression_level: int = -1, 

3675) -> tuple[dict[bytes, tuple[int, int]], bytes]: 

3676 """Write a new pack data file. 

3677 

3678 Args: 

3679 write: Write function to use 

3680 num_records: Number of records (defaults to len(records) if None) 

3681 records: Iterator over type_num, object_id, delta_base, raw 

3682 object_format: Object format (hash algorithm) to use 

3683 progress: Function to report progress to 

3684 compression_level: the zlib compression level 

3685 Returns: Dict mapping id -> (offset, crc32 checksum), pack checksum 

3686 """ 

3687 chunk_generator = PackChunkGenerator( 

3688 num_records=num_records, 

3689 records=records, 

3690 progress=progress, 

3691 compression_level=compression_level, 

3692 object_format=object_format, 

3693 ) 

3694 for chunk in chunk_generator: 

3695 if callable(write): 

3696 write(chunk) 

3697 else: 

3698 write.write(chunk) 

3699 return chunk_generator.entries, chunk_generator.sha1digest() 

3700 

3701 

3702def write_pack_index_v1( 

3703 f: IO[bytes], 

3704 entries: Iterable[tuple[bytes, int, int | None]], 

3705 pack_checksum: bytes, 

3706) -> bytes: 

3707 """Write a new pack index file. 

3708 

3709 Args: 

3710 f: A file-like object to write to 

3711 entries: List of tuples with object name (sha), offset_in_pack, 

3712 and crc32_checksum. 

3713 pack_checksum: Checksum of the pack file. 

3714 Returns: The SHA of the written index file 

3715 """ 

3716 f = SHA1Writer(f) 

3717 fan_out_table: dict[int, int] = defaultdict(lambda: 0) 

3718 for name, _offset, _entry_checksum in entries: 

3719 fan_out_table[ord(name[:1])] += 1 

3720 # Fan-out table 

3721 for i in range(0x100): 

3722 f.write(struct.pack(">L", fan_out_table[i])) 

3723 fan_out_table[i + 1] += fan_out_table[i] 

3724 for name, offset, _entry_checksum in entries: 

3725 if len(name) != 20: 

3726 raise TypeError("pack index v1 only supports SHA-1 names") 

3727 if not (offset <= 0xFFFFFFFF): 

3728 raise TypeError("pack format 1 only supports offsets < 2Gb") 

3729 f.write(struct.pack(">L20s", offset, name)) 

3730 assert len(pack_checksum) == 20 

3731 f.write(pack_checksum) 

3732 return f.write_sha() 

3733 

3734 

3735def _delta_encode_size(size: int) -> bytes: 

3736 ret = bytearray() 

3737 c = size & 0x7F 

3738 size >>= 7 

3739 while size: 

3740 ret.append(c | 0x80) 

3741 c = size & 0x7F 

3742 size >>= 7 

3743 ret.append(c) 

3744 return bytes(ret) 

3745 

3746 

3747# The length of delta compression copy operations in version 2 packs is limited 

3748# to 64K. To copy more, we use several copy operations. Version 3 packs allow 

3749# 24-bit lengths in copy operations, but we always make version 2 packs. 

3750_MAX_COPY_LEN = 0xFFFF 

3751 

3752 

3753def _encode_copy_operation(start: int, length: int) -> bytes: 

3754 scratch = bytearray([0x80]) 

3755 for i in range(4): 

3756 if start & 0xFF << i * 8: 

3757 scratch.append((start >> i * 8) & 0xFF) 

3758 scratch[0] |= 1 << i 

3759 for i in range(2): 

3760 if length & 0xFF << i * 8: 

3761 scratch.append((length >> i * 8) & 0xFF) 

3762 scratch[0] |= 1 << (4 + i) 

3763 return bytes(scratch) 

3764 

3765 

3766def _create_delta_py( 

3767 base_buf: bytes | list[bytes], target_buf: bytes | list[bytes] 

3768) -> Iterator[bytes]: 

3769 """Use python difflib to work out how to transform base_buf to target_buf. 

3770 

3771 Args: 

3772 base_buf: Base buffer 

3773 target_buf: Target buffer 

3774 """ 

3775 if isinstance(base_buf, list): 

3776 base_buf = b"".join(base_buf) 

3777 if isinstance(target_buf, list): 

3778 target_buf = b"".join(target_buf) 

3779 # write delta header 

3780 yield _delta_encode_size(len(base_buf)) 

3781 yield _delta_encode_size(len(target_buf)) 

3782 # write out delta opcodes 

3783 seq = SequenceMatcher(isjunk=None, a=base_buf, b=target_buf) 

3784 for opcode, i1, i2, j1, j2 in seq.get_opcodes(): 

3785 # Git patch opcodes don't care about deletes! 

3786 # if opcode == 'replace' or opcode == 'delete': 

3787 # pass 

3788 if opcode == "equal": 

3789 # If they are equal, unpacker will use data from base_buf 

3790 # Write out an opcode that says what range to use 

3791 copy_start = i1 

3792 copy_len = i2 - i1 

3793 while copy_len > 0: 

3794 to_copy = min(copy_len, _MAX_COPY_LEN) 

3795 yield _encode_copy_operation(copy_start, to_copy) 

3796 copy_start += to_copy 

3797 copy_len -= to_copy 

3798 if opcode == "replace" or opcode == "insert": 

3799 # If we are replacing a range or adding one, then we just 

3800 # output it to the stream (prefixed by its size) 

3801 s = j2 - j1 

3802 o = j1 

3803 while s > 127: 

3804 yield bytes([127]) 

3805 yield bytes(memoryview(target_buf)[o : o + 127]) 

3806 s -= 127 

3807 o += 127 

3808 yield bytes([s]) 

3809 yield bytes(memoryview(target_buf)[o : o + s]) 

3810 

3811 

3812# Default to pure Python implementation 

3813create_delta = _create_delta_py 

3814 

3815 

3816def apply_delta( 

3817 src_buf: bytes | list[bytes], delta: bytes | list[bytes] 

3818) -> list[bytes]: 

3819 """Based on the similar function in git's patch-delta.c. 

3820 

3821 Args: 

3822 src_buf: Source buffer 

3823 delta: Delta instructions 

3824 """ 

3825 if not isinstance(src_buf, bytes): 

3826 src_buf = b"".join(src_buf) 

3827 if not isinstance(delta, bytes): 

3828 delta = b"".join(delta) 

3829 out = [] 

3830 index = 0 

3831 delta_length = len(delta) 

3832 

3833 def get_delta_header_size(delta: bytes, index: int) -> tuple[int, int]: 

3834 size = 0 

3835 i = 0 

3836 while True: 

3837 # Bound-check explicitly: ``delta[index:index+1]`` silently 

3838 # returns b"" past the end, which would crash with TypeError 

3839 # in ``ord`` and leave the caller unable to distinguish a 

3840 # truncated delta from a programming bug. 

3841 if index >= delta_length: 

3842 raise ApplyDeltaError("delta truncated in size header") 

3843 cmd = ord(delta[index : index + 1]) 

3844 index += 1 

3845 size |= (cmd & ~0x80) << i 

3846 i += 7 

3847 if not cmd & 0x80: 

3848 break 

3849 return size, index 

3850 

3851 def read_byte(delta: bytes) -> int: 

3852 nonlocal index 

3853 # Bound-check explicitly: ``delta[index:index+1]`` silently returns 

3854 # b"" past the end, which would crash with TypeError in ``ord`` and 

3855 # leave the caller unable to distinguish a truncated delta from a 

3856 # programming bug. 

3857 if index >= delta_length: 

3858 raise ApplyDeltaError("delta truncated in copy op") 

3859 index += 1 

3860 return ord(delta[index - 1 : index]) 

3861 

3862 src_size, index = get_delta_header_size(delta, index) 

3863 dest_size, index = get_delta_header_size(delta, index) 

3864 if src_size != len(src_buf): 

3865 raise ApplyDeltaError( 

3866 f"Unexpected source buffer size: {src_size} vs {len(src_buf)}" 

3867 ) 

3868 while index < delta_length: 

3869 cmd = ord(delta[index : index + 1]) 

3870 index += 1 

3871 if cmd & 0x80: 

3872 cp_off = 0 

3873 for i in range(4): 

3874 if cmd & (1 << i): 

3875 x = read_byte(delta) 

3876 cp_off |= x << (i * 8) 

3877 cp_size = 0 

3878 # Version 3 packs can contain copy sizes larger than 64K. 

3879 for i in range(3): 

3880 if cmd & (1 << (4 + i)): 

3881 x = read_byte(delta) 

3882 cp_size |= x << (i * 8) 

3883 if cp_size == 0: 

3884 cp_size = 0x10000 

3885 if ( 

3886 cp_off + cp_size < cp_size 

3887 or cp_off + cp_size > src_size 

3888 or cp_size > dest_size 

3889 ): 

3890 break 

3891 out.append(src_buf[cp_off : cp_off + cp_size]) 

3892 elif cmd != 0: 

3893 if index + cmd > delta_length: 

3894 raise ApplyDeltaError("delta truncated in insert op") 

3895 out.append(delta[index : index + cmd]) 

3896 index += cmd 

3897 else: 

3898 raise ApplyDeltaError("Invalid opcode 0") 

3899 

3900 if index != delta_length: 

3901 raise ApplyDeltaError(f"delta not empty: {delta[index:]!r}") 

3902 

3903 if dest_size != chunks_length(out): 

3904 raise ApplyDeltaError("dest size incorrect") 

3905 

3906 return out 

3907 

3908 

3909def write_pack_index_v2( 

3910 f: IO[bytes], 

3911 entries: Iterable[tuple[bytes, int, int | None]], 

3912 pack_checksum: bytes, 

3913) -> bytes: 

3914 """Write a new pack index file. 

3915 

3916 Args: 

3917 f: File-like object to write to 

3918 entries: List of tuples with object name (sha), offset_in_pack, and 

3919 crc32_checksum. 

3920 pack_checksum: Checksum of the pack file. 

3921 Returns: The checksum of the index file written 

3922 """ 

3923 # Determine hash algorithm from pack_checksum length 

3924 if len(pack_checksum) == 20: 

3925 hash_func = sha1 

3926 elif len(pack_checksum) == 32: 

3927 hash_func = sha256 

3928 else: 

3929 raise ValueError(f"Unsupported pack checksum length: {len(pack_checksum)}") 

3930 

3931 f_writer = HashWriter(f, hash_func) 

3932 f_writer.write(b"\377tOc") # Magic! 

3933 f_writer.write(struct.pack(">L", 2)) 

3934 

3935 # Convert to list to allow multiple iterations 

3936 entries_list = list(entries) 

3937 

3938 fan_out_table: dict[int, int] = defaultdict(lambda: 0) 

3939 for name, offset, entry_checksum in entries_list: 

3940 fan_out_table[ord(name[:1])] += 1 

3941 

3942 if entries_list: 

3943 hash_size = len(entries_list[0][0]) 

3944 else: 

3945 hash_size = len(pack_checksum) # Use pack_checksum length as hash size 

3946 

3947 # Fan-out table 

3948 largetable: list[int] = [] 

3949 for i in range(0x100): 

3950 f_writer.write(struct.pack(b">L", fan_out_table[i])) 

3951 fan_out_table[i + 1] += fan_out_table[i] 

3952 for name, offset, entry_checksum in entries_list: 

3953 if len(name) != hash_size: 

3954 raise TypeError( 

3955 f"Object name has wrong length: expected {hash_size}, got {len(name)}" 

3956 ) 

3957 f_writer.write(name) 

3958 for name, offset, entry_checksum in entries_list: 

3959 f_writer.write(struct.pack(b">L", entry_checksum)) 

3960 for name, offset, entry_checksum in entries_list: 

3961 if offset < 2**31: 

3962 f_writer.write(struct.pack(b">L", offset)) 

3963 else: 

3964 f_writer.write(struct.pack(b">L", 2**31 + len(largetable))) 

3965 largetable.append(offset) 

3966 for offset in largetable: 

3967 f_writer.write(struct.pack(b">Q", offset)) 

3968 f_writer.write(pack_checksum) 

3969 return f_writer.write_hash() 

3970 

3971 

3972def write_pack_index_v3( 

3973 f: IO[bytes], 

3974 entries: Iterable[tuple[bytes, int, int | None]], 

3975 pack_checksum: bytes, 

3976 hash_format: int = 1, 

3977) -> bytes: 

3978 """Write a new pack index file in v3 format. 

3979 

3980 Args: 

3981 f: File-like object to write to 

3982 entries: List of tuples with object name (sha), offset_in_pack, and 

3983 crc32_checksum. 

3984 pack_checksum: Checksum of the pack file. 

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

3986 Returns: The SHA of the index file written 

3987 """ 

3988 if hash_format == 1: 

3989 hash_size = 20 # SHA-1 

3990 writer_cls = SHA1Writer 

3991 elif hash_format == 2: 

3992 hash_size = 32 # SHA-256 

3993 # TODO: Add SHA256Writer when SHA-256 support is implemented 

3994 raise NotImplementedError("SHA-256 support not yet implemented") 

3995 else: 

3996 raise ValueError(f"Unknown hash algorithm {hash_format}") 

3997 

3998 # Convert entries to list to allow multiple iterations 

3999 entries_list = list(entries) 

4000 

4001 # Calculate shortest unambiguous prefix length for object names 

4002 # For now, use full hash size (this could be optimized) 

4003 shortened_oid_len = hash_size 

4004 

4005 f = writer_cls(f) 

4006 f.write(b"\377tOc") # Magic! 

4007 f.write(struct.pack(">L", 3)) # Version 3 

4008 f.write(struct.pack(">L", hash_format)) # Hash algorithm 

4009 f.write(struct.pack(">L", shortened_oid_len)) # Shortened OID length 

4010 

4011 fan_out_table: dict[int, int] = defaultdict(lambda: 0) 

4012 for name, offset, entry_checksum in entries_list: 

4013 if len(name) != hash_size: 

4014 raise ValueError( 

4015 f"Object name has wrong length: expected {hash_size}, got {len(name)}" 

4016 ) 

4017 fan_out_table[ord(name[:1])] += 1 

4018 

4019 # Fan-out table 

4020 largetable: list[int] = [] 

4021 for i in range(0x100): 

4022 f.write(struct.pack(b">L", fan_out_table[i])) 

4023 fan_out_table[i + 1] += fan_out_table[i] 

4024 

4025 # Object names table 

4026 for name, offset, entry_checksum in entries_list: 

4027 f.write(name) 

4028 

4029 # CRC32 checksums table 

4030 for name, offset, entry_checksum in entries_list: 

4031 f.write(struct.pack(b">L", entry_checksum)) 

4032 

4033 # Offset table 

4034 for name, offset, entry_checksum in entries_list: 

4035 if offset < 2**31: 

4036 f.write(struct.pack(b">L", offset)) 

4037 else: 

4038 f.write(struct.pack(b">L", 2**31 + len(largetable))) 

4039 largetable.append(offset) 

4040 

4041 # Large offset table 

4042 for offset in largetable: 

4043 f.write(struct.pack(b">Q", offset)) 

4044 

4045 assert len(pack_checksum) == hash_size, ( 

4046 f"Pack checksum has wrong length: expected {hash_size}, got {len(pack_checksum)}" 

4047 ) 

4048 f.write(pack_checksum) 

4049 return f.write_sha() 

4050 

4051 

4052def write_pack_index( 

4053 f: IO[bytes], 

4054 entries: Iterable[tuple[bytes, int, int | None]], 

4055 pack_checksum: bytes, 

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

4057 version: int | None = None, 

4058) -> bytes: 

4059 """Write a pack index file. 

4060 

4061 Args: 

4062 f: File-like object to write to. 

4063 entries: List of (checksum, offset, crc32) tuples 

4064 pack_checksum: Checksum of the pack file. 

4065 progress: Progress function (not currently used) 

4066 version: Pack index version to use (1, 2, or 3). If None, defaults to DEFAULT_PACK_INDEX_VERSION. 

4067 

4068 Returns: 

4069 SHA of the written index file 

4070 

4071 Raises: 

4072 ValueError: If an unsupported version is specified 

4073 """ 

4074 if version is None: 

4075 version = DEFAULT_PACK_INDEX_VERSION 

4076 

4077 if version == 1: 

4078 return write_pack_index_v1(f, entries, pack_checksum) 

4079 elif version == 2: 

4080 return write_pack_index_v2(f, entries, pack_checksum) 

4081 elif version == 3: 

4082 return write_pack_index_v3(f, entries, pack_checksum) 

4083 else: 

4084 raise ValueError(f"Unsupported pack index version: {version}") 

4085 

4086 

4087class Pack: 

4088 """A Git pack object.""" 

4089 

4090 _data_load: Callable[[], PackData] | None 

4091 _idx_load: Callable[[], PackIndex] | None 

4092 

4093 _data: PackData | None 

4094 _idx: PackIndex | None 

4095 _bitmap: "PackBitmap | None" 

4096 

4097 def __init__( 

4098 self, 

4099 basename: str, 

4100 *, 

4101 object_format: ObjectFormat, 

4102 resolve_ext_ref: ResolveExtRefFn | None = None, 

4103 delta_window_size: int | None = None, 

4104 window_memory: int | None = None, 

4105 delta_cache_size: int | None = None, 

4106 depth: int | None = None, 

4107 threads: int | None = None, 

4108 big_file_threshold: int | None = None, 

4109 delta_base_cache_limit: int | None = None, 

4110 ) -> None: 

4111 """Initialize a Pack object. 

4112 

4113 Args: 

4114 basename: Base path for pack files (without .pack/.idx extension) 

4115 object_format: Hash algorithm used by the repository 

4116 resolve_ext_ref: Optional function to resolve external references 

4117 delta_window_size: Size of the delta compression window 

4118 window_memory: Memory limit for delta compression window 

4119 delta_cache_size: Size of the delta cache 

4120 depth: Maximum depth for delta chains 

4121 threads: Number of threads to use for operations 

4122 big_file_threshold: Size threshold for big file handling 

4123 delta_base_cache_limit: Maximum bytes for delta base object cache 

4124 """ 

4125 self._basename = basename 

4126 self.object_format = object_format 

4127 self._data = None 

4128 self._idx = None 

4129 self._bitmap = None 

4130 self._idx_path = self._basename + ".idx" 

4131 self._data_path = self._basename + ".pack" 

4132 self._bitmap_path = self._basename + ".bitmap" 

4133 self.delta_window_size = delta_window_size 

4134 self.window_memory = window_memory 

4135 self.delta_cache_size = delta_cache_size 

4136 self.depth = depth 

4137 self.threads = threads 

4138 self.big_file_threshold = big_file_threshold 

4139 self.delta_base_cache_limit = delta_base_cache_limit 

4140 self._idx_load = lambda: load_pack_index(self._idx_path, object_format) 

4141 self._data_load = lambda: PackData( 

4142 self._data_path, 

4143 delta_window_size=delta_window_size, 

4144 window_memory=window_memory, 

4145 delta_cache_size=delta_cache_size, 

4146 depth=depth, 

4147 threads=threads, 

4148 big_file_threshold=big_file_threshold, 

4149 delta_base_cache_limit=delta_base_cache_limit, 

4150 object_format=object_format, 

4151 ) 

4152 self.resolve_ext_ref = resolve_ext_ref 

4153 

4154 @classmethod 

4155 def from_lazy_objects( 

4156 cls, 

4157 data_fn: Callable[[], PackData], 

4158 idx_fn: Callable[[], PackIndex], 

4159 ) -> "Pack": 

4160 """Create a new pack object from callables to load pack data and index objects.""" 

4161 # Load index to get object format 

4162 idx = idx_fn() 

4163 ret = cls("", object_format=idx.object_format) 

4164 ret._data_load = data_fn 

4165 ret._idx = idx 

4166 ret._idx_load = None 

4167 return ret 

4168 

4169 @classmethod 

4170 def from_objects(cls, data: PackData, idx: PackIndex) -> "Pack": 

4171 """Create a new pack object from pack data and index objects.""" 

4172 ret = cls("", object_format=idx.object_format) 

4173 ret._data = data 

4174 ret._data_load = None 

4175 ret._idx = idx 

4176 ret._idx_load = None 

4177 ret.check_length_and_checksum() 

4178 return ret 

4179 

4180 def name(self) -> bytes: 

4181 """The SHA over the SHAs of the objects in this pack.""" 

4182 return self.index.objects_sha1() 

4183 

4184 @property 

4185 def data(self) -> PackData: 

4186 """The pack data object being used.""" 

4187 if self._data is None: 

4188 assert self._data_load 

4189 try: 

4190 self._data = self._data_load() 

4191 except FileNotFoundError as exc: 

4192 raise PackFileDisappeared(self) from exc 

4193 self.check_length_and_checksum() 

4194 return self._data 

4195 

4196 @property 

4197 def index(self) -> PackIndex: 

4198 """The index being used. 

4199 

4200 Note: This may be an in-memory index 

4201 """ 

4202 if self._idx is None: 

4203 assert self._idx_load 

4204 try: 

4205 self._idx = self._idx_load() 

4206 except FileNotFoundError as exc: 

4207 raise PackFileDisappeared(self) from exc 

4208 return self._idx 

4209 

4210 @property 

4211 def bitmap(self) -> "PackBitmap | None": 

4212 """The bitmap being used, if available. 

4213 

4214 Returns: 

4215 PackBitmap instance, or None if no bitmap exists or the bitmap 

4216 was built for a different pack 

4217 

4218 Raises: 

4219 ValueError: If bitmap file is invalid or corrupt 

4220 """ 

4221 if self._bitmap is None: 

4222 from .bitmap import read_bitmap 

4223 

4224 try: 

4225 self._bitmap = read_bitmap( 

4226 self._bitmap_path, 

4227 pack_index=self.index, 

4228 pack_checksum=self.get_stored_checksum(), 

4229 ) 

4230 except ChecksumMismatch: 

4231 # The bitmap records the checksum of the pack it was built for. 

4232 # A mismatch means it is stale or was swapped in from another 

4233 # pack, so its positions no longer describe this pack's objects. 

4234 # Ignore it and let callers fall back to graph traversal, the 

4235 # same as git. 

4236 logger.warning( 

4237 "Ignoring bitmap %s: checksum does not match pack", 

4238 self._bitmap_path, 

4239 ) 

4240 return None 

4241 return self._bitmap 

4242 

4243 def ensure_bitmap( 

4244 self, 

4245 object_store: "BaseObjectStore", 

4246 refs: dict["Ref", "ObjectID"], 

4247 commit_interval: int | None = None, 

4248 progress: Callable[[str], None] | None = None, 

4249 ) -> "PackBitmap": 

4250 """Ensure a bitmap exists for this pack, generating one if needed. 

4251 

4252 Args: 

4253 object_store: Object store to read objects from 

4254 refs: Dictionary of ref names to commit SHAs 

4255 commit_interval: Include every Nth commit in bitmap index 

4256 progress: Optional progress reporting callback 

4257 

4258 Returns: 

4259 PackBitmap instance (either existing or newly generated) 

4260 """ 

4261 from .bitmap import generate_bitmap, write_bitmap 

4262 

4263 # Check if bitmap already exists 

4264 try: 

4265 existing = self.bitmap 

4266 if existing is not None: 

4267 return existing 

4268 except FileNotFoundError: 

4269 pass # No bitmap, we'll generate one 

4270 

4271 # Generate new bitmap 

4272 if progress: 

4273 progress(f"Generating bitmap for {self.name().decode('utf-8')}...\n") 

4274 

4275 pack_bitmap = generate_bitmap( 

4276 self.index, 

4277 object_store, 

4278 refs, 

4279 self.get_stored_checksum(), 

4280 commit_interval=commit_interval, 

4281 progress=progress, 

4282 ) 

4283 

4284 # Write bitmap file 

4285 write_bitmap(self._bitmap_path, pack_bitmap) 

4286 

4287 if progress: 

4288 progress(f"Wrote {self._bitmap_path}\n") 

4289 

4290 # Update cached bitmap 

4291 self._bitmap = pack_bitmap 

4292 

4293 return pack_bitmap 

4294 

4295 @property 

4296 def mmap_size(self) -> int: 

4297 """Return the total mmapped memory usage of this pack. 

4298 

4299 This includes the pack data file and index file sizes, 

4300 but only for components that have been loaded (and thus mmapped). 

4301 """ 

4302 total = 0 

4303 if self._data is not None: 

4304 total += self._data._get_size() 

4305 if self._idx is not None and isinstance(self._idx, FilePackIndex): 

4306 total += self._idx._size 

4307 return total 

4308 

4309 def close(self) -> None: 

4310 """Close the pack file and index.""" 

4311 if self._data is not None: 

4312 self._data.close() 

4313 self._data = None 

4314 if self._idx is not None: 

4315 self._idx.close() 

4316 self._idx = None 

4317 

4318 def __del__(self) -> None: 

4319 """Ensure pack file is closed when Pack is garbage collected.""" 

4320 if self._data is not None or self._idx is not None: 

4321 import warnings 

4322 

4323 warnings.warn( 

4324 f"unclosed Pack {self!r}", ResourceWarning, stacklevel=2, source=self 

4325 ) 

4326 try: 

4327 self.close() 

4328 except Exception: 

4329 # Ignore errors during cleanup 

4330 pass 

4331 

4332 def __enter__(self) -> Self: 

4333 """Enter context manager.""" 

4334 return self 

4335 

4336 def __exit__( 

4337 self, 

4338 type: type | None, 

4339 value: BaseException | None, 

4340 traceback: TracebackType | None, 

4341 ) -> None: 

4342 """Exit context manager.""" 

4343 self.close() 

4344 

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

4346 """Check equality with another pack.""" 

4347 if not isinstance(other, Pack): 

4348 return False 

4349 return self.index == other.index 

4350 

4351 def __len__(self) -> int: 

4352 """Number of entries in this pack.""" 

4353 return len(self.index) 

4354 

4355 def __repr__(self) -> str: 

4356 """Return string representation of this pack.""" 

4357 return f"{self.__class__.__name__}({self._basename!r})" 

4358 

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

4360 """Iterate over all the sha1s of the objects in this pack.""" 

4361 return iter(self.index) 

4362 

4363 def check_length_and_checksum(self) -> None: 

4364 """Sanity check the length and checksum of the pack index and data.""" 

4365 assert len(self.index) == len(self.data), ( 

4366 f"Length mismatch: {len(self.index)} (index) != {len(self.data)} (data)" 

4367 ) 

4368 idx_stored_checksum = self.index.get_pack_checksum() 

4369 data_stored_checksum = self.data.get_stored_checksum() 

4370 if ( 

4371 idx_stored_checksum is not None 

4372 and idx_stored_checksum != data_stored_checksum 

4373 ): 

4374 raise ChecksumMismatch( 

4375 sha_to_hex(RawObjectID(idx_stored_checksum)), 

4376 sha_to_hex(RawObjectID(data_stored_checksum)), 

4377 ) 

4378 

4379 def check(self) -> None: 

4380 """Check the integrity of this pack. 

4381 

4382 Raises: 

4383 ChecksumMismatch: if a checksum for the index or data is wrong 

4384 """ 

4385 self.index.check() 

4386 self.data.check() 

4387 for obj in self.iterobjects(): 

4388 obj.check() 

4389 # TODO: object connectivity checks 

4390 

4391 def get_stored_checksum(self) -> bytes: 

4392 """Return the stored checksum of the pack data.""" 

4393 return self.data.get_stored_checksum() 

4394 

4395 def pack_tuples(self) -> list[tuple[ShaFile, None]]: 

4396 """Return pack tuples for all objects in pack.""" 

4397 return [(o, None) for o in self.iterobjects()] 

4398 

4399 def __contains__(self, sha1: ObjectID | RawObjectID) -> bool: 

4400 """Check whether this pack contains a particular SHA1.""" 

4401 try: 

4402 self.index.object_offset(sha1) 

4403 return True 

4404 except KeyError: 

4405 return False 

4406 

4407 def get_raw(self, sha1: RawObjectID | ObjectID) -> tuple[int, bytes]: 

4408 """Get raw object data by SHA1.""" 

4409 offset = self.index.object_offset(sha1) 

4410 obj_type, obj = self.data.get_object_at(offset) 

4411 type_num, chunks = self.resolve_object(offset, obj_type, obj) 

4412 return type_num, b"".join(chunks) # type: ignore[arg-type] 

4413 

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

4415 """Retrieve the specified SHA1.""" 

4416 type, uncomp = self.get_raw(sha1) 

4417 return ShaFile.from_raw_string(type, uncomp, sha=sha1) 

4418 

4419 def iterobjects(self) -> Iterator[ShaFile]: 

4420 """Iterate over the objects in this pack.""" 

4421 return iter( 

4422 PackInflater.for_pack_data(self.data, resolve_ext_ref=self.resolve_ext_ref) 

4423 ) 

4424 

4425 def iterobjects_subset( 

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

4427 ) -> Iterator[ShaFile]: 

4428 """Iterate over a subset of objects in this pack.""" 

4429 return ( 

4430 uo 

4431 for uo in PackInflater.for_pack_subset( 

4432 self, 

4433 shas, 

4434 allow_missing=allow_missing, 

4435 resolve_ext_ref=self.resolve_ext_ref, 

4436 ) 

4437 if uo.id in shas 

4438 ) 

4439 

4440 def iter_unpacked_subset( 

4441 self, 

4442 shas: Iterable[ObjectID | RawObjectID], 

4443 *, 

4444 include_comp: bool = False, 

4445 allow_missing: bool = False, 

4446 convert_ofs_delta: bool = False, 

4447 ) -> Iterator[UnpackedObject]: 

4448 """Iterate over unpacked objects in subset.""" 

4449 ofs_pending: dict[int, list[UnpackedObject]] = defaultdict(list) 

4450 ofs: dict[int, bytes] = {} 

4451 todo: set[ObjectID | RawObjectID] = set(shas) 

4452 for unpacked in self.iter_unpacked(include_comp=include_comp): 

4453 sha = unpacked.sha() 

4454 if unpacked.offset is not None: 

4455 ofs[unpacked.offset] = sha 

4456 hexsha = sha_to_hex(RawObjectID(sha)) 

4457 if hexsha in todo: 

4458 if unpacked.pack_type_num == OFS_DELTA: 

4459 assert isinstance(unpacked.delta_base, int) 

4460 assert unpacked.offset is not None 

4461 base_offset = unpacked.offset - unpacked.delta_base 

4462 try: 

4463 unpacked.delta_base = ofs[base_offset] 

4464 except KeyError: 

4465 ofs_pending[base_offset].append(unpacked) 

4466 continue 

4467 else: 

4468 unpacked.pack_type_num = REF_DELTA 

4469 yield unpacked 

4470 todo.remove(hexsha) 

4471 if unpacked.offset is not None: 

4472 for child in ofs_pending.pop(unpacked.offset, []): 

4473 child.pack_type_num = REF_DELTA 

4474 child.delta_base = sha 

4475 yield child 

4476 assert not ofs_pending 

4477 if not allow_missing and todo: 

4478 raise UnresolvedDeltas(list(todo)) 

4479 

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

4481 """Iterate over all unpacked objects in this pack.""" 

4482 ofs_to_entries = { 

4483 ofs: (sha, crc32) for (sha, ofs, crc32) in self.index.iterentries() 

4484 } 

4485 for unpacked in self.data.iter_unpacked(include_comp=include_comp): 

4486 assert unpacked.offset is not None 

4487 (sha, crc32) = ofs_to_entries[unpacked.offset] 

4488 unpacked._sha = sha 

4489 unpacked.crc32 = crc32 

4490 yield unpacked 

4491 

4492 def keep(self, msg: bytes | None = None) -> str: 

4493 """Add a .keep file for the pack, preventing git from garbage collecting it. 

4494 

4495 Args: 

4496 msg: A message written inside the .keep file; can be used later 

4497 to determine whether or not a .keep file is obsolete. 

4498 Returns: The path of the .keep file, as a string. 

4499 """ 

4500 keepfile_name = f"{self._basename}.keep" 

4501 with GitFile(keepfile_name, "wb") as keepfile: 

4502 if msg: 

4503 keepfile.write(msg) 

4504 keepfile.write(b"\n") 

4505 return keepfile_name 

4506 

4507 def get_ref( 

4508 self, sha: RawObjectID | ObjectID 

4509 ) -> tuple[int | None, int, OldUnpackedObject]: 

4510 """Get the object for a ref SHA, only looking in this pack.""" 

4511 # TODO: cache these results 

4512 try: 

4513 offset = self.index.object_offset(sha) 

4514 except KeyError: 

4515 offset = None 

4516 if offset: 

4517 type, obj = self.data.get_object_at(offset) 

4518 elif self.resolve_ext_ref: 

4519 type, obj = self.resolve_ext_ref(sha) 

4520 else: 

4521 raise KeyError(sha) 

4522 return offset, type, obj 

4523 

4524 def resolve_object( 

4525 self, 

4526 offset: int, 

4527 type: int, 

4528 obj: OldUnpackedObject, 

4529 get_ref: Callable[ 

4530 [RawObjectID | ObjectID], tuple[int | None, int, OldUnpackedObject] 

4531 ] 

4532 | None = None, 

4533 ) -> tuple[int, OldUnpackedObject]: 

4534 """Resolve an object, possibly resolving deltas when necessary. 

4535 

4536 Returns: Tuple with object type and contents. 

4537 """ 

4538 # Walk down the delta chain, building a stack of deltas to reach 

4539 # the requested object. 

4540 base_offset: int | None = offset 

4541 base_type = type 

4542 base_obj = obj 

4543 delta_stack = [] 

4544 while base_type in DELTA_TYPES: 

4545 prev_offset = base_offset 

4546 if get_ref is None: 

4547 get_ref = self.get_ref 

4548 assert isinstance(base_obj, tuple), ( 

4549 f"Expected delta tuple, got {base_obj.__class__.__name__}" 

4550 ) 

4551 if base_type == OFS_DELTA: 

4552 (delta_offset, delta) = base_obj 

4553 # TODO: clean up asserts and replace with nicer error messages 

4554 assert isinstance(delta_offset, int), ( 

4555 f"Expected int, got {delta_offset.__class__}" 

4556 ) 

4557 assert base_offset is not None 

4558 base_offset = base_offset - delta_offset 

4559 base_type, base_obj = self.data.get_object_at(base_offset) 

4560 assert isinstance(base_type, int) 

4561 elif base_type == REF_DELTA: 

4562 (basename, delta) = base_obj 

4563 assert ( 

4564 isinstance(basename, bytes) 

4565 and len(basename) == self.object_format.oid_length 

4566 ) 

4567 base_offset_temp, base_type, base_obj = get_ref(RawObjectID(basename)) 

4568 assert isinstance(base_type, int) 

4569 # base_offset_temp can be None for thin packs (external references) 

4570 base_offset = base_offset_temp 

4571 if base_offset == prev_offset: # object is based on itself 

4572 raise UnresolvedDeltas([basename]) 

4573 else: 

4574 raise AssertionError(f"Unexpected delta type: {base_type}") 

4575 delta_stack.append((prev_offset, base_type, delta)) 

4576 

4577 # Now grab the base object (mustn't be a delta) and apply the 

4578 # deltas all the way up the stack. 

4579 chunks = base_obj 

4580 for prev_offset, _delta_type, delta in reversed(delta_stack): 

4581 # Convert chunks to bytes for apply_delta if needed 

4582 if isinstance(chunks, list): 

4583 chunks_bytes = b"".join(chunks) 

4584 elif isinstance(chunks, tuple): 

4585 # For tuple type, second element is the actual data 

4586 _, chunk_data = chunks 

4587 if isinstance(chunk_data, list): 

4588 chunks_bytes = b"".join(chunk_data) 

4589 else: 

4590 chunks_bytes = chunk_data 

4591 else: 

4592 chunks_bytes = chunks 

4593 

4594 # Apply delta and get result as list 

4595 chunks = apply_delta(chunks_bytes, delta) 

4596 

4597 if prev_offset is not None: 

4598 self.data._offset_cache[prev_offset] = base_type, chunks 

4599 return base_type, chunks 

4600 

4601 def entries( 

4602 self, progress: Callable[[int, int], None] | None = None 

4603 ) -> Iterator[PackIndexEntry]: 

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

4605 

4606 Args: 

4607 progress: Progress function, called with current and total 

4608 object count. 

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

4610 """ 

4611 return self.data.iterentries( 

4612 progress=progress, resolve_ext_ref=self.resolve_ext_ref 

4613 ) 

4614 

4615 def sorted_entries( 

4616 self, progress: Callable[[int, int], None] | None = None 

4617 ) -> Iterator[PackIndexEntry]: 

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

4619 

4620 Args: 

4621 progress: Progress function, called with current and total 

4622 object count 

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

4624 """ 

4625 return iter( 

4626 self.data.sorted_entries( 

4627 progress=progress, resolve_ext_ref=self.resolve_ext_ref 

4628 ) 

4629 ) 

4630 

4631 def get_unpacked_object( 

4632 self, 

4633 sha: ObjectID | RawObjectID, 

4634 *, 

4635 include_comp: bool = False, 

4636 convert_ofs_delta: bool = True, 

4637 ) -> UnpackedObject: 

4638 """Get the unpacked object for a sha. 

4639 

4640 Args: 

4641 sha: SHA of object to fetch 

4642 include_comp: Whether to include compression data in UnpackedObject 

4643 convert_ofs_delta: Whether to convert offset deltas to ref deltas 

4644 """ 

4645 offset = self.index.object_offset(sha) 

4646 unpacked = self.data.get_unpacked_object_at(offset, include_comp=include_comp) 

4647 if unpacked.pack_type_num == OFS_DELTA and convert_ofs_delta: 

4648 assert isinstance(unpacked.delta_base, int) 

4649 unpacked.delta_base = self.index.object_sha1(offset - unpacked.delta_base) 

4650 unpacked.pack_type_num = REF_DELTA 

4651 return unpacked 

4652 

4653 

4654def extend_pack( 

4655 f: BinaryIO, 

4656 object_ids: Set["RawObjectID"], 

4657 get_raw: Callable[["RawObjectID | ObjectID"], tuple[int, bytes]], 

4658 object_format: "ObjectFormat", 

4659 *, 

4660 compression_level: int = -1, 

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

4662) -> tuple[bytes, list[tuple[RawObjectID, int, int]]]: 

4663 """Extend a pack file with more objects. 

4664 

4665 The caller should make sure that object_ids does not contain any objects 

4666 that are already in the pack 

4667 """ 

4668 # Update the header with the new number of objects. 

4669 f.seek(0) 

4670 _version, num_objects = read_pack_header(f.read) 

4671 

4672 if object_ids: 

4673 f.seek(0) 

4674 write_pack_header(f.write, num_objects + len(object_ids)) 

4675 

4676 # Must flush before reading (http://bugs.python.org/issue3207) 

4677 f.flush() 

4678 

4679 # Rescan the rest of the pack, computing the SHA with the new header. 

4680 new_sha = compute_file_sha( 

4681 f, hash_func=object_format.hash_func, end_ofs=-object_format.oid_length 

4682 ) 

4683 

4684 # Must reposition before writing (http://bugs.python.org/issue3207) 

4685 f.seek(0, os.SEEK_CUR) 

4686 

4687 extra_entries = [] 

4688 

4689 # Complete the pack. 

4690 for i, object_id in enumerate(object_ids): 

4691 if progress is not None: 

4692 progress( 

4693 (f"writing extra base objects: {i}/{len(object_ids)}\r").encode("ascii") 

4694 ) 

4695 assert len(object_id) == object_format.oid_length 

4696 type_num, data = get_raw(object_id) 

4697 offset = f.tell() 

4698 crc32 = write_pack_object( 

4699 f.write, 

4700 type_num, 

4701 [data], # Convert bytes to list[bytes] 

4702 sha=new_sha, 

4703 compression_level=compression_level, 

4704 object_format=object_format, 

4705 ) 

4706 extra_entries.append((object_id, offset, crc32)) 

4707 pack_sha = new_sha.digest() 

4708 f.write(pack_sha) 

4709 return pack_sha, extra_entries 

4710 

4711 

4712try: 

4713 from dulwich._pack import ( # type: ignore 

4714 apply_delta, 

4715 bisect_find_sha, 

4716 ) 

4717except ImportError: 

4718 pass 

4719 

4720# Try to import the Rust version of create_delta 

4721try: 

4722 from dulwich._pack import create_delta as _create_delta_rs 

4723except ImportError: 

4724 pass 

4725else: 

4726 # Wrap the Rust version to match the Python API (returns bytes instead of Iterator) 

4727 def _create_delta_rs_wrapper( 

4728 base_buf: bytes | list[bytes], target_buf: bytes | list[bytes] 

4729 ) -> Iterator[bytes]: 

4730 """Wrapper for Rust create_delta to match Python API.""" 

4731 if isinstance(base_buf, list): 

4732 base_buf = b"".join(base_buf) 

4733 if isinstance(target_buf, list): 

4734 target_buf = b"".join(target_buf) 

4735 yield _create_delta_rs(base_buf, target_buf) 

4736 

4737 create_delta = _create_delta_rs_wrapper