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

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

579 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 branches(self) -> "IterableList[Head]": 

407 """Alias for heads. 

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

409 in this repo. 

410 

411 :return: 

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

413 """ 

414 return self.heads 

415 

416 @property 

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

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

419 heads and remote references. 

420 

421 :return: 

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

423 """ 

424 return Reference.list_items(self) 

425 

426 @property 

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

428 """Alias for references. 

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

430 heads and remote references. 

431 

432 :return: 

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

434 """ 

435 return self.references 

436 

437 @property 

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

439 """ 

440 :return: 

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

442 

443 :note: 

444 This property can be expensive, as the returned 

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

446 It is recommended to reuse the object. 

447 """ 

448 return IndexFile(self) 

449 

450 @property 

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

452 """ 

453 :return: 

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

455 """ 

456 return HEAD(self, "HEAD") 

457 

458 @property 

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

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

461 manipulate remotes. 

462 

463 :return: 

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

465 """ 

466 return Remote.list_items(self) 

467 

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

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

470 

471 :raise ValueError: 

472 If no remote with such a name exists. 

473 """ 

474 r = Remote(self, name) 

475 if not r.exists(): 

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

477 return r 

478 

479 # { Submodules 

480 

481 @property 

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

483 """ 

484 :return: 

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

486 current head 

487 """ 

488 return Submodule.list_items(self) 

489 

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

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

492 

493 :raise ValueError: 

494 If no such submodule exists. 

495 """ 

496 try: 

497 return self.submodules[name] 

498 except IndexError as e: 

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

500 # END exception handling 

501 

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

503 """Create a new submodule. 

504 

505 :note: 

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

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

508 

509 :return: 

510 The created submodule. 

511 """ 

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

513 

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

515 """An iterator yielding Submodule instances. 

516 

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

518 and `kwargs`. 

519 

520 :return: 

521 Iterator 

522 """ 

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

524 

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

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

527 take the previous state into consideration. 

528 

529 :note: 

530 For more information, please see the documentation of 

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

532 """ 

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

534 

535 # }END submodules 

536 

537 @property 

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

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

540 this repo. 

541 

542 :return: 

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

544 """ 

545 return TagReference.list_items(self) 

546 

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

548 """ 

549 :return: 

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

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

552 

553 :param path: 

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

555 """ 

556 full_path = self._to_full_tag_path(path) 

557 return TagReference(self, full_path) 

558 

559 @staticmethod 

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

561 path_str = str(path) 

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

563 return path_str 

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

565 return Reference._common_path_default + "/" + path_str 

566 else: 

567 return TagReference._common_path_default + "/" + path_str 

568 

569 def create_head( 

570 self, 

571 path: PathLike, 

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

573 force: bool = False, 

574 logmsg: Optional[str] = None, 

575 ) -> "Head": 

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

577 

578 :note: 

579 For more documentation, please see the 

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

581 

582 :return: 

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

584 """ 

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

586 

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

588 """Delete the given heads. 

589 

590 :param kwargs: 

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

592 """ 

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

594 

595 def create_tag( 

596 self, 

597 path: PathLike, 

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

599 message: Optional[str] = None, 

600 force: bool = False, 

601 **kwargs: Any, 

602 ) -> TagReference: 

603 """Create a new tag reference. 

604 

605 :note: 

606 For more documentation, please see the 

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

608 

609 :return: 

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

611 """ 

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

613 

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

615 """Delete the given tag references.""" 

616 return TagReference.delete(self, *tags) 

617 

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

619 """Create a new remote. 

620 

621 For more information, please see the documentation of the 

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

623 

624 :return: 

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

