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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

647 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 is_git_dir, 

49 rev_parse, 

50 touch, 

51) 

52 

53# typing ------------------------------------------------------ 

54 

55from git.types import ( 

56 CallableProgress, 

57 Commit_ish, 

58 Lit_config_levels, 

59 PathLike, 

60 TBD, 

61 Tree_ish, 

62 assert_never, 

63) 

64from typing import ( 

65 Any, 

66 BinaryIO, 

67 Callable, 

68 Dict, 

69 Iterator, 

70 List, 

71 Mapping, 

72 NamedTuple, 

73 Optional, 

74 Sequence, 

75 TYPE_CHECKING, 

76 TextIO, 

77 Tuple, 

78 Type, 

79 Union, 

80 cast, 

81) 

82 

83from git.types import ConfigLevels_Tup, TypedDict 

84 

85if TYPE_CHECKING: 

86 from git.objects import Tree 

87 from git.objects.submodule.base import UpdateProgress 

88 from git.refs.symbolic import SymbolicReference 

89 from git.remote import RemoteProgress 

90 from git.util import IterableList 

91 

92# ----------------------------------------------------------- 

93 

94_logger = logging.getLogger(__name__) 

95 

96 

97class BlameEntry(NamedTuple): 

98 commit: Dict[str, Commit] 

99 linenos: range 

100 orig_path: Optional[str] 

101 orig_linenos: range 

102 

103 

104class Repo: 

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

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

107 

108 The following attributes are worth using: 

109 

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

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

112 repositories. 

113 

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

115 if we are a bare repository. 

116 

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

118 """ 

119 

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

121 

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

123 git = cast("Git", None) 

124 

125 working_dir: PathLike 

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

127 

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

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_init_options = [ 

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

146 "--template", 

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

148 "--separate-git-dir", 

149 ] 

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

151 

152 unsafe_git_clone_options = [ 

153 # Executes arbitrary commands: 

154 "--upload-pack", 

155 "-u", 

156 # Can override configuration variables that execute arbitrary commands: 

157 "--config", 

158 "-c", 

159 # Can install hooks that execute during clone: 

160 "--template", 

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

162 "--separate-git-dir", 

163 # Fetches from an additional caller-controlled URI: 

164 "--bundle-uri", 

165 ] 

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

167 

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

169 directly: 

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

171 

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

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

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

175 

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

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

178 

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

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

181 unintended network resources: 

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

183 """ 

184 

185 unsafe_git_archive_options = [ 

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

187 "--exec", 

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

189 "--output", 

190 "-o", 

191 # Reads from a caller-controlled filesystem path: 

192 "--add-file", 

193 # Injects a caller-controlled path and contents: 

194 "--add-virtual-file", 

195 ] 

196 

197 unsafe_git_revision_options = [ 

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

199 "--output", 

200 "-o", 

201 ] 

202 

203 unsafe_git_blame_options = unsafe_git_revision_options + [ 

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

205 "--contents", 

206 "-S", 

207 "--ignore-revs-file", 

208 ] 

209 

210 unsafe_git_diff_options = unsafe_git_revision_options + [ 

211 # Treats path operands as arbitrary filesystem paths. 

212 "--no-index", 

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

214 "-O", 

215 "--orderfile", 

216 ] 

217 

218 # Invariants 

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

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

221 

222 # Subclass configuration 

223 GitCommandWrapperType = Git 

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

225 type here.""" 

226 

227 def __init__( 

228 self, 

229 path: Optional[PathLike] = None, 

230 odbt: Type[LooseObjectDB] = GitCmdObjectDB, 

231 search_parent_directories: bool = False, 

232 expand_vars: bool = True, 

233 ) -> None: 

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

235 

236 .. note:: 

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

238 access does not support reftable. 

239 

240 :param path: 

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

242 

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

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

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

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

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

248 

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

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

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

252 used. 

253 

254 :param odbt: 

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

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

257 used to access all object data. 

258 

259 :param search_parent_directories: 

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

261 well. 

262 

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

264 GitPython, which is considered a bug though. 

265 

266 :raise git.exc.InvalidGitRepositoryError: 

267 

268 :raise git.exc.NoSuchPathError: 

269 

270 :return: 

271 :class:`Repo` 

272 """ 

273 

274 git_dir_env = os.getenv("GIT_DIR") 

275 object_dir_env = os.getenv("GIT_OBJECT_DIRECTORY") 

276 if object_dir_env is not None: 

