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

656 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.diff import _unquote_path 

30from git.exc import ( 

31 GitCommandError, 

32 InvalidGitRepositoryError, 

33 NoSuchPathError, 

34) 

35from git.index import IndexFile 

36from git.objects import Submodule, RootModule, Commit 

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

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

39from git.util import ( 

40 Actor, 

41 cygpath, 

42 expand_path, 

43 finalize_process, 

44 hex_to_bin, 

45 remove_password_if_present, 

46) 

47 

48from .fun import ( 

49 find_submodule_git_dir, 

50 is_git_dir, 

51 rev_parse, 

52 to_commit, 

53 touch, 

54) 

55 

56# typing ------------------------------------------------------ 

57 

58from git.types import ( 

59 CallableProgress, 

60 Commit_ish, 

61 Lit_config_levels, 

62 PathLike, 

63 TBD, 

64 Tree_ish, 

65 assert_never, 

66) 

67from typing import ( 

68 Any, 

69 BinaryIO, 

70 Callable, 

71 Dict, 

72 Iterator, 

73 List, 

74 Mapping, 

75 NamedTuple, 

76 Optional, 

77 Sequence, 

78 TYPE_CHECKING, 

79 TextIO, 

80 Tuple, 

81 Type, 

82 Union, 

83 cast, 

84) 

85 

86from git.types import ConfigLevels_Tup, TypedDict 

87 

88if TYPE_CHECKING: 

89 from git.objects import Tree 

90 from git.objects.submodule.base import UpdateProgress 

91 from git.refs.symbolic import SymbolicReference 

92 from git.remote import RemoteProgress 

93 from git.util import IterableList 

94 

95# ----------------------------------------------------------- 

96 

97_logger = logging.getLogger(__name__) 

98 

99 

100class BlameEntry(NamedTuple): 

101 commit: Commit 

102 linenos: range 

103 orig_path: Optional[str] 

104 orig_linenos: range 

105 

106 

107class Repo: 

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

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

110 

111 The following attributes are worth using: 

112 

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

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

115 repositories. 

116 

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

118 if we are a bare repository. 

119 

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

121 """ 

122 

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

124 

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

126 git = cast("Git", None) 

127 

128 working_dir: PathLike 

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

130 

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

132 _working_tree_dir: Optional[PathLike] = None 

133 

134 git_dir: PathLike 

135 """The ``.git`` repository directory.""" 

136 

137 _common_dir: PathLike = "" 

138 

139 # Precompiled regex 

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

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

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

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

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

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

146 

147 unsafe_git_init_options = [ 

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

149 "--template", 

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

151 "--separate-git-dir", 

152 ] 

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

154 

155 unsafe_git_clone_options = [ 

156 # Executes arbitrary commands: 

157 "--upload-pack", 

158 "-u", 

159 # Can override configuration variables that execute arbitrary commands: 

160 "--config", 

161 "-c", 

162 # Can install hooks that execute during clone: 

163 "--template", 

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

165 "--separate-git-dir", 

166 # Fetches from an additional caller-controlled URI: 

167 "--bundle-uri", 

168 ] 

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

170 

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

172 directly: 

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

174 

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

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

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

178 

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

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

181 

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

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

184 unintended network resources: 

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

186 """ 

187 

188 unsafe_git_archive_options = [ 

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

190 "--exec", 

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

192 "--output", 

193 "-o", 

194 # Reads from a caller-controlled filesystem path: 

195 "--add-file", 

196 # Injects a caller-controlled path and contents: 

197 "--add-virtual-file", 

198 ] 

199 

200 unsafe_git_revision_options = [ 

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

202 "--output", 

203 "-o", 

204 ] 

205 

206 unsafe_git_blame_options = unsafe_git_revision_options + [ 

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

208 "--contents", 

209 "-S", 

210 "--ignore-revs-file", 

211 ] 

212 

213 unsafe_git_diff_options = unsafe_git_revision_options + [ 

214 # Treats path operands as arbitrary filesystem paths. 

215 "--no-index", 

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

217 "-O", 

218 "--orderfile", 

219 ] 

220 

221 # Invariants 

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

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

224 

225 # Subclass configuration 

226 GitCommandWrapperType = Git 

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

228 type here.""" 

229 

230 def __init__( 

231 self, 

232 path: Optional[PathLike] = None, 

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

234 search_parent_directories: bool = False, 

235 expand_vars: bool = True, 

236 ) -> None: 

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

238 

239 .. note:: 

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

241 access does not support reftable. 

242 

243 :param path: 

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

245 

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

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

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

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

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

251 

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

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

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

255 used. 

256 

257 :param odbt: 

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

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

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

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

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

263 

264 :param search_parent_directories: 

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

266 well. 

267 

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

269 GitPython, which is considered a bug though. 

270 

271 :raise git.exc.InvalidGitRepositoryError: 

272 

273 :raise git.exc.NoSuchPathError: 

274 

275 :return: 

276 :class:`Repo` 

277 """ 

