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

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

1146 statements  

1# repo.py -- For dealing with git repositories. 

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 

24"""Repository access. 

25 

26This module contains the base class for git repositories 

27(BaseRepo) and an implementation which uses a repository on 

28local disk (Repo). 

29 

30""" 

31 

32__all__ = [ 

33 "BASE_DIRECTORIES", 

34 "COMMONDIR", 

35 "CONTROLDIR", 

36 "DEFAULT_BRANCH", 

37 "DEFAULT_OFS_DELTA", 

38 "GITDIR", 

39 "INDEX_FILENAME", 

40 "OBJECTDIR", 

41 "REFSDIR", 

42 "REFSDIR_HEADS", 

43 "REFSDIR_TAGS", 

44 "WORKTREES", 

45 "BaseRepo", 

46 "DefaultIdentityNotFound", 

47 "InvalidUserIdentity", 

48 "MemoryRepo", 

49 "ParentsProvider", 

50 "Repo", 

51 "UnsupportedExtension", 

52 "UnsupportedVersion", 

53 "check_user_identity", 

54 "get_user_identity", 

55 "parse_graftpoints", 

56 "parse_shared_repository", 

57 "read_gitfile", 

58 "serialize_graftpoints", 

59] 

60 

61import logging 

62import os 

63import stat 

64import sys 

65import time 

66import warnings 

67from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence 

68from io import BytesIO 

69from types import TracebackType 

70from typing import ( 

71 TYPE_CHECKING, 

72 Any, 

73 BinaryIO, 

74 TypeVar, 

75) 

76 

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

78 from typing import Self 

79else: 

80 from typing_extensions import Self 

81 

82if TYPE_CHECKING: 

83 # There are no circular imports here, but we try to defer imports as long 

84 # as possible to reduce start-up time for anything that doesn't need 

85 # these imports. 

86 from .attrs import GitAttributes 

87 from .config import ConditionMatcher, Config, ConfigFile, StackedConfig 

88 from .diff_tree import RenameDetector 

89 from .filters import FilterBlobNormalizer, FilterContext 

90 from .index import Index 

91 from .notes import Notes 

92 from .object_format import ObjectFormat 

93 from .object_store import BaseObjectStore, GraphWalker 

94 from .pack import UnpackedObject 

95 from .rebase import RebaseStateManager 

96 from .walk import Walker 

97 from .worktree import WorkTree 

98 

99from . import reflog 

100from .errors import ( 

101 NoIndexPresent, 

102 NotBlobError, 

103 NotCommitError, 

104 NotGitRepository, 

105 NotTagError, 

106 NotTreeError, 

107 RefFormatError, 

108) 

109from .file import GitFile 

110from .hooks import ( 

111 CommitMsgShellHook, 

112 Hook, 

113 PostCommitShellHook, 

114 PostReceiveShellHook, 

115 PreCommitShellHook, 

116 PreReceiveShellHook, 

117 UpdateShellHook, 

118) 

119from .object_store import ( 

120 DiskObjectStore, 

121 MemoryObjectStore, 

122 MissingObjectFinder, 

123 ObjectStoreGraphWalker, 

124 PackBasedObjectStore, 

125 PackCapableObjectStore, 

126 find_shallow, 

127 peel_sha, 

128) 

129from .objects import ( 

130 Blob, 

131 Commit, 

132 ObjectID, 

133 RawObjectID, 

134 ShaFile, 

135 Tag, 

136 Tree, 

137 check_hexsha, 

138 valid_hexsha, 

139) 

140from .pack import generate_unpacked_objects 

141from .refs import ( 

142 HEADREF, 

143 LOCAL_TAG_PREFIX, # noqa: F401 

144 SYMREF, # noqa: F401 

145 DictRefsContainer, 

146 DiskRefsContainer, 

147 Ref, 

148 RefsContainer, 

149 _set_default_branch, 

150 _set_head, 

151 _set_origin_head, 

152 check_ref_format, # noqa: F401 

153 extract_branch_name, 

154 is_per_worktree_ref, 

155 local_branch_name, 

156 read_packed_refs, # noqa: F401 

157 read_packed_refs_with_peeled, # noqa: F401 

158 write_packed_refs, # noqa: F401 

159) 

160 

161logger = logging.getLogger(__name__) 

162 

163CONTROLDIR = ".git" 

164OBJECTDIR = "objects" 

165DEFAULT_OFS_DELTA = True 

166 

167T = TypeVar("T", bound="ShaFile") 

168REFSDIR = "refs" 

169REFSDIR_TAGS = "tags" 

170REFSDIR_HEADS = "heads" 

171INDEX_FILENAME = "index" 

172COMMONDIR = "commondir" 

173GITDIR = "gitdir" 

174WORKTREES = "worktrees" 

175 

176BASE_DIRECTORIES = [ 

177 ["branches"], 

178 [REFSDIR], 

179 [REFSDIR, REFSDIR_TAGS], 

180 [REFSDIR, REFSDIR_HEADS], 

181 ["hooks"], 

182 ["info"], 

183] 

184 

185DEFAULT_BRANCH = b"master" 

186 

187 

188class InvalidUserIdentity(Exception): 

189 """User identity is not of the format 'user <email>'.""" 

190 

191 def __init__(self, identity: str) -> None: 

192 """Initialize InvalidUserIdentity exception.""" 

193 self.identity = identity 

194 

195 

196class DefaultIdentityNotFound(Exception): 

197 """Default identity could not be determined.""" 

198 

199 

200# TODO(jelmer): Cache? 

201def _get_default_identity(env: Mapping[str, str] | None = None) -> tuple[str, str]: 

202 import socket 

203 

204 if env is None: 

205 env = os.environ 

206 

207 for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): 

208 username = env.get(name) 

209 if username: 

210 break 

211 else: 

212 username = None 

213 

214 try: 

215 import pwd 

216 except ImportError: 

217 fullname = None 

218 else: 

219 try: 

220 entry = pwd.getpwuid(os.getuid()) # type: ignore[attr-defined,unused-ignore] 

221 except KeyError: 

222 fullname = None 

223 else: 

224 if getattr(entry, "gecos", None): 

225 fullname = entry.pw_gecos.split(",")[0] 

226 else: 

227 fullname = None 

228 if username is None: 

229 username = entry.pw_name 

230 if not fullname: 

231 if username is None: 

232 raise DefaultIdentityNotFound("no username found") 

233 fullname = username 

234 email = env.get("EMAIL") 

235 if email is None: 

236 if username is None: 

237 raise DefaultIdentityNotFound("no username found") 

238 email = f"{username}@{socket.gethostname()}" 

239 return (fullname, email) 

240 

241 

242def get_user_identity(config: "Config", kind: str | None = None) -> bytes: 

243 """Determine the identity to use for new commits. 

244 

245 If kind is set, this first checks 

246 GIT_${KIND}_NAME and GIT_${KIND}_EMAIL. 

247 

248 If those variables are not set, then it will fall back 

249 to reading the user.name and user.email settings from 

250 the specified configuration. 

251 

252 If that also fails, then it will fall back to using 

253 the current users' identity as obtained from the host 

254 system (e.g. the gecos field, $EMAIL, $USER@$(hostname -f). 

255 

256 Args: 

257 config: Configuration stack to read from 

258 kind: Optional kind to return identity for, 

259 usually either "AUTHOR" or "COMMITTER". 

260 

261 Returns: 

262 A user identity 

263 """ 

264 user: bytes | None = None 

265 email: bytes | None = None 

266 if kind: 

267 user_uc = os.environ.get("GIT_" + kind + "_NAME") 

268 if user_uc is not None: 

269 user = user_uc.encode("utf-8") 

270 email_uc = os.environ.get("GIT_" + kind + "_EMAIL") 

271 if email_uc is not None: 

272 email = email_uc.encode("utf-8") 

273 if user is None: 

274 try: 

275 user = config.get(("user",), "name") 

276 except KeyError: 

277 user = None 

278 if email is None: 

279 try: 

280 email = config.get(("user",), "email") 

281 except KeyError: 

282 email = None 

283 default_user, default_email = _get_default_identity() 

284 if user is None: 

285 user = default_user.encode("utf-8") 

286 if email is None: 

287 email = default_email.encode("utf-8") 

288 if email.startswith(b"<") and email.endswith(b">"): 

289 email = email[1:-1] 

290 return user + b" <" + email + b">" 

291 

292 

293def check_user_identity(identity: bytes) -> None: 

294 """Verify that a user identity is formatted correctly. 

295 

296 Args: 

297 identity: User identity bytestring 

298 Raises: 

299 InvalidUserIdentity: Raised when identity is invalid 

300 """ 

301 try: 

302 _fst, snd = identity.split(b" <", 1) 

303 except ValueError as exc: 

304 raise InvalidUserIdentity(identity.decode("utf-8", "replace")) from exc 

305 if b">" not in snd: 

306 raise InvalidUserIdentity(identity.decode("utf-8", "replace")) 

307 if b"\0" in identity or b"\n" in identity: 

308 raise InvalidUserIdentity(identity.decode("utf-8", "replace")) 

309 

310 

311def parse_graftpoints( 

312 graftpoints: Iterable[bytes], 

313) -> dict[ObjectID, list[ObjectID]]: 

314 """Convert a list of graftpoints into a dict. 

315 

316 Args: 

317 graftpoints: Iterator of graftpoint lines 

318 

319 Each line is formatted as: 

320 <commit sha1> <parent sha1> [<parent sha1>]* 

321 

322 Resulting dictionary is: 

323 <commit sha1>: [<parent sha1>*] 

324 

325 https://git.wiki.kernel.org/index.php/GraftPoint 

326 """ 

327 grafts: dict[ObjectID, list[ObjectID]] = {} 

328 for line in graftpoints: 

329 raw_graft = line.split(None, 1) 

330 

331 commit = ObjectID(raw_graft[0]) 

332 if len(raw_graft) == 2: 

333 parents = [ObjectID(p) for p in raw_graft[1].split()] 

334 else: 

335 parents = [] 

336 

337 for sha in [commit, *parents]: 

338 check_hexsha(sha, "Invalid graftpoint") 

339 

340 grafts[commit] = parents 

341 return grafts 

342 

343 

344def serialize_graftpoints(graftpoints: Mapping[ObjectID, Sequence[ObjectID]]) -> bytes: 

345 """Convert a dictionary of grafts into string. 

346 

347 The graft dictionary is: 

348 <commit sha1>: [<parent sha1>*] 

349 

350 Each line is formatted as: 

351 <commit sha1> <parent sha1> [<parent sha1>]* 

352 

353 https://git.wiki.kernel.org/index.php/GraftPoint 

354 

355 """ 

356 graft_lines = [] 

357 for commit, parents in graftpoints.items(): 

358 if parents: 

359 graft_lines.append(commit + b" " + b" ".join(parents)) 

360 else: 

361 graft_lines.append(commit) 

362 return b"\n".join(graft_lines) 

363 

364 

365def _set_filesystem_hidden(path: str) -> None: 

366 """Mark path as to be hidden if supported by platform and filesystem. 

367 

368 On win32 uses SetFileAttributesW api: 

369 <https://docs.microsoft.com/windows/desktop/api/fileapi/nf-fileapi-setfileattributesw> 

370 """ 

371 if sys.platform == "win32": 

372 import ctypes 

373 from ctypes.wintypes import BOOL, DWORD, LPCWSTR 

374 

375 FILE_ATTRIBUTE_HIDDEN = 2 

376 SetFileAttributesW = ctypes.WINFUNCTYPE(BOOL, LPCWSTR, DWORD)( 

377 ("SetFileAttributesW", ctypes.windll.kernel32) 

378 ) 

379 

380 if isinstance(path, bytes): 

381 path = os.fsdecode(path) 

382 if not SetFileAttributesW(path, FILE_ATTRIBUTE_HIDDEN): 

383 pass # Could raise or log `ctypes.WinError()` here 

384 

385 # Could implement other platform specific filesystem hiding here 

386 

387 

388def parse_shared_repository( 

389 value: str | bytes | bool, 

390) -> tuple[int | None, int | None]: 

391 """Parse core.sharedRepository configuration value. 

392 

393 Args: 

394 value: Configuration value (string, bytes, or boolean) 

395 

396 Returns: 

397 tuple of (file_mask, directory_mask) or (None, None) if not shared 

398 

399 The masks are permission bits to apply via chmod. 

400 """ 

401 if isinstance(value, bytes): 

402 value = value.decode("utf-8", errors="replace") 

403 

404 # Handle boolean values 

405 if isinstance(value, bool): 

406 if value: 

407 # true = group (same as "group") 

408 return (0o664, 0o2775) 

409 else: 

410 # false = umask (use system umask, no adjustment) 

411 return (None, None) 

412 

413 # Handle string values 

414 value_lower = value.lower() 

415 

416 if value_lower in ("false", "0", ""): 

417 # Use umask (no adjustment) 

418 return (None, None) 

419 