277 object_dir_env = osp.abspath(object_dir_env) 

278 epath = path or git_dir_env 

279 if not epath: 

280 epath = os.getcwd() 

281 epath = os.fspath(epath) 

282 if Git.is_cygwin(): 

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

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

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

286 epath = cygpath(epath) 

287 

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

289 warnings.warn( 

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

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

292 stacklevel=1, 

293 ) 

294 epath = expand_path(epath, expand_vars) 

295 if epath is not None: 

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

297 raise NoSuchPathError(epath) 

298 

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

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

301 git_dir: Optional[str] = None 

302 explicit_git_dir = not path and bool(git_dir_env) 

303 while curpath: 

304 # ABOUT osp.NORMPATH 

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

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

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

308 if not explicit_git_dir: 

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

310 try: 

311 sm_gitpath = find_submodule_git_dir(dotgit) 

312 except OSError: 

313 break 

314 if sm_gitpath is not None: 

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

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

317 self._working_tree_dir = curpath 

318 break 

319 

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

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

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

323 break 

324 

325 if is_git_dir(curpath): 

326 git_dir = curpath 

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

328 if "GIT_WORK_TREE" in os.environ: 

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

330 else: 

331 # Linked worktree administrative directories store the path to 

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

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

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

335 

336 if not osp.isabs(worktree_gitfile): 

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

338 

339 self._working_tree_dir = osp.dirname(worktree_gitfile) 

340 break 

341 

342 # from man git-config : core.worktree 

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

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

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

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

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

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

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

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

351 # working tree. 

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

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

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

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

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

357 if "GIT_WORK_TREE" in os.environ: 

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

359 break 

360 

361 if explicit_git_dir or not search_parent_directories: 

362 break 

363 curpath, tail = osp.split(curpath) 

364 if not tail: 

365 break 

366 # END while curpath 

367 

368 if git_dir is None: 

369 raise InvalidGitRepositoryError(epath) 

370 self.git_dir = git_dir 

371 

372 common_dir_env = os.getenv("GIT_COMMON_DIR") 

373 if common_dir_env is not None: 

374 self._common_dir = osp.abspath(common_dir_env) 

375 else: 

376 try: 

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

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

379 except OSError: 

380 self._common_dir = "" 

381 

382 self._bare = False 

383 try: 

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

385 except Exception: 

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

387 pass 

388 

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

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

391 self._bare = False 

392 

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

394 # that in the first place. 

395 if self._bare: 

396 self._working_tree_dir = None 

397 # END working dir handling 

398 

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

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

401 if common_dir_env is not None: 

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

403 elif git_dir_env is not None: 

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

405 if object_dir_env is not None: 

406 self.git.update_environment(GIT_OBJECT_DIRECTORY=object_dir_env) 

407 

408 # Special handling, in special times. 

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

410 if issubclass(odbt, GitCmdObjectDB): 

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

412 else: 

413 self.odb = odbt(rootpath) 

414 

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

416 return self 

417 

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

419 self.close() 

420 

421 def __del__(self) -> None: 

422 try: 

423 self.close() 

424 except Exception: 

425 pass 

426 

427 def close(self) -> None: 

428 if self.git: 

429 self.git.clear_cache() 

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

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

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

433 # synchronously rather than forcing a gc collection. 

434 if sys.platform == "win32": 

435 gc.collect() 

436 gitdb.util.mman.collect() 

437 if sys.platform == "win32": 

438 gc.collect() 

439 

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

441 if isinstance(rhs, Repo): 

442 return self.git_dir == rhs.git_dir 

443 return False 

444 

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

446 return not self.__eq__(rhs) 

447 

448 def __hash__(self) -> int: 

449 return hash(self.git_dir) 

450 

451 @property 

452 def description(self) -> str: 

453 """The project's description""" 

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

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

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

457 

458 @description.setter 

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

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

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

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

463 

464 @property 

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

466 """ 

467 :return: 

468 The working tree directory of our git repository. 

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

470 """ 

471 return self._working_tree_dir 

472 

473 @property 

474 def common_dir(self) -> PathLike: 

475 """ 

476 :return: 

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

478 ORIG_HEAD, COMMIT_EDITMSG, index, and logs/. 

479 """ 

480 return self._common_dir or self.git_dir 

481 

482 @property 

483 def bare(self) -> bool: 

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

485 return self._bare 

486 

487 @property 

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

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

490 in this repo. 

491 

