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

606 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 ] 

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

154 

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

156 directly: 

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

158 

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

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

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

162 """ 

163 

164 # Invariants 

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

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

167 

168 # Subclass configuration 

169 GitCommandWrapperType = Git 

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

171 type here.""" 

172 

173 def __init__( 

174 self, 

175 path: Optional[PathLike] = None, 

176 odbt: Type[LooseObjectDB] = GitCmdObjectDB, 

177 search_parent_directories: bool = False, 

178 expand_vars: bool = True, 

179 ) -> None: 

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

181 

182 :param path: 

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

184 

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

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

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

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

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

190 

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

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

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

194 used. 

195 

196 :param odbt: 

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

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

199 used to access all object data. 

200 

201 :param search_parent_directories: 

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

203 well. 

204 

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

206 GitPython, which is considered a bug though. 

207 

208 :raise git.exc.InvalidGitRepositoryError: 

209 

210 :raise git.exc.NoSuchPathError: 

211 

212 :return: 

213 :class:`Repo` 

214 """ 

215 

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

217 if not epath: 

218 epath = os.getcwd() 

219 epath = os.fspath(epath) 

220 if Git.is_cygwin(): 

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

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

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

224 epath = cygpath(epath) 

225 

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

227 warnings.warn( 

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

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

230 stacklevel=1, 

231 ) 

232 epath = expand_path(epath, expand_vars) 

233 if epath is not None: 

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

235 raise NoSuchPathError(epath) 

236 

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

238 curpath = epath 

239 git_dir = None 

240 while curpath: 

241 # ABOUT osp.NORMPATH 

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

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

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

245 if ( 

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

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

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

249 ): 

250 git_dir = curpath 

251 

252 if "GIT_WORK_TREE" in os.environ: 

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

254 else: 

255 # Linked worktree administrative directories store the path to the 

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

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

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

259 

260 if not osp.isabs(worktree_gitfile): 

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

262 

263 self._working_tree_dir = osp.dirname(worktree_gitfile) 

264 

265 break 

266 

267 if is_git_dir(curpath): 

268 git_dir = curpath 

269 # from man git-config : core.worktree 

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

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

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

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

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

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

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

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

278 # working tree. 

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

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

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

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

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

284 if "GIT_WORK_TREE" in os.environ: 

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

286 break 

287 

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

289 sm_gitpath = find_submodule_git_dir(dotgit) 

290 if sm_gitpath is not None: 

291 git_dir = osp.normpath(sm_gitpath) 

292 

293 sm_gitpath = find_submodule_git_dir(dotgit) 

294 if sm_gitpath is None: 

295 sm_gitpath = find_worktree_git_dir(dotgit) 

296 

297 if sm_gitpath is not None: 

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

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

300 self._working_tree_dir = curpath 

301 break 

302 

303 if not search_parent_directories: 

304 break 

305 curpath, tail = osp.split(curpath) 

306 if not tail: 

307 break 

308 # END while curpath 

309 

310 if git_dir is None: 

311 raise InvalidGitRepositoryError(epath) 

312 self.git_dir = git_dir 

313 

314 self._bare = False 

315 try: 

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

317 except Exception: 

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

319 pass 

320 

321 try: 

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

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

324 except OSError: 

325 self._common_dir = "" 

326 

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

328 # that in the first place. 

329 if self._bare: 

330 self._working_tree_dir = None 

331 # END working dir handling 

332 

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

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

335 

336 # Special handling, in special times. 

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

338 if issubclass(odbt, GitCmdObjectDB): 

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

340 else: 

341 self.odb = odbt(rootpath) 

342 

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

344 return self 

345 

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

347 self.close() 

348 

349 def __del__(self) -> None: 

350 try: 

351 self.close() 

352 except Exception: 

353 pass 

354 

355 def close(self) -> None: 

356 if self.git: 

357 self.git.clear_cache() 

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

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

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

361 # synchronously rather than forcing a gc collection. 

362 if sys.platform == "win32": 

363 gc.collect() 

364 gitdb.util.mman.collect() 

365 if sys.platform == "win32": 

366 gc.collect() 

367 

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

369 if isinstance(rhs, Repo): 

370 return self.git_dir == rhs.git_dir 

371 return False 

372 

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

374 return not self.__eq__(rhs) 

375 

376 def __hash__(self) -> int: 

377 return hash(self.git_dir) 

378 

379 @property 

380 def description(self) -> str: 

381 """The project's description""" 

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

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

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

