Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/repo/base.py: 45%

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

620 statements  

1# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 

2# 

3# This module is part of GitPython and is released under the 

4# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 

5 

6from __future__ import annotations 

7 

8__all__ = ["Repo"] 

9 

10import gc 

11import logging 

12import os 

13import os.path as osp 

14from pathlib import Path 

15import re 

16import shlex 

17import sys 

18import warnings 

19 

20import gitdb 

21from gitdb.db.loose import LooseObjectDB 

22from gitdb.exc import BadObject 

23 

24from git.cmd import Git, handle_process_output 

25from git.compat import defenc, safe_decode 

26from git.config import GitConfigParser 

27from git.db import GitCmdObjectDB 

28from git.exc import ( 

29 GitCommandError, 

30 InvalidGitRepositoryError, 

31 NoSuchPathError, 

32) 

33from git.index import IndexFile 

34from git.objects import Submodule, RootModule, Commit 

35from git.refs import HEAD, Head, Reference, TagReference 

36from git.remote import Remote, add_progress, to_progress_instance 

37from git.util import ( 

38 Actor, 

39 cygpath, 

40 expand_path, 

41 finalize_process, 

42 hex_to_bin, 

43 remove_password_if_present, 

44) 

45 

46from .fun import ( 

47 find_submodule_git_dir, 

48 find_worktree_git_dir, 

49 is_git_dir, 

50 rev_parse, 

51 touch, 

52) 

53 

54# typing ------------------------------------------------------ 

55 

56from git.types import ( 

57 CallableProgress, 

58 Commit_ish, 

59 Lit_config_levels, 

60 PathLike, 

61 TBD, 

62 Tree_ish, 

63 assert_never, 

64) 

65from typing import ( 

66 Any, 

67 BinaryIO, 

68 Callable, 

69 Dict, 

70 Iterator, 

71 List, 

72 Mapping, 

73 NamedTuple, 

74 Optional, 

75 Sequence, 

76 TYPE_CHECKING, 

77 TextIO, 

78 Tuple, 

79 Type, 

80 Union, 

81 cast, 

82) 

83 

84from git.types import ConfigLevels_Tup, TypedDict 

85 

86if TYPE_CHECKING: 

87 from git.objects import Tree 

88 from git.objects.submodule.base import UpdateProgress 

89 from git.refs.symbolic import SymbolicReference 

90 from git.remote import RemoteProgress 

91 from git.util import IterableList 

92 

93# ----------------------------------------------------------- 

94 

95_logger = logging.getLogger(__name__) 

96 

97 

98class BlameEntry(NamedTuple): 

99 commit: Dict[str, Commit] 

100 linenos: range 

101 orig_path: Optional[str] 

102 orig_linenos: range 

103 

104 

105class Repo: 

106 """Represents a git repository and allows you to query references, create commit 

107 information, generate diffs, create and clone repositories, and query the log. 

108 

109 The following attributes are worth using: 

110 

111 * :attr:`working_dir` is the working directory of the git command, which is the 

112 working tree directory if available or the ``.git`` directory in case of bare 

113 repositories. 

114 

115 * :attr:`working_tree_dir` is the working tree directory, but will return ``None`` 

116 if we are a bare repository. 

117 

118 * :attr:`git_dir` is the ``.git`` repository directory, which is always set. 

119 """ 

120 

121 DAEMON_EXPORT_FILE = "git-daemon-export-ok" 

122 

123 # Must exist, or __del__ will fail in case we raise on `__init__()`. 

124 git = cast("Git", None) 

125 

126 working_dir: PathLike 

127 """The working directory of the git command.""" 

128 

129 # stored as string for easier processing, but annotated as path for clearer intention 

130 _working_tree_dir: Optional[PathLike] = None 

131 

132 git_dir: PathLike 

133 """The ``.git`` repository directory.""" 

134 

135 _common_dir: PathLike = "" 

136 

137 # Precompiled regex 

138 re_whitespace = re.compile(r"\s+") 

139 re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$") 

140 re_hexsha_shortened = re.compile(r"^[0-9A-Fa-f]{4,40}$") 

141 re_envvars = re.compile(r"(\$(\{\s?)?[a-zA-Z_]\w*(\}\s?)?|%\s?[a-zA-Z_]\w*\s?%)") 

142 re_author_committer_start = re.compile(r"^(author|committer)") 

143 re_tab_full_line = re.compile(r"^\t(.*)$") 

144 

145 unsafe_git_clone_options = [ 

146 # Executes arbitrary commands: 

147 "--upload-pack", 

148 "-u", 

149 # Can override configuration variables that execute arbitrary commands: 

150 "--config", 

151 "-c", 

152 # Can install hooks that execute during clone: 

153 "--template", 

154 ] 

155 """Options to :manpage:`git-clone(1)` that allow arbitrary commands to be executed. 

156 

157 The ``--upload-pack``/``-u`` option allows users to execute arbitrary commands 

158 directly: 

159 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt 

160 

161 The ``--config``/``-c`` option allows users to override configuration variables like 

162 ``protocol.allow`` and ``core.gitProxy`` to execute arbitrary commands: 

163 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt 

164 

165 The ``--template`` option can install hooks that execute during clone: 

166 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---templatetemplate-directory 

167 """ 

168 

169 unsafe_git_archive_options = [ 

170 # Allows arbitrary command execution through the remote git-upload-archive command. 

171 "--exec", 

172 # Writes output to a caller-controlled filesystem path. 

173 "--output", 

174 "-o", 

175 ] 

176 

177 unsafe_git_revision_options = [ 

178 # This option allows output to be written to arbitrary files before revision parsing. 

179 "--output", 

180 "-o", 

181 ] 

182 

183 # Invariants 

184 config_level: ConfigLevels_Tup = ("system", "user", "global", "repository") 

185 """Represents the configuration level of a configuration file.""" 

186 

187 # Subclass configuration 

188 GitCommandWrapperType = Git 

189 """Subclasses may easily bring in their own custom types by placing a constructor or 

190 type here.""" 

191 

192 def __init__( 

193 self, 

194 path: Optional[PathLike] = None, 

195 odbt: Type[LooseObjectDB] = GitCmdObjectDB, 

196 search_parent_directories: bool = False, 

197 expand_vars: bool = True, 

198 ) -> None: 

199 R"""Create a new :class:`Repo` instance. 

200 

201 :param path: 

202 The path to either the worktree directory or the .git directory itself:: 

203 

204 repo = Repo("/Users/mtrier/Development/git-python") 

205 repo = Repo("/Users/mtrier/Development/git-python.git") 

206 repo = Repo("~/Development/git-python.git") 

207 repo = Repo("$REPOSITORIES/Development/git-python.git") 

208 repo = Repo(R"C:\Users\mtrier\Development\git-python\.git") 

209 

210 - In *Cygwin*, `path` may be a ``cygdrive/...`` prefixed path. 

211 - If `path` is ``None`` or an empty string, :envvar:`GIT_DIR` is used. If 

212 that environment variable is absent or empty, the current directory is 

213 used. 

214 

215 :param odbt: 

216 Object DataBase type - a type which is constructed by providing the 

217 directory containing the database objects, i.e. ``.git/objects``. It will be 

218 used to access all object data. 

219 

220 :param search_parent_directories: 

221 If ``True``, all parent directories will be searched for a valid repo as 

222 well. 

223 

224 Please note that this was the default behaviour in older versions of 

225 GitPython, which is considered a bug though. 

226 

227 :raise git.exc.InvalidGitRepositoryError: 

228 

229 :raise git.exc.NoSuchPathError: 

230 

231 :return: 

232 :class:`Repo` 

233 """ 

234 

235 epath = path or os.getenv("GIT_DIR") 

236 if not epath: 

237 epath = os.getcwd() 

238 epath = os.fspath(epath) 

