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

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

655 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 

21import gitdb.util 

22from gitdb.db.loose import LooseObjectDB 

23from gitdb.exc import BadObject 

24 

25from git.cmd import Git, handle_process_output 

26from git.compat import defenc, safe_decode 

27from git.config import GitConfigParser 

28from git.db import GitCmdObjectDB 

29from git.exc import ( 

30 GitCommandError, 

31 InvalidGitRepositoryError, 

32 NoSuchPathError, 

33) 

34from git.index import IndexFile 

35from git.objects import Submodule, RootModule, Commit 

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

37from git.remote import Remote, _T_RemoteName, add_progress, to_progress_instance 

38from git.util import ( 

39 Actor, 

40 cygpath, 

41 expand_path, 

42 finalize_process, 

43 hex_to_bin, 

44 remove_password_if_present, 

45) 

46 

47from .fun import ( 

48 find_submodule_git_dir, 

49 is_git_dir, 

50 rev_parse, 

51 to_commit, 

52 touch, 

53) 

54 

55# typing ------------------------------------------------------ 

56 

57from git.types import ( 

58 CallableProgress, 

59 Commit_ish, 

60 Lit_config_levels, 

61 PathLike, 

62 TBD, 

63 Tree_ish, 

64 assert_never, 

65) 

66from typing import ( 

67 Any, 

68 BinaryIO, 

69 Callable, 

70 Dict, 

71 Iterator, 

72 List, 

73 Mapping, 

74 NamedTuple, 

75 Optional, 

76 Sequence, 

77 TYPE_CHECKING, 

78 TextIO, 

79 Tuple, 

80 Type, 

81 Union, 

82 cast, 

83) 

84 

85from git.types import ConfigLevels_Tup, TypedDict 

86 

87if TYPE_CHECKING: 

88 from git.objects import Tree 

89 from git.objects.submodule.base import UpdateProgress 

90 from git.refs.symbolic import SymbolicReference 

91 from git.remote import RemoteProgress 

92 from git.util import IterableList 

93 

94# ----------------------------------------------------------- 

95 

96_logger = logging.getLogger(__name__) 

97 

98 

99class BlameEntry(NamedTuple): 

100 commit: Commit 

101 linenos: range 

102 orig_path: Optional[str] 

103 orig_linenos: range 

104 

105 

106class Repo: 

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

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

109 

110 The following attributes are worth using: 

111 

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

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

114 repositories. 

115 

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

117 if we are a bare repository. 

118 

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

120 """ 

121 

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

123 

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

125 git = cast("Git", None) 

126 

127 working_dir: PathLike 

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

129 

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

131 _working_tree_dir: Optional[PathLike] = None 

132 

133 git_dir: PathLike 

134 """The ``.git`` repository directory.""" 

135 

136 _common_dir: PathLike = "" 

137 

138 # Precompiled regex 

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

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

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

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

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

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

145 

146 unsafe_git_init_options = [ 

147 # Can install hooks that execute during later Git commands: 

148 "--template", 

149 # Redirects the repository metadata to a caller-controlled path: 

150 "--separate-git-dir", 

151 ] 

152 """Options to :manpage:`git-init(1)` that permit unsafe code execution or I/O.""" 

153 

154 unsafe_git_clone_options = [ 

155 # Executes arbitrary commands: 

156 "--upload-pack", 

157 "-u", 

158 # Can override configuration variables that execute arbitrary commands: 

159 "--config", 

160 "-c", 

161 # Can install hooks that execute during clone: 

162 "--template", 

163 # Redirects the repository metadata to a caller-controlled path: 

164 "--separate-git-dir", 

165 # Fetches from an additional caller-controlled URI: 

166 "--bundle-uri", 

167 ] 

168 """Options to :manpage:`git-clone(1)` that permit unsafe command execution or I/O. 

169 

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

171 directly: 

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

173 

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

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

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

177 

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

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

180 

181 The ``--bundle-uri`` option fetches from an additional URI before fetching from the 

182 clone URL. An untrusted value can therefore make Git access local files or 

183 unintended network resources: 

184 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---bundle-uriuri 

185 """ 

186 

187 unsafe_git_archive_options = [ 

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

189 "--exec", 

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

191 "--output", 

192 "-o", 

193 # Reads from a caller-controlled filesystem path: 

194 "--add-file", 

195 # Injects a caller-controlled path and contents: 

196 "--add-virtual-file", 

197 ] 

198 

199 unsafe_git_revision_options = [ 

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

201 "--output", 

202 "-o", 

203 ] 

204 

205 unsafe_git_blame_options = unsafe_git_revision_options + [ 

206 # These options read from arbitrary files and expose their contents through blame output. 

207 "--contents", 

208 "-S", 

209 "--ignore-revs-file", 

210 ] 

211 

212 unsafe_git_diff_options = unsafe_git_revision_options + [ 

213 # Treats path operands as arbitrary filesystem paths. 

214 "--no-index", 

215 # Reads caller-controlled order patterns from an arbitrary file. 

216 "-O", 

217 "--orderfile", 

218 ] 

219 

220 # Invariants 

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

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

223 

224 # Subclass configuration 

225 GitCommandWrapperType = Git 

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

227 type here.""" 