492 :return: 

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

494 """ 

495 return Head.list_items(self) 

496 

497 @property 

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

499 """Alias for heads. 

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 self.heads 

507 

508 @property 

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

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

511 heads and remote references. 

512 

513 :return: 

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

515 """ 

516 return Reference.list_items(self) 

517 

518 @property 

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

520 """Alias for references. 

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 self.references 

528 

529 @property 

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

531 """ 

532 :return: 

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

534 

535 :note: 

536 This property can be expensive, as the returned 

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

538 It is recommended to reuse the object. 

539 """ 

540 return IndexFile(self) 

541 

542 @property 

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

544 """ 

545 :return: 

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

547 """ 

548 return HEAD(self, "HEAD") 

549 

550 @property 

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

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

553 manipulate remotes. 

554 

555 :return: 

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

557 """ 

558 return Remote.list_items(self) 

559 

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

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

562 

563 :raise ValueError: 

564 If no remote with such a name exists. 

565 """ 

566 r = Remote(self, name) 

567 if not r.exists(): 

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

569 return r 

570 

571 # { Submodules 

572 

573 @property 

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

575 """ 

576 :return: 

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

578 current head 

579 """ 

580 return Submodule.list_items(self) 

581 

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

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

584 

585 :raise ValueError: 

586 If no such submodule exists. 

587 """ 

588 try: 

589 return self.submodules[name] 

590 except IndexError as e: 

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

592 # END exception handling 

593 

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

595 """Create a new submodule. 

596 

597 :note: 

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

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

600 

601 :return: 

602 The created submodule. 

603 """ 

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

605 

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

607 """An iterator yielding Submodule instances. 

608 

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

610 and `kwargs`. 

611 

612 :return: 

613 Iterator 

614 """ 

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

616 

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

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

619 take the previous state into consideration. 

620 

621 :note: 

622 For more information, please see the documentation of 

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

624 """ 

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

626 

627 # }END submodules 

628 

629 @property 

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

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

632 this repo. 

633 

634 :return: 

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

636 """ 

637 return TagReference.list_items(self) 

638 

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

640 """ 

641 :return: 

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

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

644 

645 :param path: 

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

647 """ 

648 full_path = self._to_full_tag_path(path) 

649 return TagReference(self, full_path) 

650 

651 @staticmethod 

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

653 path_str = str(path) 

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

655 return path_str 

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

657 return Reference._common_path_default + "/" + path_str 

658 else: 

659 return TagReference._common_path_default + "/" + path_str 

660 

661 def create_head( 

662 self, 

663 path: PathLike, 

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

665 force: bool = False, 

666 logmsg: Optional[str] = None, 

667 ) -> "Head": 

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

669 

670 :note: 

671 For more documentation, please see the 

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

673 

674 :return: 

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

676 """ 

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

678 

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

680 """Delete the given heads. 

681 

682 :param kwargs: 

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

684 """ 

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

686 

687 def create_tag( 

688 self, 

689 path: PathLike, 

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

691 message: Optional[str] = None, 

692 force: bool = False, 

693 **kwargs: Any, 

694 ) -> TagReference: 

695 """Create a new tag reference. 

696 

697 :note: 

698 For more documentation, please see the 

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

700 

701 :return: 

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

703 """ 

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

705 

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

707 """Delete the given tag references.""" 

708 return TagReference.delete(self, *tags) 

709 

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

711 """Create a new remote. 

712 

713 For more information, please see the documentation of the 

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

715 

716 :return: 

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

718 """ 

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

720 

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

722 """Delete the given remote.""" 

723 return Remote.remove(self, remote) 

724 

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

726 if git_dir is None: 

727 git_dir = self.git_dir 

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

729 # Use the global config instead. 

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

731 config_level = "global" 

732 

733 if config_level == "system": 

734 return "/etc/gitconfig" 

735 elif config_level == "user": 

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

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

738 elif config_level == "global": 

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

740 elif config_level == "repository": 

741 repo_dir = self._common_dir or git_dir 

742 if not repo_dir: 

743 raise NotADirectoryError 

744 else: 

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

746 else: 

747 assert_never( # type: ignore[unreachable] 

748 config_level, 

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

750 ) 

751 

752 def config_reader( 

753 self, 

754 config_level: Optional[Lit_config_levels] = None, 

755 ) -> GitConfigParser: 

