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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

575 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 _working_tree_dir: Optional[PathLike] = None 

130 

131 git_dir: PathLike 

132 """The ``.git`` repository directory.""" 

133 

134 _common_dir: PathLike = "" 

135 

136 # Precompiled regex 

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

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

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

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

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

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

143 

144 unsafe_git_clone_options = [ 

145 # Executes arbitrary commands: 

146 "--upload-pack", 

147 "-u", 

148 # Can override configuration variables that execute arbitrary commands: 

149 "--config", 

150 "-c", 

151 ] 

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

153 

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

155 directly: 

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

157 

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

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

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

161 """ 

162 

163 # Invariants 

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

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

166 

167 # Subclass configuration 

168 GitCommandWrapperType = Git 

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

170 type here.""" 

171 

172 def __init__( 

173 self, 

174 path: Optional[PathLike] = None, 

175 odbt: Type[LooseObjectDB] = GitCmdObjectDB, 

176 search_parent_directories: bool = False, 

177 expand_vars: bool = True, 

178 ) -> None: 

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

180 

181 :param path: 

182 The path to either the root git directory or the bare git repo:: 

183 

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

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

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

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

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

189 

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

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

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

193 used. 

194 

195 :param odbt: 

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

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

198 used to access all object data. 

199 

200 :param search_parent_directories: 

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

202 well. 

203 

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

205 GitPython, which is considered a bug though. 

206 

207 :raise git.exc.InvalidGitRepositoryError: 

208 

209 :raise git.exc.NoSuchPathError: 

210 

211 :return: 

212 :class:`Repo` 

213 """ 

214 

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

216 if not epath: 

217 epath = os.getcwd() 

218 if Git.is_cygwin(): 

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

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

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

222 epath = cygpath(str(epath)) 

223 

224 epath = epath or path or os.getcwd() 

225 if not isinstance(epath, str): 

226 epath = str(epath) 

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

228 warnings.warn( 

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

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

231 stacklevel=1, 

232 ) 

233 epath = expand_path(epath, expand_vars) 

234 if epath is not None: 

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

236 raise NoSuchPathError(epath) 

237 

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

239 curpath = epath 

240 git_dir = None 

241 while curpath: 

242 # ABOUT osp.NORMPATH 

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

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

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

246 if is_git_dir(curpath): 

247 git_dir = curpath 

248 # from man git-config : core.worktree 

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

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

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

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

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

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

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

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

257 # working tree. 

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

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

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

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

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

263 if "GIT_WORK_TREE" in os.environ: 

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

265 break 

266 

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

268 sm_gitpath = find_submodule_git_dir(dotgit) 

269 if sm_gitpath is not None: 

270 git_dir = osp.normpath(sm_gitpath) 

271 

272 sm_gitpath = find_submodule_git_dir(dotgit) 

273 if sm_gitpath is None: 

274 sm_gitpath = find_worktree_git_dir(dotgit) 

275 

276 if sm_gitpath is not None: 

277 git_dir = expand_path(sm_gitpath, expand_vars) 

278 self._working_tree_dir = curpath 

279 break 

280 

281 if not search_parent_directories: 

282 break 

283 curpath, tail = osp.split(curpath) 

284 if not tail: 

285 break 

286 # END while curpath 

287 

288 if git_dir is None: 

289 raise InvalidGitRepositoryError(epath) 

290 self.git_dir = git_dir 

291 

292 self._bare = False 

293 try: 

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

295 except Exception: 

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

297 pass 

298 

299 try: 

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

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

302 except OSError: 

303 self._common_dir = "" 

304 

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

306 # that in the first place. 

307 if self._bare: 

308 self._working_tree_dir = None 

309 # END working dir handling 

310 

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

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

313 

314 # Special handling, in special times. 

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

316 if issubclass(odbt, GitCmdObjectDB): 

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

318 else: 

319 self.odb = odbt(rootpath) 

320 

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

322 return self 

323 

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

325 self.close() 

326 

327 def __del__(self) -> None: 

328 try: 

329 self.close() 

330 except Exception: 

331 pass 

332 

333 def close(self) -> None: 

334 if self.git: 

335 self.git.clear_cache() 

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

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

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

339 # synchronously rather than forcing a gc collection. 

340 if sys.platform == "win32": 

341 gc.collect() 

342 gitdb.util.mman.collect() 

343 if sys.platform == "win32": 

344 gc.collect() 

345 

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

347 if isinstance(rhs, Repo): 

348 return self.git_dir == rhs.git_dir 

349 return False 

350 

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

352 return not self.__eq__(rhs) 

353 

354 def __hash__(self) -> int: 

355 return hash(self.git_dir) 

356 

357 # Description property 

358 def _get_description(self) -> str: 

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

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

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

362 

363 def _set_description(self, descr: str) -> None: 

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

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

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

367 

368 description = property(_get_description, _set_description, doc="the project's description") 

369 del _get_description 

370 del _set_description 

371 

372 @property 

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

374 """ 