420 if value_lower in ("true", "1", "group"): 

421 # Group writable (with setgid bit) 

422 return (0o664, 0o2775) 

423 

424 if value_lower in ("all", "world", "everybody", "2"): 

425 # World readable/writable (with setgid bit) 

426 return (0o666, 0o2777) 

427 

428 if value_lower == "umask": 

429 # Explicitly use umask 

430 return (None, None) 

431 

432 # Try to parse as octal 

433 if value.startswith("0"): 

434 try: 

435 mode = int(value, 8) 

436 # For directories, add execute bits where read bits are set 

437 # and add setgid bit for shared repositories 

438 dir_mode = mode | 0o2000 # Add setgid bit 

439 if mode & 0o004: 

440 dir_mode |= 0o001 

441 if mode & 0o040: 

442 dir_mode |= 0o010 

443 if mode & 0o400: 

444 dir_mode |= 0o100 

445 return (mode, dir_mode) 

446 except ValueError: 

447 pass 

448 

449 # Default to umask for unrecognized values 

450 return (None, None) 

451 

452 

453def _enable_relative_worktrees_extension(repo: "Repo") -> None: 

454 """Enable the relativeworktrees extension in repository config. 

455 

456 This sets core.repositoryformatversion to 1 (if not already) and 

457 enables the extensions.relativeworktrees extension. 

458 

459 Args: 

460 repo: The repository to configure 

461 """ 

462 config = repo.get_config() 

463 

464 # Ensure repository format version is at least 1 

465 try: 

466 version = int(config.get(("core",), "repositoryformatversion")) 

467 except KeyError: 

468 version = 0 

469 

470 if version < 1: 

471 config.set(("core",), "repositoryformatversion", "1") 

472 

473 # Enable the relativeworktrees extension 

474 config.set(("extensions",), "relativeworktrees", True) 

475 config.write_to_path() 

476 

477 

478class ParentsProvider: 

479 """Provider for commit parent information.""" 

480 

481 def __init__( 

482 self, 

483 store: "BaseObjectStore", 

484 grafts: dict[ObjectID, list[ObjectID]] = {}, 

485 shallows: Iterable[ObjectID] = [], 

486 ) -> None: 

487 """Initialize ParentsProvider. 

488 

489 Args: 

490 store: Object store to use 

491 grafts: Graft information 

492 shallows: Shallow commit SHAs 

493 """ 

494 self.store = store 

495 self.grafts = grafts 

496 self.shallows = set(shallows) 

497 

498 # Get commit graph once at initialization for performance 

499 self.commit_graph = store.get_commit_graph() 

500 

501 def get_parents( 

502 self, commit_id: ObjectID, commit: Commit | None = None 

503 ) -> list[ObjectID]: 

504 """Get parents for a commit using the parents provider.""" 

505 try: 

506 return self.grafts[commit_id] 

507 except KeyError: 

508 pass 

509 if commit_id in self.shallows: 

510 return [] 

511 

512 # Try to use commit graph for faster parent lookup 

513 if self.commit_graph: 

514 parents = self.commit_graph.get_parents(commit_id) 

515 if parents is not None: 

516 return parents 

517 

518 # Fallback to reading the commit object 

519 if commit is None: 

520 obj = self.store[commit_id] 

521 if not isinstance(obj, Commit): 

522 raise ValueError( 

523 f"Expected Commit object for commit_id {commit_id.decode()}, " 

524 f"got {type(obj).__name__}. This usually means a reference " 

525 f"points to a {type(obj).__name__} object instead of a Commit." 

526 ) 

527 commit = obj 

528 result: list[ObjectID] = commit.parents 

529 return result 

530 

531 

532class BaseRepo: 

533 """Base class for a git repository. 

534 

535 This base class is meant to be used for Repository implementations that e.g. 

536 work on top of a different transport than a standard filesystem path. 

537 

538 Attributes: 

539 object_store: Dictionary-like object for accessing 

540 the objects 

541 refs: Dictionary-like object with the refs in this 

542 repository 

543 """ 

544 

545 def __init__( 

546 self, 

547 object_store: "PackCapableObjectStore", 

548 refs: RefsContainer, 

549 object_format: "ObjectFormat | None" = None, 

550 ) -> None: 

551 """Open a repository. 

552 

553 This shouldn't be called directly, but rather through one of the 

554 base classes, such as MemoryRepo or Repo. 

555 

556 Args: 

557 object_store: Object store to use 

558 refs: Refs container to use 

559 object_format: Hash algorithm to use (if None, will use object_store's format) 

560 """ 

561 self.object_store = object_store 

562 self.refs = refs 

563 

564 self._graftpoints: dict[ObjectID, list[ObjectID]] = {} 

565 self.hooks: dict[str, Hook] = {} 

566 if object_format is None: 

567 self.object_format: ObjectFormat = object_store.object_format 

568 else: 

569 self.object_format = object_format 

570 

571 def _determine_file_mode(self) -> bool: 

572 """Probe the file-system to determine whether permissions can be trusted. 

573 

574 Returns: True if permissions can be trusted, False otherwise. 

575 """ 

576 raise NotImplementedError(self._determine_file_mode) 

577 

578 def _determine_symlinks(self) -> bool: 

579 """Probe the filesystem to determine whether symlinks can be created. 

580 

581 Returns: True if symlinks can be created, False otherwise. 

582 """ 

583 # For now, just mimic the old behaviour 

584 return sys.platform != "win32" 

585 

586 def _init_files( 

587 self, 

588 bare: bool, 

589 symlinks: bool | None = None, 

590 format: int | None = None, 

591 shared_repository: str | bool | None = None, 

592 object_format: str | None = None, 

593 ) -> None: 

594 """Initialize a default set of named files.""" 

595 from .config import ConfigFile 

596 

597 self._put_named_file("description", b"Unnamed repository") 

598 f = BytesIO() 

599 cf = ConfigFile() 

600 

601 # Determine the appropriate format version 

602 if object_format == "sha256": 

603 # SHA256 requires format version 1 

604 if format is None: 

605 format = 1 

606 elif format != 1: 

607 raise ValueError( 

608 "SHA256 object format requires repository format version 1" 

609 ) 

610 else: 

611 # SHA1 (default) can use format 0 or 1 

612 if format is None: 

613 format = 0 

614 

615 if format not in (0, 1): 

616 raise ValueError(f"Unsupported repository format version: {format}") 

617 

618 cf.set("core", "repositoryformatversion", str(format)) 

619 

620 # Set object format extension if using SHA256 

621 if object_format == "sha256": 

622 cf.set("extensions", "objectformat", "sha256") 

623 

624 # Set hash algorithm based on object format 

625 from .object_format import get_object_format 

626 

627 self.object_format = get_object_format(object_format) 

628 

629 if self._determine_file_mode(): 

630 cf.set("core", "filemode", True) 

631 else: 

632 cf.set("core", "filemode", False) 

633 

634 if symlinks is None and not bare: 

635 symlinks = self._determine_symlinks() 

636 

637 if symlinks is False: 

638 cf.set("core", "symlinks", symlinks) 

639 

640 # On macOS, set precomposeunicode to true since HFS+/APFS 

641 # returns filenames in NFD (decomposed) Unicode form 

642 if sys.platform == "darwin": 

643 cf.set("core", "precomposeunicode", True) 

644 

645 cf.set("core", "bare", bare) 

646 cf.set("core", "logallrefupdates", True) 

647 

648 # Set shared repository if specified 

649 if shared_repository is not None: 

650 if isinstance(shared_repository, bool): 

651 cf.set("core", "sharedRepository", shared_repository) 

652 else: 

653 cf.set("core", "sharedRepository", shared_repository) 

654 

655 cf.write_to_file(f) 

656 self._put_named_file("config", f.getvalue()) 

657 self._put_named_file(os.path.join("info", "exclude"), b"") 

658 

659 # Allow subclasses to handle config initialization 

660 self._init_config(cf) 

661 

662 def _init_config(self, config: "ConfigFile") -> None: 

663 """Initialize repository configuration. 

664 

665 This method can be overridden by subclasses to handle config initialization. 

666 

667 Args: 

668 config: The ConfigFile object that was just created 

669 """ 

670 # Default implementation does nothing 

671 

672 def get_named_file(self, path: str) -> BinaryIO | None: 

673 """Get a file from the control dir with a specific name. 

674 

675 Although the filename should be interpreted as a filename relative to 

676 the control dir in a disk-based Repo, the object returned need not be 

677 pointing to a file in that location. 

678 

679 Args: 

680 path: The path to the file, relative to the control dir. 

681 Returns: An open file object, or None if the file does not exist. 

682 """ 

683 raise NotImplementedError(self.get_named_file) 

684 

685 def _put_named_file(self, path: str, contents: bytes) -> None: 

686 """Write a file to the control dir with the given name and contents. 

687 

688 Args: 

689 path: The path to the file, relative to the control dir. 

690 contents: A string to write to the file. 

691 """ 

692 raise NotImplementedError(self._put_named_file) 

693 

694 def _del_named_file(self, path: str) -> None: 

695 """Delete a file in the control directory with the given name.""" 

696 raise NotImplementedError(self._del_named_file) 

697 

698 def open_index(self, config: "Config | None" = None) -> "Index": 

699 """Open the index for this repository. 

700 

701 Args: 

702 config: Configuration to consult for index settings. If None, 

703 implementations may fall back to ``self.get_config_stack()``. 

704 

705 Raises: 

706 NoIndexPresent: If no index is present 

707 Returns: The matching `Index` 

708 """ 

709 raise NotImplementedError(self.open_index) 

710 

711 def _change_object_format(self, object_format_name: str) -> None: 

712 """Change the object format of this repository. 

713 

714 This can only be done if the object store is empty (no objects written yet). 

715 

716 Args: 

717 object_format_name: Name of the new object format (e.g., "sha1", "sha256") 

718 

719 Raises: 

720 AssertionError: If the object store is not empty 

721 """ 

722 # Check if object store has any objects 

723 for _ in self.object_store: 

724 raise AssertionError( 

725 "Cannot change object format: repository already contains objects" 

726 ) 

727 

728 # Update the object format 

729 from .object_format import get_object_format 

730 

731 new_format = get_object_format(object_format_name) 

732 self.object_format = new_format 

733 self.object_store.object_format = new_format 

734 

735 # Update config file 

736 config = self.get_config() 

737 

738 if object_format_name == "sha1": 

739 # For SHA-1, explicitly remove objectformat extension if present 

740 try: 

741 config.remove("extensions", "objectformat") 

742 except KeyError: 

743 pass 

744 else: 

745 # For non-SHA-1 formats, set repositoryformatversion to 1 and objectformat extension 

746 config.set("core", "repositoryformatversion", "1") 

747 config.set("extensions", "objectformat", object_format_name) 

748 

749 config.write_to_path() 

750 

751 def fetch( 

752 self, 

753 target: "BaseRepo", 

754 determine_wants: Callable[[Mapping[Ref, ObjectID], int | None], list[ObjectID]] 

755 | None = None, 

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

757 depth: int | None = None, 

758 ) -> dict[Ref, ObjectID]: 

759 """Fetch objects into another repository. 

760 

761 Args: 

762 target: The target repository 

763 determine_wants: Optional function to determine what refs to 

764 fetch. 

765 progress: Optional progress function 

766 depth: Optional shallow fetch depth 

767 Returns: The local refs 

768 """ 

769 # Fix object format if needed 

770 if self.object_format != target.object_format: 

771 # Change the target repo's format if it's empty 

772 target._change_object_format(self.object_format.name) 

773 

774 if determine_wants is None: 

775 determine_wants = target.object_store.determine_wants_all 

776 count, pack_data = self.fetch_pack_data( 

777 determine_wants, 

778 target.get_graph_walker(), 

779 progress=progress, 

780 depth=depth, 

781 ) 

782 target.object_store.add_pack_data(count, pack_data, progress) 

783 return self.get_refs() 

784 

785 def fetch_pack_data( 

786 self, 

787 determine_wants: Callable[[Mapping[Ref, ObjectID], int | None], list[ObjectID]], 

788 graph_walker: "GraphWalker", 

789 progress: Callable[[bytes], None] | None, 

790 *, 

791 get_tagged: Callable[[], dict[ObjectID, ObjectID]] | None = None, 

792 depth: int | None = None, 

793 ) -> tuple[int, Iterator["UnpackedObject"]]: 

794 """Fetch the pack data required for a set of revisions. 

795 

796 Args: 

797 determine_wants: Function that takes a dictionary with heads 

798 and returns the list of heads to fetch. 

799 graph_walker: Object that can iterate over the list of revisions 

800 to fetch and has an "ack" method that will be called to acknowledge 

801 that a revision is present. 

802 progress: Simple progress function that will be called with 

803 updated progress strings. 

804 get_tagged: Function that returns a dict of pointed-to sha -> 

805 tag sha for including tags. 

806 depth: Shallow fetch depth 

807 Returns: count and iterator over pack data 

808 """ 

