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

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

595 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 worktree directory or the .git directory itself:: 

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 @property 

358 def description(self) -> str: 

359 """The project's description""" 

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

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

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

363 

364 @description.setter 

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

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

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

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

369 

370 @property 

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

372 """ 

373 :return: 

374 The working tree directory of our git repository. 

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

376 """ 

377 return self._working_tree_dir 

378 

379 @property 

380 def common_dir(self) -> PathLike: 

381 """ 

382 :return: 

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

384 ORIG_HEAD, COMMIT_EDITMSG, index, and logs/. 

385 """ 

386 return self._common_dir or self.git_dir 

387 

388 @property 

389 def bare(self) -> bool: 

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

391 return self._bare 

392 

393 @property 

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

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

396 in this repo. 

397 

398 :return: 

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

400 """ 

401 return Head.list_items(self) 

402 

403 @property 

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

405 """Alias for heads. 

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

407 in this repo. 

408 

409 :return: 

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

411 """ 

412 return self.heads 

413 

414 @property 

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

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

417 heads and remote references. 

418 

419 :return: 

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

421 """ 

422 return Reference.list_items(self) 

423 

424 @property 

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

426 """Alias for references. 

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

428 heads and remote references. 

429 

430 :return: 

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

432 """ 

433 return self.references 

434 

435 @property 

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

437 """ 

438 :return: 

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

440 

441 :note: 

442 This property can be expensive, as the returned 

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

444 It is recommended to reuse the object. 

445 """ 

446 return IndexFile(self) 

447 

448 @property 

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

450 """ 

451 :return: 

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

453 """ 

454 return HEAD(self, "HEAD") 

455 

456 @property 

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

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

459 manipulate remotes. 

460 

461 :return: 

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

463 """ 

464 return Remote.list_items(self) 

465 

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

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

468 

469 :raise ValueError: 

470 If no remote with such a name exists. 

471 """ 

472 r = Remote(self, name) 

473 if not r.exists(): 

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

475 return r 

476 

477 # { Submodules 

478 

479 @property 

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

481 """ 

482 :return: 

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

484 current head 

485 """ 

486 return Submodule.list_items(self) 

487 

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

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

490 

491 :raise ValueError: 

492 If no such submodule exists. 

493 """ 

494 try: 

495 return self.submodules[name] 

496 except IndexError as e: 

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

498 # END exception handling 

499 

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

501 """Create a new submodule. 

502 

503 :note: 

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

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

506 

507 :return: 

508 The created submodule. 

509 """ 

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

511 

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

513 """An iterator yielding Submodule instances. 

514 

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

516 and `kwargs`. 

517 

518 :return: 

519 Iterator 

520 """ 

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

522 

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

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

525 take the previous state into consideration. 

526 

527 :note: 

528 For more information, please see the documentation of 

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

530 """ 

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

532 

533 # }END submodules 

534 

535 @property 

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

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

538 this repo. 

539 

540 :return: 

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

542 """ 

543 return TagReference.list_items(self) 

544 

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

546 """ 

547 :return: 

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

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

550 

551 :param path: 

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

553 """ 

554 full_path = self._to_full_tag_path(path) 

555 return TagReference(self, full_path) 

556 

557 @staticmethod 

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

559 path_str = str(path) 

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

561 return path_str 

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

563 return Reference._common_path_default + "/" + path_str 

564 else: 

565 return TagReference._common_path_default + "/" + path_str 

566 

567 def create_head( 

568 self, 

569 path: PathLike, 

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

571 force: bool = False, 

572 logmsg: Optional[str] = None, 

573 ) -> "Head": 

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

575 

576 :note: 

577 For more documentation, please see the 

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

579 

580 :return: 

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

582 """ 

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

584 

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

586 """Delete the given heads. 

587 

588 :param kwargs: 

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

590 """ 

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

592 

593 def create_tag( 

594 self, 

595 path: PathLike, 

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

597 message: Optional[str] = None, 

598 force: bool = False, 

599 **kwargs: Any, 

600 ) -> TagReference: 

601 """Create a new tag reference. 

602 

603 :note: 

604 For more documentation, please see the 

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

606 

607 :return: 

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

609 """ 

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