375 :return: 

376 The working tree directory of our git repository. 

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

378 """ 

379 return self._working_tree_dir 

380 

381 @property 

382 def common_dir(self) -> PathLike: 

383 """ 

384 :return: 

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

386 ORIG_HEAD, COMMIT_EDITMSG, index, and logs/. 

387 """ 

388 return self._common_dir or self.git_dir 

389 

390 @property 

391 def bare(self) -> bool: 

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

393 return self._bare 

394 

395 @property 

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

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

398 in this repo. 

399 

400 :return: 

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

402 """ 

403 return Head.list_items(self) 

404 

405 @property 

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

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

408 heads and remote references. 

409 

410 :return: 

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

412 """ 

413 return Reference.list_items(self) 

414 

415 # Alias for references. 

416 refs = references 

417 

418 # Alias for heads. 

419 branches = heads 

420 

421 @property 

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

423 """ 

424 :return: 

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

426 

427 :note: 

428 This property can be expensive, as the returned 

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

430 It is recommended to reuse the object. 

431 """ 

432 return IndexFile(self) 

433 

434 @property 

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

436 """ 

437 :return: 

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

439 """ 

440 return HEAD(self, "HEAD") 

441 

442 @property 

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

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

445 manipulate remotes. 

446 

447 :return: 

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

449 """ 

450 return Remote.list_items(self) 

451 

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

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

454 

455 :raise ValueError: 

456 If no remote with such a name exists. 

457 """ 

458 r = Remote(self, name) 

459 if not r.exists(): 

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

461 return r 

462 

463 # { Submodules 

464 

465 @property 

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

467 """ 

468 :return: 

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

470 current head 

471 """ 

472 return Submodule.list_items(self) 

473 

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

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

476 

477 :raise ValueError: 

478 If no such submodule exists. 

479 """ 

480 try: 

481 return self.submodules[name] 

482 except IndexError as e: 

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

484 # END exception handling 

485 

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

487 """Create a new submodule. 

488 

489 :note: 

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

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

492 

493 :return: 

494 The created submodule. 

495 """ 

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

497 

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

499 """An iterator yielding Submodule instances. 

500 

501 See the `~git.objects.util.Traversable` interface for a description of `args` 

502 and `kwargs`. 

503 

504 :return: 

505 Iterator 

506 """ 

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

508 

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

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

511 take the previous state into consideration. 

512 

513 :note: 

514 For more information, please see the documentation of 

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

516 """ 

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

518 

519 # }END submodules 

520 

521 @property 

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

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

524 this repo. 

525 

526 :return: 

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

528 """ 

529 return TagReference.list_items(self) 

530 

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

532 """ 

533 :return: 

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

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

536 

537 :param path: 

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

539 """ 

540 full_path = self._to_full_tag_path(path) 

541 return TagReference(self, full_path) 

542 

543 @staticmethod 

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

545 path_str = str(path) 

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

547 return path_str 

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

549 return Reference._common_path_default + "/" + path_str 

550 else: 

551 return TagReference._common_path_default + "/" + path_str 

552 

553 def create_head( 

554 self, 

555 path: PathLike, 

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

557 force: bool = False, 

558 logmsg: Optional[str] = None, 

559 ) -> "Head": 

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

561 

562 :note: 

563 For more documentation, please see the 

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

565 

566 :return: 

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

568 """ 

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

570 

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

572 """Delete the given heads. 

573 

574 :param kwargs: 

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

576 """ 

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

578 

579 def create_tag( 

580 self, 

581 path: PathLike, 

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

583 message: Optional[str] = None, 

584 force: bool = False, 

585 **kwargs: Any, 

586 ) -> TagReference: 

587 """Create a new tag reference. 