626 """ 

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

628 

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

630 """Delete the given remote.""" 

631 return Remote.remove(self, remote) 

632 

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

634 if git_dir is None: 

635 git_dir = self.git_dir 

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

637 # Use the global config instead. 

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

639 config_level = "global" 

640 

641 if config_level == "system": 

642 return "/etc/gitconfig" 

643 elif config_level == "user": 

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

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

646 elif config_level == "global": 

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

648 elif config_level == "repository": 

649 repo_dir = self._common_dir or git_dir 

650 if not repo_dir: 

651 raise NotADirectoryError 

652 else: 

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

654 else: 

655 assert_never( # type: ignore[unreachable] 

656 config_level, 

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

658 ) 

659 

660 def config_reader( 

661 self, 

662 config_level: Optional[Lit_config_levels] = None, 

663 ) -> GitConfigParser: 

664 """ 

665 :return: 

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

667 configuration, but not to write it. 

668 

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

670 configuration files. 

671 

672 :param config_level: 

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

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

675 you wish to read to prevent reading multiple files. 

676 

677 :note: 

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

679 unknown, instead the global path will be used. 

680 """ 

681 return self._config_reader(config_level=config_level) 

682 

683 def _config_reader( 

684 self, 

685 config_level: Optional[Lit_config_levels] = None, 

686 git_dir: Optional[PathLike] = None, 

687 ) -> GitConfigParser: 

688 if config_level is None: 

689 files = [ 

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

691 for f in self.config_level 

692 if cast(Lit_config_levels, f) 

693 ] 

694 else: 

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

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

697 

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

699 """ 

700 :return: 

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

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

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

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

705 

706 :param config_level: 

707 One of the following values: 

708 

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

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

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

712 """ 

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

714 

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

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

717 

718 :param rev: 

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

720 

721 :return: 

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

723 """ 

724 if rev is None: 

725 return self.head.commit 

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

727 

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

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

730 

731 :note: 

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

733 """ 

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

735 

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

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

738 

739 Examples:: 

740 

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

742 

743 :param rev: 

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

745 

746 :return: 

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

748 

749 :note: 

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

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

752 subsequent operations might have unexpected results. 

753 """ 

754 if rev is None: 

755 return self.head.commit.tree 

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

757 

758 def iter_commits( 

759 self, 

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

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

762 **kwargs: Any, 

763 ) -> Iterator[Commit]: 

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

765 history of a given ref/commit. 

766 

767 :param rev: 

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

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

770 

771 :param paths: 

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

773 path or paths will be returned. 

774 

775 :param kwargs: 

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

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

778 

779 :note: 

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

781 ``"revA...revB"`` revision specifier. 

782 

783 :return: 

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

785 """ 

786 if rev is None: 

787 rev = self.head.commit 

788 

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

790 

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

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

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

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

795 

796 :param rev: 

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

798 

799 :param kwargs: 

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

801 which does all the work. 

802 

803 :return: 

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

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

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

807 exists. 

808 

809 :raise ValueError: 

810 If fewer than two revisions are provided. 

811 """ 

812 if len(rev) < 2: 

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

814 # END handle input 

815 

816 res: List[Commit] = [] 

817 try: 

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

819 except GitCommandError as err: 

820 if err.status == 128: 

821 raise 

822 # END handle invalid rev 

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

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

825 return res 

826 # END exception handling 

827 

828 for line in lines: 

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

830 # END for each merge-base 

831 

832 return res 

833 

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

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

836 

837 :param ancestor_rev: 

838 Rev which should be an ancestor. 

839 

840 :param rev: 

841 Rev to test against `ancestor_rev`. 

842 

843 :return: 

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

845 """ 

846 try: 

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

848 except GitCommandError as err: 

849 if err.status == 1: 

850 return False 

851 raise 

852 return True 

853 

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

855 try: 

856 complete_sha = self.odb.partial_to_complete_sha_hex(sha) 

857 object_info = self.odb.info(complete_sha) 

858 if object_type: 

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

860 return True 

861 else: 

862 _logger.debug( 

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

864 object_info.type.decode(), 

865 object_type, 

866 ) 

867 return False 

868 else: 

869 return True 

870 except BadObject: 

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

872 return False 

873 

874 def _get_daemon_export(self) -> bool: 

875 if self.git_dir: 

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

877 return osp.exists(filename) 

878 

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

880 if self.git_dir: 

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

882 fileexists = osp.exists(filename) 

883 if value and not fileexists: 

884 touch(filename) 

885 elif not value and fileexists: 

886 os.unlink(filename) 

887 

888 daemon_export = property( 

889 _get_daemon_export, 

890 _set_daemon_export, 

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

892 ) 

893 del _get_daemon_export 

894 del _set_daemon_export 

895 

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

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

898 

899 :return: 

900 List of strings being pathnames of alternates 

901 """ 