239 if Git.is_cygwin(): 

240 # Given how the tests are written, this seems more likely to catch Cygwin 

241 # git used from Windows than Windows git used from Cygwin. Therefore 

242 # changing to Cygwin-style paths is the relevant operation. 

243 epath = cygpath(epath) 

244 

245 if expand_vars and re.search(self.re_envvars, epath): 

246 warnings.warn( 

247 "The use of environment variables in paths is deprecated" 

248 + "\nfor security reasons and may be removed in the future!!", 

249 stacklevel=1, 

250 ) 

251 epath = expand_path(epath, expand_vars) 

252 if epath is not None: 

253 if not os.path.exists(epath): 

254 raise NoSuchPathError(epath) 

255 

256 # Walk up the path to find the `.git` dir. 

257 curpath = epath 

258 git_dir = None 

259 while curpath: 

260 # ABOUT osp.NORMPATH 

261 # It's important to normalize the paths, as submodules will otherwise 

262 # initialize their repo instances with paths that depend on path-portions 

263 # that will not exist after being removed. It's just cleaner. 

264 if ( 

265 osp.isfile(osp.join(curpath, "gitdir")) 

266 and osp.isfile(osp.join(curpath, "commondir")) 

267 and osp.isfile(osp.join(curpath, "HEAD")) 

268 ): 

269 git_dir = curpath 

270 

271 if "GIT_WORK_TREE" in os.environ: 

272 self._working_tree_dir = os.getenv("GIT_WORK_TREE") 

273 else: 

274 # Linked worktree administrative directories store the path to the 

275 # worktree's .git file in their gitdir file (without "gitdir: " prefix). 

276 with open(osp.join(git_dir, "gitdir")) as fp: 

277 worktree_gitfile = fp.read().strip() 

278 

279 if not osp.isabs(worktree_gitfile): 

280 worktree_gitfile = osp.normpath(osp.join(git_dir, worktree_gitfile)) 

281 

282 self._working_tree_dir = osp.dirname(worktree_gitfile) 

283 

284 break 

285 

286 if is_git_dir(curpath): 

287 git_dir = curpath 

288 # from man git-config : core.worktree 

289 # Set the path to the root of the working tree. If GIT_COMMON_DIR 

290 # environment variable is set, core.worktree is ignored and not used for 

291 # determining the root of working tree. This can be overridden by the 

292 # GIT_WORK_TREE environment variable. The value can be an absolute path 

293 # or relative to the path to the .git directory, which is either 

294 # specified by GIT_DIR, or automatically discovered. If GIT_DIR is 

295 # specified but none of GIT_WORK_TREE and core.worktree is specified, 

296 # the current working directory is regarded as the top level of your 

297 # working tree. 

298 self._working_tree_dir = os.path.dirname(git_dir) 

299 if os.environ.get("GIT_COMMON_DIR") is None: 

300 gitconf = self._config_reader("repository", git_dir) 

301 if gitconf.has_option("core", "worktree"): 

302 self._working_tree_dir = gitconf.get("core", "worktree") 

303 if "GIT_WORK_TREE" in os.environ: 

304 self._working_tree_dir = os.getenv("GIT_WORK_TREE") 

305 break 

306 

307 dotgit = osp.join(curpath, ".git") 

308 sm_gitpath = find_submodule_git_dir(dotgit) 

309 if sm_gitpath is not None: 

310 git_dir = osp.normpath(sm_gitpath) 

311 

312 sm_gitpath = find_submodule_git_dir(dotgit) 

313 if sm_gitpath is None: 

314 sm_gitpath = find_worktree_git_dir(dotgit) 

315 

316 if sm_gitpath is not None: 

317 # worktrees can use relative paths as of Git 2.48, so we join to curpath 

318 git_dir = osp.normpath(osp.join(curpath, sm_gitpath)) 

319 self._working_tree_dir = curpath 

320 break 

321 

322 if not search_parent_directories: 

323 break 

324 curpath, tail = osp.split(curpath) 

325 if not tail: 

326 break 

327 # END while curpath 

328 

329 if git_dir is None: 

330 raise InvalidGitRepositoryError(epath) 

331 self.git_dir = git_dir 

332 

333 self._bare = False 

334 try: 

335 self._bare = self.config_reader("repository").getboolean("core", "bare") 

336 except Exception: 

337 # Let's not assume the option exists, although it should. 

338 pass 

339 

340 try: 

341 common_dir = (Path(self.git_dir) / "commondir").read_text().splitlines()[0].strip() 

342 self._common_dir = osp.join(self.git_dir, common_dir) 

343 except OSError: 

344 self._common_dir = "" 

345 

346 # Adjust the working directory in case we are actually bare - we didn't know 

347 # that in the first place. 

348 if self._bare: 

349 self._working_tree_dir = None 

350 # END working dir handling 

351 

352 self.working_dir: PathLike = self._working_tree_dir or self.common_dir 

353 self.git = self.GitCommandWrapperType(self.working_dir) 

354 

355 # Special handling, in special times. 

356 rootpath = osp.join(self.common_dir, "objects") 

357 if issubclass(odbt, GitCmdObjectDB): 

358 self.odb = odbt(rootpath, self.git) 

359 else: 

360 self.odb = odbt(rootpath) 

361 

362 def __enter__(self) -> "Repo": 

363 return self 

364 

365 def __exit__(self, *args: Any) -> None: 

366 self.close() 

367 

368 def __del__(self) -> None: 

369 try: 

370 self.close() 

371 except Exception: 

372 pass 

373 

374 def close(self) -> None: 

375 if self.git: 

376 self.git.clear_cache() 

377 # Tempfiles objects on Windows are holding references to open files until 

378 # they are collected by the garbage collector, thus preventing deletion. 

379 # TODO: Find these references and ensure they are closed and deleted 

380 # synchronously rather than forcing a gc collection. 

381 if sys.platform == "win32": 

382 gc.collect() 

383 gitdb.util.mman.collect() 

384 if sys.platform == "win32": 

385 gc.collect() 

386 

387 def __eq__(self, rhs: object) -> bool: 

388 if isinstance(rhs, Repo): 

389 return self.git_dir == rhs.git_dir 

390 return False 

391 

392 def __ne__(self, rhs: object) -> bool: 

393 return not self.__eq__(rhs) 

394 

395 def __hash__(self) -> int: 

396 return hash(self.git_dir) 

397 

398 @property 

399 def description(self) -> str: 

400 """The project's description""" 

401 filename = osp.join(self.git_dir, "description") 

402 with open(filename, "rb") as fp: 

403 return fp.read().rstrip().decode(defenc) 

404 

405 @description.setter 

406 def description(self, descr: str) -> None: 

407 filename = osp.join(self.git_dir, "description") 

408 with open(filename, "wb") as fp: 

409 fp.write((descr + "\n").encode(defenc)) 

410 

411 @property 

412 def working_tree_dir(self) -> Optional[PathLike]: 

413 """ 

414 :return: 

415 The working tree directory of our git repository. 

416 If this is a bare repository, ``None`` is returned. 

417 """ 

418 return self._working_tree_dir 

419 

420 @property 

421 def common_dir(self) -> PathLike: 

422 """ 

423 :return: 

424 The git dir that holds everything except possibly HEAD, FETCH_HEAD, 

425 ORIG_HEAD, COMMIT_EDITMSG, index, and logs/. 

426 """ 

427 return self._common_dir or self.git_dir 

428 

429 @property 

430 def bare(self) -> bool: 

431 """:return: ``True`` if the repository is bare""" 

432 return self._bare 

433 

434 @property 

435 def heads(self) -> "IterableList[Head]": 

436 """A list of :class:`~git.refs.head.Head` objects representing the branch heads 

437 in this repo. 

438 

439 :return: 

440 ``git.IterableList(Head, ...)`` 

441 """ 