385 

386 @description.setter 

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

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

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

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

391 

392 @property 

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

394 """ 

395 :return: 

396 The working tree directory of our git repository. 

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

398 """ 

399 return self._working_tree_dir 

400 

401 @property 

402 def common_dir(self) -> PathLike: 

403 """ 

404 :return: 

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

406 ORIG_HEAD, COMMIT_EDITMSG, index, and logs/. 

407 """ 

408 return self._common_dir or self.git_dir 

409 

410 @property 

411 def bare(self) -> bool: 

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

413 return self._bare 

414 

415 @property 

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

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

418 in this repo. 

419 

420 :return: 

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

422 """ 

423 return Head.list_items(self) 

424 

425 @property 

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

427 """Alias for heads. 

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

429 in this repo. 

430 

431 :return: 

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

433 """ 

434 return self.heads 

435 

436 @property 

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

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

439 heads and remote references. 

440 

441 :return: 

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

443 """ 

444 return Reference.list_items(self) 

445 

446 @property 

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

448 """Alias for references. 

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

450 heads and remote references. 

451 

452 :return: 

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

454 """ 

455 return self.references 

456 

457 @property 

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

459 """ 

460 :return: 

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

462 

463 :note: 

464 This property can be expensive, as the returned 

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

466 It is recommended to reuse the object. 

467 """ 

468 return IndexFile(self) 

469 

470 @property 

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

472 """ 

473 :return: 

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

475 """ 

476 return HEAD(self, "HEAD") 

477 

478 @property 

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

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

481 manipulate remotes. 

482 

483 :return: 

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

485 """ 

486 return Remote.list_items(self) 

487 

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

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

490 

491 :raise ValueError: 

492 If no remote with such a name exists. 

493 """ 

494 r = Remote(self, name) 

495 if not r.exists(): 

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

497 return r 

498 

499 # { Submodules 

500 

501 @property 

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

503 """ 

504 :return: 

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

506 current head 

507 """ 

508 return Submodule.list_items(self) 

509 

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

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

512 

513 :raise ValueError: 

514 If no such submodule exists. 

515 """ 

516 try: 

517 return self.submodules[name] 

518 except IndexError as e: 

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

520 # END exception handling 

521 

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

523 """Create a new submodule. 

524 

525 :note: 

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

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

528 

529 :return: 

530 The created submodule. 

531 """ 

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

533 

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

535 """An iterator yielding Submodule instances. 

536 

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

538 and `kwargs`. 

539 

540 :return: 

541 Iterator 

542 """ 

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

544 

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

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

547 take the previous state into consideration. 

548 

549 :note: 

550 For more information, please see the documentation of 

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

552 """ 

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

554 

555 # }END submodules 

556 

557 @property 

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

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

560 this repo. 

561 

562 :return: 

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

564 """ 

565 return TagReference.list_items(self) 

566 

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

568 """ 

569 :return: 

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

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

572 

573 :param path: 

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

575 """ 

576 full_path = self._to_full_tag_path(path) 

577 return TagReference(self, full_path) 

578 

579 @staticmethod 

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

581 path_str = str(path) 

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

583 return path_str 

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

585 return Reference._common_path_default + "/" + path_str 

586 else: 

587 return TagReference._common_path_default + "/" + path_str 

588 

589 def create_head( 

590 self, 

591 path: PathLike, 

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

593 force: bool = False, 

594 logmsg: Optional[str] = None, 

595 ) -> "Head": 

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

597 

598 :note: 

599 For more documentation, please see the 

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

601 

602 :return: 

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

604 """ 

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

606 

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

608 """Delete the given heads. 

609 

610 :param kwargs: 

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

612 """ 

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

614 

615 def create_tag( 

616 self, 

617 path: PathLike, 

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

619 message: Optional[str] = None, 

620 force: bool = False, 

621 **kwargs: Any, 

622 ) -> TagReference: 

623 """Create a new tag reference. 

624 

625 :note: 

626 For more documentation, please see the 

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

628 

629 :return: 

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

631 """ 

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

633 

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