588 

589 :note: 

590 For more documentation, please see the 

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

592 

593 :return: 

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

595 """ 

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

597 

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

599 """Delete the given tag references.""" 

600 return TagReference.delete(self, *tags) 

601 

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

603 """Create a new remote. 

604 

605 For more information, please see the documentation of the 

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

607 

608 :return: 

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

610 """ 

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

612 

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

614 """Delete the given remote.""" 

615 return Remote.remove(self, remote) 

616 

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

618 if git_dir is None: 

619 git_dir = self.git_dir 

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

621 # Use the global config instead. 

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

623 config_level = "global" 

624 

625 if config_level == "system": 

626 return "/etc/gitconfig" 

627 elif config_level == "user": 

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

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

630 elif config_level == "global": 

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

632 elif config_level == "repository": 

633 repo_dir = self._common_dir or git_dir 

634 if not repo_dir: 

635 raise NotADirectoryError 

636 else: 

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

638 else: 

639 assert_never( # type: ignore[unreachable] 

640 config_level, 

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

642 ) 

643 

644 def config_reader( 

645 self, 

646 config_level: Optional[Lit_config_levels] = None, 

647 ) -> GitConfigParser: 

648 """ 

649 :return: 

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

651 configuration, but not to write it. 

652 

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

654 configuration files. 

655 

656 :param config_level: 

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

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

659 you wish to read to prevent reading multiple files. 

660 

661 :note: 

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

663 unknown, instead the global path will be used. 

664 """ 

665 return self._config_reader(config_level=config_level) 

666 

667 def _config_reader( 

668 self, 

669 config_level: Optional[Lit_config_levels] = None, 

670 git_dir: Optional[PathLike] = None, 

671 ) -> GitConfigParser: 

672 if config_level is None: 

673 files = [ 

674 self._get_config_path(cast(Lit_config_levels, f), git_dir) 

675 for f in self.config_level 

676 if cast(Lit_config_levels, f) 

677 ] 

678 else: 

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

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

681 

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

683 """ 

684 :return: 

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

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

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

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

689 

690 :param config_level: 

691 One of the following values: 

692 

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

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

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

696 """ 

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

698 

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

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

701 

702 :param rev: 

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

704 

705 :return: 

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

707 """ 

708 if rev is None: 

709 return self.head.commit 

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

711 

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

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

714 

715 :note: 

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

717 """ 

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

719 

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

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

722 

723 Examples:: 

724 

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

726 

727 :param rev: 

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

729 

730 :return: 

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

732 

733 :note: 

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

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

736 subsequent operations might have unexpected results. 

737 """ 

738 if rev is None: 

739 return self.head.commit.tree 

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

741 

742 def iter_commits( 

743 self, 

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

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

746 **kwargs: Any, 

747 ) -> Iterator[Commit]: 

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

749 history of a given ref/commit. 

750 

751 :param rev: 

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

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

754 

755 :param paths: 

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

757 path or paths will be returned. 

758 

759 :param kwargs: 

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

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

762 

763 :note: 

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

765 ``"revA...revB"`` revision specifier. 

766 

767 :return: 

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

769 """ 

770 if rev is None: 

771 rev = self.head.commit 

772 

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

774 

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

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

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

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

779 

780 :param rev: 

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

782 

783 :param kwargs: 

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

785 which does all the work. 

786 

787 :return: 

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

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

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

791 exists. 

792 

793 :raise ValueError: 

794 If fewer than two revisions are provided. 

795 """ 

796 if len(rev) < 2: 

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

798 # END handle input 

799 

800 res: List[Commit] = [] 

801 try: 

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

803 except GitCommandError as err: 

804 if err.status == 128: 

805 raise 

806 # END handle invalid rev 

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

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

809 return res 

810 # END exception handling 

811 

812 for line in lines: 

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

814 # END for each merge-base 

815 

816 return res 

817 

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

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

820 

821 :param ancestor_rev: 

822 Rev which should be an ancestor. 

823 

824 :param rev: 

825 Rev to test against `ancestor_rev`. 

826 

827 :return: 

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