442 return Head.list_items(self) 

443 

444 @property 

445 def branches(self) -> "IterableList[Head]": 

446 """Alias for heads. 

447 A list of :class:`~git.refs.head.Head` objects representing the branch heads 

448 in this repo. 

449 

450 :return: 

451 ``git.IterableList(Head, ...)`` 

452 """ 

453 return self.heads 

454 

455 @property 

456 def references(self) -> "IterableList[Reference]": 

457 """A list of :class:`~git.refs.reference.Reference` objects representing tags, 

458 heads and remote references. 

459 

460 :return: 

461 ``git.IterableList(Reference, ...)`` 

462 """ 

463 return Reference.list_items(self) 

464 

465 @property 

466 def refs(self) -> "IterableList[Reference]": 

467 """Alias for references. 

468 A list of :class:`~git.refs.reference.Reference` objects representing tags, 

469 heads and remote references. 

470 

471 :return: 

472 ``git.IterableList(Reference, ...)`` 

473 """ 

474 return self.references 

475 

476 @property 

477 def index(self) -> "IndexFile": 

478 """ 

479 :return: 

480 A :class:`~git.index.base.IndexFile` representing this repository's index. 

481 

482 :note: 

483 This property can be expensive, as the returned 

484 :class:`~git.index.base.IndexFile` will be reinitialized. 

485 It is recommended to reuse the object. 

486 """ 

487 return IndexFile(self) 

488 

489 @property 

490 def head(self) -> "HEAD": 

491 """ 

492 :return: 

493 :class:`~git.refs.head.HEAD` object pointing to the current head reference 

494 """ 

495 return HEAD(self, "HEAD") 

496 

497 @property 

498 def remotes(self) -> "IterableList[Remote]": 

499 """A list of :class:`~git.remote.Remote` objects allowing to access and 

500 manipulate remotes. 

501 

502 :return: 

503 ``git.IterableList(Remote, ...)`` 

504 """ 

505 return Remote.list_items(self) 

506 

507 def remote(self, name: str = "origin") -> "Remote": 

508 """:return: The remote with the specified name 

509 

510 :raise ValueError: 

511 If no remote with such a name exists. 

512 """ 

513 r = Remote(self, name) 

514 if not r.exists(): 

515 raise ValueError("Remote named '%s' didn't exist" % name) 

516 return r 

517 

518 # { Submodules 

519 

520 @property 

521 def submodules(self) -> "IterableList[Submodule]": 

522 """ 

523 :return: 

524 git.IterableList(Submodule, ...) of direct submodules available from the 

525 current head 

526 """ 

527 return Submodule.list_items(self) 

528 

529 def submodule(self, name: str) -> "Submodule": 

530 """:return: The submodule with the given name 

531 

532 :raise ValueError: 

533 If no such submodule exists. 

534 """ 

535 try: 

536 return self.submodules[name] 

537 except IndexError as e: 

538 raise ValueError("Didn't find submodule named %r" % name) from e 

539 # END exception handling 

540 

541 def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule: 

542 """Create a new submodule. 

543 

544 :note: 

545 For a description of the applicable parameters, see the documentation of 

546 :meth:`Submodule.add <git.objects.submodule.base.Submodule.add>`. 

547 

548 :return: 

549 The created submodule. 

550 """ 

551 return Submodule.add(self, *args, **kwargs) 

552 

553 def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]: 

554 """An iterator yielding Submodule instances. 

555 

556 See the :class:`~git.objects.util.Traversable` interface for a description of `args` 

557 and `kwargs`. 

558 

559 :return: 

560 Iterator 

561 """ 

562 return RootModule(self).traverse(*args, **kwargs) 

563 

564 def submodule_update(self, *args: Any, **kwargs: Any) -> RootModule: 

565 """Update the submodules, keeping the repository consistent as it will 

566 take the previous state into consideration. 

567 

568 :note: 

569 For more information, please see the documentation of 

570 :meth:`RootModule.update <git.objects.submodule.root.RootModule.update>`. 

571 """ 

572 return RootModule(self).update(*args, **kwargs) 

573 

574 # }END submodules 

575 

576 @property 

577 def tags(self) -> "IterableList[TagReference]": 

578 """A list of :class:`~git.refs.tag.TagReference` objects that are available in 

579 this repo. 

580 

581 :return: 

582 ``git.IterableList(TagReference, ...)`` 

583 """ 

584 return TagReference.list_items(self) 

585 

586 def tag(self, path: PathLike) -> TagReference: 

587 """ 

588 :return: 

589 :class:`~git.refs.tag.TagReference` object, reference pointing to a 

590 :class:`~git.objects.commit.Commit` or tag 

591 

592 :param path: 

593 Path to the tag reference, e.g. ``0.1.5`` or ``tags/0.1.5``. 

594 """ 

595 full_path = self._to_full_tag_path(path) 

596 return TagReference(self, full_path) 

597 

598 @staticmethod 

599 def _to_full_tag_path(path: PathLike) -> str: 

600 path_str = str(path) 

601 if path_str.startswith(TagReference._common_path_default + "/"): 

602 return path_str 

603 if path_str.startswith(TagReference._common_default + "/"): 

604 return Reference._common_path_default + "/" + path_str 

605 else: 

606 return TagReference._common_path_default + "/" + path_str 

607 

608 def create_head( 

609 self, 

610 path: PathLike, 

611 commit: Union["SymbolicReference", "str"] = "HEAD", 

612 force: bool = False, 

613 logmsg: Optional[str] = None, 

614 ) -> "Head": 

615 """Create a new head within the repository. 

616 

617 :note: 

618 For more documentation, please see the 

619 :meth:`Head.create <git.refs.head.Head.create>` method. 

620 

621 :return: 

622 Newly created :class:`~git.refs.head.Head` Reference. 

623 """ 

624 return Head.create(self, path, commit, logmsg, force) 

625 

626 def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None: 

627 """Delete the given heads. 

628 

629 :param kwargs: 

630 Additional keyword arguments to be passed to :manpage:`git-branch(1)`. 

631 """ 

632 return Head.delete(self, *heads, **kwargs) 

633 

634 def create_tag( 

635 self, 

636 path: PathLike, 

637 ref: Union[str, "SymbolicReference"] = "HEAD", 

638 message: Optional[str] = None, 

639 force: bool = False, 

640 **kwargs: Any, 

641 ) -> TagReference: 

642 """Create a new tag reference. 

643 

644 :note: 

645 For more documentation, please see the 

646 :meth:`TagReference.create <git.refs.tag.TagReference.create>` method. 

647 

648 :return: 

649 :class:`~git.refs.tag.TagReference` object 

650 """ 

651 return TagReference.create(self, path, ref, message, force, **kwargs) 

652 

653 def delete_tag(self, *tags: TagReference) -> None: 

654 """Delete the given tag references.""" 

655 return TagReference.delete(self, *tags) 

656 

657 def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote: 

658 """Create a new remote. 

659 

660 For more information, please see the documentation of the 

661 :meth:`Remote.create <git.remote.Remote.create>` method. 

662 

663 :return: 

664 :class:`~git.remote.Remote` reference 

665 """ 

666 return Remote.create(self, name, url, **kwargs) 

667 

668 def delete_remote(self, remote: "Remote") -> str: 

669 """Delete the given remote.""" 

670 return Remote.remove(self, remote) 

671 

672 def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[PathLike] = None) -> str: 

673 if git_dir is None: 

674 git_dir = self.git_dir 

675 # We do not support an absolute path of the gitconfig on Windows. 

676 # Use the global config instead. 

677 if sys.platform == "win32" and config_level == "system": 

678 config_level = "global" 

679 

680 if config_level == "system": 

681 return "/etc/gitconfig" 

682 elif config_level == "user": 

683 config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", "~"), ".config") 

684 return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config"))) 

685 elif config_level == "global": 