756 """ 

757 :return: 

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

759 configuration, but not to write it. 

760 

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

762 configuration files. 

763 

764 :param config_level: 

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

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

767 you wish to read to prevent reading multiple files. 

768 

769 :note: 

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

771 unknown, instead the global path will be used. 

772 """ 

773 return self._config_reader(config_level=config_level) 

774 

775 def _config_reader( 

776 self, 

777 config_level: Optional[Lit_config_levels] = None, 

778 git_dir: Optional[PathLike] = None, 

779 ) -> GitConfigParser: 

780 if config_level is None: 

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

782 else: 

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

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

785 

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

787 """ 

788 :return: 

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

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

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

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

793 

794 :param config_level: 

795 One of the following values: 

796 

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

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

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

800 """ 

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

802 

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

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

805 

806 :param rev: 

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

808 

809 :return: 

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

811 """ 

812 if rev is None: 

813 return self.head.commit 

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

815 

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

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

818 

819 :note: 

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

821 """ 

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

823 

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

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

826 

827 Examples:: 

828 

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

830 

831 :param rev: 

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

833 

834 :return: 

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

836 

837 :note: 

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

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

840 subsequent operations might have unexpected results. 

841 """ 

842 if rev is None: 

843 return self.head.commit.tree 

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

845 

846 def iter_commits( 

847 self, 

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

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

850 allow_unsafe_options: bool = False, 

851 **kwargs: Any, 

852 ) -> Iterator[Commit]: 

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

854 history of a given ref/commit. 

855 

856 :param rev: 

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

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

859 

860 :param paths: 

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

862 path or paths will be returned. 

863 

864 :param kwargs: 

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

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

867 

868 :param allow_unsafe_options: 

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

870 

871 :note: 

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

873 ``"revA...revB"`` revision specifier. 

874 

875 :return: 

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

877 """ 

878 if rev is None: 

879 rev = self.head.commit 

880 

881 if not allow_unsafe_options: 

882 Git.check_unsafe_options( 

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

884 ) 

885 

886 return Commit.iter_items( 

887 self, 

888 rev, 

889 paths, 

890 allow_unsafe_options=allow_unsafe_options, 

891 **kwargs, 

892 ) 

893 

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

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

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

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

898 

899 :param rev: 

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

901 

902 :param kwargs: 

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

904 which does all the work. 

905 

906 :return: 

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

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

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

910 exists. 

911 

912 :raise ValueError: 

913 If fewer than two revisions are provided. 

914 """ 

915 if len(rev) < 2: 

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

917 # END handle input 

918 

919 res: List[Commit] = [] 

920 try: 

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

922 except GitCommandError as err: 

923 if err.status == 128: 

924 raise 

925 # END handle invalid rev 

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

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

928 return res 

929 # END exception handling 

930 

931 for line in lines: 

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

933 # END for each merge-base 

934 

935 return res 

936 

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

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

939 

940 :param ancestor_rev: 

941 Rev which should be an ancestor. 

942 

943 :param rev: 

944 Rev to test against `ancestor_rev`. 

945 

946 :return: 

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

948 """ 

949 try: 

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

951 except GitCommandError as err: 

952 if err.status == 1: 

953 return False 

954 raise 

955 return True 

956 

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

958 try: 

959 complete_sha = self.odb.partial_to_complete_sha_hex(sha) 

960 object_info = self.odb.info(complete_sha) 

961 if object_type: 

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

963 return True 

964 else: 

965 _logger.debug( 

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

967 object_info.type.decode(), 

968 object_type, 

969 ) 

970 return False 

971 else: 

972 return True 

973 except BadObject: 

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

975 return False 

976 

977 def _get_daemon_export(self) -> bool: 

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

979 if git_dir is None: 

980 return False 

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

982 return osp.exists(filename) 

983 

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

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

986 if git_dir is None: 

987 return 

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

989 fileexists = osp.exists(filename) 

990 if value and not fileexists: 

991 touch(filename) 

992 elif not value and fileexists: 

993 os.unlink(filename) 

994 

995 @property 

996 def daemon_export(self) -> bool: 

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

998 return self._get_daemon_export() 

999 

1000 @daemon_export.setter 

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

1002 self._set_daemon_export(value) 

1003 

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

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

1006 

1007 :return: 

1008 List of strings being pathnames of alternates 

1009 """ 

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

1011 

1012 if osp.exists(alternates_path): 

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

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

1015 return alts.strip().splitlines() 

1016 return [] 

1017 

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

1019 """Set the alternates. 

1020 