228 

229 def __init__( 

230 self, 

231 path: Optional[PathLike] = None, 

232 odbt: Type[Union[LooseObjectDB, gitdb.GitDB]] = GitCmdObjectDB, 

233 search_parent_directories: bool = False, 

234 expand_vars: bool = True, 

235 ) -> None: 

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

237 

238 .. note:: 

239 Repositories using reftable may be opened, but GitPython's direct reference 

240 access does not support reftable. 

241 

242 :param path: 

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

244 

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

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

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

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

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

250 

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

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

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

254 used. 

255 

256 :param odbt: 

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

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

259 used to access all object data. The pure-Python ``GitDB`` backend is 

260 deprecated due to security and performance issues. Use the default 

261 :class:`~git.db.GitCmdObjectDB` instead. 

262 

263 :param search_parent_directories: 

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

265 well. 

266 

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

268 GitPython, which is considered a bug though. 

269 

270 :raise git.exc.InvalidGitRepositoryError: 

271 

272 :raise git.exc.NoSuchPathError: 

273 

274 :return: 

275 :class:`Repo` 

276 """ 

277 

278 git_dir_env = os.getenv("GIT_DIR") 

279 object_dir_env = os.getenv("GIT_OBJECT_DIRECTORY") 

280 if object_dir_env is not None: 

281 object_dir_env = osp.abspath(object_dir_env) 

282 epath = path or git_dir_env 

283 if not epath: 

284 epath = os.getcwd() 

285 epath = os.fspath(epath) 

286 if Git.is_cygwin(): 

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

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

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

290 epath = cygpath(epath) 

291 

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

293 warnings.warn( 

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

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

296 stacklevel=1, 

297 ) 

298 epath = expand_path(epath, expand_vars) 

299 if epath is not None: 

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

301 raise NoSuchPathError(epath) 

302 

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

304 curpath = os.fspath(epath) if epath is not None else "" 

305 git_dir: Optional[str] = None 

306 explicit_git_dir = not path and bool(git_dir_env) 

307 while curpath: 

308 # ABOUT osp.NORMPATH 

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

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

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

312 if not explicit_git_dir: 

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

314 try: 

315 sm_gitpath = find_submodule_git_dir(dotgit) 

316 except OSError: 

317 break 

318 if sm_gitpath is not None: 

319 # Worktrees can use relative paths as of Git 2.48, so join to curpath. 

320 git_dir = osp.normpath(osp.join(curpath, os.fspath(sm_gitpath))) 

321 self._working_tree_dir = curpath 

322 break 

323 

324 # Like Git, do not fall back to a bare repository or parent directory when 

325 # a non-directory .git entry exists but is not a valid gitfile. 

326 if osp.exists(dotgit) and not osp.isdir(dotgit): 

327 break 

328 

329 if is_git_dir(curpath): 

330 git_dir = curpath 

331 if osp.isfile(osp.join(curpath, "gitdir")) and osp.isfile(osp.join(curpath, "commondir")): 

332 if "GIT_WORK_TREE" in os.environ: 

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

334 else: 

335 # Linked worktree administrative directories store the path to 

336 # the worktree's .git file in gitdir (without a "gitdir: " prefix). 

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

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

339 

340 if not osp.isabs(worktree_gitfile): 

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

342 

343 self._working_tree_dir = osp.dirname(worktree_gitfile) 

344 break 

345 

346 # from man git-config : core.worktree 

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

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

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

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

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

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

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

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

355 # working tree. 

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

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

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

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

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

361 if "GIT_WORK_TREE" in os.environ: 

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

363 break 

364 

365 if explicit_git_dir or not search_parent_directories: 

366 break 

367 curpath, tail = osp.split(curpath) 

368 if not tail: 

369 break 

370 # END while curpath 

371 

372 if git_dir is None: 

373 raise InvalidGitRepositoryError(epath) 

374 self.git_dir = git_dir 

375 

376 common_dir_env = os.getenv("GIT_COMMON_DIR") 

377 if common_dir_env is not None: 

378 self._common_dir = osp.abspath(common_dir_env) 

379 else: 

380 try: 

381 common_dir = os.fsdecode((Path(self.git_dir) / "commondir").read_bytes()).rstrip("\r\n") 

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

383 except OSError: 

384 self._common_dir = "" 

385 

386 self._bare = False 

387 try: 

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

389 except Exception: 

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

391 pass 

392 

393 # A linked worktree is not bare even when its main repository is. 

394 if self._bare and self._working_tree_dir and osp.isfile(osp.join(self.git_dir, "commondir")): 

395 self._bare = False 

396 

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

398 # that in the first place. 

399 if self._bare: 

400 self._working_tree_dir = None 

401 # END working dir handling 

402 

403 self.working_dir = self._working_tree_dir or self.common_dir 

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

405 if common_dir_env is not None: 

406 self.git.update_environment(GIT_DIR=os.fspath(self.git_dir), GIT_COMMON_DIR=os.fspath(self.common_dir)) 

407 elif git_dir_env is not None: 

408 self.git.update_environment(GIT_DIR=os.fspath(self.git_dir)) 

409 if object_dir_env is not None: 

410 self.git.update_environment(GIT_OBJECT_DIRECTORY=object_dir_env) 

411 

412 # Special handling, in special times. 

413 rootpath = object_dir_env if object_dir_env is not None else osp.join(self.common_dir, "objects") 

414 if issubclass(odbt, GitCmdObjectDB): 

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

416 else: 

417 if issubclass(odbt, gitdb.GitDB): 

418 warnings.warn( 

419 "GitDB is deprecated as a GitPython backend due to security and performance issues. " 

420 "Use the default GitCmdObjectDB backend instead.", 

421 DeprecationWarning, 

422 stacklevel=2, 

423 ) 

424 self.odb = odbt(rootpath) 

425 

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

427 return self 

428 

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

430 self.close() 

431 

432 def __del__(self) -> None: 

433 try: 

434 self.close() 

435 except Exception: 

436 pass 

437 

438 def close(self) -> None: 

439 if self.git: 

440 self.git.clear_cache() 

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

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

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

444 # synchronously rather than forcing a gc collection. 

445 if sys.platform == "win32": 

446 gc.collect() 

447 gitdb.util.mman.collect() 

448 if sys.platform == "win32": 

449 gc.collect() 

450 

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

452 if isinstance(rhs, Repo): 

453 return self.git_dir == rhs.git_dir 

454 return False 

455 

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

457 return not self.__eq__(rhs) 

458 

459 def __hash__(self) -> int: 

460 return hash(self.git_dir) 

461 

462 @property 

463 def description(self) -> str: 

464 """The project's description""" 

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

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

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