686 return osp.normpath(osp.expanduser("~/.gitconfig")) 

687 elif config_level == "repository": 

688 repo_dir = self._common_dir or git_dir 

689 if not repo_dir: 

690 raise NotADirectoryError 

691 else: 

692 return osp.normpath(osp.join(repo_dir, "config")) 

693 else: 

694 assert_never( # type: ignore[unreachable] 

695 config_level, 

696 ValueError(f"Invalid configuration level: {config_level!r}"), 

697 ) 

698 

699 def config_reader( 

700 self, 

701 config_level: Optional[Lit_config_levels] = None, 

702 ) -> GitConfigParser: 

703 """ 

704 :return: 

705 :class:`~git.config.GitConfigParser` allowing to read the full git 

706 configuration, but not to write it. 

707 

708 The configuration will include values from the system, user and repository 

709 configuration files. 

710 

711 :param config_level: 

712 For possible values, see the :meth:`config_writer` method. If ``None``, all 

713 applicable levels will be used. Specify a level in case you know which file 

714 you wish to read to prevent reading multiple files. 

715 

716 :note: 

717 On Windows, system configuration cannot currently be read as the path is 

718 unknown, instead the global path will be used. 

719 """ 

720 return self._config_reader(config_level=config_level) 

721 

722 def _config_reader( 

723 self, 

724 config_level: Optional[Lit_config_levels] = None, 

725 git_dir: Optional[PathLike] = None, 

726 ) -> GitConfigParser: 

727 if config_level is None: 

728 files = [self._get_config_path(f, git_dir) for f in self.config_level if f] 

729 else: 

730 files = [self._get_config_path(config_level, git_dir)] 

731 return GitConfigParser(files, read_only=True, repo=self) 

732 

733 def config_writer(self, config_level: Lit_config_levels = "repository") -> GitConfigParser: 

734 """ 

735 :return: 

736 A :class:`~git.config.GitConfigParser` allowing to write values of the 

737 specified configuration file level. Config writers should be retrieved, used 

738 to change the configuration, and written right away as they will lock the 

739 configuration file in question and prevent other's to write it. 

740 

741 :param config_level: 

742 One of the following values: 

743 

744 * ``"system"`` = system wide configuration file 

745 * ``"global"`` = user level configuration file 

746 * ``"`repository"`` = configuration file for this repository only 

747 """ 

748 return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self, merge_includes=False) 

749 

750 def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit: 

751 """The :class:`~git.objects.commit.Commit` object for the specified revision. 

752 

753 :param rev: 

754 Revision specifier, see :manpage:`git-rev-parse(1)` for viable options. 

755 

756 :return: 

757 :class:`~git.objects.commit.Commit` 

758 """ 

759 if rev is None: 

760 return self.head.commit 

761 return self.rev_parse(str(rev) + "^0") 

762 

763 def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator["Tree"]: 

764 """:return: Iterator yielding :class:`~git.objects.tree.Tree` objects 

765 

766 :note: 

767 Accepts all arguments known to the :meth:`iter_commits` method. 

768 """ 

769 return (c.tree for c in self.iter_commits(*args, **kwargs)) 

770 

771 def tree(self, rev: Union[Tree_ish, str, None] = None) -> "Tree": 

772 """The :class:`~git.objects.tree.Tree` object for the given tree-ish revision. 

773 

774 Examples:: 

775 

776 repo.tree(repo.heads[0]) 

777 

778 :param rev: 

779 A revision pointing to a Treeish (being a commit or tree). 

780 

781 :return: 

782 :class:`~git.objects.tree.Tree` 

783 

784 :note: 

785 If you need a non-root level tree, find it by iterating the root tree. 

786 Otherwise it cannot know about its path relative to the repository root and 

787 subsequent operations might have unexpected results. 

788 """ 

789 if rev is None: 

790 return self.head.commit.tree 

791 return self.rev_parse(str(rev) + "^{tree}") 

792 

793 def iter_commits( 

794 self, 

795 rev: Union[str, Commit, "SymbolicReference", None] = None, 

796 paths: Union[PathLike, Sequence[PathLike]] = "", 

797 allow_unsafe_options: bool = False, 

798 **kwargs: Any, 

799 ) -> Iterator[Commit]: 

800 """An iterator of :class:`~git.objects.commit.Commit` objects representing the 

801 history of a given ref/commit. 

802 

803 :param rev: 

804 Revision specifier, see :manpage:`git-rev-parse(1)` for viable options. 

805 If ``None``, the active branch will be used. 

806 

807 :param paths: 

808 An optional path or a list of paths. If set, only commits that include the 

809 path or paths will be returned. 

810 

811 :param kwargs: 

812 Arguments to be passed to :manpage:`git-rev-list(1)`. 

813 Common ones are ``max_count`` and ``skip``. 

814 

815 :param allow_unsafe_options: 

816 Allow unsafe options in the revision argument, like ``--output``. 

817 

818 :note: 

819 To receive only commits between two named revisions, use the 

820 ``"revA...revB"`` revision specifier. 

821 

822 :return: 

823 Iterator of :class:`~git.objects.commit.Commit` objects 

824 """ 

825 if rev is None: 

826 rev = self.head.commit 

827 

828 if not allow_unsafe_options: 

829 Git.check_unsafe_options( 

830 options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options 

831 ) 

832 

833 return Commit.iter_items( 

834 self, 

835 rev, 

836 paths, 

837 allow_unsafe_options=allow_unsafe_options, 

838 **kwargs, 

839 ) 

840 

841 def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]: 

842 R"""Find the closest common ancestor for the given revision 

843 (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s, 

844 :class:`~git.refs.reference.Reference`\s, etc.). 

845 

846 :param rev: 

847 At least two revs to find the common ancestor for. 

848 

849 :param kwargs: 

850 Additional arguments to be passed to the ``repo.git.merge_base()`` command 

851 which does all the work. 

852 

853 :return: 

854 A list of :class:`~git.objects.commit.Commit` objects. If ``--all`` was 

855 not passed as a keyword argument, the list will have at max one 

856 :class:`~git.objects.commit.Commit`, or is empty if no common merge base 

857 exists. 

858 

859 :raise ValueError: 

860 If fewer than two revisions are provided. 

861 """ 

862 if len(rev) < 2: 

863 raise ValueError("Please specify at least two revs, got only %i" % len(rev)) 

864 # END handle input 

865 

866 res: List[Commit] = [] 

867 try: 

868 lines: List[str] = self.git.merge_base(*rev, **kwargs).splitlines() 

869 except GitCommandError as err: 

870 if err.status == 128: 

871 raise 

872 # END handle invalid rev 

873 # Status code 1 is returned if there is no merge-base. 

874 # (See: https://github.com/git/git/blob/v2.44.0/builtin/merge-base.c#L19) 

875 return res 

876 # END exception handling 

877 

878 for line in lines: 

879 res.append(self.commit(line)) 

880 # END for each merge-base 

881 

882 return res 

883 

884 def is_ancestor(self, ancestor_rev: Commit, rev: Commit) -> bool: 

885 """Check if a commit is an ancestor of another. 

886 

887 :param ancestor_rev: 

888 Rev which should be an ancestor. 

889 

890 :param rev: 

891 Rev to test against `ancestor_rev`. 

892 

893 :return: 

894 ``True`` if `ancestor_rev` is an ancestor to `rev`. 

895 """ 

896 try: 

897 self.git.merge_base(ancestor_rev, rev, is_ancestor=True) 

898 except GitCommandError as err: 

899 if err.status == 1: 

900 return False 

901 raise 

902 return True 

903 

904 def is_valid_object(self, sha: str, object_type: Union[str, None] = None) -> bool: 

905 try: 

906 complete_sha = self.odb.partial_to_complete_sha_hex(sha) 

907 object_info = self.odb.info(complete_sha) 