902 if self.git_dir: 

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

904 

905 if osp.exists(alternates_path): 

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

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

908 return alts.strip().splitlines() 

909 return [] 

910 

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

912 """Set the alternates. 

913 

914 :param alts: 

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

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

917 

918 :raise git.exc.NoSuchPathError: 

919 

920 :note: 

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

922 caller is responsible. 

923 """ 

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

925 if not alts: 

926 if osp.isfile(alternates_path): 

927 os.remove(alternates_path) 

928 else: 

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

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

931 

932 alternates = property( 

933 _get_alternates, 

934 _set_alternates, 

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

936 ) 

937 

938 def is_dirty( 

939 self, 

940 index: bool = True, 

941 working_tree: bool = True, 

942 untracked_files: bool = False, 

943 submodules: bool = True, 

944 path: Optional[PathLike] = None, 

945 ) -> bool: 

946 """ 

947 :return: 

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

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

950 if the index or the working copy have changes. 

951 """ 

952 if self._bare: 

953 # Bare repositories with no associated working directory are 

954 # always considered to be clean. 

955 return False 

956 

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

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

959 if not submodules: 

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

961 if path: 

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

963 if index: 

964 # diff index against HEAD. 

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

966 return True 

967 # END index handling 

968 if working_tree: 

969 # diff index against working tree. 

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

971 return True 

972 # END working tree handling 

973 if untracked_files: 

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

975 return True 

976 # END untracked files 

977 return False 

978 

979 @property 

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

981 """ 

982 :return: 

983 list(str,...) 

984 

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

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

987 

988 :note: 

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

990 

991 :note: 

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

993 please consider caching it yourself. 

994 """ 

995 return self._get_untracked_files() 

996 

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

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

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

1000 # Untracked files prefix in porcelain mode 

1001 prefix = "?? " 

1002 untracked_files = [] 

1003 for line in proc.stdout: 

1004 line = line.decode(defenc) 

1005 if not line.startswith(prefix): 

1006 continue 

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

1008 # Special characters are escaped 

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

1010 filename = filename[1:-1] 

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

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

1013 untracked_files.append(filename) 

1014 finalize_process(proc) 

1015 return untracked_files 

1016 

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

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

1019 

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

1021 

1022 :param paths: 

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

1024 

1025 :return: 

1026 Subset of those paths which are ignored 

1027 """ 

1028 try: 

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

1030 except GitCommandError as err: 

1031 if err.status == 1: 

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

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

1034 return [] 

1035 else: 

1036 # Raise the exception on all other return codes. 

1037 raise 

1038 

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

1040 

1041 @property 

1042 def active_branch(self) -> Head: 

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

1044 

1045 :raise TypeError: 

1046 If HEAD is detached. 

1047 

1048 :return: 

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

1050 """ 

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

1052 return self.head.reference 

1053 

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

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

1056 

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

1058 stream of :class:`BlameEntry` tuples. 

1059 

1060 :param rev: 

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

1062 uncommitted changes. Otherwise, anything successfully parsed by 

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

1064 

1065 :return: 

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

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

1068 the resulting file. 

1069 

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

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