635 """Delete the given tag references.""" 

636 return TagReference.delete(self, *tags) 

637 

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

639 """Create a new remote. 

640 

641 For more information, please see the documentation of the 

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

643 

644 :return: 

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

646 """ 

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

648 

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

650 """Delete the given remote.""" 

651 return Remote.remove(self, remote) 

652 

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

654 if git_dir is None: 

655 git_dir = self.git_dir 

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

657 # Use the global config instead. 

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

659 config_level = "global" 

660 

661 if config_level == "system": 

662 return "/etc/gitconfig" 

663 elif config_level == "user": 

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

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

666 elif config_level == "global": 

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

668 elif config_level == "repository": 

669 repo_dir = self._common_dir or git_dir 

670 if not repo_dir: 

671 raise NotADirectoryError 

672 else: 

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

674 else: 

675 assert_never( # type: ignore[unreachable] 

676 config_level, 

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

678 ) 

679 

680 def config_reader( 

681 self, 

682 config_level: Optional[Lit_config_levels] = None, 

683 ) -> GitConfigParser: 

684 """ 

685 :return: 

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

687 configuration, but not to write it. 

688 

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

690 configuration files. 

691 

692 :param config_level: 

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

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

695 you wish to read to prevent reading multiple files. 

696 

697 :note: 

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

699 unknown, instead the global path will be used. 

700 """ 

701 return self._config_reader(config_level=config_level) 

702 

703 def _config_reader( 

704 self, 

705 config_level: Optional[Lit_config_levels] = None, 

706 git_dir: Optional[PathLike] = None, 

707 ) -> GitConfigParser: 

708 if config_level is None: 

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

710 else: 

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

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

713 

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

715 """ 

716 :return: 

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

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

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

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

721 

722 :param config_level: 

723 One of the following values: 

724 

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

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

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

728 """ 

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

730 

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

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

733 

734 :param rev: 

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

736 

737 :return: 

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

739 """ 

740 if rev is None: 

741 return self.head.commit 

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

743 

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

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

746 

747 :note: 

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

749 """ 

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

751 

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

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

754 

755 Examples:: 

756 

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

758 

759 :param rev: 

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

761 

762 :return: 

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

764 

765 :note: 

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

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

768 subsequent operations might have unexpected results. 

769 """ 

770 if rev is None: 

771 return self.head.commit.tree 

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

773 

774 def iter_commits( 

775 self, 

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

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

778 **kwargs: Any, 

779 ) -> Iterator[Commit]: 

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

781 history of a given ref/commit. 

782 

783 :param rev: 

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

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

786 

787 :param paths: 

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

789 path or paths will be returned. 

790 

791 :param kwargs: 

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

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

794 

795 :note: 

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

797 ``"revA...revB"`` revision specifier. 

798 

799 :return: 

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

801 """ 

802 if rev is None: 

803 rev = self.head.commit 

804 

805 return Commit.iter_items(self, rev, paths, **kwargs) 

806 

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

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

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

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

811 

812 :param rev: 

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

814 

815 :param kwargs: 

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

817 which does all the work. 

818 

819 :return: 

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

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

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

823 exists. 

824 

825 :raise ValueError: 

826 If fewer than two revisions are provided. 

827 """ 

828 if len(rev) < 2: 

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

830 # END handle input 

831 

832 res: List[Commit] = [] 

833 try: 

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

835 except GitCommandError as err: 

836 if err.status == 128: 

837 raise 

838 # END handle invalid rev 

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

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

841 return res 

842 # END exception handling 

843 

844 for line in lines: 

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

846 # END for each merge-base 

847 

848 return res 

849 

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

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

852 

853 :param ancestor_rev: 

854 Rev which should be an ancestor. 

855 

856 :param rev: 

857 Rev to test against `ancestor_rev`. 

858 

859 :return: 

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

861 """ 

862 try: 

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

864 except GitCommandError as err: 

865 if err.status == 1: 

866 return False 

867 raise 

868 return True 

869 

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

871 try: 

872 complete_sha = self.odb.partial_to_complete_sha_hex(sha) 

873 object_info = self.odb.info(complete_sha) 

874 if object_type: 

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

876 return True 

877 else: 

878 _logger.debug( 

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

880 object_info.type.decode(), 

881 object_type, 

882 ) 