908 if object_type: 

909 if object_info.type == object_type.encode(): 

910 return True 

911 else: 

912 _logger.debug( 

913 "Commit hash points to an object of type '%s'. Requested were objects of type '%s'", 

914 object_info.type.decode(), 

915 object_type, 

916 ) 

917 return False 

918 else: 

919 return True 

920 except BadObject: 

921 _logger.debug("Commit hash is invalid.") 

922 return False 

923 

924 def _get_daemon_export(self) -> bool: 

925 if self.git_dir: 

926 filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) 

927 return osp.exists(filename) 

928 

929 def _set_daemon_export(self, value: object) -> None: 

930 if self.git_dir: 

931 filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE) 

932 fileexists = osp.exists(filename) 

933 if value and not fileexists: 

934 touch(filename) 

935 elif not value and fileexists: 

936 os.unlink(filename) 

937 

938 @property 

939 def daemon_export(self) -> bool: 

940 """If True, git-daemon may export this repository""" 

941 return self._get_daemon_export() 

942 

943 @daemon_export.setter 

944 def daemon_export(self, value: object) -> None: 

945 self._set_daemon_export(value) 

946 

947 def _get_alternates(self) -> List[str]: 

948 """The list of alternates for this repo from which objects can be retrieved. 

949 

950 :return: 

951 List of strings being pathnames of alternates 

952 """ 

953 if self.git_dir: 

954 alternates_path = osp.join(self.git_dir, "objects", "info", "alternates") 

955 

956 if osp.exists(alternates_path): 

957 with open(alternates_path, "rb") as f: 

958 alts = f.read().decode(defenc) 

959 return alts.strip().splitlines() 

960 return [] 

961 

962 def _set_alternates(self, alts: List[str]) -> None: 

963 """Set the alternates. 

964 

965 :param alts: 

966 The array of string paths representing the alternates at which git should 

967 look for objects, i.e. ``/home/user/repo/.git/objects``. 

968 

969 :raise git.exc.NoSuchPathError: 

970 

971 :note: 

972 The method does not check for the existence of the paths in `alts`, as the 

973 caller is responsible. 

974 """ 

975 alternates_path = osp.join(self.common_dir, "objects", "info", "alternates") 

976 if not alts: 

977 if osp.isfile(alternates_path): 

978 os.remove(alternates_path) 

979 else: 

980 with open(alternates_path, "wb") as f: 

981 f.write("\n".join(alts).encode(defenc)) 

982 

983 @property 

984 def alternates(self) -> List[str]: 

985 """Retrieve a list of alternates paths or set a list paths to be used as alternates""" 

986 return self._get_alternates() 

987 

988 @alternates.setter 

989 def alternates(self, alts: List[str]) -> None: 

990 self._set_alternates(alts) 

991 

992 def is_dirty( 

993 self, 

994 index: bool = True, 

995 working_tree: bool = True, 

996 untracked_files: bool = False, 

997 submodules: bool = True, 

998 path: Optional[PathLike] = None, 

999 ) -> bool: 

1000 """ 

1001 :return: 

1002 ``True`` if the repository is considered dirty. By default it will react 

1003 like a :manpage:`git-status(1)` without untracked files, hence it is dirty 

1004 if the index or the working copy have changes. 

1005 """ 

1006 if self._bare: 

1007 # Bare repositories with no associated working directory are 

1008 # always considered to be clean. 

1009 return False 

1010 

1011 # Start from the one which is fastest to evaluate. 

1012 default_args = ["--abbrev=40", "--full-index", "--raw"] 

1013 if not submodules: 

1014 default_args.append("--ignore-submodules") 

1015 if path: 

1016 default_args.extend(["--", os.fspath(path)]) 

1017 if index: 

1018 # diff index against HEAD. 

1019 if osp.isfile(self.index.path) and len(self.git.diff("--cached", *default_args)): 

1020 return True 

1021 # END index handling 

1022 if working_tree: 

1023 # diff index against working tree. 

1024 if len(self.git.diff(*default_args)): 

1025 return True 

1026 # END working tree handling 

1027 if untracked_files: 

1028 if len(self._get_untracked_files(path, ignore_submodules=not submodules)): 

1029 return True 

1030 # END untracked files 

1031 return False 

1032 

1033 @property 

1034 def untracked_files(self) -> List[str]: 

1035 """ 

1036 :return: 

1037 list(str,...) 

1038 

1039 Files currently untracked as they have not been staged yet. Paths are 

1040 relative to the current working directory of the git command. 

1041 

1042 :note: 

1043 Ignored files will not appear here, i.e. files mentioned in ``.gitignore``. 

1044 

1045 :note: 

1046 This property is expensive, as no cache is involved. To process the result, 

1047 please consider caching it yourself. 

1048 """ 

1049 return self._get_untracked_files() 

1050 

1051 def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]: 

1052 # Make sure we get all files, not only untracked directories. 

1053 proc = self.git.status(*args, porcelain=True, untracked_files=True, as_process=True, **kwargs) 

1054 # Untracked files prefix in porcelain mode 

1055 prefix = "?? " 

1056 untracked_files = [] 

1057 for line in proc.stdout: 

1058 line = line.decode(defenc) 

1059 if not line.startswith(prefix): 

1060 continue 

1061 filename = line[len(prefix) :].rstrip("\n") 

1062 # Special characters are escaped 

1063 if filename[0] == filename[-1] == '"': 

1064 filename = filename[1:-1] 

1065 # WHATEVER ... it's a mess, but works for me 

1066 filename = filename.encode("ascii").decode("unicode_escape").encode("latin1").decode(defenc) 

1067 untracked_files.append(filename) 

1068 finalize_process(proc) 

1069 return untracked_files 

1070 

1071 def ignored(self, *paths: PathLike) -> List[str]: 

1072 """Checks if paths are ignored via ``.gitignore``. 

1073 

1074 This does so using the :manpage:`git-check-ignore(1)` method. 

1075 

1076 :param paths: 

1077 List of paths to check whether they are ignored or not. 

1078 

1079 :return: 

1080 Subset of those paths which are ignored 

1081 """ 

1082 try: 

1083 proc: str = self.git.check_ignore(*paths) 

1084 except GitCommandError as err: 

1085 if err.status == 1: 

1086 # If return code is 1, this means none of the items in *paths are 

1087 # ignored by Git, so return an empty list. 

1088 return [] 

1089 else: 

1090 # Raise the exception on all other return codes. 

1091 raise 

1092 

1093 return proc.replace("\\\\", "\\").replace('"', "").split("\n") 

1094 

1095 @property 

1096 def active_branch(self) -> Head: 

1097 """The name of the currently active branch. 

1098 

1099 :raise TypeError: 

1100 If HEAD is detached. 

1101 

1102 :raise ValueError: 

1103 If HEAD points to the ``.invalid`` ref Git uses to mark refs as 

1104 incompatible with older clients. 

1105 

1106 :return: 

1107 :class:`~git.refs.head.Head` to the active branch 

1108 """ 

1109 active_branch = self.head.reference 

1110 if active_branch.name == ".invalid": 

1111 raise ValueError( 

1112 "HEAD points to 'refs/heads/.invalid', which Git uses to mark refs as incompatible with older clients" 

1113 ) 

1114 return active_branch 

1115 

1116 def blame_incremental( 

1117 self, rev: str | HEAD | None, file: str, allow_unsafe_options: bool = False, **kwargs: Any 

1118 ) -> Iterator["BlameEntry"]: 