829 """ 

830 try: 

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

832 except GitCommandError as err: 

833 if err.status == 1: 

834 return False 

835 raise 

836 return True 

837 

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

839 try: 

840 complete_sha = self.odb.partial_to_complete_sha_hex(sha) 

841 object_info = self.odb.info(complete_sha) 

842 if object_type: 

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

844 return True 

845 else: 

846 _logger.debug( 

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

848 object_info.type.decode(), 

849 object_type, 

850 ) 

851 return False 

852 else: 

853 return True 

854 except BadObject: 

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

856 return False 

857 

858 def _get_daemon_export(self) -> bool: 

859 if self.git_dir: 

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

861 return osp.exists(filename) 

862 

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

864 if self.git_dir: 

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

866 fileexists = osp.exists(filename) 

867 if value and not fileexists: 

868 touch(filename) 

869 elif not value and fileexists: 

870 os.unlink(filename) 

871 

872 daemon_export = property( 

873 _get_daemon_export, 

874 _set_daemon_export, 

875 doc="If True, git-daemon may export this repository", 

876 ) 

877 del _get_daemon_export 

878 del _set_daemon_export 

879 

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

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

882 

883 :return: 

884 List of strings being pathnames of alternates 

885 """ 

886 if self.git_dir: 

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

888 

889 if osp.exists(alternates_path): 

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

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

892 return alts.strip().splitlines() 

893 return [] 

894 

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

896 """Set the alternates. 

897 

898 :param alts: 

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

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

901 

902 :raise git.exc.NoSuchPathError: 

903 

904 :note: 

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

906 caller is responsible. 

907 """ 

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

909 if not alts: 

910 if osp.isfile(alternates_path): 

911 os.remove(alternates_path) 

912 else: 

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

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

915 

916 alternates = property( 

917 _get_alternates, 

918 _set_alternates, 

919 doc="Retrieve a list of alternates paths or set a list paths to be used as alternates", 

920 ) 

921 

922 def is_dirty( 

923 self, 

924 index: bool = True, 

925 working_tree: bool = True, 

926 untracked_files: bool = False, 

927 submodules: bool = True, 

928 path: Optional[PathLike] = None, 

929 ) -> bool: 

930 """ 

931 :return: 

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

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

934 if the index or the working copy have changes. 

935 """ 

936 if self._bare: 

937 # Bare repositories with no associated working directory are 

938 # always considered to be clean. 

939 return False 

940 

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

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

943 if not submodules: 

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

945 if path: 

946 default_args.extend(["--", str(path)]) 

947 if index: 

948 # diff index against HEAD. 

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

950 return True 

951 # END index handling 

952 if working_tree: 

953 # diff index against working tree. 

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

955 return True 

956 # END working tree handling 

957 if untracked_files: 

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

959 return True 

960 # END untracked files 

961 return False 

962 

963 @property 

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

965 """ 

966 :return: 

967 list(str,...) 

968 

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

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

971 

972 :note: 

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

974 

975 :note: 

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

977 please consider caching it yourself. 

978 """ 

979 return self._get_untracked_files() 

980 

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

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

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

984 # Untracked files prefix in porcelain mode 

985 prefix = "?? " 

986 untracked_files = [] 

987 for line in proc.stdout: 

988 line = line.decode(defenc) 

989 if not line.startswith(prefix): 

990 continue 

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

992 # Special characters are escaped 

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

994 filename = filename[1:-1] 

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

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

997 untracked_files.append(filename) 

998 finalize_process(proc) 

999 return untracked_files 

1000 

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

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

1003 

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

1005 

1006 :param paths: 

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

1008 

1009 :return: 

1010 Subset of those paths which are ignored 

1011 """ 

1012 try: 

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

1014 except GitCommandError as err: 

1015 if err.status == 1: 

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

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

1018 return [] 

1019 else: 

1020 # Raise the exception on all other return codes. 

1021 raise 

1022 

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

1024 

1025 @property 

1026 def active_branch(self) -> Head: 

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

1028 

1029 :raise TypeError: 

1030 If HEAD is detached. 

1031 

1032 :return: 

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

1034 """ 

1035 # reveal_type(self.head.reference) # => Reference 

1036 return self.head.reference 

1037 

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

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

1040 

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

1042 stream of :class:`BlameEntry` tuples. 

1043 

1044 :param rev: 

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

1046 uncommitted changes. Otherwise, anything successfully parsed by 

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