883 return False 

884 else: 

885 return True 

886 except BadObject: 

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

888 return False 

889 

890 def _get_daemon_export(self) -> bool: 

891 if self.git_dir: 

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

893 return osp.exists(filename) 

894 

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

896 if self.git_dir: 

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

898 fileexists = osp.exists(filename) 

899 if value and not fileexists: 

900 touch(filename) 

901 elif not value and fileexists: 

902 os.unlink(filename) 

903 

904 @property 

905 def daemon_export(self) -> bool: 

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

907 return self._get_daemon_export() 

908 

909 @daemon_export.setter 

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

911 self._set_daemon_export(value) 

912 

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

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

915 

916 :return: 

917 List of strings being pathnames of alternates 

918 """ 

919 if self.git_dir: 

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

921 

922 if osp.exists(alternates_path): 

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

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

925 return alts.strip().splitlines() 

926 return [] 

927 

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

929 """Set the alternates. 

930 

931 :param alts: 

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

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

934 

935 :raise git.exc.NoSuchPathError: 

936 

937 :note: 

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

939 caller is responsible. 

940 """ 

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

942 if not alts: 

943 if osp.isfile(alternates_path): 

944 os.remove(alternates_path) 

945 else: 

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

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

948 

949 @property 

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

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

952 return self._get_alternates() 

953 

954 @alternates.setter 

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

956 self._set_alternates(alts) 

957 

958 def is_dirty( 

959 self, 

960 index: bool = True, 

961 working_tree: bool = True, 

962 untracked_files: bool = False, 

963 submodules: bool = True, 

964 path: Optional[PathLike] = None, 

965 ) -> bool: 

966 """ 

967 :return: 

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

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

970 if the index or the working copy have changes. 

971 """ 

972 if self._bare: 

973 # Bare repositories with no associated working directory are 

974 # always considered to be clean. 

975 return False 

976 

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

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

979 if not submodules: 

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

981 if path: 

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

983 if index: 

984 # diff index against HEAD. 

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

986 return True 

987 # END index handling 

988 if working_tree: 

989 # diff index against working tree. 

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

991 return True 

992 # END working tree handling 

993 if untracked_files: 

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

995 return True 

996 # END untracked files 

997 return False 

998 

999 @property 

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

1001 """ 

1002 :return: 

1003 list(str,...) 

1004 

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

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

1007 

1008 :note: 

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

1010 

1011 :note: 

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

1013 please consider caching it yourself. 

1014 """ 

1015 return self._get_untracked_files() 

1016 

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

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

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

1020 # Untracked files prefix in porcelain mode 

1021 prefix = "?? " 

1022 untracked_files = [] 

1023 for line in proc.stdout: 

1024 line = line.decode(defenc) 

1025 if not line.startswith(prefix): 

1026 continue 

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

1028 # Special characters are escaped 

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

1030 filename = filename[1:-1] 

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

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

1033 untracked_files.append(filename) 

1034 finalize_process(proc) 

1035 return untracked_files 

1036 

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

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

1039 

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

1041 

1042 :param paths: 

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

1044 

1045 :return: 

1046 Subset of those paths which are ignored 

1047 """ 

1048 try: 

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

1050 except GitCommandError as err: 

1051 if err.status == 1: 

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

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

1054 return [] 

1055 else: 

1056 # Raise the exception on all other return codes. 

1057 raise 

1058 

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

1060 

1061 @property 

1062 def active_branch(self) -> Head: 

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

1064 

1065 :raise TypeError: 

1066 If HEAD is detached. 

1067 

1068 :raise ValueError: 

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

1070 incompatible with older clients. 

1071 

1072 :return: 

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

1074 """ 

1075 active_branch = self.head.reference 

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

1077 raise ValueError( 

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

1079 ) 

1080 return active_branch 

1081 

1082 def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> Iterator["BlameEntry"]: 

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

1084 

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

1086 stream of :class:`BlameEntry` tuples. 

1087 

1088 :param rev: 

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

1090 uncommitted changes. Otherwise, anything successfully parsed by 

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

1092 

1093 :return: 

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

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

1096 the resulting file. 

1097 

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

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

1100 """ 

1101 

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

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

1104 

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

1106 while True: 

1107 try: 

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