809 missing_objects = self.find_missing_objects( 

810 determine_wants, graph_walker, progress, get_tagged=get_tagged, depth=depth 

811 ) 

812 if missing_objects is None: 

813 return 0, iter([]) 

814 remote_has = missing_objects.get_remote_has() 

815 object_ids = list(missing_objects) 

816 return len(object_ids), generate_unpacked_objects( 

817 self.object_store, object_ids, progress=progress, other_haves=remote_has 

818 ) 

819 

820 def find_missing_objects( 

821 self, 

822 determine_wants: Callable[[Mapping[Ref, ObjectID], int | None], list[ObjectID]], 

823 graph_walker: "GraphWalker", 

824 progress: Callable[[bytes], None] | None, 

825 *, 

826 get_tagged: Callable[[], dict[ObjectID, ObjectID]] | None = None, 

827 depth: int | None = None, 

828 ) -> MissingObjectFinder | None: 

829 """Fetch the missing objects required for a set of revisions. 

830 

831 Args: 

832 determine_wants: Function that takes a dictionary with heads 

833 and returns the list of heads to fetch. 

834 graph_walker: Object that can iterate over the list of revisions 

835 to fetch and has an "ack" method that will be called to acknowledge 

836 that a revision is present. 

837 progress: Simple progress function that will be called with 

838 updated progress strings. 

839 get_tagged: Function that returns a dict of pointed-to sha -> 

840 tag sha for including tags. 

841 depth: Shallow fetch depth 

842 Returns: iterator over objects, with __len__ implemented 

843 """ 

844 # Filter out refs pointing to missing objects to avoid errors downstream. 

845 # This makes Dulwich more robust when dealing with broken refs on disk. 

846 # Previously serialize_refs() did this filtering as a side-effect. 

847 all_refs = self.get_refs() 

848 refs: dict[Ref, ObjectID] = {} 

849 for ref, sha in all_refs.items(): 

850 if sha in self.object_store: 

851 refs[ref] = sha 

852 else: 

853 logger.warning( 

854 "ref %s points at non-present sha %s", 

855 ref.decode("utf-8", "replace"), 

856 sha.decode("ascii"), 

857 ) 

858 

859 wants = determine_wants(refs, depth) 

860 if not isinstance(wants, list): 

861 raise TypeError("determine_wants() did not return a list") 

862 

863 current_shallow = set(getattr(graph_walker, "shallow", set())) 

864 

865 unshallow: set[ObjectID] = set() 

866 if depth not in (None, 0): 

867 assert depth is not None 

868 shallow, not_shallow = find_shallow(self.object_store, wants, depth) 

869 # Only update if graph_walker has shallow attribute 

870 walker_shallow: set[ObjectID] | None = getattr( 

871 graph_walker, "shallow", None 

872 ) 

873 if walker_shallow is not None: 

874 unshallow = not_shallow & current_shallow 

875 # Commits that were a shallow boundary but are now being 

876 # deepened past must drop out of the boundary, otherwise their 

877 # parents stay unreachable and never get transferred. 

878 walker_shallow.update(shallow - not_shallow) 

879 walker_shallow.difference_update(unshallow) 

880 new_shallow = walker_shallow - current_shallow 

881 setattr(graph_walker, "unshallow", unshallow) 

882 update_shallow = getattr(graph_walker, "update_shallow", None) 

883 if update_shallow is not None: 

884 update_shallow(new_shallow, unshallow) 

885 else: 

886 unshallow = getattr(graph_walker, "unshallow", set()) 

887 

888 if wants == []: 

889 # TODO(dborowitz): find a way to short-circuit that doesn't change 

890 # this interface. 

891 

892 if getattr(graph_walker, "shallow", set()) or unshallow: 

893 # Do not send a pack in shallow short-circuit path 

894 return None 

895 

896 # Return an actual MissingObjectFinder with empty wants 

897 return MissingObjectFinder( 

898 self.object_store, 

899 haves=[], 

900 wants=[], 

901 ) 

902 

903 # If the graph walker is set up with an implementation that can 

904 # ACK/NAK to the wire, it will write data to the client through 

905 # this call as a side-effect. 

906 haves = self.object_store.find_common_revisions(graph_walker) 

907 

908 # Deal with shallow requests separately because the haves do 

909 # not reflect what objects are missing 

910 if getattr(graph_walker, "shallow", set()) or unshallow: 

911 # TODO: filter the haves commits from iter_shas. the specific 

912 # commits aren't missing. 

913 haves = [] 

914 

915 parents_provider = ParentsProvider( 

916 self.object_store, 

917 shallows=getattr(graph_walker, "shallow", current_shallow), 

918 ) 

919 

920 def get_parents(commit: Commit) -> list[ObjectID]: 

921 """Get parents for a commit using the parents provider. 

922 

923 Args: 

924 commit: Commit object 

925 

926 Returns: 

927 List of parent commit SHAs 

928 """ 

929 return parents_provider.get_parents(commit.id, commit) 

930 

931 return MissingObjectFinder( 

932 self.object_store, 

933 haves=haves, 

934 wants=wants, 

935 shallow=getattr(graph_walker, "shallow", set()), 

936 progress=progress, 

937 get_tagged=get_tagged, 

938 get_parents=get_parents, 

939 ) 

940 

941 def generate_pack_data( 

942 self, 

943 have: set[ObjectID], 

944 want: set[ObjectID], 

945 *, 

946 shallow: set[ObjectID] | None = None, 

947 progress: Callable[[str], None] | None = None, 

948 ofs_delta: bool | None = None, 

949 ) -> tuple[int, Iterator["UnpackedObject"]]: 

950 """Generate pack data objects for a set of wants/haves. 

951 

952 Args: 

953 have: List of SHA1s of objects that should not be sent 

954 want: List of SHA1s of objects that should be sent 

955 shallow: Set of shallow commit SHA1s to skip (defaults to repo's shallow commits) 

956 ofs_delta: Whether OFS deltas can be included 

957 progress: Optional progress reporting method 

958 """ 

959 if shallow is None: 

960 shallow = self.get_shallow() 

961 return self.object_store.generate_pack_data( 

962 have, 

963 want, 

964 shallow=shallow, 

965 progress=progress, 

966 ofs_delta=ofs_delta if ofs_delta is not None else DEFAULT_OFS_DELTA, 

967 ) 

968 

969 def get_graph_walker( 

970 self, heads: list[ObjectID] | None = None 

971 ) -> ObjectStoreGraphWalker: 

972 """Retrieve a graph walker. 

973 

974 A graph walker is used by a remote repository (or proxy) 

975 to find out which objects are present in this repository. 

976 

977 Args: 

978 heads: Repository heads to use (optional) 

979 Returns: A graph walker object 

980 """ 

981 if heads is None: 

982 heads = [ 

983 sha 

984 for sha in self.refs.as_dict(Ref(b"refs/heads")).values() 

985 if sha in self.object_store 

986 ] 

987 parents_provider = ParentsProvider(self.object_store) 

988 return ObjectStoreGraphWalker( 

989 heads, 

990 parents_provider.get_parents, 

991 shallow=self.get_shallow(), 

992 update_shallow=self.update_shallow, 

993 ) 

994 

995 def get_refs(self) -> dict[Ref, ObjectID]: 

996 """Get dictionary with all refs. 

997 

998 Returns: A ``dict`` mapping ref names to SHA1s 

999 """ 

1000 return self.refs.as_dict() 

1001 

1002 def head(self) -> ObjectID: 

1003 """Return the SHA1 pointed at by HEAD.""" 

1004 # TODO: move this method to WorkTree 

1005 return self.refs[HEADREF] 

1006 

1007 def _get_object(self, sha: ObjectID | RawObjectID, cls: type[T]) -> T: 

1008 assert len(sha) in ( 

1009 self.object_format.oid_length, 

1010 self.object_format.hex_length, 

1011 ) 

1012 ret = self.get_object(sha) 

1013 if not isinstance(ret, cls): 

1014 if cls is Commit: 

1015 raise NotCommitError(ret.id) 

1016 elif cls is Blob: 

1017 raise NotBlobError(ret.id) 

1018 elif cls is Tree: 

1019 raise NotTreeError(ret.id) 

1020 elif cls is Tag: 

1021 raise NotTagError(ret.id) 

1022 else: 

1023 raise Exception(f"Type invalid: {ret.type_name!r} != {cls.type_name!r}") 

1024 return ret 

1025 

1026 def get_object(self, sha: ObjectID | RawObjectID) -> ShaFile: 

1027 """Retrieve the object with the specified SHA. 

1028 

1029 Args: 

1030 sha: SHA to retrieve 

1031 Returns: A ShaFile object 

1032 Raises: 

1033 KeyError: when the object can not be found 

1034 """ 

1035 return self.object_store[sha] 

1036 

1037 def parents_provider(self) -> ParentsProvider: 

1038 """Get a parents provider for this repository. 

1039 

1040 Returns: 

1041 ParentsProvider instance configured with grafts and shallows 

1042 """ 

1043 return ParentsProvider( 

1044 self.object_store, 

1045 grafts=self._graftpoints, 

1046 shallows=self.get_shallow(), 

1047 ) 

1048 

1049 def get_parents( 

1050 self, sha: ObjectID, commit: Commit | None = None 

1051 ) -> list[ObjectID]: 

1052 """Retrieve the parents of a specific commit. 

1053 

1054 If the specific commit is a graftpoint, the graft parents 

1055 will be returned instead. 

1056 

1057 Args: 

1058 sha: SHA of the commit for which to retrieve the parents 

1059 commit: Optional commit matching the sha 

1060 Returns: List of parents 

1061 """ 

1062 return self.parents_provider().get_parents(sha, commit) 

1063 

1064 def get_config(self) -> "ConfigFile": 

1065 """Retrieve the config object. 

1066 

1067 Returns: `ConfigFile` object for the ``.git/config`` file. 

1068 """ 

1069 raise NotImplementedError(self.get_config) 

1070 

1071 def get_worktree_config(self) -> "ConfigFile": 

1072 """Retrieve the worktree config object.""" 

1073 raise NotImplementedError(self.get_worktree_config) 

1074 

1075 def get_description(self) -> bytes | None: 

1076 """Retrieve the description for this repository. 

1077 

1078 Returns: Bytes with the description of the repository 

1079 as set by the user. 

1080 """ 

1081 raise NotImplementedError(self.get_description) 

1082 

1083 def set_description(self, description: bytes) -> None: 

1084 """Set the description for this repository. 

1085 

1086 Args: 

1087 description: Text to set as description for this repository. 

1088 """ 

1089 raise NotImplementedError(self.set_description) 

1090 

1091 def get_rebase_state_manager(self) -> "RebaseStateManager": 

1092 """Get the appropriate rebase state manager for this repository. 

1093 

1094 Returns: RebaseStateManager instance 

1095 """ 

1096 raise NotImplementedError(self.get_rebase_state_manager) 

1097 

1098 def get_blob_normalizer( 

1099 self, config: "Config | None" = None 

1100 ) -> "FilterBlobNormalizer": 

1101 """Return a BlobNormalizer object for checkin/checkout operations. 

1102 

1103 Args: 

1104 config: Configuration to consult for filter setup. If None, 

1105 implementations may fall back to ``self.get_config_stack()``. 

1106 

1107 Returns: BlobNormalizer instance 

1108 """ 

1109 raise NotImplementedError(self.get_blob_normalizer) 

1110 

1111 def get_gitattributes(self, tree: bytes | None = None) -> "GitAttributes": 

1112 """Read gitattributes for the repository. 

1113 

1114 Args: 

1115 tree: Tree SHA to read .gitattributes from (defaults to HEAD) 

1116 

1117 Returns: 

1118 GitAttributes object that can be used to match paths 

1119 """ 

1120 raise NotImplementedError(self.get_gitattributes) 

1121 

1122 def get_config_stack(self) -> "StackedConfig": 

1123 """Return a config stack for this repository. 

1124 

1125 This stack accesses the configuration for both this repository 

1126 itself (.git/config) and the global configuration, which usually 

1127 lives in ~/.gitconfig. 

1128 

1129 Returns: `Config` instance for this repository 

1130 """ 

1131 from .config import ConfigFile, StackedConfig 

1132 

1133 local_config = self.get_config() 

1134 backends: list[ConfigFile] = [local_config] 

1135 if local_config.get_boolean((b"extensions",), b"worktreeconfig", False): 

1136 backends.append(self.get_worktree_config()) 

1137 

1138 backends += StackedConfig.default_backends() 

1139 return StackedConfig(backends, writable=local_config) 

1140 

1141 def get_shallow(self) -> set[ObjectID]: 

1142 """Get the set of shallow commits. 

1143 

1144 Returns: Set of shallow commits. 

1145 """ 

1146 f = self.get_named_file("shallow") 

1147 if f is None: 

1148 return set() 

1149 with f: 

1150 return {ObjectID(line.strip()) for line in f} 

1151 

