Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/repo/base.py: 45%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 # stored as string for easier processing, but annotated as path for clearer intention
130 _working_tree_dir: Optional[PathLike] = None
132 git_dir: PathLike
133 """The ``.git`` repository directory."""
135 _common_dir: PathLike = ""
137 # Precompiled regex
138 re_whitespace = re.compile(r"\s+")
139 re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$")
140 re_hexsha_shortened = re.compile(r"^[0-9A-Fa-f]{4,40}$")
141 re_envvars = re.compile(r"(\$(\{\s?)?[a-zA-Z_]\w*(\}\s?)?|%\s?[a-zA-Z_]\w*\s?%)")
142 re_author_committer_start = re.compile(r"^(author|committer)")
143 re_tab_full_line = re.compile(r"^\t(.*)$")
145 unsafe_git_clone_options = [
146 # Executes arbitrary commands:
147 "--upload-pack",
148 "-u",
149 # Can override configuration variables that execute arbitrary commands:
150 "--config",
151 "-c",
152 ]
153 """Options to :manpage:`git-clone(1)` that allow arbitrary commands to be executed.
155 The ``--upload-pack``/``-u`` option allows users to execute arbitrary commands
156 directly:
157 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt
159 The ``--config``/``-c`` option allows users to override configuration variables like
160 ``protocol.allow`` and ``core.gitProxy`` to execute arbitrary commands:
161 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt
162 """
164 # Invariants
165 config_level: ConfigLevels_Tup = ("system", "user", "global", "repository")
166 """Represents the configuration level of a configuration file."""
168 # Subclass configuration
169 GitCommandWrapperType = Git
170 """Subclasses may easily bring in their own custom types by placing a constructor or
171 type here."""
173 def __init__(
174 self,
175 path: Optional[PathLike] = None,
176 odbt: Type[LooseObjectDB] = GitCmdObjectDB,
177 search_parent_directories: bool = False,
178 expand_vars: bool = True,
179 ) -> None:
180 R"""Create a new :class:`Repo` instance.
182 :param path:
183 The path to either the worktree directory or the .git directory itself::
185 repo = Repo("/Users/mtrier/Development/git-python")
186 repo = Repo("/Users/mtrier/Development/git-python.git")
187 repo = Repo("~/Development/git-python.git")
188 repo = Repo("$REPOSITORIES/Development/git-python.git")
189 repo = Repo(R"C:\Users\mtrier\Development\git-python\.git")
191 - In *Cygwin*, `path` may be a ``cygdrive/...`` prefixed path.
192 - If `path` is ``None`` or an empty string, :envvar:`GIT_DIR` is used. If
193 that environment variable is absent or empty, the current directory is
194 used.
196 :param odbt:
197 Object DataBase type - a type which is constructed by providing the
198 directory containing the database objects, i.e. ``.git/objects``. It will be
199 used to access all object data.
201 :param search_parent_directories:
202 If ``True``, all parent directories will be searched for a valid repo as
203 well.
205 Please note that this was the default behaviour in older versions of
206 GitPython, which is considered a bug though.
208 :raise git.exc.InvalidGitRepositoryError:
210 :raise git.exc.NoSuchPathError:
212 :return:
213 :class:`Repo`
214 """
216 epath = path or os.getenv("GIT_DIR")
217 if not epath:
218 epath = os.getcwd()
219 epath = os.fspath(epath)
220 if Git.is_cygwin():
221 # Given how the tests are written, this seems more likely to catch Cygwin
222 # git used from Windows than Windows git used from Cygwin. Therefore
223 # changing to Cygwin-style paths is the relevant operation.
224 epath = cygpath(epath)
226 if expand_vars and re.search(self.re_envvars, epath):
227 warnings.warn(
228 "The use of environment variables in paths is deprecated"
229 + "\nfor security reasons and may be removed in the future!!",
230 stacklevel=1,
231 )
232 epath = expand_path(epath, expand_vars)
233 if epath is not None:
234 if not os.path.exists(epath):
235 raise NoSuchPathError(epath)
237 # Walk up the path to find the `.git` dir.
238 curpath = epath
239 git_dir = None
240 while curpath:
241 # ABOUT osp.NORMPATH
242 # It's important to normalize the paths, as submodules will otherwise
243 # initialize their repo instances with paths that depend on path-portions
244 # that will not exist after being removed. It's just cleaner.
245 if (
246 osp.isfile(osp.join(curpath, "gitdir"))
247 and osp.isfile(osp.join(curpath, "commondir"))
248 and osp.isfile(osp.join(curpath, "HEAD"))
249 ):
250 git_dir = curpath
252 if "GIT_WORK_TREE" in os.environ:
253 self._working_tree_dir = os.getenv("GIT_WORK_TREE")
254 else:
255 # Linked worktree administrative directories store the path to the
256 # worktree's .git file in their gitdir file (without "gitdir: " prefix).
257 with open(osp.join(git_dir, "gitdir")) as fp:
258 worktree_gitfile = fp.read().strip()
260 if not osp.isabs(worktree_gitfile):
261 worktree_gitfile = osp.normpath(osp.join(git_dir, worktree_gitfile))
263 self._working_tree_dir = osp.dirname(worktree_gitfile)
265 break
267 if is_git_dir(curpath):
268 git_dir = curpath
269 # from man git-config : core.worktree
270 # Set the path to the root of the working tree. If GIT_COMMON_DIR
271 # environment variable is set, core.worktree is ignored and not used for
272 # determining the root of working tree. This can be overridden by the
273 # GIT_WORK_TREE environment variable. The value can be an absolute path
274 # or relative to the path to the .git directory, which is either
275 # specified by GIT_DIR, or automatically discovered. If GIT_DIR is
276 # specified but none of GIT_WORK_TREE and core.worktree is specified,
277 # the current working directory is regarded as the top level of your
278 # working tree.
279 self._working_tree_dir = os.path.dirname(git_dir)
280 if os.environ.get("GIT_COMMON_DIR") is None:
281 gitconf = self._config_reader("repository", git_dir)
282 if gitconf.has_option("core", "worktree"):
283 self._working_tree_dir = gitconf.get("core", "worktree")
284 if "GIT_WORK_TREE" in os.environ:
285 self._working_tree_dir = os.getenv("GIT_WORK_TREE")
286 break
288 dotgit = osp.join(curpath, ".git")
289 sm_gitpath = find_submodule_git_dir(dotgit)
290 if sm_gitpath is not None:
291 git_dir = osp.normpath(sm_gitpath)
293 sm_gitpath = find_submodule_git_dir(dotgit)
294 if sm_gitpath is None:
295 sm_gitpath = find_worktree_git_dir(dotgit)
297 if sm_gitpath is not None:
298 # worktrees can use relative paths as of Git 2.48, so we join to curpath
299 git_dir = osp.normpath(osp.join(curpath, sm_gitpath))
300 self._working_tree_dir = curpath
301 break
303 if not search_parent_directories:
304 break
305 curpath, tail = osp.split(curpath)
306 if not tail:
307 break
308 # END while curpath
310 if git_dir is None:
311 raise InvalidGitRepositoryError(epath)
312 self.git_dir = git_dir
314 self._bare = False
315 try:
316 self._bare = self.config_reader("repository").getboolean("core", "bare")
317 except Exception:
318 # Let's not assume the option exists, although it should.
319 pass
321 try:
322 common_dir = (Path(self.git_dir) / "commondir").read_text().splitlines()[0].strip()
323 self._common_dir = osp.join(self.git_dir, common_dir)
324 except OSError:
325 self._common_dir = ""
327 # Adjust the working directory in case we are actually bare - we didn't know
328 # that in the first place.
329 if self._bare:
330 self._working_tree_dir = None
331 # END working dir handling
333 self.working_dir: PathLike = self._working_tree_dir or self.common_dir
334 self.git = self.GitCommandWrapperType(self.working_dir)
336 # Special handling, in special times.
337 rootpath = osp.join(self.common_dir, "objects")
338 if issubclass(odbt, GitCmdObjectDB):
339 self.odb = odbt(rootpath, self.git)
340 else:
341 self.odb = odbt(rootpath)
343 def __enter__(self) -> "Repo":
344 return self
346 def __exit__(self, *args: Any) -> None:
347 self.close()
349 def __del__(self) -> None:
350 try:
351 self.close()
352 except Exception:
353 pass
355 def close(self) -> None:
356 if self.git:
357 self.git.clear_cache()
358 # Tempfiles objects on Windows are holding references to open files until
359 # they are collected by the garbage collector, thus preventing deletion.
360 # TODO: Find these references and ensure they are closed and deleted
361 # synchronously rather than forcing a gc collection.
362 if sys.platform == "win32":
363 gc.collect()
364 gitdb.util.mman.collect()
365 if sys.platform == "win32":
366 gc.collect()
368 def __eq__(self, rhs: object) -> bool:
369 if isinstance(rhs, Repo):
370 return self.git_dir == rhs.git_dir
371 return False
373 def __ne__(self, rhs: object) -> bool:
374 return not self.__eq__(rhs)
376 def __hash__(self) -> int:
377 return hash(self.git_dir)
379 @property
380 def description(self) -> str:
381 """The project's description"""
382 filename = osp.join(self.git_dir, "description")
383 with open(filename, "rb") as fp:
384 return fp.read().rstrip().decode(defenc)
386 @description.setter
387 def description(self, descr: str) -> None:
388 filename = osp.join(self.git_dir, "description")
389 with open(filename, "wb") as fp:
390 fp.write((descr + "\n").encode(defenc))
392 @property
393 def working_tree_dir(self) -> Optional[PathLike]:
394 """
395 :return:
396 The working tree directory of our git repository.
397 If this is a bare repository, ``None`` is returned.
398 """
399 return self._working_tree_dir
401 @property
402 def common_dir(self) -> PathLike:
403 """
404 :return:
405 The git dir that holds everything except possibly HEAD, FETCH_HEAD,
406 ORIG_HEAD, COMMIT_EDITMSG, index, and logs/.
407 """
408 return self._common_dir or self.git_dir
410 @property
411 def bare(self) -> bool:
412 """:return: ``True`` if the repository is bare"""
413 return self._bare
415 @property
416 def heads(self) -> "IterableList[Head]":
417 """A list of :class:`~git.refs.head.Head` objects representing the branch heads
418 in this repo.
420 :return:
421 ``git.IterableList(Head, ...)``
422 """
423 return Head.list_items(self)
425 @property
426 def branches(self) -> "IterableList[Head]":
427 """Alias for heads.
428 A list of :class:`~git.refs.head.Head` objects representing the branch heads
429 in this repo.
431 :return:
432 ``git.IterableList(Head, ...)``
433 """
434 return self.heads
436 @property
437 def references(self) -> "IterableList[Reference]":
438 """A list of :class:`~git.refs.reference.Reference` objects representing tags,
439 heads and remote references.
441 :return:
442 ``git.IterableList(Reference, ...)``
443 """
444 return Reference.list_items(self)
446 @property
447 def refs(self) -> "IterableList[Reference]":
448 """Alias for references.
449 A list of :class:`~git.refs.reference.Reference` objects representing tags,
450 heads and remote references.
452 :return:
453 ``git.IterableList(Reference, ...)``
454 """
455 return self.references
457 @property
458 def index(self) -> "IndexFile":
459 """
460 :return:
461 A :class:`~git.index.base.IndexFile` representing this repository's index.
463 :note:
464 This property can be expensive, as the returned
465 :class:`~git.index.base.IndexFile` will be reinitialized.
466 It is recommended to reuse the object.
467 """
468 return IndexFile(self)
470 @property
471 def head(self) -> "HEAD":
472 """
473 :return:
474 :class:`~git.refs.head.HEAD` object pointing to the current head reference
475 """
476 return HEAD(self, "HEAD")
478 @property
479 def remotes(self) -> "IterableList[Remote]":
480 """A list of :class:`~git.remote.Remote` objects allowing to access and
481 manipulate remotes.
483 :return:
484 ``git.IterableList(Remote, ...)``
485 """
486 return Remote.list_items(self)
488 def remote(self, name: str = "origin") -> "Remote":
489 """:return: The remote with the specified name
491 :raise ValueError:
492 If no remote with such a name exists.
493 """
494 r = Remote(self, name)
495 if not r.exists():
496 raise ValueError("Remote named '%s' didn't exist" % name)
497 return r
499 # { Submodules
501 @property
502 def submodules(self) -> "IterableList[Submodule]":
503 """
504 :return:
505 git.IterableList(Submodule, ...) of direct submodules available from the
506 current head
507 """
508 return Submodule.list_items(self)
510 def submodule(self, name: str) -> "Submodule":
511 """:return: The submodule with the given name
513 :raise ValueError:
514 If no such submodule exists.
515 """
516 try:
517 return self.submodules[name]
518 except IndexError as e:
519 raise ValueError("Didn't find submodule named %r" % name) from e
520 # END exception handling
522 def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule:
523 """Create a new submodule.
525 :note:
526 For a description of the applicable parameters, see the documentation of
527 :meth:`Submodule.add <git.objects.submodule.base.Submodule.add>`.
529 :return:
530 The created submodule.
531 """
532 return Submodule.add(self, *args, **kwargs)
534 def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
535 """An iterator yielding Submodule instances.
537 See the :class:`~git.objects.util.Traversable` interface for a description of `args`
538 and `kwargs`.
540 :return:
541 Iterator
542 """
543 return RootModule(self).traverse(*args, **kwargs)
545 def submodule_update(self, *args: Any, **kwargs: Any) -> RootModule:
546 """Update the submodules, keeping the repository consistent as it will
547 take the previous state into consideration.
549 :note:
550 For more information, please see the documentation of
551 :meth:`RootModule.update <git.objects.submodule.root.RootModule.update>`.
552 """
553 return RootModule(self).update(*args, **kwargs)
555 # }END submodules
557 @property
558 def tags(self) -> "IterableList[TagReference]":
559 """A list of :class:`~git.refs.tag.TagReference` objects that are available in
560 this repo.
562 :return:
563 ``git.IterableList(TagReference, ...)``
564 """
565 return TagReference.list_items(self)
567 def tag(self, path: PathLike) -> TagReference:
568 """
569 :return:
570 :class:`~git.refs.tag.TagReference` object, reference pointing to a
571 :class:`~git.objects.commit.Commit` or tag
573 :param path:
574 Path to the tag reference, e.g. ``0.1.5`` or ``tags/0.1.5``.
575 """
576 full_path = self._to_full_tag_path(path)
577 return TagReference(self, full_path)
579 @staticmethod
580 def _to_full_tag_path(path: PathLike) -> str:
581 path_str = str(path)
582 if path_str.startswith(TagReference._common_path_default + "/"):
583 return path_str
584 if path_str.startswith(TagReference._common_default + "/"):
585 return Reference._common_path_default + "/" + path_str
586 else:
587 return TagReference._common_path_default + "/" + path_str
589 def create_head(
590 self,
591 path: PathLike,
592 commit: Union["SymbolicReference", "str"] = "HEAD",
593 force: bool = False,
594 logmsg: Optional[str] = None,
595 ) -> "Head":
596 """Create a new head within the repository.
598 :note:
599 For more documentation, please see the
600 :meth:`Head.create <git.refs.head.Head.create>` method.
602 :return:
603 Newly created :class:`~git.refs.head.Head` Reference.
604 """
605 return Head.create(self, path, commit, logmsg, force)
607 def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None:
608 """Delete the given heads.
610 :param kwargs:
611 Additional keyword arguments to be passed to :manpage:`git-branch(1)`.
612 """
613 return Head.delete(self, *heads, **kwargs)
615 def create_tag(
616 self,
617 path: PathLike,
618 ref: Union[str, "SymbolicReference"] = "HEAD",
619 message: Optional[str] = None,
620 force: bool = False,
621 **kwargs: Any,
622 ) -> TagReference:
623 """Create a new tag reference.
625 :note:
626 For more documentation, please see the
627 :meth:`TagReference.create <git.refs.tag.TagReference.create>` method.
629 :return:
630 :class:`~git.refs.tag.TagReference` object
631 """
632 return TagReference.create(self, path, ref, message, force, **kwargs)
634 def delete_tag(self, *tags: TagReference) -> None:
635 """Delete the given tag references."""
636 return TagReference.delete(self, *tags)
638 def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote:
639 """Create a new remote.
641 For more information, please see the documentation of the
642 :meth:`Remote.create <git.remote.Remote.create>` method.
644 :return:
645 :class:`~git.remote.Remote` reference
646 """
647 return Remote.create(self, name, url, **kwargs)
649 def delete_remote(self, remote: "Remote") -> str:
650 """Delete the given remote."""
651 return Remote.remove(self, remote)
653 def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[PathLike] = None) -> str:
654 if git_dir is None:
655 git_dir = self.git_dir
656 # We do not support an absolute path of the gitconfig on Windows.
657 # Use the global config instead.
658 if sys.platform == "win32" and config_level == "system":
659 config_level = "global"
661 if config_level == "system":
662 return "/etc/gitconfig"
663 elif config_level == "user":
664 config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", "~"), ".config")
665 return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config")))
666 elif config_level == "global":
667 return osp.normpath(osp.expanduser("~/.gitconfig"))
668 elif config_level == "repository":
669 repo_dir = self._common_dir or git_dir
670 if not repo_dir:
671 raise NotADirectoryError
672 else:
673 return osp.normpath(osp.join(repo_dir, "config"))
674 else:
675 assert_never( # type: ignore[unreachable]
676 config_level,
677 ValueError(f"Invalid configuration level: {config_level!r}"),
678 )
680 def config_reader(
681 self,
682 config_level: Optional[Lit_config_levels] = None,
683 ) -> GitConfigParser:
684 """
685 :return:
686 :class:`~git.config.GitConfigParser` allowing to read the full git
687 configuration, but not to write it.
689 The configuration will include values from the system, user and repository
690 configuration files.
692 :param config_level:
693 For possible values, see the :meth:`config_writer` method. If ``None``, all
694 applicable levels will be used. Specify a level in case you know which file
695 you wish to read to prevent reading multiple files.
697 :note:
698 On Windows, system configuration cannot currently be read as the path is
699 unknown, instead the global path will be used.
700 """
701 return self._config_reader(config_level=config_level)
703 def _config_reader(
704 self,
705 config_level: Optional[Lit_config_levels] = None,
706 git_dir: Optional[PathLike] = None,
707 ) -> GitConfigParser:
708 if config_level is None:
709 files = [self._get_config_path(f, git_dir) for f in self.config_level if f]
710 else:
711 files = [self._get_config_path(config_level, git_dir)]
712 return GitConfigParser(files, read_only=True, repo=self)
714 def config_writer(self, config_level: Lit_config_levels = "repository") -> GitConfigParser:
715 """
716 :return:
717 A :class:`~git.config.GitConfigParser` allowing to write values of the
718 specified configuration file level. Config writers should be retrieved, used
719 to change the configuration, and written right away as they will lock the
720 configuration file in question and prevent other's to write it.
722 :param config_level:
723 One of the following values:
725 * ``"system"`` = system wide configuration file
726 * ``"global"`` = user level configuration file
727 * ``"`repository"`` = configuration file for this repository only
728 """
729 return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self, merge_includes=False)
731 def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit:
732 """The :class:`~git.objects.commit.Commit` object for the specified revision.
734 :param rev:
735 Revision specifier, see :manpage:`git-rev-parse(1)` for viable options.
737 :return:
738 :class:`~git.objects.commit.Commit`
739 """
740 if rev is None:
741 return self.head.commit
742 return self.rev_parse(str(rev) + "^0")
744 def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator["Tree"]:
745 """:return: Iterator yielding :class:`~git.objects.tree.Tree` objects
747 :note:
748 Accepts all arguments known to the :meth:`iter_commits` method.
749 """
750 return (c.tree for c in self.iter_commits(*args, **kwargs))
752 def tree(self, rev: Union[Tree_ish, str, None] = None) -> "Tree":
753 """The :class:`~git.objects.tree.Tree` object for the given tree-ish revision.
755 Examples::
757 repo.tree(repo.heads[0])
759 :param rev:
760 A revision pointing to a Treeish (being a commit or tree).
762 :return:
763 :class:`~git.objects.tree.Tree`
765 :note:
766 If you need a non-root level tree, find it by iterating the root tree.
767 Otherwise it cannot know about its path relative to the repository root and
768 subsequent operations might have unexpected results.
769 """
770 if rev is None:
771 return self.head.commit.tree
772 return self.rev_parse(str(rev) + "^{tree}")
774 def iter_commits(
775 self,
776 rev: Union[str, Commit, "SymbolicReference", None] = None,
777 paths: Union[PathLike, Sequence[PathLike]] = "",
778 **kwargs: Any,
779 ) -> Iterator[Commit]:
780 """An iterator of :class:`~git.objects.commit.Commit` objects representing the
781 history of a given ref/commit.
783 :param rev:
784 Revision specifier, see :manpage:`git-rev-parse(1)` for viable options.
785 If ``None``, the active branch will be used.
787 :param paths:
788 An optional path or a list of paths. If set, only commits that include the
789 path or paths will be returned.
791 :param kwargs:
792 Arguments to be passed to :manpage:`git-rev-list(1)`.
793 Common ones are ``max_count`` and ``skip``.
795 :note:
796 To receive only commits between two named revisions, use the
797 ``"revA...revB"`` revision specifier.
799 :return:
800 Iterator of :class:`~git.objects.commit.Commit` objects
801 """
802 if rev is None:
803 rev = self.head.commit
805 return Commit.iter_items(self, rev, paths, **kwargs)
807 def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]:
808 R"""Find the closest common ancestor for the given revision
809 (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s,
810 :class:`~git.refs.reference.Reference`\s, etc.).
812 :param rev:
813 At least two revs to find the common ancestor for.
815 :param kwargs:
816 Additional arguments to be passed to the ``repo.git.merge_base()`` command
817 which does all the work.
819 :return:
820 A list of :class:`~git.objects.commit.Commit` objects. If ``--all`` was
821 not passed as a keyword argument, the list will have at max one
822 :class:`~git.objects.commit.Commit`, or is empty if no common merge base
823 exists.
825 :raise ValueError:
826 If fewer than two revisions are provided.
827 """
828 if len(rev) < 2:
829 raise ValueError("Please specify at least two revs, got only %i" % len(rev))
830 # END handle input
832 res: List[Commit] = []
833 try:
834 lines: List[str] = self.git.merge_base(*rev, **kwargs).splitlines()
835 except GitCommandError as err:
836 if err.status == 128:
837 raise
838 # END handle invalid rev
839 # Status code 1 is returned if there is no merge-base.
840 # (See: https://github.com/git/git/blob/v2.44.0/builtin/merge-base.c#L19)
841 return res
842 # END exception handling
844 for line in lines:
845 res.append(self.commit(line))
846 # END for each merge-base
848 return res
850 def is_ancestor(self, ancestor_rev: Commit, rev: Commit) -> bool:
851 """Check if a commit is an ancestor of another.
853 :param ancestor_rev:
854 Rev which should be an ancestor.
856 :param rev:
857 Rev to test against `ancestor_rev`.
859 :return:
860 ``True`` if `ancestor_rev` is an ancestor to `rev`.
861 """
862 try:
863 self.git.merge_base(ancestor_rev, rev, is_ancestor=True)
864 except GitCommandError as err:
865 if err.status == 1:
866 return False
867 raise
868 return True
870 def is_valid_object(self, sha: str, object_type: Union[str, None] = None) -> bool:
871 try:
872 complete_sha = self.odb.partial_to_complete_sha_hex(sha)
873 object_info = self.odb.info(complete_sha)
874 if object_type:
875 if object_info.type == object_type.encode():
876 return True
877 else:
878 _logger.debug(
879 "Commit hash points to an object of type '%s'. Requested were objects of type '%s'",
880 object_info.type.decode(),
881 object_type,
882 )
883 return False
884 else:
885 return True
886 except BadObject:
887 _logger.debug("Commit hash is invalid.")
888 return False
890 def _get_daemon_export(self) -> bool:
891 if self.git_dir:
892 filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
893 return osp.exists(filename)
895 def _set_daemon_export(self, value: object) -> None:
896 if self.git_dir:
897 filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
898 fileexists = osp.exists(filename)
899 if value and not fileexists:
900 touch(filename)
901 elif not value and fileexists:
902 os.unlink(filename)
904 @property
905 def daemon_export(self) -> bool:
906 """If True, git-daemon may export this repository"""
907 return self._get_daemon_export()
909 @daemon_export.setter
910 def daemon_export(self, value: object) -> None:
911 self._set_daemon_export(value)
913 def _get_alternates(self) -> List[str]:
914 """The list of alternates for this repo from which objects can be retrieved.
916 :return:
917 List of strings being pathnames of alternates
918 """
919 if self.git_dir:
920 alternates_path = osp.join(self.git_dir, "objects", "info", "alternates")
922 if osp.exists(alternates_path):
923 with open(alternates_path, "rb") as f:
924 alts = f.read().decode(defenc)
925 return alts.strip().splitlines()
926 return []
928 def _set_alternates(self, alts: List[str]) -> None:
929 """Set the alternates.
931 :param alts:
932 The array of string paths representing the alternates at which git should
933 look for objects, i.e. ``/home/user/repo/.git/objects``.
935 :raise git.exc.NoSuchPathError:
937 :note:
938 The method does not check for the existence of the paths in `alts`, as the
939 caller is responsible.
940 """
941 alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
942 if not alts:
943 if osp.isfile(alternates_path):
944 os.remove(alternates_path)
945 else:
946 with open(alternates_path, "wb") as f:
947 f.write("\n".join(alts).encode(defenc))
949 @property
950 def alternates(self) -> List[str]:
951 """Retrieve a list of alternates paths or set a list paths to be used as alternates"""
952 return self._get_alternates()
954 @alternates.setter
955 def alternates(self, alts: List[str]) -> None:
956 self._set_alternates(alts)
958 def is_dirty(
959 self,
960 index: bool = True,
961 working_tree: bool = True,
962 untracked_files: bool = False,
963 submodules: bool = True,
964 path: Optional[PathLike] = None,
965 ) -> bool:
966 """
967 :return:
968 ``True`` if the repository is considered dirty. By default it will react
969 like a :manpage:`git-status(1)` without untracked files, hence it is dirty
970 if the index or the working copy have changes.
971 """
972 if self._bare:
973 # Bare repositories with no associated working directory are
974 # always considered to be clean.
975 return False
977 # Start from the one which is fastest to evaluate.
978 default_args = ["--abbrev=40", "--full-index", "--raw"]
979 if not submodules:
980 default_args.append("--ignore-submodules")
981 if path:
982 default_args.extend(["--", os.fspath(path)])
983 if index:
984 # diff index against HEAD.
985 if osp.isfile(self.index.path) and len(self.git.diff("--cached", *default_args)):
986 return True
987 # END index handling
988 if working_tree:
989 # diff index against working tree.
990 if len(self.git.diff(*default_args)):
991 return True
992 # END working tree handling
993 if untracked_files:
994 if len(self._get_untracked_files(path, ignore_submodules=not submodules)):
995 return True
996 # END untracked files
997 return False
999 @property
1000 def untracked_files(self) -> List[str]:
1001 """
1002 :return:
1003 list(str,...)
1005 Files currently untracked as they have not been staged yet. Paths are
1006 relative to the current working directory of the git command.
1008 :note:
1009 Ignored files will not appear here, i.e. files mentioned in ``.gitignore``.
1011 :note:
1012 This property is expensive, as no cache is involved. To process the result,
1013 please consider caching it yourself.
1014 """
1015 return self._get_untracked_files()
1017 def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]:
1018 # Make sure we get all files, not only untracked directories.
1019 proc = self.git.status(*args, porcelain=True, untracked_files=True, as_process=True, **kwargs)
1020 # Untracked files prefix in porcelain mode
1021 prefix = "?? "
1022 untracked_files = []
1023 for line in proc.stdout:
1024 line = line.decode(defenc)
1025 if not line.startswith(prefix):
1026 continue
1027 filename = line[len(prefix) :].rstrip("\n")
1028 # Special characters are escaped
1029 if filename[0] == filename[-1] == '"':
1030 filename = filename[1:-1]
1031 # WHATEVER ... it's a mess, but works for me
1032 filename = filename.encode("ascii").decode("unicode_escape").encode("latin1").decode(defenc)
1033 untracked_files.append(filename)
1034 finalize_process(proc)
1035 return untracked_files
1037 def ignored(self, *paths: PathLike) -> List[str]:
1038 """Checks if paths are ignored via ``.gitignore``.
1040 This does so using the :manpage:`git-check-ignore(1)` method.
1042 :param paths:
1043 List of paths to check whether they are ignored or not.
1045 :return:
1046 Subset of those paths which are ignored
1047 """
1048 try:
1049 proc: str = self.git.check_ignore(*paths)
1050 except GitCommandError as err:
1051 if err.status == 1:
1052 # If return code is 1, this means none of the items in *paths are
1053 # ignored by Git, so return an empty list.
1054 return []
1055 else:
1056 # Raise the exception on all other return codes.
1057 raise
1059 return proc.replace("\\\\", "\\").replace('"', "").split("\n")
1061 @property
1062 def active_branch(self) -> Head:
1063 """The name of the currently active branch.
1065 :raise TypeError:
1066 If HEAD is detached.
1068 :raise ValueError:
1069 If HEAD points to the ``.invalid`` ref Git uses to mark refs as
1070 incompatible with older clients.
1072 :return:
1073 :class:`~git.refs.head.Head` to the active branch
1074 """
1075 active_branch = self.head.reference
1076 if active_branch.name == ".invalid":
1077 raise ValueError(
1078 "HEAD points to 'refs/heads/.invalid', which Git uses to mark refs as incompatible with older clients"
1079 )
1080 return active_branch
1082 def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> Iterator["BlameEntry"]:
1083 """Iterator for blame information for the given file at the given revision.
1085 Unlike :meth:`blame`, this does not return the actual file's contents, only a
1086 stream of :class:`BlameEntry` tuples.
1088 :param rev:
1089 Revision specifier. If ``None``, the blame will include all the latest
1090 uncommitted changes. Otherwise, anything successfully parsed by
1091 :manpage:`git-rev-parse(1)` is a valid option.
1093 :return:
1094 Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the
1095 commit to blame for the line, and range indicates a span of line numbers in
1096 the resulting file.
1098 If you combine all line number ranges outputted by this command, you should get
1099 a continuous range spanning all line numbers in the file.
1100 """
1102 data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs)
1103 commits: Dict[bytes, Commit] = {}
1105 stream = (line for line in data.split(b"\n") if line)
1106 while True:
1107 try:
1108 # When exhausted, causes a StopIteration, terminating this function.
1109 line = next(stream)
1110 except StopIteration:
1111 return
1112 split_line = line.split()
1113 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line
1114 lineno = int(lineno_b)
1115 num_lines = int(num_lines_b)
1116 orig_lineno = int(orig_lineno_b)
1117 if hexsha not in commits:
1118 # Now read the next few lines and build up a dict of properties for this
1119 # commit.
1120 props: Dict[bytes, bytes] = {}
1121 while True:
1122 try:
1123 line = next(stream)
1124 except StopIteration:
1125 return
1126 if line == b"boundary":
1127 # "boundary" indicates a root commit and occurs instead of the
1128 # "previous" tag.
1129 continue
1131 tag, value = line.split(b" ", 1)
1132 props[tag] = value
1133 if tag == b"filename":
1134 # "filename" formally terminates the entry for --incremental.
1135 orig_filename = value
1136 break
1138 c = Commit(
1139 self,
1140 hex_to_bin(hexsha),
1141 author=Actor(
1142 safe_decode(props[b"author"]),
1143 safe_decode(props[b"author-mail"].lstrip(b"<").rstrip(b">")),
1144 ),
1145 authored_date=int(props[b"author-time"]),
1146 committer=Actor(
1147 safe_decode(props[b"committer"]),
1148 safe_decode(props[b"committer-mail"].lstrip(b"<").rstrip(b">")),
1149 ),
1150 committed_date=int(props[b"committer-time"]),
1151 )
1152 commits[hexsha] = c
1153 else:
1154 # Discard all lines until we find "filename" which is guaranteed to be
1155 # the last line.
1156 while True:
1157 try:
1158 # Will fail if we reach the EOF unexpectedly.
1159 line = next(stream)
1160 except StopIteration:
1161 return
1162 tag, value = line.split(b" ", 1)
1163 if tag == b"filename":
1164 orig_filename = value
1165 break
1167 yield BlameEntry(
1168 commits[hexsha],
1169 range(lineno, lineno + num_lines),
1170 safe_decode(orig_filename),
1171 range(orig_lineno, orig_lineno + num_lines),
1172 )
1174 def blame(
1175 self,
1176 rev: Union[str, HEAD, None],
1177 file: str,
1178 incremental: bool = False,
1179 rev_opts: Optional[List[str]] = None,
1180 **kwargs: Any,
1181 ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None:
1182 """The blame information for the given file at the given revision.
1184 :param rev:
1185 Revision specifier. If ``None``, the blame will include all the latest
1186 uncommitted changes. Otherwise, anything successfully parsed by
1187 :manpage:`git-rev-parse(1)` is a valid option.
1189 :return:
1190 list: [git.Commit, list: [<line>]]
1192 A list of lists associating a :class:`~git.objects.commit.Commit` object
1193 with a list of lines that changed within the given commit. The
1194 :class:`~git.objects.commit.Commit` objects will be given in order of
1195 appearance.
1196 """
1197 if incremental:
1198 return self.blame_incremental(rev, file, **kwargs)
1199 rev_opts = rev_opts or []
1200 data: bytes = self.git.blame(rev, *rev_opts, "--", file, p=True, stdout_as_string=False, **kwargs)
1201 commits: Dict[str, Commit] = {}
1202 blames: List[List[Commit | List[str | bytes] | None]] = []
1204 class InfoTD(TypedDict, total=False):
1205 sha: str
1206 id: str
1207 filename: str
1208 summary: str
1209 author: str
1210 author_email: str
1211 author_date: int
1212 committer: str
1213 committer_email: str
1214 committer_date: int
1216 info: InfoTD = {}
1218 keepends = True
1219 for line_bytes in data.splitlines(keepends):
1220 try:
1221 line_str = line_bytes.rstrip().decode(defenc)
1222 except UnicodeDecodeError:
1223 firstpart = ""
1224 parts = []
1225 is_binary = True
1226 else:
1227 # As we don't have an idea when the binary data ends, as it could
1228 # contain multiple newlines in the process. So we rely on being able to
1229 # decode to tell us what it is. This can absolutely fail even on text
1230 # files, but even if it does, we should be fine treating it as binary
1231 # instead.
1232 parts = self.re_whitespace.split(line_str, 1)
1233 firstpart = parts[0]
1234 is_binary = False
1235 # END handle decode of line
1237 if self.re_hexsha_only.search(firstpart):
1238 # handles
1239 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7 - indicates blame-data start
1240 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates
1241 # another line of blame with the same data
1242 digits = parts[-1].split(" ")
1243 if len(digits) == 3:
1244 info = {"id": firstpart}
1245 blames.append([None, []])
1246 elif info["id"] != firstpart:
1247 info = {"id": firstpart}
1248 blames.append([commits.get(firstpart), []])
1249 # END blame data initialization
1250 else:
1251 m = self.re_author_committer_start.search(firstpart)
1252 if m:
1253 # handles:
1254 # author Tom Preston-Werner
1255 # author-mail <tom@mojombo.com>
1256 # author-time 1192271832
1257 # author-tz -0700
1258 # committer Tom Preston-Werner
1259 # committer-mail <tom@mojombo.com>
1260 # committer-time 1192271832
1261 # committer-tz -0700 - IGNORED BY US
1262 role = m.group(0)
1263 if role == "author":
1264 if firstpart.endswith("-mail"):
1265 info["author_email"] = parts[-1]
1266 elif firstpart.endswith("-time"):
1267 info["author_date"] = int(parts[-1])
1268 elif role == firstpart:
1269 info["author"] = parts[-1]
1270 elif role == "committer":
1271 if firstpart.endswith("-mail"):
1272 info["committer_email"] = parts[-1]
1273 elif firstpart.endswith("-time"):
1274 info["committer_date"] = int(parts[-1])
1275 elif role == firstpart:
1276 info["committer"] = parts[-1]
1277 # END distinguish mail,time,name
1278 else:
1279 # handle
1280 # filename lib/grit.rb
1281 # summary add Blob
1282 # <and rest>
1283 if firstpart.startswith("filename"):
1284 info["filename"] = parts[-1]
1285 elif firstpart.startswith("summary"):
1286 info["summary"] = parts[-1]
1287 elif firstpart == "":
1288 if info:
1289 sha = info["id"]
1290 c = commits.get(sha)
1291 if c is None:
1292 c = Commit(
1293 self,
1294 hex_to_bin(sha),
1295 author=Actor._from_string(f"{info['author']} {info['author_email']}"),
1296 authored_date=info["author_date"],
1297 committer=Actor._from_string(f"{info['committer']} {info['committer_email']}"),
1298 committed_date=info["committer_date"],
1299 )
1300 commits[sha] = c
1301 blames[-1][0] = c
1302 # END if commit objects needs initial creation
1304 if blames[-1][1] is not None:
1305 line: str | bytes
1306 if not is_binary:
1307 if line_str and line_str[0] == "\t":
1308 line_str = line_str[1:]
1309 line = line_str
1310 else:
1311 line = line_bytes
1312 # NOTE: We are actually parsing lines out of binary
1313 # data, which can lead to the binary being split up
1314 # along the newline separator. We will append this
1315 # to the blame we are currently looking at, even
1316 # though it should be concatenated with the last
1317 # line we have seen.
1318 blames[-1][1].append(line)
1320 info = {"id": sha}
1321 # END if we collected commit info
1322 # END distinguish filename,summary,rest
1323 # END distinguish author|committer vs filename,summary,rest
1324 # END distinguish hexsha vs other information
1325 return blames
1327 @classmethod
1328 def init(
1329 cls,
1330 path: Union[PathLike, None] = None,
1331 mkdir: bool = True,
1332 odbt: Type[GitCmdObjectDB] = GitCmdObjectDB,
1333 expand_vars: bool = True,
1334 **kwargs: Any,
1335 ) -> "Repo":
1336 """Initialize a git repository at the given path if specified.
1338 :param path:
1339 The full path to the repo (traditionally ends with ``/<name>.git``). Or
1340 ``None``, in which case the repository will be created in the current
1341 working directory.
1343 :param mkdir:
1344 If specified, will create the repository directory if it doesn't already
1345 exist. Creates the directory with a mode=0755.
1346 Only effective if a path is explicitly given.
1348 :param odbt:
1349 Object DataBase type - a type which is constructed by providing the
1350 directory containing the database objects, i.e. ``.git/objects``. It will be
1351 used to access all object data.
1353 :param expand_vars:
1354 If specified, environment variables will not be escaped. This can lead to
1355 information disclosure, allowing attackers to access the contents of
1356 environment variables.
1358 :param kwargs:
1359 Keyword arguments serving as additional options to the
1360 :manpage:`git-init(1)` command.
1362 :return:
1363 :class:`Repo` (the newly created repo)
1364 """
1365 if path:
1366 path = expand_path(path, expand_vars)
1367 if mkdir and path and not osp.exists(path):
1368 os.makedirs(path, 0o755)
1370 # git command automatically chdir into the directory
1371 git = cls.GitCommandWrapperType(path)
1372 git.init(**kwargs)
1373 return cls(path, odbt=odbt)
1375 @classmethod
1376 def _clone(
1377 cls,
1378 git: "Git",
1379 url: PathLike,
1380 path: PathLike,
1381 odb_default_type: Type[GitCmdObjectDB],
1382 progress: Union["RemoteProgress", "UpdateProgress", Callable[..., "RemoteProgress"], None] = None,
1383 multi_options: Optional[List[str]] = None,
1384 allow_unsafe_protocols: bool = False,
1385 allow_unsafe_options: bool = False,
1386 **kwargs: Any,
1387 ) -> "Repo":
1388 odbt = kwargs.pop("odbt", odb_default_type)
1390 # url may be a path and this has no effect if it is a string
1391 url = os.fspath(url)
1392 path = os.fspath(path)
1394 ## A bug win cygwin's Git, when `--bare` or `--separate-git-dir`
1395 # it prepends the cwd or(?) the `url` into the `path, so::
1396 # git clone --bare /cygwin/d/foo.git C:\\Work
1397 # becomes::
1398 # git clone --bare /cygwin/d/foo.git /cygwin/d/C:\\Work
1399 #
1400 clone_path = Git.polish_url(path) if Git.is_cygwin() and "bare" in kwargs else path
1401 sep_dir = kwargs.get("separate_git_dir")
1402 if sep_dir:
1403 kwargs["separate_git_dir"] = Git.polish_url(sep_dir)
1404 multi = None
1405 if multi_options:
1406 multi = shlex.split(" ".join(multi_options))
1408 if not allow_unsafe_protocols:
1409 Git.check_unsafe_protocols(url)
1410 if not allow_unsafe_options:
1411 Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options)
1412 if not allow_unsafe_options and multi:
1413 Git.check_unsafe_options(options=multi, unsafe_options=cls.unsafe_git_clone_options)
1415 proc = git.clone(
1416 multi,
1417 "--",
1418 Git.polish_url(url),
1419 clone_path,
1420 with_extended_output=True,
1421 as_process=True,
1422 v=True,
1423 universal_newlines=True,
1424 **add_progress(kwargs, git, progress),
1425 )
1426 if progress:
1427 handle_process_output(
1428 proc,
1429 None,
1430 to_progress_instance(progress).new_message_handler(),
1431 finalize_process,
1432 decode_streams=False,
1433 )
1434 else:
1435 (stdout, stderr) = proc.communicate()
1436 cmdline = getattr(proc, "args", "")
1437 cmdline = remove_password_if_present(cmdline)
1439 _logger.debug("Cmd(%s)'s unused stdout: %s", cmdline, stdout)
1440 finalize_process(proc, stderr=stderr)
1442 # Our git command could have a different working dir than our actual
1443 # environment, hence we prepend its working dir if required.
1444 if not osp.isabs(path):
1445 path = osp.join(git._working_dir, path) if git._working_dir is not None else path
1447 repo = cls(path, odbt=odbt)
1449 # Retain env values that were passed to _clone().
1450 repo.git.update_environment(**git.environment())
1452 # Adjust remotes - there may be operating systems which use backslashes, These
1453 # might be given as initial paths, but when handling the config file that
1454 # contains the remote from which we were clones, git stops liking it as it will
1455 # escape the backslashes. Hence we undo the escaping just to be sure.
1456 if repo.remotes:
1457 with repo.remotes[0].config_writer as writer:
1458 writer.set_value("url", Git.polish_url(repo.remotes[0].url))
1459 # END handle remote repo
1460 return repo
1462 def clone(
1463 self,
1464 path: PathLike,
1465 progress: Optional[CallableProgress] = None,
1466 multi_options: Optional[List[str]] = None,
1467 allow_unsafe_protocols: bool = False,
1468 allow_unsafe_options: bool = False,
1469 **kwargs: Any,
1470 ) -> "Repo":
1471 """Create a clone from this repository.
1473 :param path:
1474 The full path of the new repo (traditionally ends with ``./<name>.git``).
1476 :param progress:
1477 See :meth:`Remote.push <git.remote.Remote.push>`.
1479 :param multi_options:
1480 A list of :manpage:`git-clone(1)` options that can be provided multiple
1481 times.
1483 One option per list item which is passed exactly as specified to clone.
1484 For example::
1486 [
1487 "--config core.filemode=false",
1488 "--config core.ignorecase",
1489 "--recurse-submodule=repo1_path",
1490 "--recurse-submodule=repo2_path",
1491 ]
1493 :param allow_unsafe_protocols:
1494 Allow unsafe protocols to be used, like ``ext``.
1496 :param allow_unsafe_options:
1497 Allow unsafe options to be used, like ``--upload-pack``.
1499 :param kwargs:
1500 * ``odbt`` = ObjectDatabase Type, allowing to determine the object database
1501 implementation used by the returned :class:`Repo` instance.
1502 * All remaining keyword arguments are given to the :manpage:`git-clone(1)`
1503 command.
1505 :return:
1506 :class:`Repo` (the newly cloned repo)
1507 """
1508 return self._clone(
1509 self.git,
1510 self.common_dir,
1511 path,
1512 type(self.odb),
1513 progress, # type: ignore[arg-type]
1514 multi_options,
1515 allow_unsafe_protocols=allow_unsafe_protocols,
1516 allow_unsafe_options=allow_unsafe_options,
1517 **kwargs,
1518 )
1520 @classmethod
1521 def clone_from(
1522 cls,
1523 url: PathLike,
1524 to_path: PathLike,
1525 progress: CallableProgress = None,
1526 env: Optional[Mapping[str, str]] = None,
1527 multi_options: Optional[List[str]] = None,
1528 allow_unsafe_protocols: bool = False,
1529 allow_unsafe_options: bool = False,
1530 **kwargs: Any,
1531 ) -> "Repo":
1532 """Create a clone from the given URL.
1534 :param url:
1535 Valid git url, see: https://git-scm.com/docs/git-clone#URLS
1537 :param to_path:
1538 Path to which the repository should be cloned to.
1540 :param progress:
1541 See :meth:`Remote.push <git.remote.Remote.push>`.
1543 :param env:
1544 Optional dictionary containing the desired environment variables.
1546 Note: Provided variables will be used to update the execution environment
1547 for ``git``. If some variable is not specified in `env` and is defined in
1548 :attr:`os.environ`, value from :attr:`os.environ` will be used. If you want
1549 to unset some variable, consider providing empty string as its value.
1551 :param multi_options:
1552 See the :meth:`clone` method.
1554 :param allow_unsafe_protocols:
1555 Allow unsafe protocols to be used, like ``ext``.
1557 :param allow_unsafe_options:
1558 Allow unsafe options to be used, like ``--upload-pack``.
1560 :param kwargs:
1561 See the :meth:`clone` method.
1563 :return:
1564 :class:`Repo` instance pointing to the cloned directory.
1565 """
1566 git = cls.GitCommandWrapperType(os.getcwd())
1567 if env is not None:
1568 git.update_environment(**env)
1569 return cls._clone(
1570 git,
1571 url,
1572 to_path,
1573 GitCmdObjectDB,
1574 progress, # type: ignore[arg-type]
1575 multi_options,
1576 allow_unsafe_protocols=allow_unsafe_protocols,
1577 allow_unsafe_options=allow_unsafe_options,
1578 **kwargs,
1579 )
1581 def archive(
1582 self,
1583 ostream: Union[TextIO, BinaryIO],
1584 treeish: Optional[str] = None,
1585 prefix: Optional[str] = None,
1586 **kwargs: Any,
1587 ) -> Repo:
1588 """Archive the tree at the given revision.
1590 :param ostream:
1591 File-compatible stream object to which the archive will be written as bytes.
1593 :param treeish:
1594 The treeish name/id, defaults to active branch.
1596 :param prefix:
1597 The optional prefix to prepend to each filename in the archive.
1599 :param kwargs:
1600 Additional arguments passed to :manpage:`git-archive(1)`:
1602 * Use the ``format`` argument to define the kind of format. Use specialized
1603 ostreams to write any format supported by Python.
1604 * You may specify the special ``path`` keyword, which may either be a
1605 repository-relative path to a directory or file to place into the archive,
1606 or a list or tuple of multiple paths.
1608 :raise git.exc.GitCommandError:
1609 If something went wrong.
1611 :return:
1612 self
1613 """
1614 if treeish is None:
1615 treeish = self.head.commit
1616 if prefix and "prefix" not in kwargs:
1617 kwargs["prefix"] = prefix
1618 kwargs["output_stream"] = ostream
1619 path = kwargs.pop("path", [])
1620 path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path)
1621 if not isinstance(path, (tuple, list)):
1622 path = [path]
1623 # END ensure paths is list (or tuple)
1624 self.git.archive("--", treeish, *path, **kwargs)
1625 return self
1627 def has_separate_working_tree(self) -> bool:
1628 """
1629 :return:
1630 True if our :attr:`git_dir` is not at the root of our
1631 :attr:`working_tree_dir`, but a ``.git`` file with a platform-agnostic
1632 symbolic link. Our :attr:`git_dir` will be wherever the ``.git`` file points
1633 to.
1635 :note:
1636 Bare repositories will always return ``False`` here.
1637 """
1638 if self.bare:
1639 return False
1640 if self.working_tree_dir:
1641 return osp.isfile(osp.join(self.working_tree_dir, ".git"))
1642 else:
1643 return False # Or raise Error?
1645 rev_parse = rev_parse
1647 def __repr__(self) -> str:
1648 clazz = self.__class__
1649 return "<%s.%s %r>" % (clazz.__module__, clazz.__name__, self.git_dir)
1651 def currently_rebasing_on(self) -> Commit | None:
1652 """
1653 :return:
1654 The commit which is currently being replayed while rebasing.
1656 ``None`` if we are not currently rebasing.
1657 """
1658 if self.git_dir:
1659 rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
1660 if not osp.isfile(rebase_head_file):
1661 return None
1662 with open(rebase_head_file, "rt") as f:
1663 content = f.readline().strip()
1664 return self.commit(content)