278 

279 git_dir_env = os.getenv("GIT_DIR") 

280 object_dir_env = os.getenv("GIT_OBJECT_DIRECTORY") 

281 if object_dir_env is not None: 

282 object_dir_env = osp.abspath(object_dir_env) 

283 epath = path or git_dir_env 

284 if not epath: 

285 epath = os.getcwd() 

286 epath = os.fspath(epath) 

287 if Git.is_cygwin(): 

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

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

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

291 epath = cygpath(epath) 

292 

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

294 warnings.warn( 

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

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

297 stacklevel=1, 

298 ) 

299 epath = expand_path(epath, expand_vars) 

300 if epath is not None: 

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

302 raise NoSuchPathError(epath) 

303 

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

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

306 git_dir: Optional[str] = None 

307 explicit_git_dir = not path and bool(git_dir_env) 

308 while curpath: 

309 # ABOUT osp.NORMPATH 

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

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

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

313 if not explicit_git_dir: 

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

315 try: 

316 sm_gitpath = find_submodule_git_dir(dotgit) 

317 except OSError: 

318 break 

319 if sm_gitpath is not None: 

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

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

322 self._working_tree_dir = curpath 

323 break 

324 

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

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

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

328 break 

329 

330 if is_git_dir(curpath): 

331 git_dir = curpath 

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

333 if "GIT_WORK_TREE" in os.environ: 

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

335 else: 

336 # Linked worktree administrative directories store the path to 

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

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

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

340 

341 if not osp.isabs(worktree_gitfile): 

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

343 

344 self._working_tree_dir = osp.dirname(worktree_gitfile) 

345 break 

346 

347 # from man git-config : core.worktree 

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

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

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

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

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

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

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

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

356 # working tree. 

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

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

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

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

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

362 if "GIT_WORK_TREE" in os.environ: 

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

364 break 

365 

366 if explicit_git_dir or not search_parent_directories: 

367 break 

368 curpath, tail = osp.split(curpath) 

369 if not tail: 

370 break 

371 # END while curpath 

372 

373 if git_dir is None: 

374 raise InvalidGitRepositoryError(epath) 

375 self.git_dir = git_dir 

376 

377 common_dir_env = os.getenv("GIT_COMMON_DIR") 

378 if common_dir_env is not None: 

379 self._common_dir = osp.abspath(common_dir_env) 

380 else: 

381 try: 

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

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

384 except OSError: 

385 self._common_dir = "" 

386 

387 self._bare = False 

388 try: 

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

390 except Exception: 

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

392 pass 

393 

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

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

396 self._bare = False 

397 

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

399 # that in the first place. 

400 if self._bare: 

401 self._working_tree_dir = None 

402 # END working dir handling 

403 

404 self.working_dir = self._working_tree_dir or self.common_dir 

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

406 if common_dir_env is not None: 

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

408 elif git_dir_env is not None: 

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

410 if object_dir_env is not None: 

411 self.git.update_environment(GIT_OBJECT_DIRECTORY=object_dir_env) 

412 

413 # Special handling, in special times. 

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

415 if issubclass(odbt, GitCmdObjectDB): 

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

417 else: 

418 if issubclass(odbt, gitdb.GitDB): 

419 warnings.warn( 

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

421 "Use the default GitCmdObjectDB backend instead.", 

422 DeprecationWarning, 

423 stacklevel=2, 

424 ) 

425 self.odb = odbt(rootpath) 

426 

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

428 return self 

429 

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

431 self.close() 

432 

433 def __del__(self) -> None: 

434 try: 

435 self.close() 

436 except Exception: 

437 pass 

438 

439 def close(self) -> None: 

440 if self.git: 

441 self.git.clear_cache() 

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

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

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

445 # synchronously rather than forcing a gc collection. 

446 if sys.platform == "win32": 

447 gc.collect() 

448 gitdb.util.mman.collect() 

449 if sys.platform == "win32": 

450 gc.collect() 

451 

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

453 if isinstance(rhs, Repo): 

454 return self.git_dir == rhs.git_dir 

455 return False 

456 

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

458 return not self.__eq__(rhs) 

459 

460 def __hash__(self) -> int: 

461 return hash(self.git_dir) 

462 

463 @property 

464 def description(self) -> str: 

465 """The project's description""" 

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

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

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

469 