468 

469 @description.setter 

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

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

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

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

474 

475 @property 

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

477 """ 

478 :return: 

479 The working tree directory of our git repository. 

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

481 """ 

482 return self._working_tree_dir 

483 

484 @property 

485 def common_dir(self) -> PathLike: 

486 """ 

487 :return: 

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

489 ORIG_HEAD, COMMIT_EDITMSG, index, and logs/. 

490 """ 

491 return self._common_dir or self.git_dir 

492 

493 @property 

494 def bare(self) -> bool: 

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

496 return self._bare 

497 

498 @property 

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

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

501 in this repo. 

502 

503 :return: 

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

505 """ 

506 return Head.list_items(self) 

507 

508 @property 

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

510 """Alias for heads. 

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

512 in this repo. 

513 

514 :return: 

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

516 """ 

517 return self.heads 

518 

519 @property 

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

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

522 heads and remote references. 

523 

524 :return: 

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

526 """ 

527 return Reference.list_items(self) 

528 

529 @property 

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

531 """Alias for references. 

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

533 heads and remote references. 

534 

535 :return: 

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

537 """ 

538 return self.references 

539 

540 @property 

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

542 """ 

543 :return: 

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

545 

546 :note: 

547 This property can be expensive, as the returned 

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

549 It is recommended to reuse the object. 

550 """ 

551 return IndexFile(self) 

552 

553 @property 

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

555 """ 

556 :return: 

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

558 """ 

559 return HEAD(self, "HEAD") 

560 

561 @property 

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

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

564 manipulate remotes. 

565 

566 :return: 

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

568 """ 

569 return Remote.list_items(self) 

570 

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

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

573 

574 :raise ValueError: 

575 If no remote with such a name exists. 

576 """ 

577 r = Remote(self, name) 

578 if not r.exists(): 

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

580 return r 

581 

582 # { Submodules 

583 

584 @property 

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

586 """ 

587 :return: 

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

589 current head 

590 """ 

591 return Submodule.list_items(self) 

592 

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

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

595 

596 :raise ValueError: 

597 If no such submodule exists. 

598 """ 

599 try: 

600 return self.submodules[name] 

601 except IndexError as e: 

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

603 # END exception handling 

604 

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

606 """Create a new submodule. 

607 

608 :note: 

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

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

611 

612 :return: 

613 The created submodule. 

614 """ 

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

616 

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

618 """An iterator yielding Submodule instances. 

619 

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

621 and `kwargs`. 

622 

623 :return: 

624 Iterator 

625 """ 

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

627 

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

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

630 take the previous state into consideration. 

631 

632 :note: 

633 For more information, please see the documentation of 

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

635 """ 

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

637 

638 # }END submodules 

639 

640 @property 

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

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

643 this repo. 

644 

645 :return: 

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

647 """ 

648 return TagReference.list_items(self) 

649 

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

651 """ 

652 :return: 

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

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

655 

656 :param path: 

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

658 """ 

659 full_path = self._to_full_tag_path(path) 

660 return TagReference(self, full_path) 

661 

662 @staticmethod 

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

664 path_str = str(path) 

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

666 return path_str 

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

668 return Reference._common_path_default + "/" + path_str 

669 else: 

670 return TagReference._common_path_default + "/" + path_str 

671 

672 def create_head( 

673 self, 

674 path: PathLike, 

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

676 force: bool = False, 

677 logmsg: Optional[str] = None, 

678 ) -> "Head": 

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

680 

681 :note: 

682 For more documentation, please see the 

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

684 

685 :return: 

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

687 """ 

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

689 

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

691 """Delete the given heads. 

692 

693 :param kwargs: 

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

695 """ 

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

697 