611 

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

613 """Delete the given tag references.""" 

614 return TagReference.delete(self, *tags) 

615 

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

617 """Create a new remote. 

618 

619 For more information, please see the documentation of the 

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

621 

622 :return: 

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

624 """ 

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

626 

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

628 """Delete the given remote.""" 

629 return Remote.remove(self, remote) 

630 

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

632 if git_dir is None: 

633 git_dir = self.git_dir 

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

635 # Use the global config instead. 

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

637 config_level = "global" 

638 

639 if config_level == "system": 

640 return "/etc/gitconfig" 

641 elif config_level == "user": 

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

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

644 elif config_level == "global": 

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

646 elif config_level == "repository": 

647 repo_dir = self._common_dir or git_dir 

648 if not repo_dir: 

649 raise NotADirectoryError 

650 else: 

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

652 else: 

653 assert_never( # type: ignore[unreachable] 

654 config_level, 

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

656 ) 

657 

658 def config_reader( 

659 self, 

660 config_level: Optional[Lit_config_levels] = None, 

661 ) -> GitConfigParser: 

662 """ 

663 :return: 

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

665 configuration, but not to write it. 

666 

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

668 configuration files. 

669 

670 :param config_level: 

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

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

673 you wish to read to prevent reading multiple files. 

674 

675 :note: 

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

677 unknown, instead the global path will be used. 

678 """ 

679 return self._config_reader(config_level=config_level) 

680 

681 def _config_reader( 

682 self, 

683 config_level: Optional[Lit_config_levels] = None, 

684 git_dir: Optional[PathLike] = None, 

685 ) -> GitConfigParser: 

686 if config_level is None: 

687 files = [ 

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

689 for f in self.config_level 

690 if cast(Lit_config_levels, f) 

691 ] 

692 else: 

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

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

695 

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

697 """ 

698 :return: 

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

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

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

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

703 

704 :param config_level: 

705 One of the following values: 

706 

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

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

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

710 """ 

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

712 

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

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

715 

716 :param rev: 

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

718 

719 :return: 

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

721 """ 

722 if rev is None: 

723 return self.head.commit 

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

725 

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

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

728 

729 :note: 

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

731 """ 

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

733 

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

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

736 

737 Examples:: 

738 

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

740 

741 :param rev: 

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

743 

744 :return: 

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

746 

747 :note: 

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

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

750 subsequent operations might have unexpected results. 

751 """ 

752 if rev is None: 

753 return self.head.commit.tree 

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

755 

756 def iter_commits( 

757 self, 

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

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

760 **kwargs: Any, 

761 ) -> Iterator[Commit]: 

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

763 history of a given ref/commit. 

764 

765 :param rev: 

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

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

768 

769 :param paths: 

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

771 path or paths will be returned. 

772 

773 :param kwargs: 

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

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

776 

777 :note: 

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

779 ``"revA...revB"`` revision specifier. 

780 

781 :return: 

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

783 """ 

784 if rev is None: 

785 rev = self.head.commit 

786 

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

788 

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

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

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

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

793 

794 :param rev: 

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

796 

797 :param kwargs: 

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

799 which does all the work. 

800 

801 :return: 

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

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

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

805 exists. 

806 

807 :raise ValueError: 

808 If fewer than two revisions are provided. 

809 """ 

810 if len(rev) < 2: 

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

812 # END handle input 

813 

814 res: List[Commit] = [] 

815 try: 

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

817 except GitCommandError as err: 

818 if err.status == 128: 

819 raise 

820 # END handle invalid rev 

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

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

823 return res 

824 # END exception handling 

825 

826 for line in lines: 

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

828 # END for each merge-base 

829 

830 return res 

831 

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

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

834 

835 :param ancestor_rev: 

836 Rev which should be an ancestor. 

837 

838 :param rev: 

839 Rev to test against `ancestor_rev`. 

840 

841 :return: 

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

843 """ 

844 try: 

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

846 except GitCommandError as err: 

847 if err.status == 1: 

848 return False 

849 raise 

850 return True 

851 

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

853 try: 

854 complete_sha = self.odb.partial_to_complete_sha_hex(sha) 

855 object_info = self.odb.info(complete_sha) 