470 @description.setter 

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

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

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

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

475 

476 @property 

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

478 """ 

479 :return: 

480 The working tree directory of our git repository. 

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

482 """ 

483 return self._working_tree_dir 

484 

485 @property 

486 def common_dir(self) -> PathLike: 

487 """ 

488 :return: 

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

490 ORIG_HEAD, COMMIT_EDITMSG, index, and logs/. 

491 """ 

492 return self._common_dir or self.git_dir 

493 

494 @property 

495 def bare(self) -> bool: 

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

497 return self._bare 

498 

499 @property 

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

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

502 in this repo. 

503 

504 :return: 

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

506 """ 

507 return Head.list_items(self) 

508 

509 @property 

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

511 """Alias for heads. 

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

513 in this repo. 

514 

515 :return: 

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

517 """ 

518 return self.heads 

519 

520 @property 

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

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

523 heads and remote references. 

524 

525 :return: 

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

527 """ 

528 return Reference.list_items(self) 

529 

530 @property 

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

532 """Alias for references. 

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

534 heads and remote references. 

535 

536 :return: 

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

538 """ 

539 return self.references 

540 

541 @property 

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

543 """ 

544 :return: 

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

546 

547 :note: 

548 This property can be expensive, as the returned 

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

550 It is recommended to reuse the object. 

551 """ 

552 return IndexFile(self) 

553 

554 @property 

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

556 """ 

557 :return: 

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

559 """ 

560 return HEAD(self, "HEAD") 

561 

562 @property 

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

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

565 manipulate remotes. 

566 

567 :return: 

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

569 """ 

570 return Remote.list_items(self) 

571 

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

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

574 

575 :raise ValueError: 

576 If no remote with such a name exists. 

577 """ 

578 r = Remote(self, name) 

579 if not r.exists(): 

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

581 return r 

582 

583 # { Submodules 

584 

585 @property 

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

587 """ 

588 :return: 

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

590 current head 

591 """ 

592 return Submodule.list_items(self) 

593 

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

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

596 

597 :raise ValueError: 

598 If no such submodule exists. 

599 """ 

600 try: 

601 return self.submodules[name] 

602 except IndexError as e: 

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

604 # END exception handling 

605 

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

607 """Create a new submodule. 

608 

609 :note: 

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

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

612 

613 :return: 

614 The created submodule. 

615 """ 

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

617 

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

619 """An iterator yielding Submodule instances. 

620 

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

622 and `kwargs`. 

623 

624 :return: 

625 Iterator 

626 """ 

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

628 

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

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

631 take the previous state into consideration. 

632 

633 :note: 

634 For more information, please see the documentation of 

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

636 """ 

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

638 

639 # }END submodules 

640 

641 @property 

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

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

644 this repo. 

645 

646 :return: 

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

648 """ 

649 return TagReference.list_items(self) 

650 

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

652 """ 

653 :return: 

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

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

656 

657 :param path: 

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

659 """ 

660 full_path = self._to_full_tag_path(path) 

661 return TagReference(self, full_path) 

662 

663 @staticmethod 

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

665 path_str = str(path) 

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

667 return path_str 

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

669 return Reference._common_path_default + "/" + path_str 

670 else: 

671 return TagReference._common_path_default + "/" + path_str 

672 

673 def create_head( 

674 self, 

675 path: PathLike, 

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

677 force: bool = False, 

678 logmsg: Optional[str] = None, 

679 ) -> "Head": 

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

681 

682 :note: 

683 For more documentation, please see the 

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

685 

686 :return: 

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

688 """ 

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

690 

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

692 """Delete the given heads. 

693 

694 :param kwargs: 

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

696 """ 

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

698 

699 def create_tag( 

700 self, 

701 path: PathLike, 

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

703 message: Optional[str] = None, 

704 force: bool = False, 

705 **kwargs: Any, 

706 ) -> TagReference: 

707 """Create a new tag reference. 

708 

709 :note: 

710 For more documentation, please see the 

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

712 

713 :return: 

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

715 """ 

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

717 

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

719 """Delete the given tag references.""" 

720 return TagReference.delete(self, *tags) 

721 

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

723 """Create a new remote. 

724 

725 For more information, please see the documentation of the 

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

727 

728 :return: 

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

730 """ 

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

732 

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

734 """Delete the given remote.""" 

735 return Remote.remove(self, remote) 

736 

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

738 if git_dir is None: 

739 git_dir = self.git_dir 

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

741 # Use the global config instead. 

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

743 config_level = "global" 

744 

745 if config_level == "system": 

746 return "/etc/gitconfig" 

747 elif config_level == "user": 

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

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

750 elif config_level == "global": 

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