1119 """Iterator for blame information for the given file at the given revision. 

1120 

1121 Unlike :meth:`blame`, this does not return the actual file's contents, only a 

1122 stream of :class:`BlameEntry` tuples. 

1123 

1124 :param rev: 

1125 Revision specifier. If ``None``, the blame will include all the latest 

1126 uncommitted changes. Otherwise, anything successfully parsed by 

1127 :manpage:`git-rev-parse(1)` is a valid option. 

1128 

1129 :param allow_unsafe_options: 

1130 Allow unsafe options in revision argument, like ``--output``. 

1131 

1132 :return: 

1133 Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the 

1134 commit to blame for the line, and range indicates a span of line numbers in 

1135 the resulting file. 

1136 

1137 If you combine all line number ranges outputted by this command, you should get 

1138 a continuous range spanning all line numbers in the file. 

1139 """ 

1140 if not allow_unsafe_options: 

1141 Git.check_unsafe_options( 

1142 options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options 

1143 ) 

1144 

1145 data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs) 

1146 commits: Dict[bytes, Commit] = {} 

1147 

1148 stream = (line for line in data.split(b"\n") if line) 

1149 while True: 

1150 try: 

1151 # When exhausted, causes a StopIteration, terminating this function. 

1152 line = next(stream) 

1153 except StopIteration: 

1154 return 

1155 split_line = line.split() 

1156 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line 

1157 lineno = int(lineno_b) 

1158 num_lines = int(num_lines_b) 

1159 orig_lineno = int(orig_lineno_b) 

1160 if hexsha not in commits: 

1161 # Now read the next few lines and build up a dict of properties for this 

1162 # commit. 

1163 props: Dict[bytes, bytes] = {} 

1164 while True: 

1165 try: 

1166 line = next(stream) 

1167 except StopIteration: 

1168 return 

1169 if line == b"boundary": 

1170 # "boundary" indicates a root commit and occurs instead of the 

1171 # "previous" tag. 

1172 continue 

1173 

1174 tag, value = line.split(b" ", 1) 

1175 props[tag] = value 

1176 if tag == b"filename": 

1177 # "filename" formally terminates the entry for --incremental. 

1178 orig_filename = value 

1179 break 

1180 

1181 c = Commit( 

1182 self, 

1183 hex_to_bin(hexsha), 

1184 author=Actor( 

1185 safe_decode(props[b"author"]), 

1186 safe_decode(props[b"author-mail"].lstrip(b"<").rstrip(b">")), 

1187 ), 

1188 authored_date=int(props[b"author-time"]), 

1189 committer=Actor( 

1190 safe_decode(props[b"committer"]), 

1191 safe_decode(props[b"committer-mail"].lstrip(b"<").rstrip(b">")), 

1192 ), 

1193 committed_date=int(props[b"committer-time"]), 

1194 ) 

1195 commits[hexsha] = c 

1196 else: 

1197 # Discard all lines until we find "filename" which is guaranteed to be 

1198 # the last line. 

1199 while True: 

1200 try: 

1201 # Will fail if we reach the EOF unexpectedly. 

1202 line = next(stream) 

1203 except StopIteration: 

1204 return 

1205 tag, value = line.split(b" ", 1) 

1206 if tag == b"filename": 

1207 orig_filename = value 

1208 break 

1209 

1210 yield BlameEntry( 

1211 commits[hexsha], 

1212 range(lineno, lineno + num_lines), 

1213 safe_decode(orig_filename), 

1214 range(orig_lineno, orig_lineno + num_lines), 

1215 ) 

1216 