856 if object_type: 

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

858 return True 

859 else: 

860 _logger.debug( 

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

862 object_info.type.decode(), 

863 object_type, 

864 ) 

865 return False 

866 else: 

867 return True 

868 except BadObject: 

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

870 return False 

871 

872 def _get_daemon_export(self) -> bool: 

873 if self.git_dir: 

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

875 return osp.exists(filename) 

876 

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

878 if self.git_dir: 

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

880 fileexists = osp.exists(filename) 

881 if value and not fileexists: 

882 touch(filename) 

883 elif not value and fileexists: 

884 os.unlink(filename) 

885 

886 @property 

887 def daemon_export(self) -> bool: 

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

889 return self._get_daemon_export() 

890 

891 @daemon_export.setter 

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

893 self._set_daemon_export(value) 

894 

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

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

897 

898 :return: 

899 List of strings being pathnames of alternates 

900 """ 

901 if self.git_dir: 

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

903 

904 if osp.exists(alternates_path): 

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

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

907 return alts.strip().splitlines() 

908 return [] 

909 

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

911 """Set the alternates. 

912 

913 :param alts: 

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

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

916 

917 :raise git.exc.NoSuchPathError: 

918 

919 :note: 

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

921 caller is responsible. 

922 """ 

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

924 if not alts: 

925 if osp.isfile(alternates_path): 

926 os.remove(alternates_path) 

927 else: 

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

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

930 

931 @property 

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

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

934 return self._get_alternates() 

935 

936 @alternates.setter 

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

938 self._set_alternates(alts) 

939 

940 def is_dirty( 

941 self, 

942 index: bool = True, 

943 working_tree: bool = True, 

944 untracked_files: bool = False, 

945 submodules: bool = True, 

946 path: Optional[PathLike] = None, 

947 ) -> bool: 

948 """ 

949 :return: 

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

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

952 if the index or the working copy have changes. 

953 """ 

954 if self._bare: 

955 # Bare repositories with no associated working directory are 

956 # always considered to be clean. 

957 return False 

958 

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

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

961 if not submodules: 

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

963 if path: 

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

965 if index: 

966 # diff index against HEAD. 

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

968 return True 

969 # END index handling 

970 if working_tree: 

971 # diff index against working tree. 

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

973 return True 

974 # END working tree handling 

975 if untracked_files: 

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

977 return True 

978 # END untracked files 

979 return False 

980 

981 @property 

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

983 """ 

984 :return: 

985 list(str,...) 

986 

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

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

989 

990 :note: 

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

992 

993 :note: 

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

995 please consider caching it yourself. 

996 """ 

997 return self._get_untracked_files() 

998 

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

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

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

1002 # Untracked files prefix in porcelain mode 

1003 prefix = "?? " 

1004 untracked_files = [] 

1005 for line in proc.stdout: 

1006 line = line.decode(defenc) 

1007 if not line.startswith(prefix): 

1008 continue 

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

1010 # Special characters are escaped 

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

1012 filename = filename[1:-1] 

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

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

1015 untracked_files.append(filename) 

1016 finalize_process(proc) 

1017 return untracked_files 

1018 

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

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

1021 

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

1023 

1024 :param paths: 

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

1026 

1027 :return: 

1028 Subset of those paths which are ignored 

1029 """ 

1030 try: 

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

1032 except GitCommandError as err: 

1033 if err.status == 1: 

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

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

1036 return [] 

1037 else: 

1038 # Raise the exception on all other return codes. 

1039 raise 

1040 

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

1042 

1043 @property 

1044 def active_branch(self) -> Head: 

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

1046 

1047 :raise TypeError: 

1048 If HEAD is detached. 

1049 

1050 :return: 

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

1052 """ 

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

1054 return self.head.reference 

1055 

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

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

1058 

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

1060 stream of :class:`BlameEntry` tuples. 

1061 

1062 :param rev: 

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

1064 uncommitted changes. Otherwise, anything successfully parsed by 

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

1066 

1067 :return: 

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

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

1070 the resulting file. 

1071 

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

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

1074 """ 

1075 

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

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

1078 

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

1080 while True: 

1081 try: 

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

1083 line = next(stream) 