1048 

1049 :return: 

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

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

1052 the resulting file. 

1053 

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

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

1056 """ 

1057 

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

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

1060 

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

1062 while True: 

1063 try: 

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

1065 line = next(stream) 

1066 except StopIteration: 

1067 return 

1068 split_line = line.split() 

1069 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line 

1070 lineno = int(lineno_b) 

1071 num_lines = int(num_lines_b) 

1072 orig_lineno = int(orig_lineno_b) 

1073 if hexsha not in commits: 

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

1075 # commit. 

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

1077 while True: 

1078 try: 

1079 line = next(stream) 

1080 except StopIteration: 

1081 return 

1082 if line == b"boundary": 

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

1084 # "previous" tag. 

1085 continue 

1086 

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

1088 props[tag] = value 

1089 if tag == b"filename": 

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

1091 orig_filename = value 

1092 break 

1093 

1094 c = Commit( 

1095 self, 

1096 hex_to_bin(hexsha), 

1097 author=Actor( 

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

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

1100 ), 

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

1102 committer=Actor( 

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

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

1105 ), 

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

1107 ) 

1108 commits[hexsha] = c 

1109 else: 

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

1111 # the last line. 

1112 while True: 

1113 try: 

1114 # Will fail if we reach the EOF unexpectedly. 

1115 line = next(stream) 

1116 except StopIteration: 

1117 return 

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

1119 if tag == b"filename": 

1120 orig_filename = value 

1121 break 

1122 

1123 yield BlameEntry( 

1124 commits[hexsha], 

1125 range(lineno, lineno + num_lines), 

1126 safe_decode(orig_filename), 

1127 range(orig_lineno, orig_lineno + num_lines), 

1128 ) 

1129 

1130 def blame( 

1131 self, 

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

1133 file: str, 

1134 incremental: bool = False, 

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

1136 **kwargs: Any, 

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

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

1139 

1140 :param rev: 

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

1142 uncommitted changes. Otherwise, anything successfully parsed by 

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

1144 

1145 :return: 

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

1147 

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

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

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

1151 appearance. 

1152 """ 

1153 if incremental: 

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

1155 rev_opts = rev_opts or [] 

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

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

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

1159 

1160 class InfoTD(TypedDict, total=False): 

1161 sha: str 

1162 id: str 

1163 filename: str 

1164 summary: str 

1165 author: str 

1166 author_email: str 

1167 author_date: int 

1168 committer: str 

1169 committer_email: str 

1170 committer_date: int 

1171 

1172 info: InfoTD = {} 

1173 

1174 keepends = True 

1175 for line_bytes in data.splitlines(keepends): 

1176 try: 

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

1178 except UnicodeDecodeError: 

1179 firstpart = "" 

1180 parts = [] 

1181 is_binary = True 

1182 else: 

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

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

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

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

1187 # instead. 

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

1189 firstpart = parts[0] 

1190 is_binary = False 

1191 # END handle decode of line 

1192 

1193 if self.re_hexsha_only.search(firstpart): 

1194 # handles 

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

1196 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates 

1197 # another line of blame with the same data 

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

1199 if len(digits) == 3: 

1200 info = {"id": firstpart} 

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

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

1203 info = {"id": firstpart} 

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

1205 # END blame data initialization 

1206 else: 

1207 m = self.re_author_committer_start.search(firstpart) 

1208 if m: 

1209 # handles: 

1210 # author Tom Preston-Werner 

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

1212 # author-time 1192271832 

1213 # author-tz -0700 

1214 # committer Tom Preston-Werner 

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

1216 # committer-time 1192271832 

1217 # committer-tz -0700 - IGNORED BY US 

1218 role = m.group(0) 

1219 if role == "author": 

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

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

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

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

1224 elif role == firstpart: 

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

1226 elif role == "committer": 

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

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

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

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

1231 elif role == firstpart: 

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

1233 # END distinguish mail,time,name 

1234 else: 

1235 # handle 

1236 # filename lib/grit.rb 

1237 # summary add Blob 

1238 # <and rest> 

1239 if firstpart.startswith("filename"): 

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

1241 elif firstpart.startswith("summary"): 

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

1243 elif firstpart == "": 

1244 if info: 

1245 sha = info["id"] 

1246 c = commits.get(sha) 

