Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/repo/base.py: 46%
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
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
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/
6from __future__ import annotations
8__all__ = ["Repo"]
10import gc
11import logging
12import os
13import os.path as osp
14from pathlib import Path
15import re
16import shlex
17import sys
18import warnings
20import gitdb
21from gitdb.db.loose import LooseObjectDB
22from gitdb.exc import BadObject
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)
46from .fun import (
47 find_submodule_git_dir,
48 find_worktree_git_dir,
49 is_git_dir,
50 rev_parse,
51 touch,
52)
54# typing ------------------------------------------------------
56from git.types import (
57 CallableProgress,
58 Commit_ish,
59 Lit_config_levels,
60 PathLike,
61 TBD,
62 Tree_ish,
63 assert_never,
64)
65from typing import (
66 Any,
67 BinaryIO,
68 Callable,
69 Dict,
70 Iterator,
71 List,
72 Mapping,
73 NamedTuple,
74 Optional,
75 Sequence,
76 TYPE_CHECKING,
77 TextIO,
78 Tuple,
79 Type,
80 Union,
81 cast,
82)
84from git.types import ConfigLevels_Tup, TypedDict
86if TYPE_CHECKING:
87 from git.objects import Tree
88 from git.objects.submodule.base import UpdateProgress
89 from git.refs.symbolic import SymbolicReference
90 from git.remote import RemoteProgress
91 from git.util import IterableList
93# -----------------------------------------------------------
95_logger = logging.getLogger(__name__)
98class BlameEntry(NamedTuple):
99 commit: Dict[str, Commit]
100 linenos: range
101 orig_path: Optional[str]
102 orig_linenos: range
105class Repo:
106 """Represents a git repository and allows you to query references, create commit
107 information, generate diffs, create and clone repositories, and query the log.
109 The following attributes are worth using:
111 * :attr:`working_dir` is the working directory of the git command, which is the
112 working tree directory if available or the ``.git`` directory in case of bare
113 repositories.
115 * :attr:`working_tree_dir` is the working tree directory, but will return ``None``
116 if we are a bare repository.
118 * :attr:`git_dir` is the ``.git`` repository directory, which is always set.
119 """
121 DAEMON_EXPORT_FILE = "git-daemon-export-ok"
123 # Must exist, or __del__ will fail in case we raise on `__init__()`.
124 git = cast("Git", None)
126 working_dir: PathLike
127 """The working directory of the git command."""
129 _working_tree_dir: Optional[PathLike] = None
131 git_dir: PathLike
132 """The ``.git`` repository directory."""
134 _common_dir: PathLike = ""
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(.*)$")
144 unsafe_git_clone_options = [
145 # Executes arbitrary commands:
146 "--upload-pack",
147 "-u",
148 # Can override configuration variables that execute arbitrary commands:
149 "--config",
150 "-c",
151 ]
152 """Options to :manpage:`git-clone(1)` that allow arbitrary commands to be executed.
154 The ``--upload-pack``/``-u`` option allows users to execute arbitrary commands
155 directly:
156 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt
158 The ``--config``/``-c`` option allows users to override configuration variables like
159 ``protocol.allow`` and ``core.gitProxy`` to execute arbitrary commands:
160 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt
161 """
163 # Invariants
164 config_level: ConfigLevels_Tup = ("system", "user", "global", "repository")
165 """Represents the configuration level of a configuration file."""
167 # Subclass configuration
168 GitCommandWrapperType = Git
169 """Subclasses may easily bring in their own custom types by placing a constructor or
170 type here."""
172 def __init__(
173 self,
174 path: Optional[PathLike] = None,
175 odbt: Type[LooseObjectDB] = GitCmdObjectDB,
176 search_parent_directories: bool = False,
177 expand_vars: bool = True,
178 ) -> None:
179 R"""Create a new :class:`Repo` instance.
181 :param path:
182 The path to either the worktree directory or the .git directory itself::
184 repo = Repo("/Users/mtrier/Development/git-python")
185 repo = Repo("/Users/mtrier/Development/git-python.git")
186 repo = Repo("~/Development/git-python.git")
187 repo = Repo("$REPOSITORIES/Development/git-python.git")
188 repo = Repo(R"C:\Users\mtrier\Development\git-python\.git")
190 - In *Cygwin*, `path` may be a ``cygdrive/...`` prefixed path.
191 - If `path` is ``None`` or an empty string, :envvar:`GIT_DIR` is used. If
192 that environment variable is absent or empty, the current directory is
193 used.
195 :param odbt:
196 Object DataBase type - a type which is constructed by providing the
197 directory containing the database objects, i.e. ``.git/objects``. It will be
198 used to access all object data.
200 :param search_parent_directories:
201 If ``True``, all parent directories will be searched for a valid repo as
202 well.
204 Please note that this was the default behaviour in older versions of
205 GitPython, which is considered a bug though.
207 :raise git.exc.InvalidGitRepositoryError:
209 :raise git.exc.NoSuchPathError:
211 :return:
212 :class:`Repo`
213 """
215 epath = path or os.getenv("GIT_DIR")
216 if not epath:
217 epath = os.getcwd()
218 if Git.is_cygwin():
219 # Given how the tests are written, this seems more likely to catch Cygwin
220 # git used from Windows than Windows git used from Cygwin. Therefore
221 # changing to Cygwin-style paths is the relevant operation.
222 epath = cygpath(str(epath))
224 epath = epath or path or os.getcwd()
225 if not isinstance(epath, str):
226 epath = str(epath)
227 if expand_vars and re.search(self.re_envvars, epath):
228 warnings.warn(
229 "The use of environment variables in paths is deprecated"
230 + "\nfor security reasons and may be removed in the future!!",
231 stacklevel=1,
232 )
233 epath = expand_path(epath, expand_vars)
234 if epath is not None:
235 if not os.path.exists(epath):
236 raise NoSuchPathError(epath)
238 # Walk up the path to find the `.git` dir.
239 curpath = epath
240 git_dir = None
241 while curpath:
242 # ABOUT osp.NORMPATH
243 # It's important to normalize the paths, as submodules will otherwise
244 # initialize their repo instances with paths that depend on path-portions
245 # that will not exist after being removed. It's just cleaner.
246 if is_git_dir(curpath):
247 git_dir = curpath
248 # from man git-config : core.worktree
249 # Set the path to the root of the working tree. If GIT_COMMON_DIR
250 # environment variable is set, core.worktree is ignored and not used for
251 # determining the root of working tree. This can be overridden by the
252 # GIT_WORK_TREE environment variable. The value can be an absolute path
253 # or relative to the path to the .git directory, which is either
254 # specified by GIT_DIR, or automatically discovered. If GIT_DIR is
255 # specified but none of GIT_WORK_TREE and core.worktree is specified,
256 # the current working directory is regarded as the top level of your
257 # working tree.
258 self._working_tree_dir = os.path.dirname(git_dir)
259 if os.environ.get("GIT_COMMON_DIR") is None:
260 gitconf = self._config_reader("repository", git_dir)
261 if gitconf.has_option("core", "worktree"):
262 self._working_tree_dir = gitconf.get("core", "worktree")
263 if "GIT_WORK_TREE" in os.environ:
264 self._working_tree_dir = os.getenv("GIT_WORK_TREE")
265 break
267 dotgit = osp.join(curpath, ".git")
268 sm_gitpath = find_submodule_git_dir(dotgit)
269 if sm_gitpath is not None:
270 git_dir = osp.normpath(sm_gitpath)
272 sm_gitpath = find_submodule_git_dir(dotgit)
273 if sm_gitpath is None:
274 sm_gitpath = find_worktree_git_dir(dotgit)
276 if sm_gitpath is not None:
277 git_dir = expand_path(sm_gitpath, expand_vars)
278 self._working_tree_dir = curpath
279 break
281 if not search_parent_directories:
282 break
283 curpath, tail = osp.split(curpath)
284 if not tail:
285 break
286 # END while curpath
288 if git_dir is None:
289 raise InvalidGitRepositoryError(epath)
290 self.git_dir = git_dir
292 self._bare = False
293 try:
294 self._bare = self.config_reader("repository").getboolean("core", "bare")
295 except Exception:
296 # Let's not assume the option exists, although it should.
297 pass
299 try:
300 common_dir = (Path(self.git_dir) / "commondir").read_text().splitlines()[0].strip()
301 self._common_dir = osp.join(self.git_dir, common_dir)
302 except OSError:
303 self._common_dir = ""
305 # Adjust the working directory in case we are actually bare - we didn't know
306 # that in the first place.
307 if self._bare:
308 self._working_tree_dir = None
309 # END working dir handling
311 self.working_dir: PathLike = self._working_tree_dir or self.common_dir
312 self.git = self.GitCommandWrapperType(self.working_dir)
314 # Special handling, in special times.
315 rootpath = osp.join(self.common_dir, "objects")
316 if issubclass(odbt, GitCmdObjectDB):
317 self.odb = odbt(rootpath, self.git)
318 else:
319 self.odb = odbt(rootpath)
321 def __enter__(self) -> "Repo":
322 return self
324 def __exit__(self, *args: Any) -> None:
325 self.close()
327 def __del__(self) -> None:
328 try:
329 self.close()
330 except Exception:
331 pass
333 def close(self) -> None:
334 if self.git:
335 self.git.clear_cache()
336 # Tempfiles objects on Windows are holding references to open files until
337 # they are collected by the garbage collector, thus preventing deletion.
338 # TODO: Find these references and ensure they are closed and deleted
339 # synchronously rather than forcing a gc collection.
340 if sys.platform == "win32":
341 gc.collect()
342 gitdb.util.mman.collect()
343 if sys.platform == "win32":
344 gc.collect()
346 def __eq__(self, rhs: object) -> bool:
347 if isinstance(rhs, Repo):
348 return self.git_dir == rhs.git_dir
349 return False
351 def __ne__(self, rhs: object) -> bool:
352 return not self.__eq__(rhs)
354 def __hash__(self) -> int:
355 return hash(self.git_dir)
357 @property
358 def description(self) -> str:
359 """The project's description"""
360 filename = osp.join(self.git_dir, "description")
361 with open(filename, "rb") as fp:
362 return fp.read().rstrip().decode(defenc)
364 @description.setter
365 def description(self, descr: str) -> None:
366 filename = osp.join(self.git_dir, "description")
367 with open(filename, "wb") as fp:
368 fp.write((descr + "\n").encode(defenc))
370 @property
371 def working_tree_dir(self) -> Optional[PathLike]:
372 """
373 :return:
374 The working tree directory of our git repository.
375 If this is a bare repository, ``None`` is returned.
376 """
377 return self._working_tree_dir
379 @property
380 def common_dir(self) -> PathLike:
381 """
382 :return:
383 The git dir that holds everything except possibly HEAD, FETCH_HEAD,
384 ORIG_HEAD, COMMIT_EDITMSG, index, and logs/.
385 """
386 return self._common_dir or self.git_dir
388 @property
389 def bare(self) -> bool:
390 """:return: ``True`` if the repository is bare"""
391 return self._bare
393 @property
394 def heads(self) -> "IterableList[Head]":
395 """A list of :class:`~git.refs.head.Head` objects representing the branch heads
396 in this repo.
398 :return:
399 ``git.IterableList(Head, ...)``
400 """
401 return Head.list_items(self)
403 @property
404 def branches(self) -> "IterableList[Head]":
405 """Alias for heads.
406 A list of :class:`~git.refs.head.Head` objects representing the branch heads
407 in this repo.
409 :return:
410 ``git.IterableList(Head, ...)``
411 """
412 return self.heads
414 @property
415 def references(self) -> "IterableList[Reference]":
416 """A list of :class:`~git.refs.reference.Reference` objects representing tags,
417 heads and remote references.
419 :return:
420 ``git.IterableList(Reference, ...)``
421 """
422 return Reference.list_items(self)
424 @property
425 def refs(self) -> "IterableList[Reference]":
426 """Alias for references.
427 A list of :class:`~git.refs.reference.Reference` objects representing tags,
428 heads and remote references.
430 :return:
431 ``git.IterableList(Reference, ...)``
432 """
433 return self.references
435 @property
436 def index(self) -> "IndexFile":
437 """
438 :return:
439 A :class:`~git.index.base.IndexFile` representing this repository's index.
441 :note:
442 This property can be expensive, as the returned
443 :class:`~git.index.base.IndexFile` will be reinitialized.
444 It is recommended to reuse the object.
445 """
446 return IndexFile(self)
448 @property
449 def head(self) -> "HEAD":
450 """
451 :return:
452 :class:`~git.refs.head.HEAD` object pointing to the current head reference
453 """
454 return HEAD(self, "HEAD")
456 @property
457 def remotes(self) -> "IterableList[Remote]":
458 """A list of :class:`~git.remote.Remote` objects allowing to access and
459 manipulate remotes.
461 :return:
462 ``git.IterableList(Remote, ...)``
463 """
464 return Remote.list_items(self)
466 def remote(self, name: str = "origin") -> "Remote":
467 """:return: The remote with the specified name
469 :raise ValueError:
470 If no remote with such a name exists.
471 """
472 r = Remote(self, name)
473 if not r.exists():
474 raise ValueError("Remote named '%s' didn't exist" % name)
475 return r
477 # { Submodules
479 @property
480 def submodules(self) -> "IterableList[Submodule]":
481 """
482 :return:
483 git.IterableList(Submodule, ...) of direct submodules available from the
484 current head
485 """
486 return Submodule.list_items(self)
488 def submodule(self, name: str) -> "Submodule":
489 """:return: The submodule with the given name
491 :raise ValueError:
492 If no such submodule exists.
493 """
494 try:
495 return self.submodules[name]
496 except IndexError as e:
497 raise ValueError("Didn't find submodule named %r" % name) from e
498 # END exception handling
500 def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule:
501 """Create a new submodule.
503 :note:
504 For a description of the applicable parameters, see the documentation of
505 :meth:`Submodule.add <git.objects.submodule.base.Submodule.add>`.
507 :return:
508 The created submodule.
509 """
510 return Submodule.add(self, *args, **kwargs)
512 def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
513 """An iterator yielding Submodule instances.
515 See the :class:`~git.objects.util.Traversable` interface for a description of `args`
516 and `kwargs`.
518 :return:
519 Iterator
520 """
521 return RootModule(self).traverse(*args, **kwargs)
523 def submodule_update(self, *args: Any, **kwargs: Any) -> RootModule:
524 """Update the submodules, keeping the repository consistent as it will
525 take the previous state into consideration.
527 :note:
528 For more information, please see the documentation of
529 :meth:`RootModule.update <git.objects.submodule.root.RootModule.update>`.
530 """
531 return RootModule(self).update(*args, **kwargs)
533 # }END submodules
535 @property
536 def tags(self) -> "IterableList[TagReference]":
537 """A list of :class:`~git.refs.tag.TagReference` objects that are available in
538 this repo.
540 :return:
541 ``git.IterableList(TagReference, ...)``
542 """
543 return TagReference.list_items(self)
545 def tag(self, path: PathLike) -> TagReference:
546 """
547 :return:
548 :class:`~git.refs.tag.TagReference` object, reference pointing to a
549 :class:`~git.objects.commit.Commit` or tag
551 :param path:
552 Path to the tag reference, e.g. ``0.1.5`` or ``tags/0.1.5``.
553 """
554 full_path = self._to_full_tag_path(path)
555 return TagReference(self, full_path)
557 @staticmethod
558 def _to_full_tag_path(path: PathLike) -> str:
559 path_str = str(path)
560 if path_str.startswith(TagReference._common_path_default + "/"):
561 return path_str
562 if path_str.startswith(TagReference._common_default + "/"):
563 return Reference._common_path_default + "/" + path_str
564 else:
565 return TagReference._common_path_default + "/" + path_str
567 def create_head(
568 self,
569 path: PathLike,
570 commit: Union["SymbolicReference", "str"] = "HEAD",
571 force: bool = False,
572 logmsg: Optional[str] = None,
573 ) -> "Head":
574 """Create a new head within the repository.
576 :note:
577 For more documentation, please see the
578 :meth:`Head.create <git.refs.head.Head.create>` method.
580 :return:
581 Newly created :class:`~git.refs.head.Head` Reference.
582 """
583 return Head.create(self, path, commit, logmsg, force)
585 def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None:
586 """Delete the given heads.
588 :param kwargs:
589 Additional keyword arguments to be passed to :manpage:`git-branch(1)`.
590 """
591 return Head.delete(self, *heads, **kwargs)
593 def create_tag(
594 self,
595 path: PathLike,
596 ref: Union[str, "SymbolicReference"] = "HEAD",
597 message: Optional[str] = None,
598 force: bool = False,
599 **kwargs: Any,
600 ) -> TagReference:
601 """Create a new tag reference.
603 :note:
604 For more documentation, please see the
605 :meth:`TagReference.create <git.refs.tag.TagReference.create>` method.
607 :return:
608 :class:`~git.refs.tag.TagReference` object
609 """
610 return TagReference.create(self, path, ref, message, force, **kwargs)
612 def delete_tag(self, *tags: TagReference) -> None:
613 """Delete the given tag references."""
614 return TagReference.delete(self, *tags)
616 def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote:
617 """Create a new remote.
619 For more information, please see the documentation of the
620 :meth:`Remote.create <git.remote.Remote.create>` method.
622 :return:
623 :class:`~git.remote.Remote` reference
624 """
625 return Remote.create(self, name, url, **kwargs)
627 def delete_remote(self, remote: "Remote") -> str:
628 """Delete the given remote."""
629 return Remote.remove(self, remote)
631 def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[PathLike] = None) -> str:
632 if git_dir is None:
633 git_dir = self.git_dir
634 # We do not support an absolute path of the gitconfig on Windows.
635 # Use the global config instead.
636 if sys.platform == "win32" and config_level == "system":
637 config_level = "global"
639 if config_level == "system":
640 return "/etc/gitconfig"
641 elif config_level == "user":
642 config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", "~"), ".config")
643 return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config")))
644 elif config_level == "global":
645 return osp.normpath(osp.expanduser("~/.gitconfig"))
646 elif config_level == "repository":
647 repo_dir = self._common_dir or git_dir
648 if not repo_dir:
649 raise NotADirectoryError
650 else:
651 return osp.normpath(osp.join(repo_dir, "config"))
652 else:
653 assert_never( # type: ignore[unreachable]
654 config_level,
655 ValueError(f"Invalid configuration level: {config_level!r}"),
656 )
658 def config_reader(
659 self,
660 config_level: Optional[Lit_config_levels] = None,
661 ) -> GitConfigParser:
662 """
663 :return:
664 :class:`~git.config.GitConfigParser` allowing to read the full git
665 configuration, but not to write it.
667 The configuration will include values from the system, user and repository
668 configuration files.
670 :param config_level:
671 For possible values, see the :meth:`config_writer` method. If ``None``, all
672 applicable levels will be used. Specify a level in case you know which file
673 you wish to read to prevent reading multiple files.
675 :note:
676 On Windows, system configuration cannot currently be read as the path is
677 unknown, instead the global path will be used.
678 """
679 return self._config_reader(config_level=config_level)
681 def _config_reader(
682 self,
683 config_level: Optional[Lit_config_levels] = None,
684 git_dir: Optional[PathLike] = None,
685 ) -> GitConfigParser:
686 if config_level is None:
687 files = [self._get_config_path(f, git_dir) for f in self.config_level if f]
688 else:
689 files = [self._get_config_path(config_level, git_dir)]
690 return GitConfigParser(files, read_only=True, repo=self)
692 def config_writer(self, config_level: Lit_config_levels = "repository") -> GitConfigParser:
693 """
694 :return:
695 A :class:`~git.config.GitConfigParser` allowing to write values of the
696 specified configuration file level. Config writers should be retrieved, used
697 to change the configuration, and written right away as they will lock the
698 configuration file in question and prevent other's to write it.
700 :param config_level:
701 One of the following values:
703 * ``"system"`` = system wide configuration file
704 * ``"global"`` = user level configuration file
705 * ``"`repository"`` = configuration file for this repository only
706 """
707 return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self, merge_includes=False)
709 def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit:
710 """The :class:`~git.objects.commit.Commit` object for the specified revision.
712 :param rev:
713 Revision specifier, see :manpage:`git-rev-parse(1)` for viable options.
715 :return:
716 :class:`~git.objects.commit.Commit`
717 """
718 if rev is None:
719 return self.head.commit
720 return self.rev_parse(str(rev) + "^0")
722 def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator["Tree"]:
723 """:return: Iterator yielding :class:`~git.objects.tree.Tree` objects
725 :note:
726 Accepts all arguments known to the :meth:`iter_commits` method.
727 """
728 return (c.tree for c in self.iter_commits(*args, **kwargs))
730 def tree(self, rev: Union[Tree_ish, str, None] = None) -> "Tree":
731 """The :class:`~git.objects.tree.Tree` object for the given tree-ish revision.
733 Examples::
735 repo.tree(repo.heads[0])
737 :param rev:
738 A revision pointing to a Treeish (being a commit or tree).
740 :return:
741 :class:`~git.objects.tree.Tree`
743 :note:
744 If you need a non-root level tree, find it by iterating the root tree.
745 Otherwise it cannot know about its path relative to the repository root and
746 subsequent operations might have unexpected results.
747 """
748 if rev is None:
749 return self.head.commit.tree
750 return self.rev_parse(str(rev) + "^{tree}")
752 def iter_commits(
753 self,
754 rev: Union[str, Commit, "SymbolicReference", None] = None,
755 paths: Union[PathLike, Sequence[PathLike]] = "",
756 **kwargs: Any,
757 ) -> Iterator[Commit]:
758 """An iterator of :class:`~git.objects.commit.Commit` objects representing the
759 history of a given ref/commit.
761 :param rev:
762 Revision specifier, see :manpage:`git-rev-parse(1)` for viable options.
763 If ``None``, the active branch will be used.
765 :param paths:
766 An optional path or a list of paths. If set, only commits that include the
767 path or paths will be returned.
769 :param kwargs:
770 Arguments to be passed to :manpage:`git-rev-list(1)`.
771 Common ones are ``max_count`` and ``skip``.
773 :note:
774 To receive only commits between two named revisions, use the
775 ``"revA...revB"`` revision specifier.
777 :return:
778 Iterator of :class:`~git.objects.commit.Commit` objects
779 """
780 if rev is None:
781 rev = self.head.commit
783 return Commit.iter_items(self, rev, paths, **kwargs)
785 def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]:
786 R"""Find the closest common ancestor for the given revision
787 (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s,
788 :class:`~git.refs.reference.Reference`\s, etc.).
790 :param rev:
791 At least two revs to find the common ancestor for.
793 :param kwargs:
794 Additional arguments to be passed to the ``repo.git.merge_base()`` command
795 which does all the work.
797 :return:
798 A list of :class:`~git.objects.commit.Commit` objects. If ``--all`` was
799 not passed as a keyword argument, the list will have at max one
800 :class:`~git.objects.commit.Commit`, or is empty if no common merge base
801 exists.
803 :raise ValueError:
804 If fewer than two revisions are provided.
805 """
806 if len(rev) < 2:
807 raise ValueError("Please specify at least two revs, got only %i" % len(rev))
808 # END handle input
810 res: List[Commit] = []
811 try:
812 lines: List[str] = self.git.merge_base(*rev, **kwargs).splitlines()
813 except GitCommandError as err:
814 if err.status == 128:
815 raise
816 # END handle invalid rev
817 # Status code 1 is returned if there is no merge-base.
818 # (See: https://github.com/git/git/blob/v2.44.0/builtin/merge-base.c#L19)
819 return res
820 # END exception handling
822 for line in lines:
823 res.append(self.commit(line))
824 # END for each merge-base
826 return res
828 def is_ancestor(self, ancestor_rev: Commit, rev: Commit) -> bool:
829 """Check if a commit is an ancestor of another.
831 :param ancestor_rev:
832 Rev which should be an ancestor.
834 :param rev:
835 Rev to test against `ancestor_rev`.
837 :return:
838 ``True`` if `ancestor_rev` is an ancestor to `rev`.
839 """
840 try:
841 self.git.merge_base(ancestor_rev, rev, is_ancestor=True)
842 except GitCommandError as err:
843 if err.status == 1:
844 return False
845 raise
846 return True
848 def is_valid_object(self, sha: str, object_type: Union[str, None] = None) -> bool:
849 try:
850 complete_sha = self.odb.partial_to_complete_sha_hex(sha)
851 object_info = self.odb.info(complete_sha)
852 if object_type:
853 if object_info.type == object_type.encode():
854 return True
855 else:
856 _logger.debug(
857 "Commit hash points to an object of type '%s'. Requested were objects of type '%s'",
858 object_info.type.decode(),
859 object_type,
860 )
861 return False
862 else:
863 return True
864 except BadObject:
865 _logger.debug("Commit hash is invalid.")
866 return False
868 def _get_daemon_export(self) -> bool:
869 if self.git_dir:
870 filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
871 return osp.exists(filename)
873 def _set_daemon_export(self, value: object) -> None:
874 if self.git_dir:
875 filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
876 fileexists = osp.exists(filename)
877 if value and not fileexists:
878 touch(filename)
879 elif not value and fileexists:
880 os.unlink(filename)
882 @property
883 def daemon_export(self) -> bool:
884 """If True, git-daemon may export this repository"""
885 return self._get_daemon_export()
887 @daemon_export.setter
888 def daemon_export(self, value: object) -> None:
889 self._set_daemon_export(value)
891 def _get_alternates(self) -> List[str]:
892 """The list of alternates for this repo from which objects can be retrieved.
894 :return:
895 List of strings being pathnames of alternates
896 """
897 if self.git_dir:
898 alternates_path = osp.join(self.git_dir, "objects", "info", "alternates")
900 if osp.exists(alternates_path):
901 with open(alternates_path, "rb") as f:
902 alts = f.read().decode(defenc)
903 return alts.strip().splitlines()
904 return []
906 def _set_alternates(self, alts: List[str]) -> None:
907 """Set the alternates.
909 :param alts:
910 The array of string paths representing the alternates at which git should
911 look for objects, i.e. ``/home/user/repo/.git/objects``.
913 :raise git.exc.NoSuchPathError:
915 :note:
916 The method does not check for the existence of the paths in `alts`, as the
917 caller is responsible.
918 """
919 alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
920 if not alts:
921 if osp.isfile(alternates_path):
922 os.remove(alternates_path)
923 else:
924 with open(alternates_path, "wb") as f:
925 f.write("\n".join(alts).encode(defenc))
927 @property
928 def alternates(self) -> List[str]:
929 """Retrieve a list of alternates paths or set a list paths to be used as alternates"""
930 return self._get_alternates()
932 @alternates.setter
933 def alternates(self, alts: List[str]) -> None:
934 self._set_alternates(alts)
936 def is_dirty(
937 self,
938 index: bool = True,
939 working_tree: bool = True,
940 untracked_files: bool = False,
941 submodules: bool = True,
942 path: Optional[PathLike] = None,
943 ) -> bool:
944 """
945 :return:
946 ``True`` if the repository is considered dirty. By default it will react
947 like a :manpage:`git-status(1)` without untracked files, hence it is dirty
948 if the index or the working copy have changes.
949 """
950 if self._bare:
951 # Bare repositories with no associated working directory are
952 # always considered to be clean.
953 return False
955 # Start from the one which is fastest to evaluate.
956 default_args = ["--abbrev=40", "--full-index", "--raw"]
957 if not submodules:
958 default_args.append("--ignore-submodules")
959 if path:
960 default_args.extend(["--", str(path)])
961 if index:
962 # diff index against HEAD.
963 if osp.isfile(self.index.path) and len(self.git.diff("--cached", *default_args)):
964 return True
965 # END index handling
966 if working_tree:
967 # diff index against working tree.
968 if len(self.git.diff(*default_args)):
969 return True
970 # END working tree handling
971 if untracked_files:
972 if len(self._get_untracked_files(path, ignore_submodules=not submodules)):
973 return True
974 # END untracked files
975 return False
977 @property
978 def untracked_files(self) -> List[str]:
979 """
980 :return:
981 list(str,...)
983 Files currently untracked as they have not been staged yet. Paths are
984 relative to the current working directory of the git command.
986 :note:
987 Ignored files will not appear here, i.e. files mentioned in ``.gitignore``.
989 :note:
990 This property is expensive, as no cache is involved. To process the result,
991 please consider caching it yourself.
992 """
993 return self._get_untracked_files()
995 def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]:
996 # Make sure we get all files, not only untracked directories.
997 proc = self.git.status(*args, porcelain=True, untracked_files=True, as_process=True, **kwargs)
998 # Untracked files prefix in porcelain mode
999 prefix = "?? "
1000 untracked_files = []
1001 for line in proc.stdout:
1002 line = line.decode(defenc)
1003 if not line.startswith(prefix):
1004 continue
1005 filename = line[len(prefix) :].rstrip("\n")
1006 # Special characters are escaped
1007 if filename[0] == filename[-1] == '"':
1008 filename = filename[1:-1]
1009 # WHATEVER ... it's a mess, but works for me
1010 filename = filename.encode("ascii").decode("unicode_escape").encode("latin1").decode(defenc)
1011 untracked_files.append(filename)
1012 finalize_process(proc)
1013 return untracked_files
1015 def ignored(self, *paths: PathLike) -> List[str]:
1016 """Checks if paths are ignored via ``.gitignore``.
1018 This does so using the :manpage:`git-check-ignore(1)` method.
1020 :param paths:
1021 List of paths to check whether they are ignored or not.
1023 :return:
1024 Subset of those paths which are ignored
1025 """
1026 try:
1027 proc: str = self.git.check_ignore(*paths)
1028 except GitCommandError as err:
1029 if err.status == 1:
1030 # If return code is 1, this means none of the items in *paths are
1031 # ignored by Git, so return an empty list.
1032 return []
1033 else:
1034 # Raise the exception on all other return codes.
1035 raise
1037 return proc.replace("\\\\", "\\").replace('"', "").split("\n")
1039 @property
1040 def active_branch(self) -> Head:
1041 """The name of the currently active branch.
1043 :raise TypeError:
1044 If HEAD is detached.
1046 :return:
1047 :class:`~git.refs.head.Head` to the active branch
1048 """
1049 # reveal_type(self.head.reference) # => Reference
1050 return self.head.reference
1052 def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> Iterator["BlameEntry"]:
1053 """Iterator for blame information for the given file at the given revision.
1055 Unlike :meth:`blame`, this does not return the actual file's contents, only a
1056 stream of :class:`BlameEntry` tuples.
1058 :param rev:
1059 Revision specifier. If ``None``, the blame will include all the latest
1060 uncommitted changes. Otherwise, anything successfully parsed by
1061 :manpage:`git-rev-parse(1)` is a valid option.
1063 :return:
1064 Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the
1065 commit to blame for the line, and range indicates a span of line numbers in
1066 the resulting file.
1068 If you combine all line number ranges outputted by this command, you should get
1069 a continuous range spanning all line numbers in the file.
1070 """
1072 data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs)
1073 commits: Dict[bytes, Commit] = {}
1075 stream = (line for line in data.split(b"\n") if line)
1076 while True:
1077 try:
1078 # When exhausted, causes a StopIteration, terminating this function.
1079 line = next(stream)
1080 except StopIteration:
1081 return
1082 split_line = line.split()
1083 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line
1084 lineno = int(lineno_b)
1085 num_lines = int(num_lines_b)
1086 orig_lineno = int(orig_lineno_b)
1087 if hexsha not in commits:
1088 # Now read the next few lines and build up a dict of properties for this
1089 # commit.
1090 props: Dict[bytes, bytes] = {}
1091 while True:
1092 try:
1093 line = next(stream)
1094 except StopIteration:
1095 return
1096 if line == b"boundary":
1097 # "boundary" indicates a root commit and occurs instead of the
1098 # "previous" tag.
1099 continue
1101 tag, value = line.split(b" ", 1)
1102 props[tag] = value
1103 if tag == b"filename":
1104 # "filename" formally terminates the entry for --incremental.
1105 orig_filename = value
1106 break
1108 c = Commit(
1109 self,
1110 hex_to_bin(hexsha),
1111 author=Actor(
1112 safe_decode(props[b"author"]),
1113 safe_decode(props[b"author-mail"].lstrip(b"<").rstrip(b">")),
1114 ),
1115 authored_date=int(props[b"author-time"]),
1116 committer=Actor(
1117 safe_decode(props[b"committer"]),
1118 safe_decode(props[b"committer-mail"].lstrip(b"<").rstrip(b">")),
1119 ),
1120 committed_date=int(props[b"committer-time"]),
1121 )
1122 commits[hexsha] = c
1123 else:
1124 # Discard all lines until we find "filename" which is guaranteed to be
1125 # the last line.
1126 while True:
1127 try:
1128 # Will fail if we reach the EOF unexpectedly.
1129 line = next(stream)
1130 except StopIteration:
1131 return
1132 tag, value = line.split(b" ", 1)
1133 if tag == b"filename":
1134 orig_filename = value
1135 break
1137 yield BlameEntry(
1138 commits[hexsha],
1139 range(lineno, lineno + num_lines),
1140 safe_decode(orig_filename),
1141 range(orig_lineno, orig_lineno + num_lines),
1142 )
1144 def blame(
1145 self,
1146 rev: Union[str, HEAD, None],
1147 file: str,
1148 incremental: bool = False,
1149 rev_opts: Optional[List[str]] = None,
1150 **kwargs: Any,
1151 ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None:
1152 """The blame information for the given file at the given revision.
1154 :param rev:
1155 Revision specifier. If ``None``, the blame will include all the latest
1156 uncommitted changes. Otherwise, anything successfully parsed by
1157 :manpage:`git-rev-parse(1)` is a valid option.
1159 :return:
1160 list: [git.Commit, list: [<line>]]
1162 A list of lists associating a :class:`~git.objects.commit.Commit` object
1163 with a list of lines that changed within the given commit. The
1164 :class:`~git.objects.commit.Commit` objects will be given in order of
1165 appearance.
1166 """
1167 if incremental:
1168 return self.blame_incremental(rev, file, **kwargs)
1169 rev_opts = rev_opts or []
1170 data: bytes = self.git.blame(rev, *rev_opts, "--", file, p=True, stdout_as_string=False, **kwargs)
1171 commits: Dict[str, Commit] = {}
1172 blames: List[List[Commit | List[str | bytes] | None]] = []
1174 class InfoTD(TypedDict, total=False):
1175 sha: str
1176 id: str
1177 filename: str
1178 summary: str
1179 author: str
1180 author_email: str
1181 author_date: int
1182 committer: str
1183 committer_email: str
1184 committer_date: int
1186 info: InfoTD = {}
1188 keepends = True
1189 for line_bytes in data.splitlines(keepends):
1190 try:
1191 line_str = line_bytes.rstrip().decode(defenc)
1192 except UnicodeDecodeError:
1193 firstpart = ""
1194 parts = []
1195 is_binary = True
1196 else:
1197 # As we don't have an idea when the binary data ends, as it could
1198 # contain multiple newlines in the process. So we rely on being able to
1199 # decode to tell us what it is. This can absolutely fail even on text
1200 # files, but even if it does, we should be fine treating it as binary
1201 # instead.
1202 parts = self.re_whitespace.split(line_str, 1)
1203 firstpart = parts[0]
1204 is_binary = False
1205 # END handle decode of line
1207 if self.re_hexsha_only.search(firstpart):
1208 # handles
1209 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7 - indicates blame-data start
1210 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates
1211 # another line of blame with the same data
1212 digits = parts[-1].split(" ")
1213 if len(digits) == 3:
1214 info = {"id": firstpart}
1215 blames.append([None, []])
1216 elif info["id"] != firstpart:
1217 info = {"id": firstpart}
1218 blames.append([commits.get(firstpart), []])
1219 # END blame data initialization
1220 else:
1221 m = self.re_author_committer_start.search(firstpart)
1222 if m:
1223 # handles:
1224 # author Tom Preston-Werner
1225 # author-mail <tom@mojombo.com>
1226 # author-time 1192271832
1227 # author-tz -0700
1228 # committer Tom Preston-Werner
1229 # committer-mail <tom@mojombo.com>
1230 # committer-time 1192271832
1231 # committer-tz -0700 - IGNORED BY US
1232 role = m.group(0)
1233 if role == "author":
1234 if firstpart.endswith("-mail"):
1235 info["author_email"] = parts[-1]
1236 elif firstpart.endswith("-time"):
1237 info["author_date"] = int(parts[-1])
1238 elif role == firstpart:
1239 info["author"] = parts[-1]
1240 elif role == "committer":
1241 if firstpart.endswith("-mail"):
1242 info["committer_email"] = parts[-1]
1243 elif firstpart.endswith("-time"):
1244 info["committer_date"] = int(parts[-1])
1245 elif role == firstpart:
1246 info["committer"] = parts[-1]
1247 # END distinguish mail,time,name
1248 else:
1249 # handle
1250 # filename lib/grit.rb
1251 # summary add Blob
1252 # <and rest>
1253 if firstpart.startswith("filename"):
1254 info["filename"] = parts[-1]
1255 elif firstpart.startswith("summary"):
1256 info["summary"] = parts[-1]
1257 elif firstpart == "":
1258 if info:
1259 sha = info["id"]
1260 c = commits.get(sha)
1261 if c is None:
1262 c = Commit(
1263 self,
1264 hex_to_bin(sha),
1265 author=Actor._from_string(f"{info['author']} {info['author_email']}"),
1266 authored_date=info["author_date"],
1267 committer=Actor._from_string(f"{info['committer']} {info['committer_email']}"),
1268 committed_date=info["committer_date"],
1269 )
1270 commits[sha] = c
1271 blames[-1][0] = c
1272 # END if commit objects needs initial creation
1274 if blames[-1][1] is not None:
1275 line: str | bytes
1276 if not is_binary:
1277 if line_str and line_str[0] == "\t":
1278 line_str = line_str[1:]
1279 line = line_str
1280 else:
1281 line = line_bytes
1282 # NOTE: We are actually parsing lines out of binary
1283 # data, which can lead to the binary being split up
1284 # along the newline separator. We will append this
1285 # to the blame we are currently looking at, even
1286 # though it should be concatenated with the last
1287 # line we have seen.
1288 blames[-1][1].append(line)
1290 info = {"id": sha}
1291 # END if we collected commit info
1292 # END distinguish filename,summary,rest
1293 # END distinguish author|committer vs filename,summary,rest
1294 # END distinguish hexsha vs other information
1295 return blames
1297 @classmethod
1298 def init(
1299 cls,
1300 path: Union[PathLike, None] = None,
1301 mkdir: bool = True,
1302 odbt: Type[GitCmdObjectDB] = GitCmdObjectDB,
1303 expand_vars: bool = True,
1304 **kwargs: Any,
1305 ) -> "Repo":
1306 """Initialize a git repository at the given path if specified.
1308 :param path:
1309 The full path to the repo (traditionally ends with ``/<name>.git``). Or
1310 ``None``, in which case the repository will be created in the current
1311 working directory.
1313 :param mkdir:
1314 If specified, will create the repository directory if it doesn't already
1315 exist. Creates the directory with a mode=0755.
1316 Only effective if a path is explicitly given.
1318 :param odbt:
1319 Object DataBase type - a type which is constructed by providing the
1320 directory containing the database objects, i.e. ``.git/objects``. It will be
1321 used to access all object data.
1323 :param expand_vars:
1324 If specified, environment variables will not be escaped. This can lead to
1325 information disclosure, allowing attackers to access the contents of
1326 environment variables.
1328 :param kwargs:
1329 Keyword arguments serving as additional options to the
1330 :manpage:`git-init(1)` command.
1332 :return:
1333 :class:`Repo` (the newly created repo)
1334 """
1335 if path:
1336 path = expand_path(path, expand_vars)
1337 if mkdir and path and not osp.exists(path):
1338 os.makedirs(path, 0o755)
1340 # git command automatically chdir into the directory
1341 git = cls.GitCommandWrapperType(path)
1342 git.init(**kwargs)
1343 return cls(path, odbt=odbt)
1345 @classmethod
1346 def _clone(
1347 cls,
1348 git: "Git",
1349 url: PathLike,
1350 path: PathLike,
1351 odb_default_type: Type[GitCmdObjectDB],
1352 progress: Union["RemoteProgress", "UpdateProgress", Callable[..., "RemoteProgress"], None] = None,
1353 multi_options: Optional[List[str]] = None,
1354 allow_unsafe_protocols: bool = False,
1355 allow_unsafe_options: bool = False,
1356 **kwargs: Any,
1357 ) -> "Repo":
1358 odbt = kwargs.pop("odbt", odb_default_type)
1360 # When pathlib.Path or other class-based path is passed
1361 if not isinstance(path, str):
1362 path = str(path)
1364 ## A bug win cygwin's Git, when `--bare` or `--separate-git-dir`
1365 # it prepends the cwd or(?) the `url` into the `path, so::
1366 # git clone --bare /cygwin/d/foo.git C:\\Work
1367 # becomes::
1368 # git clone --bare /cygwin/d/foo.git /cygwin/d/C:\\Work
1369 #
1370 clone_path = Git.polish_url(path) if Git.is_cygwin() and "bare" in kwargs else path
1371 sep_dir = kwargs.get("separate_git_dir")
1372 if sep_dir:
1373 kwargs["separate_git_dir"] = Git.polish_url(sep_dir)
1374 multi = None
1375 if multi_options:
1376 multi = shlex.split(" ".join(multi_options))
1378 if not allow_unsafe_protocols:
1379 Git.check_unsafe_protocols(str(url))
1380 if not allow_unsafe_options:
1381 Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options)
1382 if not allow_unsafe_options and multi_options:
1383 Git.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options)
1385 proc = git.clone(
1386 multi,
1387 "--",
1388 Git.polish_url(str(url)),
1389 clone_path,
1390 with_extended_output=True,
1391 as_process=True,
1392 v=True,
1393 universal_newlines=True,
1394 **add_progress(kwargs, git, progress),
1395 )
1396 if progress:
1397 handle_process_output(
1398 proc,
1399 None,
1400 to_progress_instance(progress).new_message_handler(),
1401 finalize_process,
1402 decode_streams=False,
1403 )
1404 else:
1405 (stdout, stderr) = proc.communicate()
1406 cmdline = getattr(proc, "args", "")
1407 cmdline = remove_password_if_present(cmdline)
1409 _logger.debug("Cmd(%s)'s unused stdout: %s", cmdline, stdout)
1410 finalize_process(proc, stderr=stderr)
1412 # Our git command could have a different working dir than our actual
1413 # environment, hence we prepend its working dir if required.
1414 if not osp.isabs(path):
1415 path = osp.join(git._working_dir, path) if git._working_dir is not None else path
1417 repo = cls(path, odbt=odbt)
1419 # Retain env values that were passed to _clone().
1420 repo.git.update_environment(**git.environment())
1422 # Adjust remotes - there may be operating systems which use backslashes, These
1423 # might be given as initial paths, but when handling the config file that
1424 # contains the remote from which we were clones, git stops liking it as it will
1425 # escape the backslashes. Hence we undo the escaping just to be sure.
1426 if repo.remotes:
1427 with repo.remotes[0].config_writer as writer:
1428 writer.set_value("url", Git.polish_url(repo.remotes[0].url))
1429 # END handle remote repo
1430 return repo
1432 def clone(
1433 self,
1434 path: PathLike,
1435 progress: Optional[CallableProgress] = None,
1436 multi_options: Optional[List[str]] = None,
1437 allow_unsafe_protocols: bool = False,
1438 allow_unsafe_options: bool = False,
1439 **kwargs: Any,
1440 ) -> "Repo":
1441 """Create a clone from this repository.
1443 :param path:
1444 The full path of the new repo (traditionally ends with ``./<name>.git``).
1446 :param progress:
1447 See :meth:`Remote.push <git.remote.Remote.push>`.
1449 :param multi_options:
1450 A list of :manpage:`git-clone(1)` options that can be provided multiple
1451 times.
1453 One option per list item which is passed exactly as specified to clone.
1454 For example::
1456 [
1457 "--config core.filemode=false",
1458 "--config core.ignorecase",
1459 "--recurse-submodule=repo1_path",
1460 "--recurse-submodule=repo2_path",
1461 ]
1463 :param allow_unsafe_protocols:
1464 Allow unsafe protocols to be used, like ``ext``.
1466 :param allow_unsafe_options:
1467 Allow unsafe options to be used, like ``--upload-pack``.
1469 :param kwargs:
1470 * ``odbt`` = ObjectDatabase Type, allowing to determine the object database
1471 implementation used by the returned :class:`Repo` instance.
1472 * All remaining keyword arguments are given to the :manpage:`git-clone(1)`
1473 command.
1475 :return:
1476 :class:`Repo` (the newly cloned repo)
1477 """
1478 return self._clone(
1479 self.git,
1480 self.common_dir,
1481 path,
1482 type(self.odb),
1483 progress, # type: ignore[arg-type]
1484 multi_options,
1485 allow_unsafe_protocols=allow_unsafe_protocols,
1486 allow_unsafe_options=allow_unsafe_options,
1487 **kwargs,
1488 )
1490 @classmethod
1491 def clone_from(
1492 cls,
1493 url: PathLike,
1494 to_path: PathLike,
1495 progress: CallableProgress = None,
1496 env: Optional[Mapping[str, str]] = None,
1497 multi_options: Optional[List[str]] = None,
1498 allow_unsafe_protocols: bool = False,
1499 allow_unsafe_options: bool = False,
1500 **kwargs: Any,
1501 ) -> "Repo":
1502 """Create a clone from the given URL.
1504 :param url:
1505 Valid git url, see: https://git-scm.com/docs/git-clone#URLS
1507 :param to_path:
1508 Path to which the repository should be cloned to.
1510 :param progress:
1511 See :meth:`Remote.push <git.remote.Remote.push>`.
1513 :param env:
1514 Optional dictionary containing the desired environment variables.
1516 Note: Provided variables will be used to update the execution environment
1517 for ``git``. If some variable is not specified in `env` and is defined in
1518 :attr:`os.environ`, value from :attr:`os.environ` will be used. If you want
1519 to unset some variable, consider providing empty string as its value.
1521 :param multi_options:
1522 See the :meth:`clone` method.
1524 :param allow_unsafe_protocols:
1525 Allow unsafe protocols to be used, like ``ext``.
1527 :param allow_unsafe_options:
1528 Allow unsafe options to be used, like ``--upload-pack``.
1530 :param kwargs:
1531 See the :meth:`clone` method.
1533 :return:
1534 :class:`Repo` instance pointing to the cloned directory.
1535 """
1536 git = cls.GitCommandWrapperType(os.getcwd())
1537 if env is not None:
1538 git.update_environment(**env)
1539 return cls._clone(
1540 git,
1541 url,
1542 to_path,
1543 GitCmdObjectDB,
1544 progress, # type: ignore[arg-type]
1545 multi_options,
1546 allow_unsafe_protocols=allow_unsafe_protocols,
1547 allow_unsafe_options=allow_unsafe_options,
1548 **kwargs,
1549 )
1551 def archive(
1552 self,
1553 ostream: Union[TextIO, BinaryIO],
1554 treeish: Optional[str] = None,
1555 prefix: Optional[str] = None,
1556 **kwargs: Any,
1557 ) -> Repo:
1558 """Archive the tree at the given revision.
1560 :param ostream:
1561 File-compatible stream object to which the archive will be written as bytes.
1563 :param treeish:
1564 The treeish name/id, defaults to active branch.
1566 :param prefix:
1567 The optional prefix to prepend to each filename in the archive.
1569 :param kwargs:
1570 Additional arguments passed to :manpage:`git-archive(1)`:
1572 * Use the ``format`` argument to define the kind of format. Use specialized
1573 ostreams to write any format supported by Python.
1574 * You may specify the special ``path`` keyword, which may either be a
1575 repository-relative path to a directory or file to place into the archive,
1576 or a list or tuple of multiple paths.
1578 :raise git.exc.GitCommandError:
1579 If something went wrong.
1581 :return:
1582 self
1583 """
1584 if treeish is None:
1585 treeish = self.head.commit
1586 if prefix and "prefix" not in kwargs:
1587 kwargs["prefix"] = prefix
1588 kwargs["output_stream"] = ostream
1589 path = kwargs.pop("path", [])
1590 path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path)
1591 if not isinstance(path, (tuple, list)):
1592 path = [path]
1593 # END ensure paths is list (or tuple)
1594 self.git.archive("--", treeish, *path, **kwargs)
1595 return self
1597 def has_separate_working_tree(self) -> bool:
1598 """
1599 :return:
1600 True if our :attr:`git_dir` is not at the root of our
1601 :attr:`working_tree_dir`, but a ``.git`` file with a platform-agnostic
1602 symbolic link. Our :attr:`git_dir` will be wherever the ``.git`` file points
1603 to.
1605 :note:
1606 Bare repositories will always return ``False`` here.
1607 """
1608 if self.bare:
1609 return False
1610 if self.working_tree_dir:
1611 return osp.isfile(osp.join(self.working_tree_dir, ".git"))
1612 else:
1613 return False # Or raise Error?
1615 rev_parse = rev_parse
1617 def __repr__(self) -> str:
1618 clazz = self.__class__
1619 return "<%s.%s %r>" % (clazz.__module__, clazz.__name__, self.git_dir)
1621 def currently_rebasing_on(self) -> Commit | None:
1622 """
1623 :return:
1624 The commit which is currently being replayed while rebasing.
1626 ``None`` if we are not currently rebasing.
1627 """
1628 if self.git_dir:
1629 rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
1630 if not osp.isfile(rebase_head_file):
1631 return None
1632 with open(rebase_head_file, "rt") as f:
1633 content = f.readline().strip()
1634 return self.commit(content)