1084 except StopIteration: 

1085 return 

1086 split_line = line.split() 

1087 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line 

1088 lineno = int(lineno_b) 

1089 num_lines = int(num_lines_b) 

1090 orig_lineno = int(orig_lineno_b) 

1091 if hexsha not in commits: 

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

1093 # commit. 

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

1095 while True: 

1096 try: 

1097 line = next(stream) 

1098 except StopIteration: 

1099 return 

1100 if line == b"boundary": 

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

1102 # "previous" tag. 

1103 continue 

1104 

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

1106 props[tag] = value 

1107 if tag == b"filename": 

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

1109 orig_filename = value 

1110 break 

1111 

1112 c = Commit( 

1113 self, 

1114 hex_to_bin(hexsha), 

1115 author=Actor( 

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

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

1118 ), 

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

1120 committer=Actor( 

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

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

1123 ), 

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

1125 ) 

1126 commits[hexsha] = c 

1127 else: 

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

1129 # the last line. 

1130 while True: 

1131 try: 

1132 # Will fail if we reach the EOF unexpectedly. 

1133 line = next(stream) 

1134 except StopIteration: 

1135 return 

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

1137 if tag == b"filename": 

1138 orig_filename = value 

1139 break 

1140 

1141 yield BlameEntry( 

1142 commits[hexsha], 

1143 range(lineno, lineno + num_lines), 

1144 safe_decode(orig_filename), 

1145 range(orig_lineno, orig_lineno + num_lines), 

1146 ) 

1147 