1109 line = next(stream) 

1110 except StopIteration: 

1111 return 

1112 split_line = line.split() 

1113 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line 

1114 lineno = int(lineno_b) 

1115 num_lines = int(num_lines_b) 

1116 orig_lineno = int(orig_lineno_b) 

1117 if hexsha not in commits: 

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

1119 # commit. 

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

1121 while True: 

1122 try: 

1123 line = next(stream) 

1124 except StopIteration: 

1125 return 

1126 if line == b"boundary": 

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

1128 # "previous" tag. 

1129 continue 

1130 

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

1132 props[tag] = value 

1133 if tag == b"filename": 

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

1135 orig_filename = value 

1136 break 

1137 

1138 c = Commit( 

1139 self, 

1140 hex_to_bin(hexsha), 

1141 author=Actor( 

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

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

1144 ), 

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

1146 committer=Actor( 

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

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

1149 ), 

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

1151 ) 

1152 commits[hexsha] = c 

1153 else: 

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

1155 # the last line. 

1156 while True: 

1157 try: 

1158 # Will fail if we reach the EOF unexpectedly. 

1159 line = next(stream) 

1160 except StopIteration: 

1161 return 

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

1163 if tag == b"filename": 

1164 orig_filename = value 

1165 break 

1166 

1167 yield BlameEntry( 

1168 commits[hexsha], 

1169 range(lineno, lineno + num_lines), 

1170 safe_decode(orig_filename), 

1171 range(orig_lineno, orig_lineno + num_lines), 

1172 ) 

1173 