752 elif config_level == "repository": 

753 repo_dir = self._common_dir or git_dir 

754 if not repo_dir: 

755 raise NotADirectoryError 

756 else: 

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

758 else: 

759 assert_never( # type: ignore[unreachable] 

760 config_level, 

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

762 ) 

763 

764 def config_reader( 

765 self, 

766 config_level: Optional[Lit_config_levels] = None, 

767 ) -> GitConfigParser: 

768 """ 

769 :return: 

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

771 configuration, but not to write it. 

772 

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

774 configuration files. 

775 

776 :param config_level: 

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

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

779 you wish to read to prevent reading multiple files. 

780 

781 :note: 

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

783 unknown, instead the global path will be used. 

784 """ 

785 return self._config_reader(config_level=config_level) 

786 

787 def _config_reader( 

788 self, 

789 config_level: Optional[Lit_config_levels] = None, 

790 git_dir: Optional[PathLike] = None, 

791 ) -> GitConfigParser: 

792 if config_level is None: 

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

794 else: 

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

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

797 

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

799 """ 

800 :return: 

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

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

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

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

805 

806 :param config_level: 

807 One of the following values: 

808 

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

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

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

812 """ 

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

814 

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

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

817 

818 :param rev: 

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

820 

821 :return: 

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

823 """ 

824 if rev is None: 

825 return self.head.commit 

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

827 

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

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

830 

831 :note: 

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

833 """ 

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

835 

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

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

838 

839 Examples:: 

840 

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

842 

843 :param rev: 

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

845 

846 :return: 

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

848 

849 :note: 

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

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

852 subsequent operations might have unexpected results. 

853 """ 

854 if rev is None: 

855 return self.head.commit.tree 

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

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

858 

859 def iter_commits( 

860 self, 

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

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

863 allow_unsafe_options: bool = False, 

864 **kwargs: Any, 

865 ) -> Iterator[Commit]: 

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

867 history of a given ref/commit. 

868 

869 :param rev: 

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

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

872 

873 :param paths: 

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

875 path or paths will be returned. 

876 

877 :param kwargs: 

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

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

880 

881 :param allow_unsafe_options: 

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

883 

884 :note: 

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

886 ``"revA...revB"`` revision specifier. 

887 

888 :return: 

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

890 """ 

891 if rev is None: 

892 rev = self.head.commit 

893 

894 if not allow_unsafe_options: 

895 Git.check_unsafe_options( 

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

897 ) 

898 

899 return Commit.iter_items( 

900 self, 

901 rev, 

902 paths, 

903 allow_unsafe_options=allow_unsafe_options, 

904 **kwargs, 

905 ) 

906 

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

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

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

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

911 

912 :param rev: 

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

914 

915 :param allow_unsafe_options: 

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

917 

918 :param kwargs: 

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

920 which does all the work. 

921 

922 :return: 

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

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

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

926 exists. 

927 

928 :raise ValueError: 

929 If fewer than two revisions are provided. 

930 

931 :raise git.exc.GitCommandError: 

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

933 """ 

934 if len(rev) < 2: 

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

936 # END handle input 

937 

938 if not allow_unsafe_options: 

939 Git.check_unsafe_options( 

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

941 ) 

942 

943 res: List[Commit] = [] 

944 try: 

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

946 except GitCommandError as err: 

947 if err.status != 1: 

948 raise 

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

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

951 return res 

952 # END exception handling 

953 

954 for line in lines: 

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

956 # END for each merge-base 

957 

958 return res 

959 

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

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

962 

963 :param ancestor_rev: 

964 Rev which should be an ancestor. 

965 

966 :param rev: 

967 Rev to test against `ancestor_rev`. 

968 

969 :return: 

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

971 """ 

972 try: 

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

974 except GitCommandError as err: 

975 if err.status == 1: 

976 return False 

977 raise 

978 return True 

979 

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

981 try: 

982 complete_sha = self.odb.partial_to_complete_sha_hex(sha) 

983 object_info = self.odb.info(complete_sha) 

984 if object_type: 

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

986 return True 

987 else: 

988 _logger.debug( 

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

990 object_info.type.decode(), 

991 object_type, 

992 ) 

993 return False 

994 else: 

995 return True 

996 except BadObject: 

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

998 return False 

999 

1000 def _get_daemon_export(self) -> bool: 

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

1002 if git_dir is None: 

1003 return False 

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

1005 return osp.exists(filename) 

1006 

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

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

1009 if git_dir is None: 

1010 return 

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

1012 fileexists = osp.exists(filename) 

1013 if value and not fileexists: 

1014 touch(filename) 

1015 elif not value and fileexists: 