1247 if c is None: 

1248 c = Commit( 

1249 self, 

1250 hex_to_bin(sha), 

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

1252 authored_date=info["author_date"], 

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

1254 committed_date=info["committer_date"], 

1255 ) 

1256 commits[sha] = c 

1257 blames[-1][0] = c 

1258 # END if commit objects needs initial creation 

1259 

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

1261 line: str | bytes 

1262 if not is_binary: 

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

1264 line_str = line_str[1:] 

1265 line = line_str 

1266 else: 

1267 line = line_bytes 

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

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

1270 # along the newline separator. We will append this 

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

1272 # though it should be concatenated with the last 

1273 # line we have seen. 

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

1275 

1276 info = {"id": sha} 

1277 # END if we collected commit info 

1278 # END distinguish filename,summary,rest 

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

1280 # END distinguish hexsha vs other information 

1281 return blames 

1282 

1283 @classmethod 

1284 def init( 

1285 cls, 

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

1287 mkdir: bool = True, 

1288 odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, 

1289 expand_vars: bool = True, 

1290 **kwargs: Any, 

1291 ) -> "Repo": 

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

1293 

1294 :param path: 

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

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

1297 working directory. 

1298 

1299 :param mkdir: 

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

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

1302 Only effective if a path is explicitly given. 

1303 

1304 :param odbt: 

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

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

1307 used to access all object data. 

1308 

1309 :param expand_vars: 

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

1311 information disclosure, allowing attackers to access the contents of 

1312 environment variables. 

1313 

1314 :param kwargs: 

1315 Keyword arguments serving as additional options to the 

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

1317 

1318 :return: 

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

1320 """ 

1321 if path: 

1322 path = expand_path(path, expand_vars) 

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

1324 os.makedirs(path, 0o755) 

1325 

1326 # git command automatically chdir into the directory 

1327 git = cls.GitCommandWrapperType(path) 

1328 git.init(**kwargs) 

1329 return cls(path, odbt=odbt) 

1330 

1331 @classmethod 

1332 def _clone( 

1333 cls, 

1334 git: "Git", 

1335 url: PathLike, 

1336 path: PathLike, 

1337 odb_default_type: Type[GitCmdObjectDB], 

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

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

1340 allow_unsafe_protocols: bool = False, 

1341 allow_unsafe_options: bool = False, 

1342 **kwargs: Any, 

1343 ) -> "Repo": 

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

1345 

1346 # When pathlib.Path or other class-based path is passed 

1347 if not isinstance(path, str): 

1348 path = str(path) 

1349 

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

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

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

1353 # becomes:: 

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

1355 # 

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

1357 sep_dir = kwargs.get("separate_git_dir") 

1358 if sep_dir: 

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

1360 multi = None 

1361 if multi_options: 

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

1363 

1364 if not allow_unsafe_protocols: 

1365 Git.check_unsafe_protocols(str(url)) 

1366 if not allow_unsafe_options: 

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

1368 if not allow_unsafe_options and multi_options: 

1369 Git.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options) 

1370 

1371 proc = git.clone( 

1372 multi, 

1373 "--", 

1374 Git.polish_url(str(url)), 

1375 clone_path, 

1376 with_extended_output=True, 

1377 as_process=True, 

1378 v=True, 

1379 universal_newlines=True, 

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

1381 ) 

1382 if progress: 

1383 handle_process_output( 

1384 proc, 

1385 None, 

1386 to_progress_instance(progress).new_message_handler(), 

1387 finalize_process, 

1388 decode_streams=False, 

1389 ) 

1390 else: 

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

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

1393 cmdline = remove_password_if_present(cmdline) 

1394 

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

1396 finalize_process(proc, stderr=stderr) 

1397 

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

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

1400 if not osp.isabs(path): 

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

1402 

1403 repo = cls(path, odbt=odbt) 

1404 

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

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

1407 

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

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

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

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

1412 if repo.remotes: 

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

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

1415 # END handle remote repo 

1416 return repo 

1417 

1418 def clone( 

1419 self, 

1420 path: PathLike, 

1421 progress: Optional[CallableProgress] = None, 

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

1423 allow_unsafe_protocols: bool = False, 

1424 allow_unsafe_options: bool = False, 

1425 **kwargs: Any, 

1426 ) -> "Repo": 

1427 """Create a clone from this repository. 