698 def create_tag( 

699 self, 

700 path: PathLike, 

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

702 message: Optional[str] = None, 

703 force: bool = False, 

704 **kwargs: Any, 

705 ) -> TagReference: 

706 """Create a new tag reference. 

707 

708 :note: 

709 For more documentation, please see the 

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

711 

712 :return: 

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

714 """ 

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

716 

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

718 """Delete the given tag references.""" 

719 return TagReference.delete(self, *tags) 

720 

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

722 """Create a new remote. 

723 

724 For more information, please see the documentation of the 

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

726 

727 :return: 

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

729 """ 

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

731 

732 def delete_remote(self, remote: _T_RemoteName) -> _T_RemoteName: 

733 """Delete the given remote.""" 

734 return Remote.remove(self, remote) 

735 

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

737 if git_dir is None: 

738 git_dir = self.git_dir 

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

740 # Use the global config instead. 

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

742 config_level = "global" 

743 

744 if config_level == "system": 

745 return "/etc/gitconfig" 

746 elif config_level == "user": 

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

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

749 elif config_level == "global": 

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

751 elif config_level == "repository": 

752 repo_dir = self._common_dir or git_dir 

753 if not repo_dir: 

754 raise NotADirectoryError 

755 else: 

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

757 else: 

758 assert_never( # type: ignore[unreachable] 

759 config_level, 

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

761 ) 

762 

763 def config_reader( 

764 self, 

765 config_level: Optional[Lit_config_levels] = None, 

766 ) -> GitConfigParser: 

767 """ 

768 :return: 

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

770 configuration, but not to write it. 

771 

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

773 configuration files. 

774 

775 :param config_level: 

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

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

778 you wish to read to prevent reading multiple files. 

779 

780 :note: 

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

782 unknown, instead the global path will be used. 

783 """ 

784 return self._config_reader(config_level=config_level) 

785 

786 def _config_reader( 

787 self, 

788 config_level: Optional[Lit_config_levels] = None, 

789 git_dir: Optional[PathLike] = None, 

790 ) -> GitConfigParser: 

791 if config_level is None: 

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

793 else: 

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

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

796 

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

798 """ 

799 :return: 

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

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

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

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

804 

805 :param config_level: 

806 One of the following values: 

807 

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

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

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

811 """ 

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

813 

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

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

816 

817 :param rev: 

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

819 

820 :return: 

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

822 """ 

823 if rev is None: 

824 return self.head.commit 

825 return to_commit(self.rev_parse(str(rev))) 

826 

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

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

829 

830 :note: 

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

832 """ 

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

834 

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

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

837 

838 Examples:: 

839 

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

841 

842 :param rev: 

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

844 

845 :return: 

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

847 

848 :note: 

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

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

851 subsequent operations might have unexpected results. 

852 """ 

853 if rev is None: 

854 return self.head.commit.tree 

855 obj = self.rev_parse(str(rev)) 

856 return obj if obj.type == "tree" else to_commit(obj).tree 

857 

858 def iter_commits( 

859 self, 

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

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

862 allow_unsafe_options: bool = False, 

863 **kwargs: Any, 

864 ) -> Iterator[Commit]: 

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

866 history of a given ref/commit. 

867 

868 :param rev: 

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

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

871 

872 :param paths: 

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

874 path or paths will be returned. 

875 

876 :param kwargs: 

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

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

879 

880 :param allow_unsafe_options: 

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

882 

883 :note: 

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

885 ``"revA...revB"`` revision specifier. 

886 

887 :return: 

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

889 """ 

890 if rev is None: 

891 rev = self.head.commit 

892 

893 if not allow_unsafe_options: 

894 Git.check_unsafe_options( 

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

896 ) 

897 

898 return Commit.iter_items( 

899 self, 

900 rev, 

901 paths, 

902 allow_unsafe_options=allow_unsafe_options, 

903 **kwargs, 

904 ) 

905 

906 def merge_base(self, *rev: TBD, allow_unsafe_options: bool = False, **kwargs: Any) -> List[Commit]: 

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

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

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

910 

911 :param rev: 

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

913 

914 :param allow_unsafe_options: 

915 Allow unsafe options in the revision arguments, like ``--output``. 

916 

917 :param kwargs: 

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

919 which does all the work. 

920 

921 :return: 

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

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

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

925 exists. 

926 

927 :raise ValueError: 

928 If fewer than two revisions are provided. 

929 

930 :raise git.exc.GitCommandError: 

931 If git fails for a reason other than having no common merge base. 

932 """ 

933 if len(rev) < 2: 

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

935 # END handle input 

936 

937 if not allow_unsafe_options: 

938 Git.check_unsafe_options( 

939 options=Git._option_candidates(rev, kwargs), unsafe_options=self.unsafe_git_revision_options 

940 ) 

941 

942 res: List[Commit] = [] 

943 try: 

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

945 except GitCommandError as err: 

946 if err.status != 1: 

947 raise 

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

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

950 return res 

951 # END exception handling 

952 

953 for line in lines: 

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

955 # END for each merge-base 

956 

957 return res 

958 

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

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

961 

962 :param ancestor_rev: 

963 Rev which should be an ancestor. 

964 

965 :param rev: 

966 Rev to test against `ancestor_rev`. 

967 

968 :return: 

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

970 """ 