1152 def update_shallow( 

1153 self, new_shallow: set[ObjectID] | None, new_unshallow: set[ObjectID] | None 

1154 ) -> None: 

1155 """Update the list of shallow objects. 

1156 

1157 Args: 

1158 new_shallow: Newly shallow objects 

1159 new_unshallow: Newly no longer shallow objects 

1160 """ 

1161 shallow = self.get_shallow() 

1162 if new_shallow: 

1163 shallow.update(new_shallow) 

1164 if new_unshallow: 

1165 shallow.difference_update(new_unshallow) 

1166 if shallow: 

1167 self._put_named_file("shallow", b"".join([sha + b"\n" for sha in shallow])) 

1168 else: 

1169 self._del_named_file("shallow") 

1170 

1171 def get_peeled(self, ref: Ref) -> ObjectID: 

1172 """Get the peeled value of a ref. 

1173 

1174 Args: 

1175 ref: The refname to peel. 

1176 Returns: The fully-peeled SHA1 of a tag object, after peeling all 

1177 intermediate tags; if the original ref does not point to a tag, 

1178 this will equal the original SHA1. 

1179 """ 

1180 cached = self.refs.get_peeled(ref) 

1181 if cached is not None: 

1182 return cached 

1183 return peel_sha(self.object_store, self.refs[ref])[1].id 

1184 

1185 @property 

1186 def notes(self) -> "Notes": 

1187 """Access notes functionality for this repository. 

1188 

1189 Returns: 

1190 Notes object for accessing notes 

1191 """ 

1192 from .notes import Notes 

1193 

1194 return Notes(self.object_store, self.refs) 

1195 

1196 def get_walker( 

1197 self, 

1198 include: Sequence[ObjectID] | None = None, 

1199 exclude: Sequence[ObjectID] | None = None, 

1200 order: str = "date", 

1201 reverse: bool = False, 

1202 max_entries: int | None = None, 

1203 paths: Sequence[bytes] | None = None, 

1204 rename_detector: "RenameDetector | None" = None, 

1205 follow: bool = False, 

1206 since: int | None = None, 

1207 until: int | None = None, 

1208 queue_cls: type | None = None, 

1209 ) -> "Walker": 

1210 """Obtain a walker for this repository. 

1211 

1212 Args: 

1213 include: Iterable of SHAs of commits to include along with their 

1214 ancestors. Defaults to [HEAD] 

1215 exclude: Iterable of SHAs of commits to exclude along with their 

1216 ancestors, overriding includes. 

1217 order: ORDER_* constant specifying the order of results. 

1218 Anything other than ORDER_DATE may result in O(n) memory usage. 

1219 reverse: If True, reverse the order of output, requiring O(n) 

1220 memory. 

1221 max_entries: The maximum number of entries to yield, or None for 

1222 no limit. 

1223 paths: Iterable of file or subtree paths to show entries for. 

1224 rename_detector: diff.RenameDetector object for detecting 

1225 renames. 

1226 follow: If True, follow path across renames/copies. Forces a 

1227 default rename_detector. 

1228 since: Timestamp to list commits after. 

1229 until: Timestamp to list commits before. 

1230 queue_cls: A class to use for a queue of commits, supporting the 

1231 iterator protocol. The constructor takes a single argument, the Walker. 

1232 

1233 Returns: A `Walker` object 

1234 """ 

1235 from .walk import Walker, _CommitTimeQueue 

1236 

1237 if include is None: 

1238 include = [self.head()] 

1239 

1240 # Pass all arguments to Walker explicitly to avoid type issues with **kwargs 

1241 return Walker( 

1242 self.object_store, 

1243 include, 

1244 exclude=exclude, 

1245 order=order, 

1246 reverse=reverse, 

1247 max_entries=max_entries, 

1248 paths=paths, 

1249 rename_detector=rename_detector, 

1250 follow=follow, 

1251 since=since, 

1252 until=until, 

1253 get_parents=lambda commit: self.get_parents(commit.id, commit), 

1254 queue_cls=queue_cls if queue_cls is not None else _CommitTimeQueue, 

1255 ) 

1256 

1257 def __getitem__(self, name: ObjectID | Ref | bytes) -> "ShaFile": 

1258 """Retrieve a Git object by SHA1 or ref. 

1259 

1260 Args: 

1261 name: A Git object SHA1 or a ref name 

1262 Returns: A `ShaFile` object, such as a Commit or Blob 

1263 Raises: 

1264 KeyError: when the specified ref or object does not exist 

1265 """ 

1266 if not isinstance(name, bytes): 

1267 raise TypeError(f"'name' must be bytestring, not {type(name).__name__:.80}") 

1268 # If it looks like a ref name, only try refs 

1269 if name == b"HEAD" or name.startswith(b"refs/"): 

1270 try: 

1271 return self.object_store[self.refs[Ref(name)]] 

1272 except (RefFormatError, KeyError): 

1273 pass 

1274 # Otherwise, try as object ID if length matches 

1275 if len(name) in ( 

1276 self.object_store.object_format.oid_length, 

1277 self.object_store.object_format.hex_length, 

1278 ): 

1279 try: 

1280 return self.object_store[ 

1281 ObjectID(name) 

1282 if len(name) == self.object_store.object_format.hex_length 

1283 else RawObjectID(name) 

1284 ] 

1285 except (KeyError, ValueError): 

1286 pass 

1287 # If nothing worked, raise KeyError 

1288 raise KeyError(name) 

1289 

1290 def __contains__(self, name: bytes) -> bool: 

1291 """Check if a specific Git object or ref is present. 

1292 

1293 Args: 

1294 name: Git object SHA1/SHA256 or ref name 

1295 """ 

1296 # Check if it's a binary or hex SHA 

1297 if len(name) == self.object_format.hex_length and valid_hexsha(name): 

1298 return ObjectID(name) in self.object_store or Ref(name) in self.refs 

1299 return Ref(name) in self.refs 

1300 

1301 def __setitem__(self, name: bytes, value: ShaFile | bytes) -> None: 

1302 """Set a ref. 

1303 

1304 Args: 

1305 name: ref name 

1306 value: Ref value - either a ShaFile object, or a hex sha 

1307 """ 

1308 if name.startswith(b"refs/") or name == HEADREF: 

1309 ref_name = Ref(name) 

1310 if isinstance(value, ShaFile): 

1311 self.refs[ref_name] = value.id 

1312 elif isinstance(value, bytes): 

1313 self.refs[ref_name] = ObjectID(value) 

1314 else: 

1315 raise TypeError(value) 

1316 else: 

1317 raise ValueError(name) 

1318 

1319 def __delitem__(self, name: bytes) -> None: 

1320 """Remove a ref. 

1321 

1322 Args: 

1323 name: Name of the ref to remove 

1324 """ 

1325 if name.startswith(b"refs/") or name == HEADREF: 

1326 del self.refs[Ref(name)] 

1327 else: 

1328 raise ValueError(name) 

1329 

1330 def _get_user_identity( 

1331 self, config: "StackedConfig", kind: str | None = None 

1332 ) -> bytes: 

1333 """Determine the identity to use for new commits.""" 

1334 warnings.warn( 

1335 "use get_user_identity() rather than Repo._get_user_identity", 

1336 DeprecationWarning, 

1337 ) 

1338 return get_user_identity(config) 

1339 

1340 def _add_graftpoints( 

1341 self, updated_graftpoints: dict[ObjectID, list[ObjectID]] 

1342 ) -> None: 

1343 """Add or modify graftpoints. 

1344 

1345 Args: 

1346 updated_graftpoints: Dict of commit shas to list of parent shas 

1347 """ 

1348 # Simple validation 

1349 for commit, parents in updated_graftpoints.items(): 

1350 for sha in [commit, *parents]: 

1351 check_hexsha(sha, "Invalid graftpoint") 

1352 

1353 self._graftpoints.update(updated_graftpoints) 

1354 

1355 def _remove_graftpoints(self, to_remove: Sequence[ObjectID] = ()) -> None: 

1356 """Remove graftpoints. 

1357 

1358 Args: 

1359 to_remove: List of commit shas 

1360 """ 

1361 for sha in to_remove: 

1362 del self._graftpoints[sha] 

1363 

1364 def _read_heads(self, name: str) -> list[ObjectID]: 

1365 f = self.get_named_file(name) 

1366 if f is None: 

1367 return [] 

1368 with f: 

1369 return [ObjectID(line.strip()) for line in f.readlines() if line.strip()] 

1370 

1371 def get_worktree(self) -> "WorkTree": 

1372 """Get the working tree for this repository. 

1373 

1374 Returns: 

1375 WorkTree instance for performing working tree operations 

1376 

1377 Raises: 

1378 NotImplementedError: If the repository doesn't support working trees 

1379 """ 

1380 raise NotImplementedError( 

1381 "Working tree operations not supported by this repository type" 

1382 ) 

1383 

1384 

1385def read_gitfile(f: BinaryIO) -> str: 

1386 """Read a ``.git`` file. 

1387 

1388 The first line of the file should start with "gitdir: " 

1389 

1390 Args: 

1391 f: File-like object to read from 

1392 Returns: A path 

1393 """ 

1394 cs = f.read() 

1395 if not cs.startswith(b"gitdir: "): 

1396 raise ValueError("Expected file to start with 'gitdir: '") 

1397 return cs[len(b"gitdir: ") :].rstrip(b"\r\n").decode("utf-8") 

1398 

1399 

1400class UnsupportedVersion(Exception): 

1401 """Unsupported repository version.""" 

1402 

1403 def __init__(self, version: int) -> None: 

1404 """Initialize UnsupportedVersion exception. 

1405 

1406 Args: 

1407 version: The unsupported repository version 

1408 """ 

1409 self.version = version 

1410 

1411 

1412class UnsupportedExtension(Exception): 

1413 """Unsupported repository extension.""" 

1414 

1415 def __init__(self, extension: str) -> None: 

1416 """Initialize UnsupportedExtension exception. 

1417 

1418 Args: 

1419 extension: The unsupported repository extension 

1420 """ 

1421 self.extension = extension 

1422 

1423 

1424class InvalidWorktreeConfiguration(Exception): 

1425 """core.worktree is set on a bare repository.""" 

1426 

1427 

1428class Repo(BaseRepo): 

1429 """A git repository backed by local disk. 

1430 

1431 To open an existing repository, call the constructor with 

1432 the path of the repository. 

1433 

1434 To create a new repository, use the Repo.init class method. 

1435 

1436 Note that a repository object may hold on to resources such 

1437 as file handles for performance reasons; call .close() to free 

1438 up those resources. 

1439 

1440 Attributes: 

1441 path: Path to the working copy (if it exists) or repository control 

1442 directory (if the repository is bare). ``core.worktree`` overrides 

1443 this, so it is not necessarily the parent of the control directory. 

1444 bare: Whether this is a bare repository 

1445 """ 

1446 

1447 path: str 

1448 bare: bool 

1449 object_store: DiskObjectStore 

1450 filter_context: "FilterContext | None" 

1451 

1452 def __init__( 

1453 self, 

1454 root: str | bytes | os.PathLike[str], 

1455 object_store: PackBasedObjectStore | None = None, 

1456 bare: bool | None = None, 

1457 ) -> None: 

1458 """Open a repository on disk. 

1459 

1460 Args: 

1461 root: Path to the repository's root. 

1462 object_store: ObjectStore to use; if omitted, we use the 

1463 repository's default object store 

1464 bare: True if this is a bare repository. 

1465 """ 

1466 root = os.fspath(root) 

1467 if isinstance(root, bytes): 

1468 root = os.fsdecode(root) 

1469 hidden_path = os.path.join(root, CONTROLDIR) 

1470 if bare is None: 

1471 if os.path.isfile(hidden_path) or os.path.isdir( 

1472 os.path.join(hidden_path, OBJECTDIR) 

1473 ): 

1474 bare = False 

1475 elif os.path.isdir(os.path.join(root, OBJECTDIR)) and os.path.isdir( 

1476 os.path.join(root, REFSDIR) 

1477 ): 

1478 bare = True 

1479 else: 

1480 raise NotGitRepository( 

1481 "No git repository was found at {path}".format(**dict(path=root)) 

1482 ) 

1483 

1484 self.bare = bare 

1485 if bare is False: 

1486 if os.path.isfile(hidden_path): 

1487 with open(hidden_path, "rb") as f: 

1488 path = read_gitfile(f) 

1489 self._controldir = os.path.join(root, path) 

1490 else: 

1491 self._controldir = hidden_path 

1492 else: 

1493 self._controldir = root 

1494 commondir = self.get_named_file(COMMONDIR) 

1495 if commondir is not None: 

1496 with commondir: 

1497 self._commondir = os.path.join( 

1498 self.controldir(), 

1499 os.fsdecode(commondir.read().rstrip(b"\r\n")), 

1500 ) 

1501 else: 

1502 self._commondir = self._controldir 

1503 self.path = root 

1504 