1428 

1429 :param path: 

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

1431 

1432 :param progress: 

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

1434 

1435 :param multi_options: 

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

1437 times. 

1438 

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

1440 For example:: 

1441 

1442 [ 

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

1444 "--config core.ignorecase", 

1445 "--recurse-submodule=repo1_path", 

1446 "--recurse-submodule=repo2_path", 

1447 ] 

1448 

1449 :param allow_unsafe_protocols: 

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

1451 

1452 :param allow_unsafe_options: 

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

1454 

1455 :param kwargs: 

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

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

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

1459 command. 

1460 

1461 :return: 

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

1463 """ 

1464 return self._clone( 

1465 self.git, 

1466 self.common_dir, 

1467 path, 

1468 type(self.odb), 

1469 progress, 

1470 multi_options, 

1471 allow_unsafe_protocols=allow_unsafe_protocols, 

1472 allow_unsafe_options=allow_unsafe_options, 

1473 **kwargs, 

1474 ) 

1475 

1476 @classmethod 

1477 def clone_from( 

1478 cls, 

1479 url: PathLike, 

1480 to_path: PathLike, 

1481 progress: CallableProgress = None, 

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

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

1484 allow_unsafe_protocols: bool = False, 

1485 allow_unsafe_options: bool = False, 

1486 **kwargs: Any, 

1487 ) -> "Repo": 

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

1489 

1490 :param url: 

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

1492 

1493 :param to_path: 

1494 Path to which the repository should be cloned to. 

1495 

1496 :param progress: 

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

1498 

1499 :param env: 

1500 Optional dictionary containing the desired environment variables. 

1501 

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

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

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

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

1506 

1507 :param multi_options: 

1508 See the :meth:`clone` method. 

1509 

1510 :param allow_unsafe_protocols: 

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

1512 

1513 :param allow_unsafe_options: 

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

1515 

1516 :param kwargs: 

1517 See the :meth:`clone` method. 

1518 

1519 :return: 

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

1521 """ 

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

1523 if env is not None: 

1524 git.update_environment(**env) 

1525 return cls._clone( 

1526 git, 

1527 url, 

1528 to_path, 

1529 GitCmdObjectDB, 

1530 progress, 

1531 multi_options, 

1532 allow_unsafe_protocols=allow_unsafe_protocols, 

1533 allow_unsafe_options=allow_unsafe_options, 

1534 **kwargs, 

1535 ) 

1536 

1537 def archive( 

1538 self, 

1539 ostream: Union[TextIO, BinaryIO], 

1540 treeish: Optional[str] = None, 

1541 prefix: Optional[str] = None, 

1542 **kwargs: Any, 

1543 ) -> Repo: 

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

1545 

1546 :param ostream: 

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

1548 

1549 :param treeish: 

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

1551 

1552 :param prefix: 

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

1554 

1555 :param kwargs: 

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

1557 

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

1559 ostreams to write any format supported by Python. 

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

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

1562 or a list or tuple of multiple paths. 

1563 

1564 :raise git.exc.GitCommandError: 

1565 If something went wrong. 

1566 

1567 :return: 

1568 self 

1569 """ 

1570 if treeish is None: 

1571 treeish = self.head.commit 

1572 if prefix and "prefix" not in kwargs: 

1573 kwargs["prefix"] = prefix 

1574 kwargs["output_stream"] = ostream 

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

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

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

1578 path = [path] 

1579 # END ensure paths is list (or tuple) 

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

1581 return self 

1582 

1583 def has_separate_working_tree(self) -> bool: 

1584 """ 

1585 :return: 

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

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

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

1589 to. 

1590 

1591 :note: 

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

1593 """ 

1594 if self.bare: 

1595 return False 

1596 if self.working_tree_dir: 

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

1598 else: 

1599 return False # Or raise Error? 

1600 

1601 rev_parse = rev_parse 

1602 

1603 def __repr__(self) -> str: 

1604 clazz = self.__class__ 

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

1606 

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

1608 """ 

1609 :return: 

1610 The commit which is currently being replayed while rebasing. 

1611 

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

1613 """ 

1614 if self.git_dir: 

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

1616 if not osp.isfile(rebase_head_file): 

1617 return None 

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

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

1620 return self.commit(content)