971 try: 

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

973 except GitCommandError as err: 

974 if err.status == 1: 

975 return False 

976 raise 

977 return True 

978 

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

980 try: 

981 complete_sha = self.odb.partial_to_complete_sha_hex(sha) 

982 object_info = self.odb.info(complete_sha) 

983 if object_type: 

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

985 return True 

986 else: 

987 _logger.debug( 

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

989 object_info.type.decode(), 

990 object_type, 

991 ) 

992 return False 

993 else: 

994 return True 

995 except BadObject: 

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

997 return False 

998 

999 def _get_daemon_export(self) -> bool: 

1000 git_dir = getattr(self, "git_dir", None) 

1001 if git_dir is None: 

1002 return False 

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

1004 return osp.exists(filename) 

1005 

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

1007 git_dir = getattr(self, "git_dir", None) 

1008 if git_dir is None: 

1009 return 

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

1011 fileexists = osp.exists(filename) 

1012 if value and not fileexists: 

1013 touch(filename) 

1014 elif not value and fileexists: 

1015 os.unlink(filename) 

1016 

1017 @property 

1018 def daemon_export(self) -> bool: 

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

1020 return self._get_daemon_export() 

1021 

1022 @daemon_export.setter 

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

1024 self._set_daemon_export(value) 

1025 

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

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

1028 

1029 :return: 

1030 List of strings being pathnames of alternates 

1031 """ 

1032 alternates_path = osp.join(self.odb.root_path(), "info", "alternates") 

1033 

1034 if osp.exists(alternates_path): 

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

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

1037 return alts.strip().splitlines() 

1038 return [] 

1039 

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

1041 """Set the alternates. 

1042 

1043 :param alts: 

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

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

1046 

1047 :raise git.exc.NoSuchPathError: 

1048 

1049 :note: 

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

1051 caller is responsible. 

1052 """ 

1053 alternates_path = osp.join(self.odb.root_path(), "info", "alternates") 

1054 if not alts: 

1055 if osp.isfile(alternates_path): 

1056 os.remove(alternates_path) 

1057 else: 

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

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

1060 

1061 @property 

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

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

1064 return self._get_alternates() 

1065 

1066 @alternates.setter 

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

1068 self._set_alternates(alts) 

1069 

1070 def is_dirty( 

1071 self, 

1072 index: bool = True, 

1073 working_tree: bool = True, 

1074 untracked_files: bool = False, 

1075 submodules: bool = True, 

1076 path: Optional[PathLike] = None, 

1077 ) -> bool: 

1078 """ 

1079 :return: 

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

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

1082 if the index or the working copy have changes. 

1083 """ 

1084 if self._bare: 

1085 # Bare repositories with no associated working directory are 

1086 # always considered to be clean. 

1087 return False 

1088 

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

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

1091 if not submodules: 

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

1093 if path: 

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

1095 if index: 

1096 # diff index against HEAD. 

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

1098 return True 

1099 # END index handling 

1100 if working_tree: 

1101 # diff index against working tree. 

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

1103 return True 

1104 # END working tree handling 

1105 if untracked_files: 

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

1107 return True 

1108 # END untracked files 

1109 return False 

1110 

1111 @property 

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

1113 """ 

1114 :return: 

1115 list(str,...) 

1116 

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

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

1119 

1120 :note: 

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

1122 

1123 :note: 

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

1125 please consider caching it yourself. 

1126 """ 

1127 return self._get_untracked_files() 

1128 

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

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

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

1132 # Untracked files prefix in porcelain mode 

1133 prefix = "?? " 

1134 untracked_files = [] 

1135 for line in proc.stdout: 

1136 line = line.decode(defenc) 

1137 if not line.startswith(prefix): 

1138 continue 

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

1140 # Special characters are escaped 

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

1142 filename = filename[1:-1] 

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

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

1145 untracked_files.append(filename) 

1146 finalize_process(proc) 

1147 return untracked_files 

1148 

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

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

1151 

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

1153 

1154 :param paths: 

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

1156 

1157 :return: 

1158 Subset of those paths which are ignored 

1159 """ 

1160 try: 

1161 proc: str = self.git.check_ignore("--", *paths) 

1162 except GitCommandError as err: 

1163 if err.status == 1: 

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

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

1166 return [] 

1167 else: 

1168 # Raise the exception on all other return codes. 

1169 raise 

1170 

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

1172 

1173 @property 

1174 def active_branch(self) -> Head: 

1175 """The currently active branch. 

1176 

1177 Check ``repo.head.is_detached`` before accessing this property if HEAD 

1178 may be detached. To access the current commit in either state, use 

1179 ``repo.head.commit`` instead. 

1180 

1181 :raise TypeError: 

1182 If HEAD is detached. 

1183 

1184 :raise ValueError: 

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

1186 incompatible with older clients. 

1187 

1188 :return: 

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

1190 """ 

1191 active_branch = self.head.reference 

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

1193 raise ValueError( 

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

1195 ) 

1196 return active_branch 

1197 

1198 def blame_incremental( 

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

1200 ) -> Iterator["BlameEntry"]: 

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

1202 

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

1204 stream of :class:`BlameEntry` tuples. 