1217 def blame( 

1218 self, 

1219 rev: Union[str, HEAD, None], 

1220 file: str, 

1221 incremental: bool = False, 

1222 rev_opts: Optional[Sequence[str]] = None, 

1223 allow_unsafe_options: bool = False, 

1224 **kwargs: Any, 

1225 ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None: 

1226 """The blame information for the given file at the given revision. 

1227 

1228 :param rev: 

1229 Revision specifier. If ``None``, the blame will include all the latest 

1230 uncommitted changes. Otherwise, anything successfully parsed by 

1231 :manpage:`git-rev-parse(1)` is a valid option. 

1232 

1233 :param allow_unsafe_options: 

1234 Allow unsafe options in revision argument, like ``--output``. 

1235 

1236 :return: 

1237 list: [git.Commit, list: [<line>]] 

1238 

1239 A list of lists associating a :class:`~git.objects.commit.Commit` object 

1240 with a list of lines that changed within the given commit. The 

1241 :class:`~git.objects.commit.Commit` objects will be given in order of 

1242 appearance. 

1243 """ 

1244 if incremental: 

1245 return self.blame_incremental(rev, file, allow_unsafe_options=allow_unsafe_options, **kwargs) 

1246 rev_opts_list = list(rev_opts or []) 

1247 if not allow_unsafe_options: 

1248 Git.check_unsafe_options( 

1249 options=Git._option_candidates([rev, rev_opts_list], kwargs), 

1250 unsafe_options=self.unsafe_git_revision_options, 

1251 ) 

1252 data: bytes = self.git.blame(rev, *rev_opts_list, "--", file, p=True, stdout_as_string=False, **kwargs) 

1253 commits: Dict[str, Commit] = {} 

1254 blames: List[List[Commit | List[str | bytes] | None]] = [] 

1255 

1256 class InfoTD(TypedDict, total=False): 

1257 sha: str 

1258 id: str 

1259 filename: str 

1260 summary: str 

1261 author: str 

1262 author_email: str 

1263 author_date: int 

1264 committer: str 

1265 committer_email: str 

1266 committer_date: int 

1267 

1268 info: InfoTD = {} 

1269 

1270 keepends = True 

1271 for line_bytes in data.splitlines(keepends): 

1272 try: 

1273 line_str = line_bytes.rstrip().decode(defenc) 

1274 except UnicodeDecodeError: 

1275 firstpart = "" 

1276 parts = [] 

1277 is_binary = True 

1278 else: 

1279 # As we don't have an idea when the binary data ends, as it could 

1280 # contain multiple newlines in the process. So we rely on being able to 

1281 # decode to tell us what it is. This can absolutely fail even on text 

1282 # files, but even if it does, we should be fine treating it as binary 

1283 # instead. 

1284 parts = self.re_whitespace.split(line_str, 1) 

1285 firstpart = parts[0] 

1286 is_binary = False 

1287 # END handle decode of line 

1288 

1289 if self.re_hexsha_only.search(firstpart): 

1290 # handles 

1291 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7 - indicates blame-data start 

1292 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates 

1293 # another line of blame with the same data 

1294 digits = parts[-1].split(" ") 

1295 if len(digits) == 3: 

1296 info = {"id": firstpart} 

1297 blames.append([None, []]) 

1298 elif info["id"] != firstpart: 

1299 info = {"id": firstpart} 

1300 blames.append([commits.get(firstpart), []]) 

1301 # END blame data initialization 

1302 else: 

1303 m = self.re_author_committer_start.search(firstpart) 

1304 if m: 

1305 # handles: 

1306 # author Tom Preston-Werner 

1307 # author-mail <tom@mojombo.com> 

1308 # author-time 1192271832 

1309 # author-tz -0700 

1310 # committer Tom Preston-Werner 

1311 # committer-mail <tom@mojombo.com> 

1312 # committer-time 1192271832 

1313 # committer-tz -0700 - IGNORED BY US 

1314 role = m.group(0) 

1315 if role == "author": 

1316 if firstpart.endswith("-mail"): 

1317 info["author_email"] = parts[-1] 

1318 elif firstpart.endswith("-time"): 

1319 info["author_date"] = int(parts[-1]) 

1320 elif role == firstpart: 

1321 info["author"] = parts[-1] 

1322 elif role == "committer": 

1323 if firstpart.endswith("-mail"): 

1324 info["committer_email"] = parts[-1] 

1325 elif firstpart.endswith("-time"): 

1326 info["committer_date"] = int(parts[-1]) 

1327 elif role == firstpart: 

1328 info["committer"] = parts[-1] 

1329 # END distinguish mail,time,name 

1330 else: 

1331 # handle 

1332 # filename lib/grit.rb 

1333 # summary add Blob 

1334 # <and rest> 

1335 if firstpart.startswith("filename"): 

1336 info["filename"] = parts[-1] 

1337 elif firstpart.startswith("summary"): 

1338 info["summary"] = parts[-1] 

1339 elif firstpart == "": 

1340 if info: 

1341 sha = info["id"] 

1342 c = commits.get(sha) 

1343 if c is None: 

1344 c = Commit( 

1345 self, 

1346 hex_to_bin(sha), 

1347 author=Actor._from_string(f"{info['author']} {info['author_email']}"), 

1348 authored_date=info["author_date"], 

1349 committer=Actor._from_string(f"{info['committer']} {info['committer_email']}"), 

1350 committed_date=info["committer_date"], 

1351 ) 

1352 commits[sha] = c 

1353 blames[-1][0] = c 

1354 # END if commit objects needs initial creation 

1355 

1356 if blames[-1][1] is not None: 

1357 line: str | bytes 

1358 if not is_binary: 

1359 if line_str and line_str[0] == "\t": 

1360 line_str = line_str[1:] 

1361 line = line_str 

1362 else: 

1363 line = line_bytes 

1364 # NOTE: We are actually parsing lines out of binary 

1365 # data, which can lead to the binary being split up 

1366 # along the newline separator. We will append this 

1367 # to the blame we are currently looking at, even 

1368 # though it should be concatenated with the last 

1369 # line we have seen. 

1370 blames[-1][1].append(line) 

1371 

1372 info = {"id": sha} 

1373 # END if we collected commit info 

1374 # END distinguish filename,summary,rest 

1375 # END distinguish author|committer vs filename,summary,rest 

1376 # END distinguish hexsha vs other information 

1377 return blames 

1378 

1379 @classmethod 

1380 def init( 

1381 cls, 

1382 path: Union[PathLike, None] = None, 

1383 mkdir: bool = True, 

1384 odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, 

1385 expand_vars: bool = True, 

1386 **kwargs: Any, 

1387 ) -> "Repo": 

1388 """Initialize a git repository at the given path if specified. 

1389 

1390 :param path: 

1391 The full path to the repo (traditionally ends with ``/<name>.git``). Or 

1392 ``None``, in which case the repository will be created in the current 

1393 working directory. 

1394 

1395 :param mkdir: 

1396 If specified, will create the repository directory if it doesn't already 

1397 exist. Creates the directory with a mode=0755. 

1398 Only effective if a path is explicitly given. 

1399 

1400 :param odbt: 

1401 Object DataBase type - a type which is constructed by providing the 

1402 directory containing the database objects, i.e. ``.git/objects``. It will be 

1403 used to access all object data. 

1404 

1405 :param expand_vars: 

1406 If specified, environment variables will not be escaped. This can lead to 

1407 information disclosure, allowing attackers to access the contents of 

1408 environment variables. 

1409 

1410 :param kwargs: 

1411 Keyword arguments serving as additional options to the 

1412 :manpage:`git-init(1)` command. 

1413 

1414 :return: 

1415 :class:`Repo` (the newly created repo) 

1416 """ 

1417 if path: 

1418 path = expand_path(path, expand_vars) 

1419 if mkdir and path and not osp.exists(path): 

1420 os.makedirs(path, 0o755) 

1421 

1422 # git command automatically chdir into the directory 

1423 git = cls.GitCommandWrapperType(path) 

1424 git.init(**kwargs) 

1425 return cls(path, odbt=odbt) 

1426 

1427 @classmethod 

1428 def _clone( 

1429 cls, 

1430 git: "Git", 

1431 url: PathLike, 

1432 path: PathLike, 

1433 odb_default_type: Type[GitCmdObjectDB], 

1434 progress: Union["RemoteProgress", "UpdateProgress", Callable[..., "RemoteProgress"], None] = None, 

1435 multi_options: Optional[List[str]] = None, 

1436 allow_unsafe_protocols: bool = False, 

1437 allow_unsafe_options: bool = False, 

1438 **kwargs: Any, 

1439 ) -> "Repo": 

1440 odbt = kwargs.pop("odbt", odb_default_type) 

1441 

1442 # url may be a path and this has no effect if it is a string 

1443 url = os.fspath(url) 

1444 path = os.fspath(path) 

1445 

1446 ## A bug win cygwin's Git, when `--bare` or `--separate-git-dir` 

1447 # it prepends the cwd or(?) the `url` into the `path, so:: 

1448 # git clone --bare /cygwin/d/foo.git C:\\Work 

1449 # becomes:: 

1450 # git clone --bare /cygwin/d/foo.git /cygwin/d/C:\\Work 

1451 # 

1452 clone_path = Git.polish_url(path) if Git.is_cygwin() and "bare" in kwargs else path 

1453 sep_dir = kwargs.get("separate_git_dir") 

1454 if sep_dir: 

1455 kwargs["separate_git_dir"] = Git.polish_url(sep_dir) 

1456 multi = None 

1457 if multi_options: 

1458 multi = shlex.split(" ".join(multi_options)) 

1459 

1460 clone_url = Git.polish_url(url, expand_vars=False) 

1461 if not allow_unsafe_protocols: 

1462 Git.check_unsafe_protocols(clone_url) 

1463 if not allow_unsafe_options: 

1464 Git.check_unsafe_options( 

1465 options=Git._option_candidates([], kwargs), 

1466 unsafe_options=cls.unsafe_git_clone_options, 

1467 ) 

1468 if not allow_unsafe_options and multi: 

1469 Git.check_unsafe_options(options=multi, unsafe_options=cls.unsafe_git_clone_options) 

1470 

1471 proc = git.clone( 

1472 multi, 

1473 "--", 

1474 clone_url, 

1475 clone_path, 

1476 with_extended_output=True, 

1477 as_process=True, 

1478 v=True, 

1479 universal_newlines=True, 

1480 **add_progress(kwargs, git, progress), 

1481 ) 

1482 if progress: 

1483 handle_process_output( 

1484 proc, 

1485 None, 

1486 to_progress_instance(progress).new_message_handler(), 

1487 finalize_process, 

1488 decode_streams=False, 

1489 ) 

1490 else: 

1491 (stdout, stderr) = proc.communicate() 

1492 cmdline = getattr(proc, "args", "") 

1493 cmdline = remove_password_if_present(cmdline) 

1494 

1495 _logger.debug("Cmd(%s)'s unused stdout: %s", cmdline, stdout) 

1496 finalize_process(proc, stderr=stderr) 

1497 

1498 # Our git command could have a different working dir than our actual 

1499 # environment, hence we prepend its working dir if required. 

1500 if not osp.isabs(path): 

1501 path = osp.join(git._working_dir, path) if git._working_dir is not None else path 

1502 

1503 repo = cls(path, odbt=odbt) 

1504 

1505 # Retain env values that were passed to _clone(). 

1506 repo.git.update_environment(**git.environment()) 

1507 

1508 # Adjust remotes - there may be operating systems which use backslashes, These 

1509 # might be given as initial paths, but when handling the config file that 

1510 # contains the remote from which we were clones, git stops liking it as it will 

1511 # escape the backslashes. Hence we undo the escaping just to be sure. 

1512 if repo.remotes: 

1513 with repo.remotes[0].config_writer as writer: 

1514 writer.set_value("url", Git.polish_url(repo.remotes[0].url, expand_vars=False)) 

1515 # END handle remote repo 

1516 return repo 

1517 

1518 def clone( 

1519 self, 

1520 path: PathLike, 

1521 progress: Optional[CallableProgress] = None, 

1522 multi_options: Optional[List[str]] = None, 

1523 allow_unsafe_protocols: bool = False, 

1524 allow_unsafe_options: bool = False, 

1525 **kwargs: Any, 

1526 ) -> "Repo": 

1527 """Create a clone from this repository. 

1528 

1529 :param path: 

1530 The full path of the new repo (traditionally ends with ``./<name>.git``). 

1531 

1532 :param progress: 

1533 See :meth:`Remote.push <git.remote.Remote.push>`. 

1534 

1535 :param multi_options: 

1536 A list of :manpage:`git-clone(1)` options that can be provided multiple 

1537 times. 

1538 

1539 One option per list item which is passed exactly as specified to clone. 

1540 For example:: 

1541 

1542 [ 

1543 "--config core.filemode=false", 

1544 "--config core.ignorecase", 

1545 "--recurse-submodule=repo1_path", 

1546 "--recurse-submodule=repo2_path", 

1547 ] 

1548 

1549 :param allow_unsafe_protocols: 

1550 Allow unsafe protocols to be used, like ``ext``. 

1551 

1552 :param allow_unsafe_options: 

1553 Allow unsafe options to be used, like ``--upload-pack``. 

1554 

1555 :param kwargs: 

1556 * ``odbt`` = ObjectDatabase Type, allowing to determine the object database 

1557 implementation used by the returned :class:`Repo` instance. 

1558 * All remaining keyword arguments are given to the :manpage:`git-clone(1)` 

1559 command. 

1560 

1561 :return: 

1562 :class:`Repo` (the newly cloned repo) 

1563 """ 

1564 return self._clone( 

1565 self.git, 

1566 self.common_dir, 

1567 path, 

1568 type(self.odb), 

1569 progress, # type: ignore[arg-type] 

1570 multi_options, 

1571 allow_unsafe_protocols=allow_unsafe_protocols, 

1572 allow_unsafe_options=allow_unsafe_options, 

1573 **kwargs, 

1574 ) 

1575 

1576 @classmethod 

1577 def clone_from( 

1578 cls, 

1579 url: PathLike, 

1580 to_path: PathLike, 

1581 progress: CallableProgress = None, 

1582 env: Optional[Mapping[str, str]] = None, 

1583 multi_options: Optional[List[str]] = None, 

1584 allow_unsafe_protocols: bool = False, 

1585 allow_unsafe_options: bool = False, 

1586 **kwargs: Any, 

1587 ) -> "Repo": 

1588 """Create a clone from the given URL. 

1589 

1590 :param url: 

1591 Valid git url, see: https://git-scm.com/docs/git-clone#URLS 

1592 

1593 :param to_path: 

1594 Path to which the repository should be cloned to. 

1595 

1596 :param progress: 

1597 See :meth:`Remote.push <git.remote.Remote.push>`. 

1598 

1599 :param env: 

1600 Optional dictionary containing the desired environment variables. 

1601 

1602 Note: Provided variables will be used to update the execution environment 

1603 for ``git``. If some variable is not specified in `env` and is defined in 

1604 :attr:`os.environ`, value from :attr:`os.environ` will be used. If you want 

1605 to unset some variable, consider providing empty string as its value. 

1606 

1607 :param multi_options: 

1608 See the :meth:`clone` method. 

1609 

1610 :param allow_unsafe_protocols: 

1611 Allow unsafe protocols to be used, like ``ext``. 

1612 

1613 :param allow_unsafe_options: 

1614 Allow unsafe options to be used, like ``--upload-pack``. 

1615 

1616 :param kwargs: 

1617 See the :meth:`clone` method. 

1618 

1619 :return: 

1620 :class:`Repo` instance pointing to the cloned directory. 

1621 """ 

1622 git = cls.GitCommandWrapperType(os.getcwd()) 

1623 if env is not None: 

1624 git.update_environment(**env) 

1625 return cls._clone( 

1626 git, 

1627 url, 

1628 to_path, 

1629 GitCmdObjectDB, 

1630 progress, # type: ignore[arg-type] 

1631 multi_options, 

1632 allow_unsafe_protocols=allow_unsafe_protocols, 

1633 allow_unsafe_options=allow_unsafe_options, 

1634 **kwargs, 

1635 ) 

1636 

1637 def archive( 

1638 self, 

1639 ostream: Union[TextIO, BinaryIO], 

1640 treeish: Optional[str] = None, 

1641 prefix: Optional[str] = None, 

1642 allow_unsafe_options: bool = False, 

1643 allow_unsafe_protocols: bool = False, 

1644 **kwargs: Any, 

1645 ) -> Repo: 

1646 """Archive the tree at the given revision. 

1647 

1648 :param ostream: 

1649 File-compatible stream object to which the archive will be written as bytes. 

1650 

1651 :param treeish: 

1652 The treeish name/id, defaults to active branch. 

1653 

1654 :param prefix: 

1655 The optional prefix to prepend to each filename in the archive. 

1656 

1657 :param kwargs: 

1658 Additional arguments passed to :manpage:`git-archive(1)`: 

1659 

1660 * Use the ``format`` argument to define the kind of format. Use specialized 

1661 ostreams to write any format supported by Python. 

1662 * You may specify the special ``path`` keyword, which may either be a 

1663 repository-relative path to a directory or file to place into the archive, 

1664 or a list or tuple of multiple paths. 

1665 

1666 :param allow_unsafe_options: 

1667 Allow unsafe options, like ``--exec`` or ``--output``. 

1668 

1669 :param allow_unsafe_protocols: 

1670 Allow unsafe protocols to be used in ``remote``, like ``ext``. 

1671 

1672 :raise git.exc.GitCommandError: 

1673 If something went wrong. 

1674 

1675 :return: 

1676 self 

1677 """ 

1678 if treeish is None: 

1679 treeish = self.head.commit 

1680 if prefix and "prefix" not in kwargs: 

1681 kwargs["prefix"] = prefix 

1682 remote = kwargs.get("remote") 

1683 if not allow_unsafe_protocols and remote is not None: 

1684 Git.check_unsafe_protocols(str(remote)) 

1685 if not allow_unsafe_options: 

1686 Git.check_unsafe_options( 

1687 options=Git._option_candidates([], kwargs), 

1688 unsafe_options=self.unsafe_git_archive_options, 

1689 ) 

1690 kwargs["output_stream"] = ostream 

1691 path = kwargs.pop("path", []) 

1692 path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path) 