1016 os.unlink(filename) 

1017 

1018 @property 

1019 def daemon_export(self) -> bool: 

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

1021 return self._get_daemon_export() 

1022 

1023 @daemon_export.setter 

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

1025 self._set_daemon_export(value) 

1026 

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

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

1029 

1030 :return: 

1031 List of strings being pathnames of alternates 

1032 """ 

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

1034 

1035 if osp.exists(alternates_path): 

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

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

1038 return alts.strip().splitlines() 

1039 return [] 

1040 

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

1042 """Set the alternates. 

1043 

1044 :param alts: 

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

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

1047 

1048 :raise git.exc.NoSuchPathError: 

1049 

1050 :note: 

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

1052 caller is responsible. 

1053 """ 

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

1055 if not alts: 

1056 if osp.isfile(alternates_path): 

1057 os.remove(alternates_path) 

1058 else: 

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

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

1061 

1062 @property 

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

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

1065 return self._get_alternates() 

1066 

1067 @alternates.setter 

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

1069 self._set_alternates(alts) 

1070 

1071 def is_dirty( 

1072 self, 

1073 index: bool = True, 

1074 working_tree: bool = True, 

1075 untracked_files: bool = False, 

1076 submodules: bool = True, 

1077 path: Optional[PathLike] = None, 

1078 ) -> bool: 

1079 """ 

1080 :return: 

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

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

1083 if the index or the working copy have changes. 

1084 """ 

1085 if self._bare: 

1086 # Bare repositories with no associated working directory are 

1087 # always considered to be clean. 

1088 return False 

1089 

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

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

1092 if not submodules: 

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

1094 if path: 

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

1096 if index: 

1097 # diff index against HEAD. 

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

1099 return True 

1100 # END index handling 

1101 if working_tree: 

1102 # diff index against working tree. 

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

1104 return True 

1105 # END working tree handling 

1106 if untracked_files: 

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

1108 return True 

1109 # END untracked files 

1110 return False 

1111 

1112 @property 

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

1114 """ 

1115 :return: 

1116 list(str,...) 

1117 

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

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

1120 

1121 :note: 

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

1123 

1124 :note: 

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

1126 please consider caching it yourself. 

1127 """ 

1128 return self._get_untracked_files() 

1129 

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

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

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

1133 # Untracked files prefix in porcelain mode 

1134 prefix = "?? " 

1135 untracked_files = [] 

1136 for line in proc.stdout: 

1137 line = line.decode(defenc) 

1138 if not line.startswith(prefix): 

1139 continue 

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

1141 # Special characters are escaped 

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

1143 filename = filename[1:-1] 

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

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

1146 untracked_files.append(filename) 

1147 finalize_process(proc) 

1148 return untracked_files 

1149 

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

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

1152 

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

1154 

1155 :param paths: 

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

1157 

1158 :return: 

1159 Subset of those paths which are ignored 

1160 """ 

1161 try: 

1162 proc: bytes = self.git.check_ignore("--", *paths, stdout_as_string=False) 

1163 except GitCommandError as err: 

1164 if err.status == 1: 

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

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

1167 return [] 

1168 else: 

1169 # Raise the exception on all other return codes. 

1170 raise 

1171 

1172 # Git C-quotes a path containing special characters, including a line feed, 

1173 # so each path is on its own line. 

1174 return [ 

1175 safe_decode(_unquote_path(path[1:-1]) if path[:1] == path[-1:] == b'"' else path) 

1176 for path in proc.split(b"\n") 

1177 ] 

1178 

1179 @property 

1180 def active_branch(self) -> Head: 

1181 """The currently active branch. 

1182 

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

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

1185 ``repo.head.commit`` instead. 

1186 

1187 :raise TypeError: 

1188 If HEAD is detached. 

1189 

1190 :raise ValueError: 

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

1192 incompatible with older clients. 

1193 

1194 :return: 

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

1196 """ 

1197 active_branch = self.head.reference 

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

1199 raise ValueError( 

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

1201 ) 

1202 return active_branch 

1203 

1204 def blame_incremental( 

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

1206 ) -> Iterator["BlameEntry"]: 

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

1208 

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

1210 stream of :class:`BlameEntry` tuples. 

1211 

1212 :param rev: 

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

1214 uncommitted changes. Otherwise, anything successfully parsed by 

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

1216 

1217 :param allow_unsafe_options: 

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

1219 

1220 :return: 

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

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

1223 the resulting file. 

1224 

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

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