1505 # Initialize refs early so they're available for config condition matchers 

1506 self.refs = DiskRefsContainer( 

1507 self.commondir(), self._controldir, logger=self._write_reflog 

1508 ) 

1509 

1510 # Initialize worktrees container 

1511 from .worktree import WorkTreeContainer 

1512 

1513 self.worktrees = WorkTreeContainer(self) 

1514 

1515 config = self.get_config() 

1516 try: 

1517 repository_format_version = config.get("core", "repositoryformatversion") 

1518 format_version = ( 

1519 0 

1520 if repository_format_version is None 

1521 else int(repository_format_version) 

1522 ) 

1523 except KeyError: 

1524 format_version = 0 

1525 

1526 if format_version not in (0, 1): 

1527 raise UnsupportedVersion(format_version) 

1528 

1529 try: 

1530 worktree = config.get((b"core",), b"worktree") 

1531 except KeyError: 

1532 pass 

1533 else: 

1534 # A repository is bare because core.bare says so, not because it 

1535 # was opened at its control directory: a submodule or a 

1536 # --separate-git-dir repository is opened that way but still has a 

1537 # working tree, named here. 

1538 if config.get_boolean((b"core",), b"bare", False): 

1539 raise InvalidWorktreeConfiguration( 

1540 "core.bare and core.worktree are incompatible" 

1541 ) 

1542 self.bare = False 

1543 # Relative paths are resolved against the control directory, not 

1544 # against the current directory or the repository root. 

1545 self.path = os.path.join(self._controldir, os.fsdecode(worktree)) 

1546 

1547 # Track extensions we encounter 

1548 has_reftable_extension = False 

1549 for extension, value in config.items((b"extensions",)): 

1550 if extension.lower() == b"refstorage": 

1551 if value == b"reftable": 

1552 has_reftable_extension = True 

1553 else: 

1554 raise UnsupportedExtension(f"refStorage = {value.decode()}") 

1555 elif extension.lower() not in ( 

1556 b"worktreeconfig", 

1557 b"objectformat", 

1558 b"relativeworktrees", 

1559 ): 

1560 raise UnsupportedExtension(extension.decode("utf-8")) 

1561 

1562 if object_store is None: 

1563 # Get shared repository permissions from config 

1564 try: 

1565 shared_value = config.get(("core",), "sharedRepository") 

1566 file_mode, dir_mode = parse_shared_repository(shared_value) 

1567 except KeyError: 

1568 file_mode, dir_mode = None, None 

1569 

1570 object_store = DiskObjectStore.from_config( 

1571 os.path.join(self.commondir(), OBJECTDIR), 

1572 config, 

1573 file_mode=file_mode, 

1574 dir_mode=dir_mode, 

1575 ) 

1576 

1577 # Use reftable if extension is configured 

1578 if has_reftable_extension: 

1579 from .reftable import ReftableRefsContainer 

1580 

1581 self.refs = ReftableRefsContainer(self.commondir()) 

1582 # Update worktrees container after refs change 

1583 self.worktrees = WorkTreeContainer(self) 

1584 BaseRepo.__init__(self, object_store, self.refs) 

1585 

1586 # Determine hash algorithm from config if not already set 

1587 if self.object_format is None: 

1588 from .object_format import DEFAULT_OBJECT_FORMAT, get_object_format 

1589 

1590 if format_version == 1: 

1591 try: 

1592 object_format = config.get((b"extensions",), b"objectformat") 

1593 self.object_format = get_object_format( 

1594 object_format.decode("ascii") 

1595 ) 

1596 except KeyError: 

1597 self.object_format = DEFAULT_OBJECT_FORMAT 

1598 else: 

1599 self.object_format = DEFAULT_OBJECT_FORMAT 

1600 

1601 self._graftpoints = {} 

1602 graft_file = self.get_named_file( 

1603 os.path.join("info", "grafts"), basedir=self.commondir() 

1604 ) 

1605 if graft_file: 

1606 with graft_file: 

1607 self._graftpoints.update(parse_graftpoints(graft_file)) 

1608 graft_file = self.get_named_file("shallow", basedir=self.commondir()) 

1609 if graft_file: 

1610 with graft_file: 

1611 self._graftpoints.update(parse_graftpoints(graft_file)) 

1612 

1613 self.hooks["pre-commit"] = PreCommitShellHook(self.path, self.controldir()) 

1614 self.hooks["commit-msg"] = CommitMsgShellHook(self.controldir()) 

1615 self.hooks["post-commit"] = PostCommitShellHook(self.controldir()) 

1616 self.hooks["pre-receive"] = PreReceiveShellHook(self.controldir()) 

1617 self.hooks["update"] = UpdateShellHook(self.controldir()) 

1618 self.hooks["post-receive"] = PostReceiveShellHook(self.controldir()) 

1619 

1620 # Initialize filter context as None, will be created lazily 

1621 self.filter_context = None 

1622 

1623 def get_worktree(self) -> "WorkTree": 

1624 """Get the working tree for this repository. 

1625 

1626 Returns: 

1627 WorkTree instance for performing working tree operations 

1628 """ 

1629 from .worktree import WorkTree 

1630 

1631 return WorkTree(self, self.path) 

1632 

1633 def _write_reflog( 

1634 self, 

1635 ref: bytes, 

1636 old_sha: bytes, 

1637 new_sha: bytes, 

1638 committer: bytes | None, 

1639 timestamp: int | None, 

1640 timezone: int | None, 

1641 message: bytes, 

1642 ) -> None: 

1643 from .reflog import format_reflog_line 

1644 

1645 path = self._reflog_path(ref) 

1646 

1647 # Get shared repository permissions 

1648 file_mode, dir_mode = self._get_shared_repository_permissions() 

1649 

1650 # Create directory with appropriate permissions 

1651 parent_dir = os.path.dirname(path) 

1652 # Create directory tree, setting permissions on each level if needed 

1653 parts = [] 

1654 current = parent_dir 

1655 while current and not os.path.exists(current): 

1656 parts.append(current) 

1657 current = os.path.dirname(current) 

1658 parts.reverse() 

1659 for part in parts: 

1660 os.mkdir(part) 

1661 if dir_mode is not None: 

1662 os.chmod(part, dir_mode) 

1663 if committer is None: 

1664 config = self.get_config_stack() 

1665 committer = get_user_identity(config) 

1666 check_user_identity(committer) 

1667 if timestamp is None: 

1668 timestamp = int(time.time()) 

1669 if timezone is None: 

1670 timezone = 0 # FIXME 

1671 with open(path, "ab") as f: 

1672 f.write( 

1673 format_reflog_line( 

1674 old_sha, new_sha, committer, timestamp, timezone, message 

1675 ) 

1676 + b"\n" 

1677 ) 

1678 

1679 # Set file permissions (open() respects umask, so we need chmod to set the actual mode) 

1680 # Always chmod to ensure correct permissions even if file already existed 

1681 if file_mode is not None: 

1682 os.chmod(path, file_mode) 

1683 

1684 def _reflog_path(self, ref: bytes) -> str: 

1685 if ref.startswith((b"main-worktree/", b"worktrees/")): 

1686 raise NotImplementedError(f"refs {ref.decode()} are not supported") 

1687 

1688 base = self.controldir() if is_per_worktree_ref(ref) else self.commondir() 

1689 return os.path.join(base, "logs", os.fsdecode(ref)) 

1690 

1691 def read_reflog(self, ref: bytes) -> Generator[reflog.Entry, None, None]: 

1692 """Read reflog entries for a reference. 

1693 

1694 Args: 

1695 ref: Reference name (e.g. b'HEAD', b'refs/heads/master') 

1696 

1697 Yields: 

1698 reflog.Entry objects in chronological order (oldest first) 

1699 """ 

1700 from .reflog import read_reflog 

1701 

1702 path = self._reflog_path(ref) 

1703 try: 

1704 with open(path, "rb") as f: 

1705 yield from read_reflog(f) 

1706 except FileNotFoundError: 

1707 return 

1708 

1709 @classmethod 

1710 def discover(cls, start: str | bytes | os.PathLike[str] = ".") -> "Repo": 

1711 """Iterate parent directories to discover a repository. 

1712 

1713 Return a Repo object for the first parent directory that looks like a 

1714 Git repository. 

1715 

1716 Args: 

1717 start: The directory to start discovery from (defaults to '.') 

1718 """ 

1719 path = os.path.abspath(start) 

1720 while True: 

1721 try: 

1722 return cls(path) 

1723 except NotGitRepository: 

1724 new_path, _tail = os.path.split(path) 

1725 if new_path == path: # Root reached 

1726 break 

1727 path = new_path 

1728 start_str = os.fspath(start) 

1729 if isinstance(start_str, bytes): 

1730 start_str = start_str.decode("utf-8") 

1731 raise NotGitRepository(f"No git repository was found at {start_str}") 

1732 

1733 def controldir(self) -> str: 

1734 """Return the path of the control directory.""" 

1735 return self._controldir 

1736 

1737 def commondir(self) -> str: 

1738 """Return the path of the common directory. 

1739 

1740 For a main working tree, it is identical to controldir(). 

1741 

1742 For a linked working tree, it is the control directory of the 

1743 main working tree. 

1744 """ 

1745 return self._commondir 

1746 

1747 def _determine_file_mode(self) -> bool: 

1748 """Probe the file-system to determine whether permissions can be trusted. 

1749 

1750 Returns: True if permissions can be trusted, False otherwise. 

1751 """ 

1752 fname = os.path.join(self.path, ".probe-permissions") 

1753 with open(fname, "w") as f: 

1754 f.write("") 

1755 

1756 st1 = os.lstat(fname) 

1757 try: 

1758 os.chmod(fname, st1.st_mode ^ stat.S_IXUSR) 

1759 except PermissionError: 

1760 return False 

1761 st2 = os.lstat(fname) 

1762 

1763 os.unlink(fname) 

1764 

1765 mode_differs = st1.st_mode != st2.st_mode 

1766 st2_has_exec = (st2.st_mode & stat.S_IXUSR) != 0 

1767 

1768 return mode_differs and st2_has_exec 

1769 

1770 def _determine_symlinks(self) -> bool: 

1771 """Probe the filesystem to determine whether symlinks can be created. 

1772 

1773 Returns: True if symlinks can be created, False otherwise. 

1774 """ 

1775 # TODO(jelmer): Actually probe disk / look at filesystem 

1776 return sys.platform != "win32" 

1777 

1778 def _get_shared_repository_permissions( 

1779 self, 

1780 ) -> tuple[int | None, int | None]: 

1781 """Get shared repository file and directory permissions from config. 

1782 

1783 Returns: 

1784 tuple of (file_mask, directory_mask) or (None, None) if not shared 

1785 """ 

1786 try: 

1787 config = self.get_config() 

1788 value = config.get(("core",), "sharedRepository") 

1789 return parse_shared_repository(value) 

1790 except KeyError: 

1791 return (None, None) 

1792 

1793 def _put_named_file(self, path: str, contents: bytes) -> None: 

1794 """Write a file to the control dir with the given name and contents. 

1795 

1796 Args: 

1797 path: The path to the file, relative to the control dir. 

1798 contents: A string to write to the file. 

1799 """ 

1800 path = path.lstrip(os.path.sep) 

1801 

1802 # Get shared repository permissions 

1803 file_mode, _ = self._get_shared_repository_permissions() 

1804 

1805 # Create file with appropriate permissions 

1806 if file_mode is not None: 

1807 with GitFile( 

1808 os.path.join(self.controldir(), path), "wb", mask=file_mode 

1809 ) as f: 

1810 f.write(contents) 

1811 else: 

1812 with GitFile(os.path.join(self.controldir(), path), "wb") as f: 

1813 f.write(contents) 

1814 

1815 def _del_named_file(self, path: str) -> None: 

1816 try: 

1817 os.unlink(os.path.join(self.controldir(), path)) 

1818 except FileNotFoundError: 

1819 return 

1820 

1821 def get_named_file( 

1822 self, 

1823 path: str | bytes, 

1824 basedir: str | None = None, 

1825 ) -> BinaryIO | None: 

1826 """Get a file from the control dir with a specific name. 

1827 

1828 Although the filename should be interpreted as a filename relative to 

1829 the control dir in a disk-based Repo, the object returned need not be 

1830 pointing to a file in that location. 

1831 

1832 Args: 

1833 path: The path to the file, relative to the control dir. 

1834 basedir: Optional argument that specifies an alternative to the 

1835 control dir. 

1836 Returns: An open file object, or None if the file does not exist. 

1837 """ 

1838 # TODO(dborowitz): sanitize filenames, since this is used directly by 

1839 # the dumb web serving code. 

1840 if basedir is None: 

1841 basedir = self.controldir() 

1842 if isinstance(path, bytes): 

1843 path = path.decode("utf-8") 

1844 path = path.lstrip(os.path.sep) 

1845 try: 

1846 return open(os.path.join(basedir, path), "rb") 

1847 except FileNotFoundError: 