1021 :param alts: 

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

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

1024 

1025 :raise git.exc.NoSuchPathError: 

1026 

1027 :note: 

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

1029 caller is responsible. 

1030 """ 

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

1032 if not alts: 

1033 if osp.isfile(alternates_path): 

1034 os.remove(alternates_path) 

1035 else: 

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

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

1038 

1039 @property 

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

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

1042 return self._get_alternates() 

1043 

1044 @alternates.setter 

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

1046 self._set_alternates(alts) 

1047 

1048 def is_dirty( 

1049 self, 

1050 index: bool = True, 

1051 working_tree: bool = True, 

1052 untracked_files: bool = False, 

1053 submodules: bool = True, 

1054 path: Optional[PathLike] = None, 

1055 ) -> bool: 

1056 """ 

1057 :return: 

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

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

1060 if the index or the working copy have changes. 

1061 """ 

1062 if self._bare: 

1063 # Bare repositories with no associated working directory are 

1064 # always considered to be clean. 

1065 return False 

1066 

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

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

1069 if not submodules: 

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

1071 if path: 

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

1073 if index: 

1074 # diff index against HEAD. 

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

1076 return True 

1077 # END index handling 

1078 if working_tree: 

1079 # diff index against working tree. 

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

1081 return True 

1082 # END working tree handling 

1083 if untracked_files: 

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

1085 return True 

1086 # END untracked files 

1087 return False 

1088 

1089 @property 

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

1091 """ 

1092 :return: 

1093 list(str,...) 

1094 

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

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

1097 

1098 :note: 

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

1100 

1101 :note: 

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

1103 please consider caching it yourself. 

1104 """ 

1105 return self._get_untracked_files() 

1106 

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

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

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

1110 # Untracked files prefix in porcelain mode 

1111 prefix = "?? " 

1112 untracked_files = [] 

1113 for line in proc.stdout: 

1114 line = line.decode(defenc) 

1115 if not line.startswith(prefix): 

1116 continue 

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

1118 # Special characters are escaped 

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

1120 filename = filename[1:-1] 

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

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

1123 untracked_files.append(filename) 

1124 finalize_process(proc) 

1125 return untracked_files 

1126 

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

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

1129 

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

1131 

1132 :param paths: 

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

1134 

1135 :return: 

1136 Subset of those paths which are ignored 

1137 """ 

1138 try: 

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

1140 except GitCommandError as err: 

1141 if err.status == 1: 

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

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

1144 return [] 

1145 else: 

1146 # Raise the exception on all other return codes. 

1147 raise 

1148 

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

1150 

1151 @property 

1152 def active_branch(self) -> Head: 

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

1154 

1155 :raise TypeError: 

1156 If HEAD is detached. 

1157 

1158 :raise ValueError: 

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

1160 incompatible with older clients. 

1161 

1162 :return: 

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

1164 """ 

1165 active_branch = self.head.reference 

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

1167 raise ValueError( 

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

1169 ) 

1170 return active_branch 

1171 

1172 def blame_incremental( 

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

1174 ) -> Iterator["BlameEntry"]: 

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

1176 

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

1178 stream of :class:`BlameEntry` tuples. 

1179 

1180 :param rev: 

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

1182 uncommitted changes. Otherwise, anything successfully parsed by 

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

1184 

1185 :param allow_unsafe_options: 

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

1187 

1188 :return: 

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

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

1191 the resulting file. 

1192 

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

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

1195 """ 

1196 if not allow_unsafe_options: 

1197 Git.check_unsafe_options( 

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

1199 unsafe_options=self.unsafe_git_blame_options, 

1200 clusterable_short_options="46bceflnpqstvw", 

1201 ) 

1202 

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

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

1205 

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

1207 while True: 

1208 try: 

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

1210 line = next(stream) 

1211 except StopIteration: 

1212 return 

1213 split_line = line.split() 

1214 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line 

1215 lineno = int(lineno_b) 

1216 num_lines = int(num_lines_b) 

1217 orig_lineno = int(orig_lineno_b) 

1218 if hexsha not in commits: 

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

1220 # commit. 

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

1222 while True: 

1223 try: 

1224 line = next(stream) 

1225 except StopIteration: 

1226 return 

1227 if line == b"boundary": 

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

1229 # "previous" tag. 

1230 continue 

1231 

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

1233 props[tag] = value 

1234 if tag == b"filename": 

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

1236 orig_filename = value 

1237 break 