1227 """ 

1228 if not allow_unsafe_options: 

1229 Git.check_unsafe_options( 

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

1231 unsafe_options=self.unsafe_git_blame_options, 

1232 clusterable_short_options="46bceflnpqstvw", 

1233 ) 

1234 

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

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

1237 

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

1239 while True: 

1240 try: 

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

1242 line = next(stream) 

1243 except StopIteration: 

1244 return 

1245 split_line = line.split() 

1246 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line 

1247 lineno = int(lineno_b) 

1248 num_lines = int(num_lines_b) 

1249 orig_lineno = int(orig_lineno_b) 

1250 if hexsha not in commits: 

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

1252 # commit. 

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

1254 while True: 

1255 try: 

1256 line = next(stream) 

1257 except StopIteration: 

1258 return 

1259 if line == b"boundary": 

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

1261 # "previous" tag. 

1262 continue 

1263 

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

1265 props[tag] = value 

1266 if tag == b"filename": 

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

1268 orig_filename = value 

1269 break 

1270 

1271 c = Commit( 

1272 self, 

1273 hex_to_bin(hexsha), 

1274 author=Actor( 

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

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

1277 ), 

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

1279 committer=Actor( 

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

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

1282 ), 

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

1284 ) 

1285 commits[hexsha] = c 

1286 else: 

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

1288 # the last line. 

1289 while True: 

1290 try: 

1291 # Will fail if we reach the EOF unexpectedly. 

1292 line = next(stream) 

1293 except StopIteration: 

1294 return 

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

1296 if tag == b"filename": 

1297 orig_filename = value 

1298 break 

1299 

1300 yield BlameEntry( 

1301 commits[hexsha], 

1302 range(lineno, lineno + num_lines), 

1303 safe_decode(orig_filename), 

1304 range(orig_lineno, orig_lineno + num_lines), 

1305 ) 

1306 

1307 def blame( 

1308 self, 

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

1310 file: str, 

1311 incremental: bool = False, 

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

1313 allow_unsafe_options: bool = False, 

1314 **kwargs: Any, 

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

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

1317 

1318 :param rev: 

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

1320 uncommitted changes. Otherwise, anything successfully parsed by 

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

1322 

1323 :param allow_unsafe_options: 

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

1325 

1326 :return: 

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

1328 

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

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

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

1332 appearance. 

1333 """ 

1334 if incremental: 

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

1336 rev_opts_list = list(rev_opts or []) 

1337 if not allow_unsafe_options: 

1338 Git.check_unsafe_options( 

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

1340 unsafe_options=self.unsafe_git_blame_options, 

1341 clusterable_short_options="46bceflnpqstvw", 

1342 ) 

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

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

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

1346 

1347 class InfoTD(TypedDict, total=False): 

1348 sha: str 

1349 id: str 

1350 filename: str 

1351 summary: str 

1352 author: str 

1353 author_email: str 

1354 author_date: int 

1355 committer: str 

1356 committer_email: str 

1357 committer_date: int 

1358 

1359 info: InfoTD = {} 

1360 

1361 keepends = True 

1362 for line_bytes in data.splitlines(keepends): 

1363 line_str = "" 

1364 try: 

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

1366 except UnicodeDecodeError: 

1367 firstpart = "" 

1368 parts = [] 

1369 is_binary = True 

1370 else: 

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

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

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

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

1375 # instead. 

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

1377 firstpart = parts[0] 

1378 is_binary = False 

1379 # END handle decode of line 

1380 

1381 if self.re_hexsha_only.search(firstpart): 

1382 # handles 

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

1384 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates 

1385 # another line of blame with the same data 

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

1387 if len(digits) == 3: 

1388 info = {"id": firstpart} 

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

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

1391 info = {"id": firstpart} 

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

1393 # END blame data initialization 

1394 else: 

1395 m = self.re_author_committer_start.search(firstpart) 

1396 if m: 

1397 # handles: 

1398 # author Tom Preston-Werner 

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

1400 # author-time 1192271832 

1401 # author-tz -0700 

1402 # committer Tom Preston-Werner 

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

1404 # committer-time 1192271832 

1405 # committer-tz -0700 - IGNORED BY US 

1406 role = m.group(0) 

1407 if role == "author": 

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

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

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

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

1412 elif role == firstpart: 

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

1414 elif role == "committer": 

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

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

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

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

1419 elif role == firstpart: 

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

1421 # END distinguish mail,time,name 

1422 else: 

1423 # handle 

1424 # filename lib/grit.rb 

1425 # summary add Blob 

1426 # <and rest> 

1427 if firstpart.startswith("filename"): 

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

1429 elif firstpart.startswith("summary"): 

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

1431 elif firstpart == "": 

1432 if info: 

1433 sha = info["id"] 

1434 c = commits.get(sha) 

1435 if c is None: 