1693 if not isinstance(path, (tuple, list)): 

1694 path = [path] 

1695 # END ensure paths is list (or tuple) 

1696 self.git.archive("--", treeish, *path, **kwargs) 

1697 return self 

1698 

1699 def has_separate_working_tree(self) -> bool: 

1700 """ 

1701 :return: 

1702 True if our :attr:`git_dir` is not at the root of our 

1703 :attr:`working_tree_dir`, but a ``.git`` file with a platform-agnostic 

1704 symbolic link. Our :attr:`git_dir` will be wherever the ``.git`` file points 

1705 to. 

1706 

1707 :note: 

1708 Bare repositories will always return ``False`` here. 

1709 """ 

1710 if self.bare: 

1711 return False 

1712 if self.working_tree_dir: 

1713 return osp.isfile(osp.join(self.working_tree_dir, ".git")) 

1714 else: 

1715 return False # Or raise Error? 

1716 

1717 rev_parse = rev_parse 

1718 

1719 def __repr__(self) -> str: 

1720 clazz = self.__class__ 

1721 return "<%s.%s %r>" % (clazz.__module__, clazz.__name__, self.git_dir) 

1722 

1723 def currently_rebasing_on(self) -> Commit | None: 

1724 """ 

1725 :return: 

1726 The commit which is currently being replayed while rebasing. 

1727 

1728 ``None`` if we are not currently rebasing. 

1729 """ 

1730 if self.git_dir: 

1731 rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD") 

1732 if not osp.isfile(rebase_head_file): 

1733 return None 

1734 with open(rebase_head_file, "rt") as f: 

1735 content = f.readline().strip() 

1736 return self.commit(content)