1238 

1239 c = Commit( 

1240 self, 

1241 hex_to_bin(hexsha), 

1242 author=Actor( 

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

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

1245 ), 

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

1247 committer=Actor( 

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

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

1250 ), 

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

1252 ) 

1253 commits[hexsha] = c 

1254 else: 

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

1256 # the last line. 

1257 while True: 

1258 try: 

1259 # Will fail if we reach the EOF unexpectedly. 

1260 line = next(stream) 

1261 except StopIteration: 

1262 return 

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

1264 if tag == b"filename": 

1265 orig_filename = value 

1266 break 

1267 

1268 yield BlameEntry( 

1269 commits[hexsha], 

1270 range(lineno, lineno + num_lines), 

1271 safe_decode(orig_filename), 

1272 range(orig_lineno, orig_lineno + num_lines), 

1273 ) 

1274 

1275 def blame( 

1276 self, 

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

1278 file: str, 

1279 incremental: bool = False, 

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

1281 allow_unsafe_options: bool = False, 

1282 **kwargs: Any, 

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

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

1285 

1286 :param rev: 

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

1288 uncommitted changes. Otherwise, anything successfully parsed by 

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

1290 

1291 :param allow_unsafe_options: 

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

1293 

1294 :return: 

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

1296 

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

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

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

1300 appearance. 

1301 """ 

1302 if incremental: 

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

1304 rev_opts_list = list(rev_opts or []) 

1305 if not allow_unsafe_options: 

1306 Git.check_unsafe_options( 

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

1308 unsafe_options=self.unsafe_git_blame_options, 

1309 clusterable_short_options="46bceflnpqstvw", 

1310 ) 

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

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

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

1314 

1315 class InfoTD(TypedDict, total=False): 

1316 sha: str 

1317 id: str 

1318 filename: str 

1319 summary: str 

1320 author: str 

1321 author_email: str 

1322 author_date: int 

1323 committer: str 

1324 committer_email: str 

1325 committer_date: int 

1326 

1327 info: InfoTD = {} 

1328 

1329 keepends = True 

1330 for line_bytes in data.splitlines(keepends): 

1331 line_str = "" 

1332 try: 

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

1334 except UnicodeDecodeError: 

1335 firstpart = "" 

1336 parts = [] 

1337 is_binary = True 

1338 else: 

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

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

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

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

1343 # instead. 

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

1345 firstpart = parts[0] 

1346 is_binary = False 

1347 # END handle decode of line 

1348 

1349 if self.re_hexsha_only.search(firstpart): 

1350 # handles 

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

1352 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates 

1353 # another line of blame with the same data 

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

1355 if len(digits) == 3: 

1356 info = {"id": firstpart} 

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

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

1359 info = {"id": firstpart} 

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

1361 # END blame data initialization 

1362 else: 

1363 m = self.re_author_committer_start.search(firstpart) 

1364 if m: 

1365 # handles: 

1366 # author Tom Preston-Werner 

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

1368 # author-time 1192271832 

1369 # author-tz -0700 

1370 # committer Tom Preston-Werner 

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

1372 # committer-time 1192271832 

1373 # committer-tz -0700 - IGNORED BY US 

1374 role = m.group(0) 

1375 if role == "author": 

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

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

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

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

1380 elif role == firstpart: 

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

1382 elif role == "committer": 

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

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

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

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

1387 elif role == firstpart: 

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

1389 # END distinguish mail,time,name 

1390 else: 

1391 # handle 

1392 # filename lib/grit.rb 

1393 # summary add Blob 

1394 # <and rest> 

1395 if firstpart.startswith("filename"): 

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

1397 elif firstpart.startswith("summary"): 

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

1399 elif firstpart == "": 

1400 if info: 

1401 sha = info["id"] 

1402 c = commits.get(sha) 

1403 if c is None: 

1404 c = Commit( 

1405 self, 

1406 hex_to_bin(sha), 

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

1408 authored_date=info["author_date"], 

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

1410 committed_date=info["committer_date"], 

1411 ) 

1412 commits[sha] = c 

1413 blames[-1][0] = c 

1414 # END if commit objects needs initial creation 

1415 

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

1417 line: str | bytes 

1418 if not is_binary: 

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

1420 line_str = line_str[1:] 

1421 line = line_str 

1422 else: 

1423 line = line_bytes 

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

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

1426 # along the newline separator. We will append this 

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

1428 # though it should be concatenated with the last 

1429 # line we have seen. 

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

1431 

1432 info = {"id": sha} 

1433 # END if we collected commit info 

1434 # END distinguish filename,summary,rest 

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

1436 # END distinguish hexsha vs other information 

1437 return blames 

1438 

1439 @classmethod 

1440 def init( 

1441 cls, 

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

1443 mkdir: bool = True, 

1444 odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, 

1445 expand_vars: bool = True, 

1446 allow_unsafe_options: bool = False, 

1447 **kwargs: Any, 

1448 ) -> "Repo": 

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

1450 

1451 :param path: 

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

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

1454 working directory. 

1455 

1456 :param mkdir: 

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

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

1459 Only effective if a path is explicitly given. 

1460 

1461 :param odbt: 

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

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

1464 used to access all object data. 

1465 

1466 :param expand_vars: 

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

1468 information disclosure, allowing attackers to access the contents of 

1469 environment variables. 

1470 

1471 :param allow_unsafe_options: 

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

1473 ``--separate-git-dir``. 

1474 

1475 :param kwargs: 

1476 Keyword arguments serving as additional options to the 

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

1478 

1479 :return: 

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

1481 """ 

1482 if not allow_unsafe_options: 

1483 Git.check_unsafe_options( 

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

1485 unsafe_options=cls.unsafe_git_init_options, 

1486 ) 

1487 if path: 

1488 path = expand_path(path, expand_vars) 

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

1490 os.makedirs(path, 0o755) 

1491 

1492 # git command automatically chdir into the directory 

1493 git = cls.GitCommandWrapperType(path) 

1494 git.init(**kwargs) 

1495 return cls(path, odbt=odbt) 

1496 

1497 @classmethod 

1498 def _clone( 

1499 cls, 

1500 git: "Git", 

1501 url: PathLike, 

1502 path: PathLike, 

1503 odb_default_type: Type[GitCmdObjectDB], 

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

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

1506 allow_unsafe_protocols: bool = False, 

1507 allow_unsafe_options: bool = False, 

1508 **kwargs: Any, 

1509 ) -> "Repo": 

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

1511 

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

1513 url = os.fspath(url) 

1514 path = os.fspath(path) 

1515 

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

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

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

1519 # becomes:: 

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

1521 # 

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

1523 sep_dir = kwargs.get("separate_git_dir") 

1524 if sep_dir: 

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

1526 multi = None 

1527 if multi_options: 

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

1529 

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

1531 if not allow_unsafe_protocols: 

1532 Git.check_unsafe_protocols(clone_url) 

1533 if not allow_unsafe_options: 

1534 Git.check_unsafe_options( 

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

1536 unsafe_options=cls.unsafe_git_clone_options, 

1537 ) 

1538 if not allow_unsafe_options and multi: 

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

1540 

1541 proc = git.clone( 

1542 multi, 

1543 "--", 

1544 clone_url, 

1545 clone_path, 

1546 with_extended_output=True, 

1547 as_process=True, 

1548 v=True, 

1549 universal_newlines=True, 

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

1551 ) 

1552 if progress: 

1553 handle_process_output( 

1554 proc, 

1555 None, 

1556 to_progress_instance(progress).new_message_handler(), 

1557 finalize_process, 

1558 decode_streams=False, 

1559 ) 

1560 else: 

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

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

1563 cmdline = remove_password_if_present(cmdline) 

1564 

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

1566 finalize_process(proc, stderr=stderr) 

1567 

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

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

1570 if not osp.isabs(path): 

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

1572 

1573 repo = cls(path, odbt=odbt) 

1574 

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

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

1577 

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

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

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

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

1582 if repo.remotes: 

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

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

1585 # END handle remote repo 

1586 return repo 

1587 

1588 def clone( 

1589 self, 

1590 path: PathLike, 

1591 progress: Optional[CallableProgress] = None, 

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

1593 allow_unsafe_protocols: bool = False, 

1594 allow_unsafe_options: bool = False, 

1595 **kwargs: Any, 

1596 ) -> "Repo": 

1597 """Create a clone from this repository. 

1598 

1599 :param path: 

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

1601 

1602 :param progress: 

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

1604 

1605 :param multi_options: 

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

1607 times. 

1608 

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

1610 For example:: 

1611 

1612 [ 

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

1614 "--config core.ignorecase", 

1615 "--recurse-submodule=repo1_path", 

1616 "--recurse-submodule=repo2_path", 

1617 ] 

1618 

1619 :param allow_unsafe_protocols: 

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

1621 

1622 :param allow_unsafe_options: 

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

1624 

1625 :param kwargs: 

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

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

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

1629 command. 

1630 

1631 :return: 

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

1633 """ 

1634 return self._clone( 

1635 self.git, 

1636 self.common_dir, 

1637 path, 

1638 type(self.odb), 

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

1640 multi_options, 

1641 allow_unsafe_protocols=allow_unsafe_protocols, 

1642 allow_unsafe_options=allow_unsafe_options, 

1643 **kwargs, 

1644 ) 

1645 

1646 @classmethod 

1647 def clone_from( 

1648 cls, 

1649 url: PathLike, 

1650 to_path: PathLike, 

1651 progress: CallableProgress = None, 

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

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

1654 allow_unsafe_protocols: bool = False, 

1655 allow_unsafe_options: bool = False, 

1656 **kwargs: Any, 

1657 ) -> "Repo": 

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

1659 

1660 :param url: 

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

1662 

1663 :param to_path: 

1664 Path to which the repository should be cloned to. 

1665 

1666 :param progress: 

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

1668 

1669 :param env: 

1670 Optional dictionary containing the desired environment variables. 

1671 

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

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

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

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

1676 

1677 :param multi_options: 

1678 See the :meth:`clone` method. 

1679 

1680 :param allow_unsafe_protocols: 

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

1682 

1683 :param allow_unsafe_options: 

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

1685 

1686 :param kwargs: 

1687 See the :meth:`clone` method. 

1688 

1689 :return: 

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

1691 """ 

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

1693 if env is not None: 

1694 git.update_environment(**env) 

1695 return cls._clone( 

1696 git, 

1697 url, 

1698 to_path, 

1699 GitCmdObjectDB, 

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

1701 multi_options, 

1702 allow_unsafe_protocols=allow_unsafe_protocols, 

1703 allow_unsafe_options=allow_unsafe_options, 

1704 **kwargs, 

1705 ) 

1706 

1707 def archive( 

1708 self, 

1709 ostream: Union[TextIO, BinaryIO], 

1710 treeish: Optional[str] = None, 

1711 prefix: Optional[str] = None, 

1712 allow_unsafe_options: bool = False, 

1713 allow_unsafe_protocols: bool = False, 

1714 **kwargs: Any, 

1715 ) -> Repo: 

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

1717 

1718 :param ostream: 

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

1720 

1721 :param treeish: 

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

1723 

1724 :param prefix: 

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

1726 

1727 :param kwargs: 

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

1729 

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

1731 ostreams to write any format supported by Python. 

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

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

1734 or a list or tuple of multiple paths. 

1735 

1736 :param allow_unsafe_options: 

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

1738 

1739 :param allow_unsafe_protocols: 

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

1741 

1742 :raise git.exc.GitCommandError: 

1743 If something went wrong. 

1744 

1745 :return: 

1746 self 

1747 """ 

1748 if treeish is None: 

1749 treeish = self.head.commit 

1750 if prefix and "prefix" not in kwargs: 

1751 kwargs["prefix"] = prefix 

1752 remote = kwargs.get("remote") 

1753 if not allow_unsafe_protocols and remote is not None: 

1754 Git.check_unsafe_protocols(str(remote)) 

1755 if not allow_unsafe_options: 

1756 Git.check_unsafe_options( 

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

1758 unsafe_options=self.unsafe_git_archive_options, 

1759 ) 

1760 kwargs["output_stream"] = ostream 

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

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

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

1764 path = [path] 

1765 # END ensure paths is list (or tuple) 

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

1767 return self 

1768 

1769 def has_separate_working_tree(self) -> bool: 

1770 """ 

1771 :return: 

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

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

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

1775 to. 

1776 

1777 :note: 

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

1779 """ 

1780 if self.bare: 

1781 return False 

1782 if self.working_tree_dir: 

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

1784 else: 

1785 return False # Or raise Error? 

1786 

1787 rev_parse = rev_parse 

1788 

1789 def __repr__(self) -> str: 

1790 clazz = self.__class__ 

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

1792 

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

1794 """ 

1795 :return: 

1796 The commit which is currently being replayed while rebasing. 

1797 

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

1799 """ 

1800 if not self.git_dir: 

1801 return None 

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

1803 if not osp.isfile(rebase_head_file): 

1804 return None 

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

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

1807 return self.commit(content)