1436 c = Commit( 

1437 self, 

1438 hex_to_bin(sha), 

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

1440 authored_date=info["author_date"], 

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

1442 committed_date=info["committer_date"], 

1443 ) 

1444 commits[sha] = c 

1445 blames[-1][0] = c 

1446 # END if commit objects needs initial creation 

1447 

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

1449 line: str | bytes 

1450 if not is_binary: 

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

1452 line_str = line_str[1:] 

1453 line = line_str 

1454 else: 

1455 line = line_bytes 

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

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

1458 # along the newline separator. We will append this 

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

1460 # though it should be concatenated with the last 

1461 # line we have seen. 

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

1463 

1464 info = {"id": sha} 

1465 # END if we collected commit info 

1466 # END distinguish filename,summary,rest 

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

1468 # END distinguish hexsha vs other information 

1469 return blames 

1470 

1471 @classmethod 

1472 def init( 

1473 cls, 

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

1475 mkdir: bool = True, 

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

1477 expand_vars: bool = True, 

1478 allow_unsafe_options: bool = False, 

1479 **kwargs: Any, 

1480 ) -> "Repo": 

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

1482 

1483 :param path: 

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

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

1486 working directory. 

1487 

1488 :param mkdir: 

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

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

1491 Only effective if a path is explicitly given. 

1492 

1493 :param odbt: 

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

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

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

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

1498 

1499 :param expand_vars: 

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

1501 information disclosure, allowing attackers to access the contents of 

1502 environment variables. 

1503 

1504 :param allow_unsafe_options: 

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

1506 ``--separate-git-dir``. 

1507 

1508 :param kwargs: 

1509 Keyword arguments serving as additional options to the 

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

1511 

1512 :return: 

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

1514 """ 

1515 if not allow_unsafe_options: 

1516 Git.check_unsafe_options( 

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

1518 unsafe_options=cls.unsafe_git_init_options, 

1519 ) 

1520 if path: 

1521 path = expand_path(path, expand_vars) 

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

1523 os.makedirs(path, 0o755) 

1524 

1525 # git command automatically chdir into the directory 

1526 git = cls.GitCommandWrapperType(path) 

1527 git.init(**kwargs) 

1528 return cls(path, odbt=odbt) 

1529 

1530 @classmethod 

1531 def _clone( 

1532 cls, 

1533 git: "Git", 

1534 url: PathLike, 

1535 path: PathLike, 

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

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

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

1539 allow_unsafe_protocols: bool = False, 

1540 allow_unsafe_options: bool = False, 

1541 **kwargs: Any, 

1542 ) -> "Repo": 

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

1544 

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

1546 url = os.fspath(url) 

1547 path = os.fspath(path) 

1548 

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

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

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

1552 # becomes:: 

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

1554 # 

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

1556 sep_dir = kwargs.get("separate_git_dir") 

1557 if sep_dir: 

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

1559 multi = None 

1560 if multi_options: 

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

1562 

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

1564 if not allow_unsafe_protocols: 

1565 Git.check_unsafe_protocols(clone_url) 

1566 if not allow_unsafe_options: 

1567 Git.check_unsafe_options( 

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

1569 unsafe_options=cls.unsafe_git_clone_options, 

1570 ) 

1571 if not allow_unsafe_options and multi: 

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

1573 

1574 proc = git.clone( 

1575 multi, 

1576 "--", 

1577 clone_url, 

1578 clone_path, 

1579 with_extended_output=True, 

1580 as_process=True, 

1581 v=True, 

1582 universal_newlines=True, 

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

1584 ) 

1585 if progress: 

1586 handle_process_output( 

1587 proc, 

1588 None, 

1589 to_progress_instance(progress).new_message_handler(), 

1590 finalize_process, 

1591 decode_streams=False, 

1592 ) 

1593 else: 

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

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

1596 cmdline = remove_password_if_present(cmdline) 

1597 

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

1599 finalize_process(proc, stderr=stderr) 

1600 

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

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

1603 if not osp.isabs(path): 

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

1605 

1606 repo = cls(path, odbt=odbt) 

1607 

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

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

1610 

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

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

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

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

1615 if repo.remotes: 

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

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

1618 # END handle remote repo 

1619 return repo 

1620 

1621 def clone( 

1622 self, 

1623 path: PathLike, 

1624 progress: Optional[CallableProgress] = None, 

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

1626 allow_unsafe_protocols: bool = False, 

1627 allow_unsafe_options: bool = False, 

1628 **kwargs: Any, 

1629 ) -> "Repo": 

1630 """Create a clone from this repository. 

1631 

1632 :param path: 

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

1634 

1635 :param progress: 

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