1848 return None 

1849 

1850 def index_path(self) -> str: 

1851 """Return path to the index file.""" 

1852 return os.path.join(self.controldir(), INDEX_FILENAME) 

1853 

1854 def open_index(self, config: "Config | None" = None) -> "Index": 

1855 """Open the index for this repository. 

1856 

1857 Args: 

1858 config: Configuration to consult for index settings. If None, 

1859 falls back to ``self.get_config_stack()``. 

1860 

1861 Raises: 

1862 NoIndexPresent: If no index is present 

1863 Returns: The matching `Index` 

1864 """ 

1865 from .index import Index, make_path_normalizer 

1866 

1867 if not self.has_index(): 

1868 raise NoIndexPresent 

1869 

1870 if config is None: 

1871 config = self.get_config_stack() 

1872 many_files = config.get_boolean(b"feature", b"manyFiles", False) 

1873 skip_hash = False 

1874 index_version = None 

1875 

1876 if many_files: 

1877 # When feature.manyFiles is enabled, set index.version=4 and index.skipHash=true 

1878 try: 

1879 index_version_str = config.get(b"index", b"version") 

1880 index_version = int(index_version_str) 

1881 except KeyError: 

1882 index_version = 4 # Default to version 4 for manyFiles 

1883 skip_hash = config.get_boolean(b"index", b"skipHash", True) 

1884 else: 

1885 # Check for explicit index settings 

1886 try: 

1887 index_version_str = config.get(b"index", b"version") 

1888 index_version = int(index_version_str) 

1889 except KeyError: 

1890 index_version = None 

1891 skip_hash = config.get_boolean(b"index", b"skipHash", False) 

1892 

1893 # Get shared repository permissions for index file 

1894 file_mode, _ = self._get_shared_repository_permissions() 

1895 

1896 return Index( 

1897 self.index_path(), 

1898 skip_hash=skip_hash, 

1899 version=index_version, 

1900 file_mode=file_mode, 

1901 path_normalizer=make_path_normalizer(config), 

1902 ) 

1903 

1904 def has_index(self) -> bool: 

1905 """Check if an index is present.""" 

1906 # Bare repos must never have index files; non-bare repos may have a 

1907 # missing index file, which is treated as empty. 

1908 return not self.bare 

1909 

1910 def clone( 

1911 self, 

1912 target_path: str | bytes | os.PathLike[str], 

1913 *, 

1914 mkdir: bool = True, 

1915 bare: bool = False, 

1916 origin: bytes = b"origin", 

1917 checkout: bool | None = None, 

1918 branch: bytes | None = None, 

1919 progress: Callable[[str], None] | None = None, 

1920 depth: int | None = None, 

1921 symlinks: bool | None = None, 

1922 ) -> "Repo": 

1923 """Clone this repository. 

1924 

1925 Args: 

1926 target_path: Target path 

1927 mkdir: Create the target directory 

1928 bare: Whether to create a bare repository 

1929 checkout: Whether or not to check-out HEAD after cloning 

1930 origin: Base name for refs in target repository 

1931 cloned from this repository 

1932 branch: Optional branch or tag to be used as HEAD in the new repository 

1933 instead of this repository's HEAD. 

1934 progress: Optional progress function 

1935 depth: Depth at which to fetch 

1936 symlinks: Symlinks setting (default to autodetect) 

1937 Returns: Created repository as `Repo` 

1938 """ 

1939 encoded_path = os.fsencode(self.path) 

1940 

1941 if mkdir: 

1942 os.mkdir(target_path) 

1943 

1944 try: 

1945 if not bare: 

1946 target = Repo.init(target_path, symlinks=symlinks) 

1947 if checkout is None: 

1948 checkout = True 

1949 else: 

1950 if checkout: 

1951 raise ValueError("checkout and bare are incompatible") 

1952 target = Repo.init_bare(target_path) 

1953 

1954 try: 

1955 target_config = target.get_config() 

1956 target_config.set((b"remote", origin), b"url", encoded_path) 

1957 target_config.set( 

1958 (b"remote", origin), 

1959 b"fetch", 

1960 b"+refs/heads/*:refs/remotes/" + origin + b"/*", 

1961 ) 

1962 target_config.write_to_path() 

1963 

1964 ref_message = b"clone: from " + encoded_path 

1965 self.fetch(target, depth=depth) 

1966 target.refs.import_refs( 

1967 Ref(b"refs/remotes/" + origin), 

1968 self.refs.as_dict(Ref(b"refs/heads")), 

1969 message=ref_message, 

1970 ) 

1971 target.refs.import_refs( 

1972 Ref(b"refs/tags"), 

1973 self.refs.as_dict(Ref(b"refs/tags")), 

1974 message=ref_message, 

1975 ) 

1976 

1977 head_chain, origin_sha = self.refs.follow(HEADREF) 

1978 origin_head = head_chain[-1] if head_chain else None 

1979 head: ObjectID | None = None 

1980 if origin_sha and not origin_head: 

1981 # set detached HEAD 

1982 target.refs[HEADREF] = origin_sha 

1983 head = origin_sha 

1984 else: 

1985 _set_origin_head(target.refs, origin, origin_head) 

1986 head_ref = _set_default_branch( 

1987 target.refs, origin, origin_head, branch, ref_message 

1988 ) 

1989 

1990 # Update target head 

1991 if head_ref: 

1992 head = _set_head(target.refs, head_ref, ref_message) 

1993 else: 

1994 head = None 

1995 

1996 if checkout and head is not None: 

1997 target.get_worktree().reset_index(config=target.get_config_stack()) 

1998 except BaseException: 

1999 target.close() 

2000 raise 

2001 except BaseException: 

2002 if mkdir: 

2003 import shutil 

2004 

2005 shutil.rmtree(target_path) 

2006 raise 

2007 return target 

2008 

2009 def _get_config_condition_matchers(self) -> dict[str, "ConditionMatcher"]: 

2010 """Get condition matchers for includeIf conditions. 

2011 

2012 Returns a dict of condition prefix to matcher function. 

2013 """ 

2014 from pathlib import Path 

2015 

2016 from .config import ConditionMatcher, match_glob_pattern 

2017 

2018 # Add gitdir matchers 

2019 def match_gitdir(pattern: str, case_sensitive: bool = True) -> bool: 

2020 """Match gitdir against a pattern. 

2021 

2022 Args: 

2023 pattern: Pattern to match against 

2024 case_sensitive: Whether to match case-sensitively 

2025 

2026 Returns: 

2027 True if gitdir matches pattern 

2028 """ 

2029 # Handle relative patterns (starting with ./) 

2030 if pattern.startswith("./"): 

2031 # Can't handle relative patterns without config directory context 

2032 return False 

2033 

2034 # Normalize repository path 

2035 try: 

2036 repo_path = str(Path(self._controldir).resolve()) 

2037 except (OSError, ValueError): 

2038 return False 

2039 

2040 # Expand ~ in pattern and normalize 

2041 pattern = os.path.expanduser(pattern) 

2042 

2043 # Normalize pattern following Git's rules 

2044 pattern = pattern.replace("\\", "/") 

2045 if not pattern.startswith(("~/", "./", "/", "**")): 

2046 # Check for Windows absolute path 

2047 if len(pattern) >= 2 and pattern[1] == ":": 

2048 pass 

2049 else: 

2050 pattern = "**/" + pattern 

2051 if pattern.endswith("/"): 

2052 pattern = pattern + "**" 

2053 

2054 # Use the existing _match_gitdir_pattern function 

2055 from .config import _match_gitdir_pattern 

2056 

2057 pattern_bytes = pattern.encode("utf-8", errors="replace") 

2058 repo_path_bytes = repo_path.encode("utf-8", errors="replace") 

2059 

2060 return _match_gitdir_pattern( 

2061 repo_path_bytes, pattern_bytes, ignorecase=not case_sensitive 

2062 ) 

2063 

2064 # Add onbranch matcher 

2065 def match_onbranch(pattern: str) -> bool: 

2066 """Match current branch against a pattern. 

2067 

2068 Args: 

2069 pattern: Pattern to match against 

2070 

2071 Returns: 

2072 True if current branch matches pattern 

2073 """ 

2074 try: 

2075 # Get the current branch using refs 

2076 ref_chain, _ = self.refs.follow(HEADREF) 

2077 head_ref = ref_chain[-1] # Get the final resolved ref 

2078 except KeyError: 

2079 pass 

2080 else: 

2081 if head_ref and head_ref.startswith(b"refs/heads/"): 

2082 # Extract branch name from ref 

2083 branch = extract_branch_name(head_ref).decode( 

2084 "utf-8", errors="replace" 

2085 ) 

2086 return match_glob_pattern(branch, pattern) 

2087 return False 

2088 

2089 matchers: dict[str, ConditionMatcher] = { 

2090 "onbranch:": match_onbranch, 

2091 "gitdir:": lambda pattern: match_gitdir(pattern, True), 

2092 "gitdir/i:": lambda pattern: match_gitdir(pattern, False), 

2093 } 

2094 

2095 return matchers 

2096 

2097 def get_worktree_config(self) -> "ConfigFile": 

2098 """Get the worktree-specific config. 

2099 

2100 Returns: 

2101 ConfigFile object for the worktree config 

2102 """ 

2103 from .config import ConfigFile 

2104 

2105 path = os.path.join(self.commondir(), "config.worktree") 

2106 try: 

2107 # Pass condition matchers for includeIf evaluation 

2108 condition_matchers = self._get_config_condition_matchers() 

2109 return ConfigFile.from_path(path, condition_matchers=condition_matchers) 

2110 except FileNotFoundError: 

2111 cf = ConfigFile() 

2112 cf.path = path 

2113 return cf 

2114 

2115 def get_config(self) -> "ConfigFile": 

2116 """Retrieve the config object. 

2117 

2118 Returns: `ConfigFile` object for the ``.git/config`` file. 

2119 """ 

2120 from .config import ConfigFile 

2121 

2122 path = os.path.join(self._commondir, "config") 

2123 try: 

2124 # Pass condition matchers for includeIf evaluation 

2125 condition_matchers = self._get_config_condition_matchers() 

2126 return ConfigFile.from_path(path, condition_matchers=condition_matchers) 

2127 except FileNotFoundError: 

2128 ret = ConfigFile() 

2129 ret.path = path 

2130 return ret 

2131 

2132 def get_rebase_state_manager(self) -> "RebaseStateManager": 

2133 """Get the appropriate rebase state manager for this repository. 

2134 

2135 Returns: DiskRebaseStateManager instance 

2136 """ 

2137 import os 

2138 

2139 from .rebase import DiskRebaseStateManager 

2140 

2141 path = os.path.join(self.controldir(), "rebase-merge") 

2142 return DiskRebaseStateManager(path) 

2143 

2144 def get_description(self) -> bytes | None: 

2145 """Retrieve the description of this repository. 

2146 

2147 Returns: Description as bytes or None. 

2148 """ 

2149 path = os.path.join(self._controldir, "description") 

2150 try: 

2151 with GitFile(path, "rb") as f: 

2152 return f.read() 

2153 except FileNotFoundError: 

2154 return None 

2155 

2156 def __repr__(self) -> str: 

2157 """Return string representation of this repository.""" 

2158 return f"<Repo at {self.path!r}>" 

2159 

2160 def set_description(self, description: bytes) -> None: 

2161 """Set the description for this repository. 

2162 

2163 Args: 

2164 description: Text to set as description for this repository. 

2165 """ 

2166 self._put_named_file("description", description) 

2167 

2168 @classmethod 

2169 def _init_maybe_bare( 

2170 cls, 

2171 path: str | bytes | os.PathLike[str], 

2172 controldir: str | bytes | os.PathLike[str], 

2173 bare: bool, 

2174 object_store: PackBasedObjectStore | None = None, 

2175 config: "StackedConfig | None" = None, 

2176 default_branch: bytes | None = None, 

2177 symlinks: bool | None = None, 

2178 format: int | None = None, 

2179 shared_repository: str | bool | None = None, 

2180 object_format: str | None = None, 

2181 ) -> "Repo": 

2182 path = os.fspath(path) 

2183 if isinstance(path, bytes): 

2184 path = os.fsdecode(path) 

2185 controldir = os.fspath(controldir) 

2186 if isinstance(controldir, bytes): 

2187 controldir = os.fsdecode(controldir) 

2188 

2189 # Determine shared repository permissions early 

2190 file_mode: int | None = None 

2191 dir_mode: int | None = None 

2192 if shared_repository is not None: 

2193 file_mode, dir_mode = parse_shared_repository(shared_repository) 

2194 

2195 # Create base directories with appropriate permissions 

2196 for d in BASE_DIRECTORIES: 

2197 dir_path = os.path.join(controldir, *d) 

2198 os.mkdir(dir_path) 

2199 if dir_mode is not None: 

2200 os.chmod(dir_path, dir_mode) 

2201 

2202 # Determine hash algorithm 

