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