1072 """ 

1073 

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

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

1076 

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

1078 while True: 

1079 try: 

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

1081 line = next(stream) 

1082 except StopIteration: 

1083 return 

1084 split_line = line.split() 

1085 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line 

1086 lineno = int(lineno_b) 

1087 num_lines = int(num_lines_b) 

1088 orig_lineno = int(orig_lineno_b) 

1089 if hexsha not in commits: 

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

1091 # commit. 

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

1093 while True: 

1094 try: 

1095 line = next(stream) 

1096 except StopIteration: 

1097 return 

1098 if line == b"boundary": 

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

1100 # "previous" tag. 

1101 continue 

1102 

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

1104 props[tag] = value 

1105 if tag == b"filename": 

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

1107 orig_filename = value 

1108 break 

1109 

1110 c = Commit( 

1111 self, 

1112 hex_to_bin(hexsha), 

1113 author=Actor( 

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

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

1116 ), 

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

1118 committer=Actor( 

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

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

1121 ), 

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

1123 ) 

1124 commits[hexsha] = c 

1125 else: 

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

1127 # the last line. 

1128 while True: 

1129 try: 

1130 # Will fail if we reach the EOF unexpectedly. 

1131 line = next(stream) 

1132 except StopIteration: 

1133 return 

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

1135 if tag == b"filename": 

1136 orig_filename = value 

1137 break 

1138 

1139 yield BlameEntry( 

1140 commits[hexsha], 

1141 range(lineno, lineno + num_lines), 

1142 safe_decode(orig_filename), 

1143 range(orig_lineno, orig_lineno + num_lines), 

1144 ) 

1145 

1146 def blame( 

1147 self, 

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

1149 file: str, 

1150 incremental: bool = False, 

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

1152 **kwargs: Any, 

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

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

1155 

1156 :param rev: 

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

1158 uncommitted changes. Otherwise, anything successfully parsed by 

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

1160 

1161 :return: 

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

1163 

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

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

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

1167 appearance. 

1168 """ 

1169 if incremental: 

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

1171 rev_opts = rev_opts or [] 

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

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

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

1175 

1176 class InfoTD(TypedDict, total=False): 

1177 sha: str 

1178 id: str 

1179 filename: str 

1180 summary: str 

1181 author: str 

1182 author_email: str 

1183 author_date: int 

1184 committer: str 

1185 committer_email: str 

1186 committer_date: int 

1187 

1188 info: InfoTD = {} 

1189 

1190 keepends = True 

1191 for line_bytes in data.splitlines(keepends): 

1192 try: 

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

1194 except UnicodeDecodeError: 

1195 firstpart = "" 

1196 parts = [] 

1197 is_binary = True 

1198 else: 

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

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

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

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

1203 # instead. 

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

1205 firstpart = parts[0] 

1206 is_binary = False 

1207 # END handle decode of line 

1208 

1209 if self.re_hexsha_only.search(firstpart): 

1210 # handles 

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

1212 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates 

1213 # another line of blame with the same data 

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

1215 if len(digits) == 3: 

1216 info = {"id": firstpart} 

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

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

1219 info = {"id": firstpart} 

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

1221 # END blame data initialization 

1222 else: 

1223 m = self.re_author_committer_start.search(firstpart) 

1224 if m: 

1225 # handles: 

1226 # author Tom Preston-Werner 

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

1228 # author-time 1192271832 

1229 # author-tz -0700 

1230 # committer Tom Preston-Werner 

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

1232 # committer-time 1192271832 

1233 # committer-tz -0700 - IGNORED BY US 

1234 role = m.group(0) 

1235 if role == "author": 

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

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

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

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

1240 elif role == firstpart: 

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

1242 elif role == "committer": 

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

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

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

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

1247 elif role == firstpart: 

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

1249 # END distinguish mail,time,name 

1250 else: 

1251 # handle 

1252 # filename lib/grit.rb 

1253 # summary add Blob 

1254 # <and rest> 

1255 if firstpart.startswith("filename"): 

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

1257 elif firstpart.startswith("summary"): 

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

1259 elif firstpart == "": 

1260 if info: 

1261 sha = info["id"] 

1262 c = commits.get(sha) 

1263 if c is None: 

1264 c = Commit( 

1265 self, 

1266 hex_to_bin(sha), 

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

1268 authored_date=info["author_date"], 

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

1270 committed_date=info["committer_date"], 

1271 ) 

1272 commits[sha] = c 

1273 blames[-1][0] = c 

1274 # END if commit objects needs initial creation 

1275 

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

1277 line: str | bytes 

1278 if not is_binary: 

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