1174 def blame( 

1175 self, 

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

1177 file: str, 

1178 incremental: bool = False, 

1179 rev_opts: Optional[List[str]] = None, 

1180 **kwargs: Any, 

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

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

1183 

1184 :param rev: 

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

1186 uncommitted changes. Otherwise, anything successfully parsed by 

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

1188 

1189 :return: 

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

1191 

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

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

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

1195 appearance. 

1196 """ 

1197 if incremental: 

1198 return self.blame_incremental(rev, file, **kwargs) 

1199 rev_opts = rev_opts or [] 

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

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

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

1203 

1204 class InfoTD(TypedDict, total=False): 

1205 sha: str 

1206 id: str 

1207 filename: str 

1208 summary: str 

1209 author: str 

1210 author_email: str 

1211 author_date: int 

1212 committer: str 

1213 committer_email: str 

1214 committer_date: int 

1215 

1216 info: InfoTD = {} 

1217 

1218 keepends = True 

1219 for line_bytes in data.splitlines(keepends): 

1220 try: 

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

1222 except UnicodeDecodeError: 

1223 firstpart = "" 

1224 parts = [] 

1225 is_binary = True 

1226 else: 

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

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

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

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

1231 # instead. 

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

1233 firstpart = parts[0] 

1234 is_binary = False 

1235 # END handle decode of line 

1236 

1237 if self.re_hexsha_only.search(firstpart): 

1238 # handles 

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

1240 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates 

1241 # another line of blame with the same data 

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

1243 if len(digits) == 3: 

1244 info = {"id": firstpart} 

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

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

1247 info = {"id": firstpart} 

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

1249 # END blame data initialization 

1250 else: 

1251 m = self.re_author_committer_start.search(firstpart) 

1252 if m: 

1253 # handles: 

1254 # author Tom Preston-Werner 

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

1256 # author-time 1192271832 

1257 # author-tz -0700 

1258 # committer Tom Preston-Werner 

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

1260 # committer-time 1192271832 

1261 # committer-tz -0700 - IGNORED BY US 

1262 role = m.group(0) 

1263 if role == "author": 

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

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

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

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

1268 elif role == firstpart: 

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

1270 elif role == "committer": 

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

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

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

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

1275 elif role == firstpart: 

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

1277 # END distinguish mail,time,name 

1278 else: 

1279 # handle 

1280 # filename lib/grit.rb 

1281 # summary add Blob 

1282 # <and rest> 

1283 if firstpart.startswith("filename"): 

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

1285 elif firstpart.startswith("summary"): 

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

1287 elif firstpart == "": 

1288 if info: 

1289 sha = info["id"] 

1290 c = commits.get(sha) 

1291 if c is None: 

1292 c = Commit( 

1293 self, 

1294 hex_to_bin(sha), 

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

1296 authored_date=info["author_date"], 

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

1298 committed_date=info["committer_date"], 

1299 ) 

1300 commits[sha] = c 

1301 blames[-1][0] = c 

1302 # END if commit objects needs initial creation 

1303 

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

1305 line: str | bytes 

1306 if not is_binary: 

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

1308 line_str = line_str[1:] 

1309 line = line_str 

1310 else: 

1311 line = line_bytes 

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

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

1314 # along the newline separator. We will append this 

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

1316 # though it should be concatenated with the last 

1317 # line we have seen. 

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

1319 

1320 info = {"id": sha} 

1321 # END if we collected commit info 

1322 # END distinguish filename,summary,rest 

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

1324 # END distinguish hexsha vs other information 

1325 return blames 

1326 

1327 @classmethod 

1328 def init( 

1329 cls, 

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

1331 mkdir: bool = True, 

1332 odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, 

1333 expand_vars: bool = True, 

1334 **kwargs: Any, 

1335 ) -> "Repo": 

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

1337 

1338 :param path: 

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

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

1341 working directory. 

1342 

1343 :param mkdir: 

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

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

1346 Only effective if a path is explicitly given. 

1347 

1348 :param odbt: 

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

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

1351 used to access all object data. 

1352 

1353 :param expand_vars: 

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

1355 information disclosure, allowing attackers to access the contents of 

1356 environment variables. 

1357 

1358 :param kwargs: 

1359 Keyword arguments serving as additional options to the 

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

1361 

1362 :return: 

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

1364 """ 

1365 if path: 

1366 path = expand_path(path, expand_vars) 

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

1368 os.makedirs(path, 0o755) 

1369 

1370 # git command automatically chdir into the directory 

1371 git = cls.GitCommandWrapperType(path) 

1372 git.init(**kwargs) 

1373 return cls(path, odbt=odbt) 

1374 

1375 @classmethod 

1376 def _clone( 

1377 cls, 

1378 git: "Git", 

1379 url: PathLike, 

1380 path: PathLike, 

1381 odb_default_type: Type[GitCmdObjectDB], 

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

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

1384 allow_unsafe_protocols: bool = False, 

1385 allow_unsafe_options: bool = False, 

1386 **kwargs: Any, 

1387 ) -> "Repo": 

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

1389 

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

1391 url = os.fspath(url) 

1392 path = os.fspath(path) 

1393 

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

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

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

1397 # becomes:: 

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

1399 # 

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

1401 sep_dir = kwargs.get("separate_git_dir") 

1402 if sep_dir: 

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

1404 multi = None 

1405 if multi_options: 

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

1407 

1408 if not allow_unsafe_protocols: 

1409 Git.check_unsafe_protocols(url) 

1410 if not allow_unsafe_options: 

1411 Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options) 

1412 if not allow_unsafe_options and multi: 

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

1414 

1415 proc = git.clone( 

1416 multi, 

1417 "--", 

1418 Git.polish_url(url), 

1419 clone_path, 

1420 with_extended_output=True, 

1421 as_process=True, 

1422 v=True, 

1423 universal_newlines=True, 

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

1425 ) 

1426 if progress: 

1427 handle_process_output( 

1428 proc, 

1429 None, 

1430 to_progress_instance(progress).new_message_handler(), 

1431 finalize_process, 

1432 decode_streams=False, 

1433 ) 

1434 else: 

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

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

1437 cmdline = remove_password_if_present(cmdline) 

1438 

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

1440 finalize_process(proc, stderr=stderr) 

1441 

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

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

1444 if not osp.isabs(path): 

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

1446 

1447 repo = cls(path, odbt=odbt) 

1448 

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

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

1451 

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

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

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

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

1456 if repo.remotes: 

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

1458 writer.set_value("url", Git.polish_url(repo.remotes[0].url)) 

1459 # END handle remote repo 

1460 return repo 

1461 

1462 def clone( 

1463 self, 

1464 path: PathLike, 

1465 progress: Optional[CallableProgress] = None, 

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

1467 allow_unsafe_protocols: bool = False, 

1468 allow_unsafe_options: bool = False, 

1469 **kwargs: Any, 

1470 ) -> "Repo": 