2203 from .object_format import get_object_format 

2204 

2205 hash_alg = get_object_format(object_format) 

2206 

2207 if object_store is None: 

2208 object_store = DiskObjectStore.init( 

2209 os.path.join(controldir, OBJECTDIR), 

2210 file_mode=file_mode, 

2211 dir_mode=dir_mode, 

2212 object_format=hash_alg, 

2213 ) 

2214 ret = cls(path, bare=bare, object_store=object_store) 

2215 if default_branch is None: 

2216 if config is None: 

2217 from .config import StackedConfig 

2218 

2219 config = StackedConfig.default() 

2220 try: 

2221 default_branch = config.get("init", "defaultBranch") 

2222 except KeyError: 

2223 default_branch = DEFAULT_BRANCH 

2224 ret.refs.set_symbolic_ref(HEADREF, local_branch_name(default_branch)) 

2225 ret._init_files( 

2226 bare=bare, 

2227 symlinks=symlinks, 

2228 format=format, 

2229 shared_repository=shared_repository, 

2230 object_format=object_format, 

2231 ) 

2232 return ret 

2233 

2234 @classmethod 

2235 def init( 

2236 cls, 

2237 path: str | bytes | os.PathLike[str], 

2238 *, 

2239 mkdir: bool = False, 

2240 config: "StackedConfig | None" = None, 

2241 default_branch: bytes | None = None, 

2242 symlinks: bool | None = None, 

2243 format: int | None = None, 

2244 shared_repository: str | bool | None = None, 

2245 object_format: str | None = None, 

2246 ) -> "Repo": 

2247 """Create a new repository. 

2248 

2249 Args: 

2250 path: Path in which to create the repository 

2251 mkdir: Whether to create the directory 

2252 config: Configuration object 

2253 default_branch: Default branch name 

2254 symlinks: Whether to support symlinks 

2255 format: Repository format version (defaults to 0) 

2256 shared_repository: Shared repository setting (group, all, umask, or octal) 

2257 object_format: Object format to use ("sha1" or "sha256", defaults to "sha1") 

2258 Returns: `Repo` instance 

2259 """ 

2260 path = os.fspath(path) 

2261 if isinstance(path, bytes): 

2262 path = os.fsdecode(path) 

2263 if mkdir: 

2264 os.mkdir(path) 

2265 controldir = os.path.join(path, CONTROLDIR) 

2266 os.mkdir(controldir) 

2267 _set_filesystem_hidden(controldir) 

2268 return cls._init_maybe_bare( 

2269 path, 

2270 controldir, 

2271 False, 

2272 config=config, 

2273 default_branch=default_branch, 

2274 symlinks=symlinks, 

2275 format=format, 

2276 shared_repository=shared_repository, 

2277 object_format=object_format, 

2278 ) 

2279 

2280 @classmethod 

2281 def _init_new_working_directory( 

2282 cls, 

2283 path: str | bytes | os.PathLike[str], 

2284 main_repo: "Repo", 

2285 identifier: str | None = None, 

2286 mkdir: bool = False, 

2287 relative_paths: bool = False, 

2288 ) -> "Repo": 

2289 """Create a new working directory linked to a repository. 

2290 

2291 Args: 

2292 path: Path in which to create the working tree. 

2293 main_repo: Main repository to reference 

2294 identifier: Worktree identifier 

2295 mkdir: Whether to create the directory 

2296 relative_paths: Whether to use relative paths for gitdir references 

2297 Returns: `Repo` instance 

2298 """ 

2299 path = os.fspath(path) 

2300 if isinstance(path, bytes): 

2301 path = os.fsdecode(path) 

2302 if mkdir: 

2303 os.mkdir(path) 

2304 if identifier is None: 

2305 identifier = os.path.basename(path) 

2306 # Ensure we use absolute path for the worktree control directory 

2307 main_controldir = os.path.abspath(main_repo.controldir()) 

2308 main_worktreesdir = os.path.join(main_controldir, WORKTREES) 

2309 worktree_controldir = os.path.join(main_worktreesdir, identifier) 

2310 gitdirfile_abs = os.path.abspath(os.path.join(path, CONTROLDIR)) 

2311 

2312 # Write gitdir reference in .git file (can be relative) 

2313 # Import helper from worktree module to avoid duplication 

2314 from .worktree import _compute_gitdir_path 

2315 

2316 gitdir_ref = _compute_gitdir_path( 

2317 main_repo, 

2318 worktree_controldir, 

2319 os.path.dirname(gitdirfile_abs), 

2320 relative_paths, 

2321 ) 

2322 

2323 with open(gitdirfile_abs, "wb") as f: 

2324 f.write(b"gitdir: " + os.fsencode(gitdir_ref) + b"\n") 

2325 

2326 # Get shared repository permissions from main repository 

2327 _, dir_mode = main_repo._get_shared_repository_permissions() 

2328 

2329 # Create directories with appropriate permissions 

2330 try: 

2331 os.mkdir(main_worktreesdir) 

2332 if dir_mode is not None: 

2333 os.chmod(main_worktreesdir, dir_mode) 

2334 except FileExistsError: 

2335 pass 

2336 try: 

2337 os.mkdir(worktree_controldir) 

2338 if dir_mode is not None: 

2339 os.chmod(worktree_controldir, dir_mode) 

2340 except FileExistsError: 

2341 pass 

2342 

2343 # Write gitdir path in control directory (can be relative) 

2344 gitdir_path = _compute_gitdir_path( 

2345 main_repo, gitdirfile_abs, worktree_controldir, relative_paths 

2346 ) 

2347 

2348 with open(os.path.join(worktree_controldir, GITDIR), "wb") as f: 

2349 f.write(os.fsencode(gitdir_path) + b"\n") 

2350 with open(os.path.join(worktree_controldir, COMMONDIR), "wb") as f: 

2351 f.write(b"../..\n") 

2352 with open(os.path.join(worktree_controldir, "HEAD"), "wb") as f: 

2353 f.write(main_repo.head() + b"\n") 

2354 r = cls(os.path.normpath(path)) 

2355 r.get_worktree().reset_index(config=r.get_config_stack()) 

2356 return r 

2357 

2358 @classmethod 

2359 def init_bare( 

2360 cls, 

2361 path: str | bytes | os.PathLike[str], 

2362 *, 

2363 mkdir: bool = False, 

2364 object_store: PackBasedObjectStore | None = None, 

2365 config: "StackedConfig | None" = None, 

2366 default_branch: bytes | None = None, 

2367 format: int | None = None, 

2368 shared_repository: str | bool | None = None, 

2369 object_format: str | None = None, 

2370 ) -> "Repo": 

2371 """Create a new bare repository. 

2372 

2373 ``path`` should already exist and be an empty directory. 

2374 

2375 Args: 

2376 path: Path to create bare repository in 

2377 mkdir: Whether to create the directory 

2378 object_store: Object store to use 

2379 config: Configuration object 

2380 default_branch: Default branch name 

2381 format: Repository format version (defaults to 0) 

2382 shared_repository: Shared repository setting (group, all, umask, or octal) 

2383 object_format: Object format to use ("sha1" or "sha256", defaults to "sha1") 

2384 Returns: a `Repo` instance 

2385 """ 

2386 path = os.fspath(path) 

2387 if isinstance(path, bytes): 

2388 path = os.fsdecode(path) 

2389 if mkdir: 

2390 os.mkdir(path) 

2391 return cls._init_maybe_bare( 

2392 path, 

2393 path, 

2394 True, 

2395 object_store=object_store, 

2396 config=config, 

2397 default_branch=default_branch, 

2398 format=format, 

2399 shared_repository=shared_repository, 

2400 object_format=object_format, 

2401 ) 

2402 

2403 create = init_bare 

2404 

2405 def close(self) -> None: 

2406 """Close any files opened by this repository.""" 

2407 self.object_store.close() 

2408 # Clean up filter context if it was created 

2409 if self.filter_context is not None: 

2410 self.filter_context.close() 

2411 self.filter_context = None 

2412 

2413 def __enter__(self) -> Self: 

2414 """Enter context manager.""" 

2415 return self 

2416 

2417 def __exit__( 

2418 self, 

2419 exc_type: type[BaseException] | None, 

2420 exc_val: BaseException | None, 

2421 exc_tb: TracebackType | None, 

2422 ) -> None: 

2423 """Exit context manager and close repository.""" 

2424 self.close() 

2425 

2426 def _read_gitattributes(self) -> dict[bytes, dict[bytes, bytes]]: 

2427 """Read .gitattributes file from working tree. 

2428 

2429 Returns: 

2430 Dictionary mapping file patterns to attributes 

2431 """ 

2432 gitattributes = {} 

2433 gitattributes_path = os.path.join(self.path, ".gitattributes") 

2434 

2435 if os.path.exists(gitattributes_path): 

2436 with open(gitattributes_path, "rb") as f: 

2437 for line in f: 

2438 line = line.strip() 

2439 if not line or line.startswith(b"#"): 

2440 continue 

2441 

2442 parts = line.split() 

2443 if len(parts) < 2: 

2444 continue 

2445 

2446 pattern = parts[0] 

2447 attrs = {} 

2448 

2449 for attr in parts[1:]: 

2450 if attr.startswith(b"-"): 

2451 # Unset attribute 

2452 attrs[attr[1:]] = b"false" 

2453 elif b"=" in attr: 

2454 # Set to value 

2455 key, value = attr.split(b"=", 1) 

2456 attrs[key] = value 

2457 else: 

2458 # Set attribute 

2459 attrs[attr] = b"true" 

2460 

2461 gitattributes[pattern] = attrs 

2462 

2463 return gitattributes 

2464 

2465 def get_blob_normalizer( 

2466 self, config: "Config | None" = None 

2467 ) -> "FilterBlobNormalizer": 

2468 """Return a BlobNormalizer object. 

2469 

2470 Args: 

2471 config: Configuration to consult for filter setup. If None, 

2472 falls back to ``self.get_config_stack()``. 

2473 """ 

2474 from .filters import FilterBlobNormalizer, FilterContext, FilterRegistry 

2475 

2476 if config is None: 

2477 config = self.get_config_stack() 

2478 git_attributes = self.get_gitattributes() 

2479 

2480 # Lazily create FilterContext if needed 

2481 if self.filter_context is None: 

2482 filter_registry = FilterRegistry(config, self) 

2483 self.filter_context = FilterContext(filter_registry) 

2484 else: 

2485 # Refresh the context with current config to handle config changes 

2486 self.filter_context.refresh_config(config) 

2487 

2488 return FilterBlobNormalizer( 

2489 config, git_attributes, filter_context=self.filter_context 

2490 ) 

2491 

2492 def get_gitattributes(self, tree: bytes | None = None) -> "GitAttributes": 

2493 """Read gitattributes for the repository. 

2494 

2495 Args: 

2496 tree: Tree SHA to read .gitattributes from (defaults to HEAD) 

2497 

2498 Returns: 

2499 GitAttributes object that can be used to match paths 

2500 """ 

2501 from .attrs import ( 

2502 GitAttributes, 

2503 Pattern, 

2504 parse_git_attributes, 

2505 ) 

2506 

2507 patterns = [] 

2508 

2509 # Read system gitattributes (TODO: implement this) 

2510 # Read global gitattributes (TODO: implement this) 

2511 

2512 # Read repository .gitattributes from index/tree 

2513 if tree is None: 

2514 try: 

2515 # Try to get from HEAD 

2516 head = self[b"HEAD"] 

2517 # Peel tags to get to the underlying commit 

2518 while isinstance(head, Tag): 

2519 _cls, obj = head.object 

2520 head = self.get_object(obj) 

2521 if not isinstance(head, Commit): 

2522 raise ValueError( 

2523 f"Expected HEAD to point to a Commit, got {type(head).__name__}. " 

2524 f"This usually means HEAD points to a {type(head).__name__} object " 

2525 f"instead of a Commit." 

2526 ) 

2527 tree = head.tree 

2528 except KeyError: 

2529 # No HEAD, no attributes from tree 

2530 pass 

2531 

2532 if tree is not None: 

2533 try: 

2534 tree_obj = self[tree] 

2535 assert isinstance(tree_obj, Tree) 

2536 if b".gitattributes" in tree_obj: 

2537 _, attrs_sha = tree_obj[b".gitattributes"] 

2538 attrs_blob = self[attrs_sha] 

2539 if isinstance(attrs_blob, Blob): 

2540 attrs_data = BytesIO(attrs_blob.data) 

2541 for pattern_bytes, attrs in parse_git_attributes(attrs_data): 

2542 pattern = Pattern(pattern_bytes) 

2543 patterns.append((pattern, attrs)) 

2544 except (KeyError, NotTreeError): 

2545 pass 

2546 

2547 # Read .git/info/attributes 

2548 info_attrs_path = os.path.join(self.controldir(), "info", "attributes") 

2549 if os.path.exists(info_attrs_path): 

2550 with open(info_attrs_path, "rb") as f: 