1280 line_str = line_str[1:] 

1281 line = line_str 

1282 else: 

1283 line = line_bytes 

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

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

1286 # along the newline separator. We will append this 

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

1288 # though it should be concatenated with the last 

1289 # line we have seen. 

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

1291 

1292 info = {"id": sha} 

1293 # END if we collected commit info 

1294 # END distinguish filename,summary,rest 

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

1296 # END distinguish hexsha vs other information 

1297 return blames 

1298 

1299 @classmethod 

1300 def init( 

1301 cls, 

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

1303 mkdir: bool = True, 

1304 odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, 

1305 expand_vars: bool = True, 

1306 **kwargs: Any, 

1307 ) -> "Repo": 

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

1309 

1310 :param path: 

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

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

1313 working directory. 

1314 

1315 :param mkdir: 

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

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

1318 Only effective if a path is explicitly given. 

1319 

1320 :param odbt: 

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

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

1323 used to access all object data. 

1324 

1325 :param expand_vars: 

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

1327 information disclosure, allowing attackers to access the contents of 

1328 environment variables. 

1329 

1330 :param kwargs: 

1331 Keyword arguments serving as additional options to the 

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

1333 

1334 :return: 

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

1336 """ 

1337 if path: 

1338 path = expand_path(path, expand_vars) 

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

1340 os.makedirs(path, 0o755) 

1341 

1342 # git command automatically chdir into the directory 

1343 git = cls.GitCommandWrapperType(path) 

1344 git.init(**kwargs) 

1345 return cls(path, odbt=odbt) 

1346 

1347 @classmethod 

1348 def _clone( 

1349 cls, 

1350 git: "Git", 

1351 url: PathLike, 

1352 path: PathLike, 

1353 odb_default_type: Type[GitCmdObjectDB], 

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

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

1356 allow_unsafe_protocols: bool = False, 

1357 allow_unsafe_options: bool = False, 

1358 **kwargs: Any, 

1359 ) -> "Repo": 

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

1361 

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

1363 if not isinstance(path, str): 

1364 path = str(path) 

1365 

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

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

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

1369 # becomes:: 

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

1371 # 

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

1373 sep_dir = kwargs.get("separate_git_dir") 

1374 if sep_dir: 

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

1376 multi = None 

1377 if multi_options: 

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

1379 

1380 if not allow_unsafe_protocols: 

1381 Git.check_unsafe_protocols(str(url)) 

1382 if not allow_unsafe_options: 

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

1384 if not allow_unsafe_options and multi_options: 

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

1386 

1387 proc = git.clone( 

1388 multi, 

1389 "--", 

1390 Git.polish_url(str(url)), 

1391 clone_path, 

1392 with_extended_output=True, 

1393 as_process=True, 

1394 v=True, 

1395 universal_newlines=True, 

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

1397 ) 

1398 if progress: 

1399 handle_process_output( 

1400 proc, 

1401 None, 

1402 to_progress_instance(progress).new_message_handler(), 

1403 finalize_process, 

1404 decode_streams=False, 

1405 ) 

1406 else: 

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

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

1409 cmdline = remove_password_if_present(cmdline) 

1410 

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

1412 finalize_process(proc, stderr=stderr) 

1413 

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

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

1416 if not osp.isabs(path): 

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

1418 

1419 repo = cls(path, odbt=odbt) 

1420 

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

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

1423 

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

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

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

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

1428 if repo.remotes: 

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

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

1431 # END handle remote repo 

1432 return repo 

1433 

1434 def clone( 

1435 self, 

1436 path: PathLike, 

1437 progress: Optional[CallableProgress] = None, 

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

1439 allow_unsafe_protocols: bool = False, 

1440 allow_unsafe_options: bool = False, 

1441 **kwargs: Any, 

1442 ) -> "Repo": 

1443 """Create a clone from this repository. 

1444 

1445 :param path: 

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

1447 

1448 :param progress: 

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

1450 

1451 :param multi_options: 

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

1453 times. 

1454 

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

1456 For example:: 

1457 