1637 

1638 :param multi_options: 

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

1640 times. 

1641 

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

1643 For example:: 

1644 

1645 [ 

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

1647 "--config core.ignorecase", 

1648 "--recurse-submodule=repo1_path", 

1649 "--recurse-submodule=repo2_path", 

1650 ] 

1651 

1652 :param allow_unsafe_protocols: 

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

1654 

1655 :param allow_unsafe_options: 

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

1657 

1658 :param kwargs: 

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

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

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

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

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

1664 command. 

1665 

1666 :return: 

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

1668 """ 

1669 return self._clone( 

1670 self.git, 

1671 self.common_dir, 

1672 path, 

1673 type(self.odb), 

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

1675 multi_options, 

1676 allow_unsafe_protocols=allow_unsafe_protocols, 

1677 allow_unsafe_options=allow_unsafe_options, 

1678 **kwargs, 

1679 ) 

1680 

1681 @classmethod 

1682 def clone_from( 

1683 cls, 

1684 url: PathLike, 

1685 to_path: PathLike, 

1686 progress: CallableProgress = None, 

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

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

1689 allow_unsafe_protocols: bool = False, 

1690 allow_unsafe_options: bool = False, 

1691 **kwargs: Any, 

1692 ) -> "Repo": 

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

1694 

1695 :param url: 

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

1697 

1698 :param to_path: 

1699 Path to which the repository should be cloned to. 

1700 

1701 :param progress: 

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

1703 

1704 :param env: 

1705 Optional dictionary containing the desired environment variables. 

1706 

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

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

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

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

1711 

1712 :param multi_options: 

1713 See the :meth:`clone` method. 

1714 

1715 :param allow_unsafe_protocols: 

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

1717 

1718 :param allow_unsafe_options: 

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

1720 

1721 :param kwargs: 

1722 See the :meth:`clone` method. 

1723 

1724 :return: 

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

1726 """ 

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

1728 if env is not None: 

1729 git.update_environment(**env) 

1730 return cls._clone( 

1731 git, 

1732 url, 

1733 to_path, 

1734 GitCmdObjectDB, 

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

1736 multi_options, 

1737 allow_unsafe_protocols=allow_unsafe_protocols, 

1738 allow_unsafe_options=allow_unsafe_options, 

1739 **kwargs, 

1740 ) 

1741 

1742 def archive( 

1743 self, 

1744 ostream: Union[TextIO, BinaryIO], 

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

1746 prefix: Optional[str] = None, 

1747 allow_unsafe_options: bool = False, 

1748 allow_unsafe_protocols: bool = False, 

1749 **kwargs: Any, 

1750 ) -> Repo: 

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

1752 

1753 :param ostream: 

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

1755 

1756 :param treeish: 

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

1758 

1759 :param prefix: 

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

1761 

1762 :param kwargs: 

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

1764 

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

1766 ostreams to write any format supported by Python. 

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

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

1769 or a list or tuple of multiple paths. 

1770 

1771 :param allow_unsafe_options: 

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

1773 

1774 :param allow_unsafe_protocols: 

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

1776 

1777 :raise git.exc.GitCommandError: 

1778 If something went wrong. 

1779 

1780 :return: 

1781 self 

1782 """ 

1783 if treeish is None: 

1784 treeish = self.head.commit 

1785 if prefix and "prefix" not in kwargs: 

1786 kwargs["prefix"] = prefix 

1787 if not allow_unsafe_protocols: 

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

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

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

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

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

1793 Git.check_unsafe_protocols(remote) 

1794 if not allow_unsafe_options: 

1795 Git.check_unsafe_options( 

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

1797 unsafe_options=self.unsafe_git_archive_options, 

1798 ) 

1799 kwargs["output_stream"] = ostream 

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

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

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

1803 path = [path] 

1804 # END ensure paths is list (or tuple) 

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

1806 return self 

1807 

1808 def has_separate_working_tree(self) -> bool: 

1809 """ 

1810 :return: 

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

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

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

1814 to. 

1815 

1816 :note: 

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

1818 """ 

1819 if self.bare: 

1820 return False 

1821 if self.working_tree_dir: 

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

1823 else: 

1824 return False # Or raise Error? 

1825 

1826 rev_parse = rev_parse 

1827 

1828 def __repr__(self) -> str: 

1829 clazz = self.__class__ 

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

1831 

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

1833 """ 

1834 :return: 

1835 The commit which is currently being replayed while rebasing. 

1836 

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

1838 """ 

1839 if not self.git_dir: 

1840 return None 

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

1842 if not osp.isfile(rebase_head_file): 

1843 return None 

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

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

1846 return self.commit(content)