1205 

1206 :param rev: 

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

1208 uncommitted changes. Otherwise, anything successfully parsed by 

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

1210 

1211 :param allow_unsafe_options: 

1212 Allow unsafe options in revision argument, like ``--output`` or ``--contents``. 

1213 

1214 :return: 

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

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

1217 the resulting file. 

1218 

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

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

1221 """ 

1222 if not allow_unsafe_options: 

1223 Git.check_unsafe_options( 

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

1225 unsafe_options=self.unsafe_git_blame_options, 

1226 clusterable_short_options="46bceflnpqstvw", 

1227 ) 

1228 

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

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

1231 

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

1233 while True: 

1234 try: 

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

1236 line = next(stream) 

1237 except StopIteration: 

1238 return 

1239 split_line = line.split() 

1240 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line 

1241 lineno = int(lineno_b) 

1242 num_lines = int(num_lines_b) 

1243 orig_lineno = int(orig_lineno_b) 

1244 if hexsha not in commits: 

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

1246 # commit. 

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

1248 while True: 

1249 try: 

1250 line = next(stream) 

1251 except StopIteration: 

1252 return 

1253 if line == b"boundary": 

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

1255 # "previous" tag. 

1256 continue 

1257 

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

1259 props[tag] = value 

1260 if tag == b"filename": 

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

1262 orig_filename = value 

1263 break 

1264 

1265 c = Commit( 

1266 self, 

1267 hex_to_bin(hexsha), 

1268 author=Actor( 

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

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

1271 ), 

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

1273 committer=Actor( 

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

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

1276 ), 

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

1278 ) 

1279 commits[hexsha] = c 

1280 else: 

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

1282 # the last line. 

1283 while True: 

1284 try: 

1285 # Will fail if we reach the EOF unexpectedly. 

1286 line = next(stream) 

1287 except StopIteration: 

1288 return 

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

1290 if tag == b"filename": 

1291 orig_filename = value 

1292 break 

1293 

1294 yield BlameEntry( 

1295 commits[hexsha], 

1296 range(lineno, lineno + num_lines), 

1297 safe_decode(orig_filename), 

1298 range(orig_lineno, orig_lineno + num_lines), 

1299 ) 

1300 

1301 def blame( 

1302 self, 

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

1304 file: str, 

1305 incremental: bool = False, 

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

1307 allow_unsafe_options: bool = False, 

1308 **kwargs: Any, 

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

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

1311 

1312 :param rev: 

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

1314 uncommitted changes. Otherwise, anything successfully parsed by 

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

1316 

1317 :param allow_unsafe_options: 

1318 Allow unsafe options in revision argument, like ``--output`` or ``--contents``. 

1319 

1320 :return: 

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

1322 

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

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

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

1326 appearance. 

1327 """ 

1328 if incremental: 

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

1330 rev_opts_list = list(rev_opts or []) 

1331 if not allow_unsafe_options: 

1332 Git.check_unsafe_options( 

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

1334 unsafe_options=self.unsafe_git_blame_options, 

1335 clusterable_short_options="46bceflnpqstvw", 

1336 ) 

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

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

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

1340 

1341 class InfoTD(TypedDict, total=False): 

1342 sha: str 

1343 id: str 

1344 filename: str 

1345 summary: str 

1346 author: str 

1347 author_email: str 

1348 author_date: int 

1349 committer: str 

1350 committer_email: str 

1351 committer_date: int 

1352 

1353 info: InfoTD = {} 

1354 

1355 keepends = True 

1356 for line_bytes in data.splitlines(keepends): 

1357 line_str = "" 

1358 try: 

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

1360 except UnicodeDecodeError: 

1361 firstpart = "" 

1362 parts = [] 

1363 is_binary = True 

1364 else: 

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

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

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

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

1369 # instead. 

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

1371 firstpart = parts[0] 

1372 is_binary = False 

1373 # END handle decode of line 

1374 

1375 if self.re_hexsha_only.search(firstpart): 

1376 # handles 

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

1378 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates 

1379 # another line of blame with the same data 

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

1381 if len(digits) == 3: 

1382 info = {"id": firstpart} 

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

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

1385 info = {"id": firstpart} 

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

1387 # END blame data initialization 

1388 else: 

1389 m = self.re_author_committer_start.search(firstpart) 

1390 if m: 

1391 # handles: 

1392 # author Tom Preston-Werner 

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

1394 # author-time 1192271832 

1395 # author-tz -0700 

1396 # committer Tom Preston-Werner 

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

1398 # committer-time 1192271832 

1399 # committer-tz -0700 - IGNORED BY US 

1400 role = m.group(0) 

1401 if role == "author": 

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

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

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

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

1406 elif role == firstpart: 

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

1408 elif role == "committer": 

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

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

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

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

1413 elif role == firstpart: 

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

1415 # END distinguish mail,time,name 

1416 else: 

1417 # handle 

1418 # filename lib/grit.rb 

1419 # summary add Blob 

1420 # <and rest> 

1421 if firstpart.startswith("filename"): 

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

1423 elif firstpart.startswith("summary"): 

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

1425 elif firstpart == "": 

1426 if info: 

1427 sha = info["id"] 