1458 [ 

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

1460 "--config core.ignorecase", 

1461 "--recurse-submodule=repo1_path", 

1462 "--recurse-submodule=repo2_path", 

1463 ] 

1464 

1465 :param allow_unsafe_protocols: 

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

1467 

1468 :param allow_unsafe_options: 

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

1470 

1471 :param kwargs: 

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

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

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

1475 command. 

1476 

1477 :return: 

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

1479 """ 

1480 return self._clone( 

1481 self.git, 

1482 self.common_dir, 

1483 path, 

1484 type(self.odb), 

1485 progress, 

1486 multi_options, 

1487 allow_unsafe_protocols=allow_unsafe_protocols, 

1488 allow_unsafe_options=allow_unsafe_options, 

1489 **kwargs, 

1490 ) 

1491 

1492 @classmethod 

1493 def clone_from( 

1494 cls, 

1495 url: PathLike, 

1496 to_path: PathLike, 

1497 progress: CallableProgress = None, 

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

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

1500 allow_unsafe_protocols: bool = False, 

1501 allow_unsafe_options: bool = False, 

1502 **kwargs: Any, 

1503 ) -> "Repo": 

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

1505 

1506 :param url: 

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

1508 

1509 :param to_path: 

1510 Path to which the repository should be cloned to. 

1511 

1512 :param progress: 

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

1514 

1515 :param env: 

1516 Optional dictionary containing the desired environment variables. 

1517 

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

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

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

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

1522 

1523 :param multi_options: 

1524 See the :meth:`clone` method. 

1525 

1526 :param allow_unsafe_protocols: 

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

1528 

1529 :param allow_unsafe_options: 

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

1531 

1532 :param kwargs: 

1533 See the :meth:`clone` method. 

1534 

1535 :return: 

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

1537 """ 

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

1539 if env is not None: 

1540 git.update_environment(**env) 

1541 return cls._clone( 

1542 git, 

1543 url, 

1544 to_path, 

1545 GitCmdObjectDB, 

1546 progress, 

1547 multi_options, 

1548 allow_unsafe_protocols=allow_unsafe_protocols, 

1549 allow_unsafe_options=allow_unsafe_options, 

1550 **kwargs, 

1551 ) 

1552 

1553 def archive( 

1554 self, 

1555 ostream: Union[TextIO, BinaryIO], 

1556 treeish: Optional[str] = None, 

1557 prefix: Optional[str] = None, 

1558 **kwargs: Any, 

1559 ) -> Repo: 

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

1561 

1562 :param ostream: 

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

1564 

1565 :param treeish: 

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

1567 

1568 :param prefix: 

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

1570 

1571 :param kwargs: 

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

1573 

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

1575 ostreams to write any format supported by Python. 

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

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

1578 or a list or tuple of multiple paths. 

1579 

1580 :raise git.exc.GitCommandError: 

1581 If something went wrong. 

1582 

1583 :return: 

1584 self 

1585 """ 

1586 if treeish is None: 

1587 treeish = self.head.commit 

1588 if prefix and "prefix" not in kwargs: 

1589 kwargs["prefix"] = prefix 

1590 kwargs["output_stream"] = ostream 

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

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

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

1594 path = [path] 

1595 # END ensure paths is list (or tuple) 

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

1597 return self 

1598 

1599 def has_separate_working_tree(self) -> bool: 

1600 """ 

1601 :return: 

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

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

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

1605 to. 

1606 

1607 :note: 

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

1609 """ 

1610 if self.bare: 

1611 return False 

1612 if self.working_tree_dir: 

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

1614 else: 

1615 return False # Or raise Error? 

1616 

1617 rev_parse = rev_parse 

1618 

1619 def __repr__(self) -> str: 

1620 clazz = self.__class__ 

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

1622 

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

1624 """ 

1625 :return: 

1626 The commit which is currently being replayed while rebasing. 

1627 

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

1629 """ 

1630 if self.git_dir: 

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

1632 if not osp.isfile(rebase_head_file): 

1633 return None 

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

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

1636 return self.commit(content)