2551 for pattern_bytes, attrs in parse_git_attributes(f): 

2552 pattern = Pattern(pattern_bytes) 

2553 patterns.append((pattern, attrs)) 

2554 

2555 # Read .gitattributes from working directory (if it exists) 

2556 working_attrs_path = os.path.join(self.path, ".gitattributes") 

2557 if os.path.exists(working_attrs_path): 

2558 with open(working_attrs_path, "rb") as f: 

2559 for pattern_bytes, attrs in parse_git_attributes(f): 

2560 pattern = Pattern(pattern_bytes) 

2561 patterns.append((pattern, attrs)) 

2562 

2563 return GitAttributes(patterns) 

2564 

2565 

2566class MemoryRepo(BaseRepo): 

2567 """Repo that stores refs, objects, and named files in memory. 

2568 

2569 MemoryRepos are always bare: they have no working tree and no index, since 

2570 those have a stronger dependency on the filesystem. 

2571 """ 

2572 

2573 filter_context: "FilterContext | None" 

2574 

2575 def __init__(self) -> None: 

2576 """Create a new repository in memory.""" 

2577 from .config import ConfigFile 

2578 from .object_format import DEFAULT_OBJECT_FORMAT 

2579 

2580 self._reflog: list[Any] = [] 

2581 refs_container = DictRefsContainer({}, logger=self._append_reflog) 

2582 BaseRepo.__init__(self, MemoryObjectStore(), refs_container) 

2583 self._named_files: dict[str, bytes] = {} 

2584 self.bare = True 

2585 self._config = ConfigFile() 

2586 self._description: bytes | None = None 

2587 self.filter_context = None 

2588 # MemoryRepo defaults to default object format 

2589 self.object_format = DEFAULT_OBJECT_FORMAT 

2590 

2591 def _append_reflog( 

2592 self, 

2593 ref: bytes, 

2594 old_sha: bytes | None, 

2595 new_sha: bytes | None, 

2596 committer: bytes | None, 

2597 timestamp: int | None, 

2598 timezone: int | None, 

2599 message: bytes | None, 

2600 ) -> None: 

2601 self._reflog.append( 

2602 (ref, old_sha, new_sha, committer, timestamp, timezone, message) 

2603 ) 

2604 

2605 def set_description(self, description: bytes) -> None: 

2606 """Set the description for this repository. 

2607 

2608 Args: 

2609 description: Text to set as description 

2610 """ 

2611 self._description = description 

2612 

2613 def get_description(self) -> bytes | None: 

2614 """Get the description of this repository. 

2615 

2616 Returns: 

2617 Repository description as bytes 

2618 """ 

2619 return self._description 

2620 

2621 def _determine_file_mode(self) -> bool: 

2622 """Probe the file-system to determine whether permissions can be trusted. 

2623 

2624 Returns: True if permissions can be trusted, False otherwise. 

2625 """ 

2626 return sys.platform != "win32" 

2627 

2628 def _determine_symlinks(self) -> bool: 

2629 """Probe the file-system to determine whether permissions can be trusted. 

2630 

2631 Returns: True if permissions can be trusted, False otherwise. 

2632 """ 

2633 return sys.platform != "win32" 

2634 

2635 def _put_named_file(self, path: str, contents: bytes) -> None: 

2636 """Write a file to the control dir with the given name and contents. 

2637 

2638 Args: 

2639 path: The path to the file, relative to the control dir. 

2640 contents: A string to write to the file. 

2641 """ 

2642 self._named_files[path] = contents 

2643 

2644 def _del_named_file(self, path: str) -> None: 

2645 try: 

2646 del self._named_files[path] 

2647 except KeyError: 

2648 pass 

2649 

2650 def get_named_file( 

2651 self, 

2652 path: str | bytes, 

2653 basedir: str | None = None, 

2654 ) -> BytesIO | None: 

2655 """Get a file from the control dir with a specific name. 

2656 

2657 Although the filename should be interpreted as a filename relative to 

2658 the control dir in a disk-baked Repo, the object returned need not be 

2659 pointing to a file in that location. 

2660 

2661 Args: 

2662 path: The path to the file, relative to the control dir. 

2663 basedir: Optional base directory for the path 

2664 Returns: An open file object, or None if the file does not exist. 

2665 """ 

2666 path_str = path.decode() if isinstance(path, bytes) else path 

2667 contents = self._named_files.get(path_str, None) 

2668 if contents is None: 

2669 return None 

2670 return BytesIO(contents) 

2671 

2672 def open_index(self, config: "Config | None" = None) -> "Index": 

2673 """Fail to open index for this repo, since it is bare. 

2674 

2675 Args: 

2676 config: Unused; kept for signature compatibility with ``BaseRepo``. 

2677 

2678 Raises: 

2679 NoIndexPresent: Raised when no index is present 

2680 """ 

2681 raise NoIndexPresent 

2682 

2683 def _init_config(self, config: "ConfigFile") -> None: 

2684 """Initialize repository configuration for MemoryRepo.""" 

2685 self._config = config 

2686 

2687 def get_config(self) -> "ConfigFile": 

2688 """Retrieve the config object. 

2689 

2690 Returns: `ConfigFile` object. 

2691 """ 

2692 return self._config 

2693 

2694 def get_rebase_state_manager(self) -> "RebaseStateManager": 

2695 """Get the appropriate rebase state manager for this repository. 

2696 

2697 Returns: MemoryRebaseStateManager instance 

2698 """ 

2699 from .rebase import MemoryRebaseStateManager 

2700 

2701 return MemoryRebaseStateManager(self) 

2702 

2703 def get_blob_normalizer( 

2704 self, config: "Config | None" = None 

2705 ) -> "FilterBlobNormalizer": 

2706 """Return a BlobNormalizer object for checkin/checkout operations. 

2707 

2708 Args: 

2709 config: Configuration to consult for filter setup. If None, 

2710 falls back to ``self.get_config_stack()``. 

2711 """ 

2712 from .filters import FilterBlobNormalizer, FilterContext, FilterRegistry 

2713 

2714 if config is None: 

2715 config = self.get_config_stack() 

2716 git_attributes = self.get_gitattributes() 

2717 

2718 if self.filter_context is None: 

2719 filter_registry = FilterRegistry(config, self) 

2720 self.filter_context = FilterContext(filter_registry) 

2721 else: 

2722 self.filter_context.refresh_config(config) 

2723 

2724 return FilterBlobNormalizer( 

2725 config, git_attributes, filter_context=self.filter_context 

2726 ) 

2727 

2728 def get_gitattributes(self, tree: bytes | None = None) -> "GitAttributes": 

2729 """Read gitattributes for the repository.""" 

2730 from .attrs import GitAttributes 

2731 

2732 # Memory repos don't have working trees or gitattributes files 

2733 # Return empty GitAttributes 

2734 return GitAttributes([]) 

2735 

2736 def close(self) -> None: 

2737 """Close any resources opened by this repository.""" 

2738 # Clean up filter context if it was created 

2739 if self.filter_context is not None: 

2740 self.filter_context.close() 

2741 self.filter_context = None 

2742 # Close object store to release pack files 

2743 self.object_store.close() 

2744 

2745 def do_commit( 

2746 self, 

2747 message: bytes | None = None, 

2748 committer: bytes | None = None, 

2749 author: bytes | None = None, 

2750 commit_timestamp: float | None = None, 

2751 commit_timezone: int | None = None, 

2752 author_timestamp: float | None = None, 

2753 author_timezone: int | None = None, 

2754 tree: ObjectID | None = None, 

2755 encoding: bytes | None = None, 

2756 ref: Ref | None = HEADREF, 

2757 merge_heads: list[ObjectID] | None = None, 

2758 no_verify: bool = False, 

2759 sign: bool = False, 

2760 config: "Config | None" = None, 

2761 ) -> bytes: 

2762 """Create a new commit. 

2763 

2764 This is a simplified implementation for in-memory repositories that 

2765 doesn't support worktree operations or hooks. 

2766 

2767 Args: 

2768 message: Commit message 

2769 committer: Committer fullname 

2770 author: Author fullname 

2771 commit_timestamp: Commit timestamp (defaults to now) 

2772 commit_timezone: Commit timestamp timezone (defaults to GMT) 

2773 author_timestamp: Author timestamp (defaults to commit timestamp) 

2774 author_timezone: Author timestamp timezone (defaults to commit timezone) 

2775 tree: SHA1 of the tree root to use 

2776 encoding: Encoding 

2777 ref: Optional ref to commit to (defaults to current branch). 

2778 If None, creates a dangling commit without updating any ref. 

2779 merge_heads: Merge heads 

2780 no_verify: Skip pre-commit and commit-msg hooks (ignored for MemoryRepo) 

2781 sign: GPG Sign the commit (ignored for MemoryRepo) 

2782 config: Configuration to consult for committer/author identity. If 

2783 None, falls back to ``self.get_config_stack()``. 

2784 

2785 Returns: 

2786 New commit SHA1 

2787 """ 

2788 import time 

2789 

2790 from .objects import Commit 

2791 

2792 if tree is None: 

2793 raise ValueError("tree must be specified for MemoryRepo") 

2794 

2795 c = Commit() 

2796 if len(tree) != self.object_format.hex_length: 

2797 raise ValueError( 

2798 f"tree must be a {self.object_format.hex_length}-character hex sha string" 

2799 ) 

2800 c.tree = tree 

2801 

2802 if config is None: 

2803 config = self.get_config_stack() 

2804 if merge_heads is None: 

2805 merge_heads = [] 

2806 if committer is None: 

2807 committer = get_user_identity(config, kind="COMMITTER") 

2808 check_user_identity(committer) 

2809 c.committer = committer 

2810 if commit_timestamp is None: 

2811 commit_timestamp = time.time() 

2812 c.commit_time = int(commit_timestamp) 

2813 if commit_timezone is None: 

2814 commit_timezone = 0 

2815 c.commit_timezone = commit_timezone 

2816 if author is None: 

2817 author = get_user_identity(config, kind="AUTHOR") 

2818 c.author = author 

2819 check_user_identity(author) 

2820 if author_timestamp is None: 

2821 author_timestamp = commit_timestamp 

2822 c.author_time = int(author_timestamp) 

2823 if author_timezone is None: 

2824 author_timezone = commit_timezone 

2825 c.author_timezone = author_timezone 

2826 if encoding is None: 

2827 try: 

2828 encoding = config.get(("i18n",), "commitEncoding") 

2829 except KeyError: 

2830 pass 

2831 if encoding is not None: 

2832 c.encoding = encoding 

2833 

2834 # Handle message (for MemoryRepo, we don't support callable messages) 

2835 if callable(message): 

2836 message = message(self, c) 

2837 if message is None: 

2838 raise ValueError("Message callback returned None") 

2839 

2840 if message is None: 

2841 raise ValueError("No commit message specified") 

2842 

2843 c.message = message 

2844 

2845 if ref is None: 

2846 # Create a dangling commit 

2847 c.parents = merge_heads 

2848 self.object_store.add_object(c) 

2849 else: 

2850 try: 

2851 old_head = self.refs[ref] 

2852 c.parents = [old_head, *merge_heads] 

2853 self.object_store.add_object(c) 

2854 ok = self.refs.set_if_equals( 

2855 ref, 

2856 old_head, 

2857 c.id, 

2858 message=b"commit: " + message, 

2859 committer=committer, 

2860 timestamp=int(commit_timestamp), 

2861 timezone=commit_timezone, 

2862 ) 

2863 except KeyError: 

2864 c.parents = merge_heads 

2865 self.object_store.add_object(c) 

2866 ok = self.refs.add_if_new( 

2867 ref, 

2868 c.id, 

2869 message=b"commit: " + message, 

2870 committer=committer, 

2871 timestamp=int(commit_timestamp), 

2872 timezone=commit_timezone, 

2873 ) 

2874 if not ok: 

2875 from .errors import CommitError 

2876 

2877 raise CommitError(f"{ref!r} changed during commit") 

2878 

2879 return c.id 

2880 

2881 @classmethod 

2882 def init_bare( 

2883 cls, 

2884 objects: Iterable[ShaFile], 

2885 refs: Mapping[Ref, ObjectID], 

2886 format: int | None = None, 

2887 object_format: str | None = None, 

2888 ) -> "MemoryRepo": 

2889 """Create a new bare repository in memory. 

2890 

2891 Args: 

2892 objects: Objects for the new repository, 

2893 as iterable 

2894 refs: Refs as dictionary, mapping names 

2895 to object SHA1s 

2896 format: Repository format version (defaults to 0) 

2897 object_format: Object format to use ("sha1" or "sha256", defaults to "sha1") 

2898 """ 

2899 ret = cls() 

2900 for obj in objects: 

2901 ret.object_store.add_object(obj) 

2902 for refname, sha in refs.items(): 

2903 ret.refs.add_if_new(refname, sha) 

2904 ret._init_files(bare=True, format=format, object_format=object_format) 

2905 return ret