1148 def blame( 

1149 self, 

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

1151 file: str, 

1152 incremental: bool = False, 

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

1154 **kwargs: Any, 

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

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

1157 

1158 :param rev: 

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

1160 uncommitted changes. Otherwise, anything successfully parsed by 

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

1162 

1163 :return: 

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

1165 

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

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

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

1169 appearance. 

1170 """ 

1171 if incremental: 

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

1173 rev_opts = rev_opts or [] 

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

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

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

1177 

1178 class InfoTD(TypedDict, total=False): 

1179 sha: str 

1180 id: str 

1181 filename: str 

1182 summary: str 

1183 author: str 

1184 author_email: str 

1185 author_date: int 

1186 committer: str 

1187 committer_email: str 

1188 committer_date: int 

1189 

1190 info: InfoTD = {} 

1191 

1192 keepends = True 

1193 for line_bytes in data.splitlines(keepends): 

1194 try: 

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

1196 except UnicodeDecodeError: 

1197 firstpart = "" 

1198 parts = [] 

1199 is_binary = True 

1200 else: 

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

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

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

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

1205 # instead. 

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

1207 firstpart = parts[0] 

1208 is_binary = False 

1209 # END handle decode of line 

1210 

1211 if self.re_hexsha_only.search(firstpart): 

1212 # handles 

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

1214 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates 

1215 # another line of blame with the same data 

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

1217 if len(digits) == 3: 

1218 info = {"id": firstpart} 

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

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

1221 info = {"id": firstpart} 

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

1223 # END blame data initialization 

1224 else: 

1225 m = self.re_author_committer_start.search(firstpart) 

1226 if m: 

1227 # handles: 

1228 # author Tom Preston-Werner 

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

1230 # author-time 1192271832 

1231 # author-tz -0700 

1232 # committer Tom Preston-Werner 

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

1234 # committer-time 1192271832 

1235 # committer-tz -0700 - IGNORED BY US 

1236 role = m.group(0) 

1237 if role == "author": 

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

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

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

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

1242 elif role == firstpart: 

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

1244 elif role == "committer": 

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

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

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

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

1249 elif role == firstpart: 

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

1251 # END distinguish mail,time,name 

1252 else: 

1253 # handle 

1254 # filename lib/grit.rb 

1255 # summary add Blob 

1256 # <and rest> 

1257 if firstpart.startswith("filename"): 

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

1259 elif firstpart.startswith("summary"): 

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

1261 elif firstpart == "": 

1262 if info: 

1263 sha = info["id"] 

1264 c = commits.get(sha) 

1265 if c is None: 

1266 c = Commit( 

1267 self, 

1268 hex_to_bin(sha), 

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

1270 authored_date=info["author_date"], 

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

1272 committed_date=info["committer_date"], 

1273 ) 

1274 commits[sha] = c 

1275 blames[-1][0] = c 

1276 # END if commit objects needs initial creation 

1277 

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

1279 line: str | bytes 

1280 if not is_binary: 

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

1282 line_str = line_str[1:] 

1283 line = line_str 

1284 else: 

1285 line = line_bytes 

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

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

1288 # along the newline separator. We will append this 

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

1290 # though it should be concatenated with the last 

1291 # line we have seen. 

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

1293 

1294 info = {"id": sha} 

1295 # END if we collected commit info 

1296 # END distinguish filename,summary,rest 

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

1298 # END distinguish hexsha vs other information 

1299 return blames 

1300 

1301 @classmethod 

1302 def init( 

1303 cls, 

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

1305 mkdir: bool = True, 

1306 odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, 

1307 expand_vars: bool = True, 

1308 **kwargs: Any, 

1309 ) -> "Repo": 

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

1311 

1312 :param path: 

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

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

1315 working directory. 

1316 

1317 :param mkdir: 

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

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

1320 Only effective if a path is explicitly given. 

1321 

1322 :param odbt: 

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

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

1325 used to access all object data. 

1326 

1327 :param expand_vars: 

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

1329 information disclosure, allowing attackers to access the contents of 

1330 environment variables. 

1331 

1332 :param kwargs: 

1333 Keyword arguments serving as additional options to the 

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

1335 

1336 :return: 

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

1338 """ 

1339 if path: 

1340 path = expand_path(path, expand_vars) 

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

1342 os.makedirs(path, 0o755) 

1343 

1344 # git command automatically chdir into the directory 

1345 git = cls.GitCommandWrapperType(path) 

1346 git.init(**kwargs) 

1347 return cls(path, odbt=odbt) 

1348 

1349 @classmethod 

1350 def _clone( 

1351 cls, 

1352 git: "Git", 

1353 url: PathLike, 

1354 path: PathLike, 

1355 odb_default_type: Type[GitCmdObjectDB], 

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

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

1358 allow_unsafe_protocols: bool = False, 

1359 allow_unsafe_options: bool = False, 

1360 **kwargs: Any, 

1361 ) -> "Repo": 

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

1363 

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

1365 if not isinstance(path, str): 

1366 path = str(path) 

1367 

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

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

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

1371 # becomes:: 

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

1373 # 

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

1375 sep_dir = kwargs.get("separate_git_dir") 

1376 if sep_dir: 

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

1378 multi = None 

1379 if multi_options: 

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

1381 

1382 if not allow_unsafe_protocols: 

1383 Git.check_unsafe_protocols(str(url)) 

1384 if not allow_unsafe_options: 

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

1386 if not allow_unsafe_options and multi_options: 

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

1388 

1389 proc = git.clone( 

1390 multi, 

1391 "--", 

1392 Git.polish_url(str(url)), 

1393 clone_path, 

1394 with_extended_output=True, 

1395 as_process=True, 

1396 v=True, 

1397 universal_newlines=True, 

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

1399 ) 

1400 if progress: 

1401 handle_process_output( 

1402 proc, 

1403 None, 

1404 to_progress_instance(progress).new_message_handler(), 

1405 finalize_process, 

1406 decode_streams=False, 

1407 ) 

1408 else: 

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

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

1411 cmdline = remove_password_if_present(cmdline) 

1412 

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

1414 finalize_process(proc, stderr=stderr) 

1415 

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

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

1418 if not osp.isabs(path): 

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

1420 

1421 repo = cls(path, odbt=odbt) 

1422 

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

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

1425 

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

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

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

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

1430 if repo.remotes: 

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

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

1433 # END handle remote repo 

1434 return repo 

1435 

1436 def clone( 

1437 self, 

1438 path: PathLike, 

1439 progress: Optional[CallableProgress] = None, 

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

1441 allow_unsafe_protocols: bool = False, 

1442 allow_unsafe_options: bool = False, 

1443 **kwargs: Any, 

1444 ) -> "Repo": 

1445 """Create a clone from this repository. 

1446 

1447 :param path: 

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

1449 

1450 :param progress: 

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

1452 

1453 :param multi_options: 

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

1455 times. 

1456 

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

1458 For example:: 

1459 

1460 [ 

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

1462 "--config core.ignorecase", 

1463 "--recurse-submodule=repo1_path", 

1464 "--recurse-submodule=repo2_path", 

1465 ] 

1466 

1467 :param allow_unsafe_protocols: 

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

1469 

1470 :param allow_unsafe_options: 

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

1472 

1473 :param kwargs: 

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

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

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

1477 command. 

1478 

1479 :return: 

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

1481 """ 

1482 return self._clone( 

1483 self.git, 

1484 self.common_dir, 

1485 path, 

1486 type(self.odb), 

1487 progress, 

1488 multi_options, 

1489 allow_unsafe_protocols=allow_unsafe_protocols, 

1490 allow_unsafe_options=allow_unsafe_options, 

1491 **kwargs, 

1492 ) 

1493 

1494 @classmethod 

1495 def clone_from( 

1496 cls, 

1497 url: PathLike, 

1498 to_path: PathLike, 

1499 progress: CallableProgress = None, 

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

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

1502 allow_unsafe_protocols: bool = False, 

1503 allow_unsafe_options: bool = False, 

1504 **kwargs: Any, 

1505 ) -> "Repo": 

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

1507 

1508 :param url: 

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

1510 

1511 :param to_path: 

1512 Path to which the repository should be cloned to. 

1513 

1514 :param progress: 

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

1516 

1517 :param env: 

1518 Optional dictionary containing the desired environment variables. 

1519 

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

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

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

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

1524 

1525 :param multi_options: 

1526 See the :meth:`clone` method. 

1527 

1528 :param allow_unsafe_protocols: 

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

1530 

1531 :param allow_unsafe_options: 

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

1533 

1534 :param kwargs: 

1535 See the :meth:`clone` method. 

1536 

1537 :return: 

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

1539 """ 

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

1541 if env is not None: 

1542 git.update_environment(**env) 

1543 return cls._clone( 

1544 git, 

1545 url, 

1546 to_path, 

1547 GitCmdObjectDB, 

1548 progress, 

1549 multi_options, 

1550 allow_unsafe_protocols=allow_unsafe_protocols, 

1551 allow_unsafe_options=allow_unsafe_options, 

1552 **kwargs, 

1553 ) 

1554 

1555 def archive( 

1556 self, 

1557 ostream: Union[TextIO, BinaryIO], 

1558 treeish: Optional[str] = None, 

1559 prefix: Optional[str] = None, 

1560 **kwargs: Any, 

1561 ) -> Repo: 

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

1563 

1564 :param ostream: 

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

1566 

1567 :param treeish: 

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

1569 

1570 :param prefix: 

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

1572 

1573 :param kwargs: 

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

1575 

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

1577 ostreams to write any format supported by Python. 

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

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

1580 or a list or tuple of multiple paths. 

1581 

1582 :raise git.exc.GitCommandError: 

1583 If something went wrong. 

1584 

1585 :return: 

1586 self 

1587 """ 

1588 if treeish is None: 

1589 treeish = self.head.commit 

1590 if prefix and "prefix" not in kwargs: 

1591 kwargs["prefix"] = prefix 

1592 kwargs["output_stream"] = ostream 

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

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

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

1596 path = [path] 

1597 # END ensure paths is list (or tuple) 

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

1599 return self 

1600 

1601 def has_separate_working_tree(self) -> bool: 

1602 """ 

1603 :return: 

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

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

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

1607 to. 

1608 

1609 :note: 

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

1611 """ 

1612 if self.bare: 

1613 return False 

1614 if self.working_tree_dir: 

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

1616 else: 

1617 return False # Or raise Error? 

1618 

1619 rev_parse = rev_parse 

1620 

1621 def __repr__(self) -> str: 

1622 clazz = self.__class__ 

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

1624 

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

1626 """ 

1627 :return: 

1628 The commit which is currently being replayed while rebasing. 

1629 

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

1631 """ 

1632 if self.git_dir: 

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

1634 if not osp.isfile(rebase_head_file): 

1635 return None 

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

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

1638 return self.commit(content)