1471 """Create a clone from this repository. 

1472 

1473 :param path: 

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

1475 

1476 :param progress: 

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

1478 

1479 :param multi_options: 

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

1481 times. 

1482 

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

1484 For example:: 

1485 

1486 [ 

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

1488 "--config core.ignorecase", 

1489 "--recurse-submodule=repo1_path", 

1490 "--recurse-submodule=repo2_path", 

1491 ] 

1492 

1493 :param allow_unsafe_protocols: 

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

1495 

1496 :param allow_unsafe_options: 

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

1498 

1499 :param kwargs: 

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

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

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

1503 command. 

1504 

1505 :return: 

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

1507 """ 

1508 return self._clone( 

1509 self.git, 

1510 self.common_dir, 

1511 path, 

1512 type(self.odb), 

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

1514 multi_options, 

1515 allow_unsafe_protocols=allow_unsafe_protocols, 

1516 allow_unsafe_options=allow_unsafe_options, 

1517 **kwargs, 

1518 ) 

1519 

1520 @classmethod 

1521 def clone_from( 

1522 cls, 

1523 url: PathLike, 

1524 to_path: PathLike, 

1525 progress: CallableProgress = None, 

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

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

1528 allow_unsafe_protocols: bool = False, 

1529 allow_unsafe_options: bool = False, 

1530 **kwargs: Any, 

1531 ) -> "Repo": 

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

1533 

1534 :param url: 

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

1536 

1537 :param to_path: 

1538 Path to which the repository should be cloned to. 

1539 

1540 :param progress: 

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

1542 

1543 :param env: 

1544 Optional dictionary containing the desired environment variables. 

1545 

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

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

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

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

1550 

1551 :param multi_options: 

1552 See the :meth:`clone` method. 

1553 

1554 :param allow_unsafe_protocols: 

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

1556 

1557 :param allow_unsafe_options: 

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

1559 

1560 :param kwargs: 

1561 See the :meth:`clone` method. 

1562 

1563 :return: 

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

1565 """ 

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

1567 if env is not None: 

1568 git.update_environment(**env) 

1569 return cls._clone( 

1570 git, 

1571 url, 

1572 to_path, 

1573 GitCmdObjectDB, 

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

1575 multi_options, 

1576 allow_unsafe_protocols=allow_unsafe_protocols, 

1577 allow_unsafe_options=allow_unsafe_options, 

1578 **kwargs, 

1579 ) 

1580 

1581 def archive( 

1582 self, 

1583 ostream: Union[TextIO, BinaryIO], 

1584 treeish: Optional[str] = None, 

1585 prefix: Optional[str] = None, 

1586 **kwargs: Any, 

1587 ) -> Repo: 

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

1589 

1590 :param ostream: 

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

1592 

1593 :param treeish: 

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

1595 

1596 :param prefix: 

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

1598 

1599 :param kwargs: 

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

1601 

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

1603 ostreams to write any format supported by Python. 

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

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

1606 or a list or tuple of multiple paths. 

1607 

1608 :raise git.exc.GitCommandError: 

1609 If something went wrong. 

1610 

1611 :return: 

1612 self 

1613 """ 

1614 if treeish is None: 

1615 treeish = self.head.commit 

1616 if prefix and "prefix" not in kwargs: 

1617 kwargs["prefix"] = prefix 

1618 kwargs["output_stream"] = ostream 

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

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

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

1622 path = [path] 

1623 # END ensure paths is list (or tuple) 

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

1625 return self 

1626 

1627 def has_separate_working_tree(self) -> bool: 

1628 """ 

1629 :return: 

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

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

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

1633 to. 

1634 

1635 :note: 

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

1637 """ 

1638 if self.bare: 

1639 return False 

1640 if self.working_tree_dir: 

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

1642 else: 

1643 return False # Or raise Error? 

1644 

1645 rev_parse = rev_parse 

1646 

1647 def __repr__(self) -> str: 

1648 clazz = self.__class__ 

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

1650 

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

1652 """ 

1653 :return: 

1654 The commit which is currently being replayed while rebasing. 

1655 

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

1657 """ 

1658 if self.git_dir: 

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

1660 if not osp.isfile(rebase_head_file): 

1661 return None 

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

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

1664 return self.commit(content)