1428 c = commits.get(sha) 

1429 if c is None: 

1430 c = Commit( 

1431 self, 

1432 hex_to_bin(sha), 

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

1434 authored_date=info["author_date"], 

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

1436 committed_date=info["committer_date"], 

1437 ) 

1438 commits[sha] = c 

1439 blames[-1][0] = c 

1440 # END if commit objects needs initial creation 

1441 

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

1443 line: str | bytes 

1444 if not is_binary: 

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

1446 line_str = line_str[1:] 

1447 line = line_str 

1448 else: 

1449 line = line_bytes 

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

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

1452 # along the newline separator. We will append this 

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

1454 # though it should be concatenated with the last 

1455 # line we have seen. 

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

1457 

1458 info = {"id": sha} 

1459 # END if we collected commit info 

1460 # END distinguish filename,summary,rest 

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

1462 # END distinguish hexsha vs other information 

1463 return blames 

1464 

1465 @classmethod 

1466 def init( 

1467 cls, 

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

1469 mkdir: bool = True, 

1470 odbt: Type[Union[LooseObjectDB, gitdb.GitDB]] = GitCmdObjectDB, 

1471 expand_vars: bool = True, 

1472 allow_unsafe_options: bool = False, 

1473 **kwargs: Any, 

1474 ) -> "Repo": 

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

1476 

1477 :param path: 

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

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

1480 working directory. 

1481 

1482 :param mkdir: 

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

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

1485 Only effective if a path is explicitly given. 

1486 

1487 :param odbt: 

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

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

1490 used to access all object data. The pure-Python ``GitDB`` backend is 

1491 deprecated; use the default :class:`~git.db.GitCmdObjectDB` instead. 

1492 

1493 :param expand_vars: 

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

1495 information disclosure, allowing attackers to access the contents of 

1496 environment variables. 

1497 

1498 :param allow_unsafe_options: 

1499 Allow unsafe options to be used, such as ``--template`` and 

1500 ``--separate-git-dir``. 

1501 

1502 :param kwargs: 

1503 Keyword arguments serving as additional options to the 

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

1505 

1506 :return: 

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

1508 """ 

1509 if not allow_unsafe_options: 

1510 Git.check_unsafe_options( 

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

1512 unsafe_options=cls.unsafe_git_init_options, 

1513 ) 

1514 if path: 

1515 path = expand_path(path, expand_vars) 

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

1517 os.makedirs(path, 0o755) 

1518 

1519 # git command automatically chdir into the directory 

1520 git = cls.GitCommandWrapperType(path) 

1521 git.init(**kwargs) 

1522 return cls(path, odbt=odbt) 

1523 

1524 @classmethod 

1525 def _clone( 

1526 cls, 

1527 git: "Git", 

1528 url: PathLike, 

1529 path: PathLike, 

1530 odb_default_type: Type[Union[LooseObjectDB, gitdb.GitDB]], 

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

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

1533 allow_unsafe_protocols: bool = False, 

1534 allow_unsafe_options: bool = False, 

1535 **kwargs: Any, 

1536 ) -> "Repo": 

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

1538 

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

1540 url = os.fspath(url) 

1541 path = os.fspath(path) 

1542 

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

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

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

1546 # becomes:: 

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

1548 # 

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

1550 sep_dir = kwargs.get("separate_git_dir") 

1551 if sep_dir: 

1552 kwargs["separate_git_dir"] = Git.polish_url(os.fspath(sep_dir), expand_vars=False) 

1553 multi = None 

1554 if multi_options: 

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

1556 

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

1558 if not allow_unsafe_protocols: 

1559 Git.check_unsafe_protocols(clone_url) 

1560 if not allow_unsafe_options: 

1561 Git.check_unsafe_options( 

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

1563 unsafe_options=cls.unsafe_git_clone_options, 

1564 ) 

1565 if not allow_unsafe_options and multi: 

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

1567 

1568 proc = git.clone( 

1569 multi, 

1570 "--", 

1571 clone_url, 

1572 clone_path, 

1573 with_extended_output=True, 

1574 as_process=True, 

1575 v=True, 

1576 universal_newlines=True, 

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

1578 ) 

1579 if progress: 

1580 handle_process_output( 

1581 proc, 

1582 None, 

1583 to_progress_instance(progress).new_message_handler(), 

1584 finalize_process, 

1585 decode_streams=False, 

1586 ) 

1587 else: 

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

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

1590 cmdline = remove_password_if_present(cmdline) 

1591 

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

1593 finalize_process(proc, stderr=stderr) 

1594 

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

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

1597 if not osp.isabs(path): 

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

1599 

1600 repo = cls(path, odbt=odbt) 

1601 

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

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

1604 

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

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

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

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

1609 if repo.remotes: 

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

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

1612 # END handle remote repo 

1613 return repo 

1614 

1615 def clone( 

1616 self, 

1617 path: PathLike, 

1618 progress: Optional[CallableProgress] = None, 

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

1620 allow_unsafe_protocols: bool = False, 

1621 allow_unsafe_options: bool = False, 

1622 **kwargs: Any, 

1623 ) -> "Repo": 

1624 """Create a clone from this repository. 

1625 

1626 :param path: 

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

1628 

1629 :param progress: 

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

1631 

1632 :param multi_options: 

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

1634 times. 

1635 

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

1637 For example:: 

1638 

1639 [ 

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

1641 "--config core.ignorecase", 

1642 "--recurse-submodule=repo1_path", 

1643 "--recurse-submodule=repo2_path", 

1644 ] 

1645 

1646 :param allow_unsafe_protocols: 

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

1648 

1649 :param allow_unsafe_options: 

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

1651 

1652 :param kwargs: 

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

1654 implementation used by the returned :class:`Repo` instance. The 

1655 pure-Python ``GitDB`` backend is deprecated; use the default 

1656 :class:`~git.db.GitCmdObjectDB` instead. 

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

1658 command. 

1659 

1660 :return: 

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

1662 """ 

1663 return self._clone( 

1664 self.git, 

1665 self.common_dir, 

1666 path, 

1667 type(self.odb), 

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

1669 multi_options, 

1670 allow_unsafe_protocols=allow_unsafe_protocols, 

1671 allow_unsafe_options=allow_unsafe_options, 

1672 **kwargs, 

1673 ) 

1674 

1675 @classmethod 

1676 def clone_from( 

1677 cls, 

1678 url: PathLike, 

1679 to_path: PathLike, 

1680 progress: CallableProgress = None, 

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

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

1683 allow_unsafe_protocols: bool = False, 

1684 allow_unsafe_options: bool = False, 

1685 **kwargs: Any, 

1686 ) -> "Repo": 

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

1688 

1689 :param url: 

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

1691 

1692 :param to_path: 

1693 Path to which the repository should be cloned to. 

1694 

1695 :param progress: 

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

1697 

1698 :param env: 

1699 Optional dictionary containing the desired environment variables. 

1700 

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

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

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

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

1705 

1706 :param multi_options: 

1707 See the :meth:`clone` method. 

1708 

1709 :param allow_unsafe_protocols: 

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

1711 

1712 :param allow_unsafe_options: 

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

1714 

1715 :param kwargs: 

1716 See the :meth:`clone` method. 

1717 

1718 :return: 

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

1720 """ 

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

1722 if env is not None: 

1723 git.update_environment(**env) 

1724 return cls._clone( 

1725 git, 

1726 url, 

1727 to_path, 

1728 GitCmdObjectDB, 

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

1730 multi_options, 

1731 allow_unsafe_protocols=allow_unsafe_protocols, 

1732 allow_unsafe_options=allow_unsafe_options, 

1733 **kwargs, 

1734 ) 

1735 

1736 def archive( 

1737 self, 

1738 ostream: Union[TextIO, BinaryIO], 

1739 treeish: Union[str, Commit, None] = None, 

1740 prefix: Optional[str] = None, 

1741 allow_unsafe_options: bool = False, 

1742 allow_unsafe_protocols: bool = False, 

1743 **kwargs: Any, 

1744 ) -> Repo: 

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

1746 

1747 :param ostream: 

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

1749 

1750 :param treeish: 

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

1752 

1753 :param prefix: 

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

1755 

1756 :param kwargs: 

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

1758 

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

1760 ostreams to write any format supported by Python. 

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

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

1763 or a list or tuple of multiple paths. 

1764 

1765 :param allow_unsafe_options: 

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

1767 

1768 :param allow_unsafe_protocols: 

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

1770 

1771 :raise git.exc.GitCommandError: 

1772 If something went wrong. 

1773 

1774 :return: 

1775 self 

1776 """ 

1777 if treeish is None: 

1778 treeish = self.head.commit 

1779 if prefix and "prefix" not in kwargs: 

1780 kwargs["prefix"] = prefix 

1781 if not allow_unsafe_protocols: 

1782 # Check the emitted URL, including repeated values and Git's long-option 

1783 # abbreviations, rather than only the untransformed `remote` keyword. 

1784 for arg in self.git.transform_kwargs(**kwargs): 

1785 option, separator, remote = arg.partition("=") 

1786 if separator and option.startswith("--r") and "--remote".startswith(option): 

1787 Git.check_unsafe_protocols(remote) 

1788 if not allow_unsafe_options: 

1789 Git.check_unsafe_options( 

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

1791 unsafe_options=self.unsafe_git_archive_options, 

1792 ) 

1793 kwargs["output_stream"] = ostream 

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

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

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

1797 path = [path] 

1798 # END ensure paths is list (or tuple) 

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

1800 return self 

1801 

1802 def has_separate_working_tree(self) -> bool: 

1803 """ 

1804 :return: 

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

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

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

1808 to. 

1809 

1810 :note: 

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

1812 """ 

1813 if self.bare: 

1814 return False 

1815 if self.working_tree_dir: 

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

1817 else: 

1818 return False # Or raise Error? 

1819 

1820 rev_parse = rev_parse 

1821 

1822 def __repr__(self) -> str: 

1823 clazz = self.__class__ 

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

1825 

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

1827 """ 

1828 :return: 

1829 The commit which is currently being replayed while rebasing. 

1830 

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

1832 """ 

1833 if not self.git_dir: 

1834 return None 

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

1836 if not osp.isfile(rebase_head_file): 

1837 return None 

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

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

1840 return self.commit(content)