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 is_git_dir,
49 rev_parse,
50 touch,
51)
53# typing ------------------------------------------------------
55from git.types import (
56 CallableProgress,
57 Commit_ish,
58 Lit_config_levels,
59 PathLike,
60 TBD,
61 Tree_ish,
62 assert_never,
63)
64from typing import (
65 Any,
66 BinaryIO,
67 Callable,
68 Dict,
69 Iterator,
70 List,
71 Mapping,
72 NamedTuple,
73 Optional,
74 Sequence,
75 TYPE_CHECKING,
76 TextIO,
77 Tuple,
78 Type,
79 Union,
80 cast,
81)
83from git.types import ConfigLevels_Tup, TypedDict
85if TYPE_CHECKING:
86 from git.objects import Tree
87 from git.objects.submodule.base import UpdateProgress
88 from git.refs.symbolic import SymbolicReference
89 from git.remote import RemoteProgress
90 from git.util import IterableList
92# -----------------------------------------------------------
94_logger = logging.getLogger(__name__)
97class BlameEntry(NamedTuple):
98 commit: Dict[str, Commit]
99 linenos: range
100 orig_path: Optional[str]
101 orig_linenos: range
104class Repo:
105 """Represents a git repository and allows you to query references, create commit
106 information, generate diffs, create and clone repositories, and query the log.
108 The following attributes are worth using:
110 * :attr:`working_dir` is the working directory of the git command, which is the
111 working tree directory if available or the ``.git`` directory in case of bare
112 repositories.
114 * :attr:`working_tree_dir` is the working tree directory, but will return ``None``
115 if we are a bare repository.
117 * :attr:`git_dir` is the ``.git`` repository directory, which is always set.
118 """
120 DAEMON_EXPORT_FILE = "git-daemon-export-ok"
122 # Must exist, or __del__ will fail in case we raise on `__init__()`.
123 git = cast("Git", None)
125 working_dir: PathLike
126 """The working directory of the git command."""
128 # stored as string for easier processing, but annotated as path for clearer intention
129 _working_tree_dir: Optional[PathLike] = None
131 git_dir: PathLike
132 """The ``.git`` repository directory."""
134 _common_dir: PathLike = ""
136 # Precompiled regex
137 re_whitespace = re.compile(r"\s+")
138 re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$")
139 re_hexsha_shortened = re.compile(r"^[0-9A-Fa-f]{4,40}$")
140 re_envvars = re.compile(r"(\$(\{\s?)?[a-zA-Z_]\w*(\}\s?)?|%\s?[a-zA-Z_]\w*\s?%)")
141 re_author_committer_start = re.compile(r"^(author|committer)")
142 re_tab_full_line = re.compile(r"^\t(.*)$")
144 unsafe_git_init_options = [
145 # Can install hooks that execute during later Git commands:
146 "--template",
147 # Redirects the repository metadata to a caller-controlled path:
148 "--separate-git-dir",
149 ]
150 """Options to :manpage:`git-init(1)` that permit unsafe code execution or I/O."""
152 unsafe_git_clone_options = [
153 # Executes arbitrary commands:
154 "--upload-pack",
155 "-u",
156 # Can override configuration variables that execute arbitrary commands:
157 "--config",
158 "-c",
159 # Can install hooks that execute during clone:
160 "--template",
161 # Redirects the repository metadata to a caller-controlled path:
162 "--separate-git-dir",
163 # Fetches from an additional caller-controlled URI:
164 "--bundle-uri",
165 ]
166 """Options to :manpage:`git-clone(1)` that permit unsafe command execution or I/O.
168 The ``--upload-pack``/``-u`` option allows users to execute arbitrary commands
169 directly:
170 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt
172 The ``--config``/``-c`` option allows users to override configuration variables like
173 ``protocol.allow`` and ``core.gitProxy`` to execute arbitrary commands:
174 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt
176 The ``--template`` option can install hooks that execute during clone:
177 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---templatetemplate-directory
179 The ``--bundle-uri`` option fetches from an additional URI before fetching from the
180 clone URL. An untrusted value can therefore make Git access local files or
181 unintended network resources:
182 https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---bundle-uriuri
183 """
185 unsafe_git_archive_options = [
186 # Allows arbitrary command execution through the remote git-upload-archive command.
187 "--exec",
188 # Writes output to a caller-controlled filesystem path.
189 "--output",
190 "-o",
191 # Reads from a caller-controlled filesystem path:
192 "--add-file",
193 # Injects a caller-controlled path and contents:
194 "--add-virtual-file",
195 ]
197 unsafe_git_revision_options = [
198 # This option allows output to be written to arbitrary files before revision parsing.
199 "--output",
200 "-o",
201 ]
203 unsafe_git_blame_options = unsafe_git_revision_options + [
204 # These options read from arbitrary files and expose their contents through blame output.
205 "--contents",
206 "-S",
207 "--ignore-revs-file",
208 ]
210 unsafe_git_diff_options = unsafe_git_revision_options + [
211 # Treats path operands as arbitrary filesystem paths.
212 "--no-index",
213 # Reads caller-controlled order patterns from an arbitrary file.
214 "-O",
215 "--orderfile",
216 ]
218 # Invariants
219 config_level: ConfigLevels_Tup = ("system", "user", "global", "repository")
220 """Represents the configuration level of a configuration file."""
222 # Subclass configuration
223 GitCommandWrapperType = Git
224 """Subclasses may easily bring in their own custom types by placing a constructor or
225 type here."""
227 def __init__(
228 self,
229 path: Optional[PathLike] = None,
230 odbt: Type[LooseObjectDB] = GitCmdObjectDB,
231 search_parent_directories: bool = False,
232 expand_vars: bool = True,
233 ) -> None:
234 R"""Create a new :class:`Repo` instance.
236 .. note::
237 Repositories using reftable may be opened, but GitPython's direct reference
238 access does not support reftable.
240 :param path:
241 The path to either the worktree directory or the .git directory itself::
243 repo = Repo("/Users/mtrier/Development/git-python")
244 repo = Repo("/Users/mtrier/Development/git-python.git")
245 repo = Repo("~/Development/git-python.git")
246 repo = Repo("$REPOSITORIES/Development/git-python.git")
247 repo = Repo(R"C:\Users\mtrier\Development\git-python\.git")
249 - In *Cygwin*, `path` may be a ``cygdrive/...`` prefixed path.
250 - If `path` is ``None`` or an empty string, :envvar:`GIT_DIR` is used. If
251 that environment variable is absent or empty, the current directory is
252 used.
254 :param odbt:
255 Object DataBase type - a type which is constructed by providing the
256 directory containing the database objects, i.e. ``.git/objects``. It will be
257 used to access all object data.
259 :param search_parent_directories:
260 If ``True``, all parent directories will be searched for a valid repo as
261 well.
263 Please note that this was the default behaviour in older versions of
264 GitPython, which is considered a bug though.
266 :raise git.exc.InvalidGitRepositoryError:
268 :raise git.exc.NoSuchPathError:
270 :return:
271 :class:`Repo`
272 """
274 git_dir_env = os.getenv("GIT_DIR")
275 object_dir_env = os.getenv("GIT_OBJECT_DIRECTORY")
276 if object_dir_env is not None:
277 object_dir_env = osp.abspath(object_dir_env)
278 epath = path or git_dir_env
279 if not epath:
280 epath = os.getcwd()
281 epath = os.fspath(epath)
282 if Git.is_cygwin():
283 # Given how the tests are written, this seems more likely to catch Cygwin
284 # git used from Windows than Windows git used from Cygwin. Therefore
285 # changing to Cygwin-style paths is the relevant operation.
286 epath = cygpath(epath)
288 if expand_vars and re.search(self.re_envvars, epath):
289 warnings.warn(
290 "The use of environment variables in paths is deprecated"
291 + "\nfor security reasons and may be removed in the future!!",
292 stacklevel=1,
293 )
294 epath = expand_path(epath, expand_vars)
295 if epath is not None:
296 if not os.path.exists(epath):
297 raise NoSuchPathError(epath)
299 # Walk up the path to find the `.git` dir.
300 curpath = os.fspath(epath) if epath is not None else ""
301 git_dir: Optional[str] = None
302 explicit_git_dir = not path and bool(git_dir_env)
303 while curpath:
304 # ABOUT osp.NORMPATH
305 # It's important to normalize the paths, as submodules will otherwise
306 # initialize their repo instances with paths that depend on path-portions
307 # that will not exist after being removed. It's just cleaner.
308 if not explicit_git_dir:
309 dotgit = osp.join(curpath, ".git")
310 try:
311 sm_gitpath = find_submodule_git_dir(dotgit)
312 except OSError:
313 break
314 if sm_gitpath is not None:
315 # Worktrees can use relative paths as of Git 2.48, so join to curpath.
316 git_dir = osp.normpath(osp.join(curpath, os.fspath(sm_gitpath)))
317 self._working_tree_dir = curpath
318 break
320 # Like Git, do not fall back to a bare repository or parent directory when
321 # a non-directory .git entry exists but is not a valid gitfile.
322 if osp.exists(dotgit) and not osp.isdir(dotgit):
323 break
325 if is_git_dir(curpath):
326 git_dir = curpath
327 if osp.isfile(osp.join(curpath, "gitdir")) and osp.isfile(osp.join(curpath, "commondir")):
328 if "GIT_WORK_TREE" in os.environ:
329 self._working_tree_dir = os.getenv("GIT_WORK_TREE")
330 else:
331 # Linked worktree administrative directories store the path to
332 # the worktree's .git file in gitdir (without a "gitdir: " prefix).
333 with open(osp.join(git_dir, "gitdir")) as fp:
334 worktree_gitfile = fp.read().strip()
336 if not osp.isabs(worktree_gitfile):
337 worktree_gitfile = osp.normpath(osp.join(git_dir, worktree_gitfile))
339 self._working_tree_dir = osp.dirname(worktree_gitfile)
340 break
342 # from man git-config : core.worktree
343 # Set the path to the root of the working tree. If GIT_COMMON_DIR
344 # environment variable is set, core.worktree is ignored and not used for
345 # determining the root of working tree. This can be overridden by the
346 # GIT_WORK_TREE environment variable. The value can be an absolute path
347 # or relative to the path to the .git directory, which is either
348 # specified by GIT_DIR, or automatically discovered. If GIT_DIR is
349 # specified but none of GIT_WORK_TREE and core.worktree is specified,
350 # the current working directory is regarded as the top level of your
351 # working tree.
352 self._working_tree_dir = os.path.dirname(git_dir)
353 if os.environ.get("GIT_COMMON_DIR") is None:
354 gitconf = self._config_reader("repository", git_dir)
355 if gitconf.has_option("core", "worktree"):
356 self._working_tree_dir = gitconf.get("core", "worktree")
357 if "GIT_WORK_TREE" in os.environ:
358 self._working_tree_dir = os.getenv("GIT_WORK_TREE")
359 break
361 if explicit_git_dir or not search_parent_directories:
362 break
363 curpath, tail = osp.split(curpath)
364 if not tail:
365 break
366 # END while curpath
368 if git_dir is None:
369 raise InvalidGitRepositoryError(epath)
370 self.git_dir = git_dir
372 common_dir_env = os.getenv("GIT_COMMON_DIR")
373 if common_dir_env is not None:
374 self._common_dir = osp.abspath(common_dir_env)
375 else:
376 try:
377 common_dir = os.fsdecode((Path(self.git_dir) / "commondir").read_bytes()).rstrip("\r\n")
378 self._common_dir = osp.join(self.git_dir, common_dir)
379 except OSError:
380 self._common_dir = ""
382 self._bare = False
383 try:
384 self._bare = self.config_reader("repository").getboolean("core", "bare")
385 except Exception:
386 # Let's not assume the option exists, although it should.
387 pass
389 # A linked worktree is not bare even when its main repository is.
390 if self._bare and self._working_tree_dir and osp.isfile(osp.join(self.git_dir, "commondir")):
391 self._bare = False
393 # Adjust the working directory in case we are actually bare - we didn't know
394 # that in the first place.
395 if self._bare:
396 self._working_tree_dir = None
397 # END working dir handling
399 self.working_dir: PathLike = self._working_tree_dir or self.common_dir
400 self.git = self.GitCommandWrapperType(self.working_dir)
401 if common_dir_env is not None:
402 self.git.update_environment(GIT_DIR=os.fspath(self.git_dir), GIT_COMMON_DIR=os.fspath(self.common_dir))
403 elif git_dir_env is not None:
404 self.git.update_environment(GIT_DIR=os.fspath(self.git_dir))
405 if object_dir_env is not None:
406 self.git.update_environment(GIT_OBJECT_DIRECTORY=object_dir_env)
408 # Special handling, in special times.
409 rootpath = object_dir_env if object_dir_env is not None else osp.join(self.common_dir, "objects")
410 if issubclass(odbt, GitCmdObjectDB):
411 self.odb = odbt(rootpath, self.git)
412 else:
413 self.odb = odbt(rootpath)
415 def __enter__(self) -> "Repo":
416 return self
418 def __exit__(self, *args: Any) -> None:
419 self.close()
421 def __del__(self) -> None:
422 try:
423 self.close()
424 except Exception:
425 pass
427 def close(self) -> None:
428 if self.git:
429 self.git.clear_cache()
430 # Tempfiles objects on Windows are holding references to open files until
431 # they are collected by the garbage collector, thus preventing deletion.
432 # TODO: Find these references and ensure they are closed and deleted
433 # synchronously rather than forcing a gc collection.
434 if sys.platform == "win32":
435 gc.collect()
436 gitdb.util.mman.collect()
437 if sys.platform == "win32":
438 gc.collect()
440 def __eq__(self, rhs: object) -> bool:
441 if isinstance(rhs, Repo):
442 return self.git_dir == rhs.git_dir
443 return False
445 def __ne__(self, rhs: object) -> bool:
446 return not self.__eq__(rhs)
448 def __hash__(self) -> int:
449 return hash(self.git_dir)
451 @property
452 def description(self) -> str:
453 """The project's description"""
454 filename = osp.join(self.git_dir, "description")
455 with open(filename, "rb") as fp:
456 return fp.read().rstrip().decode(defenc)
458 @description.setter
459 def description(self, descr: str) -> None:
460 filename = osp.join(self.git_dir, "description")
461 with open(filename, "wb") as fp:
462 fp.write((descr + "\n").encode(defenc))
464 @property
465 def working_tree_dir(self) -> Optional[PathLike]:
466 """
467 :return:
468 The working tree directory of our git repository.
469 If this is a bare repository, ``None`` is returned.
470 """
471 return self._working_tree_dir
473 @property
474 def common_dir(self) -> PathLike:
475 """
476 :return:
477 The git dir that holds everything except possibly HEAD, FETCH_HEAD,
478 ORIG_HEAD, COMMIT_EDITMSG, index, and logs/.
479 """
480 return self._common_dir or self.git_dir
482 @property
483 def bare(self) -> bool:
484 """:return: ``True`` if the repository is bare"""
485 return self._bare
487 @property
488 def heads(self) -> "IterableList[Head]":
489 """A list of :class:`~git.refs.head.Head` objects representing the branch heads
490 in this repo.
492 :return:
493 ``git.IterableList(Head, ...)``
494 """
495 return Head.list_items(self)
497 @property
498 def branches(self) -> "IterableList[Head]":
499 """Alias for heads.
500 A list of :class:`~git.refs.head.Head` objects representing the branch heads
501 in this repo.
503 :return:
504 ``git.IterableList(Head, ...)``
505 """
506 return self.heads
508 @property
509 def references(self) -> "IterableList[Reference]":
510 """A list of :class:`~git.refs.reference.Reference` objects representing tags,
511 heads and remote references.
513 :return:
514 ``git.IterableList(Reference, ...)``
515 """
516 return Reference.list_items(self)
518 @property
519 def refs(self) -> "IterableList[Reference]":
520 """Alias for references.
521 A list of :class:`~git.refs.reference.Reference` objects representing tags,
522 heads and remote references.
524 :return:
525 ``git.IterableList(Reference, ...)``
526 """
527 return self.references
529 @property
530 def index(self) -> "IndexFile":
531 """
532 :return:
533 A :class:`~git.index.base.IndexFile` representing this repository's index.
535 :note:
536 This property can be expensive, as the returned
537 :class:`~git.index.base.IndexFile` will be reinitialized.
538 It is recommended to reuse the object.
539 """
540 return IndexFile(self)
542 @property
543 def head(self) -> "HEAD":
544 """
545 :return:
546 :class:`~git.refs.head.HEAD` object pointing to the current head reference
547 """
548 return HEAD(self, "HEAD")
550 @property
551 def remotes(self) -> "IterableList[Remote]":
552 """A list of :class:`~git.remote.Remote` objects allowing to access and
553 manipulate remotes.
555 :return:
556 ``git.IterableList(Remote, ...)``
557 """
558 return Remote.list_items(self)
560 def remote(self, name: str = "origin") -> "Remote":
561 """:return: The remote with the specified name
563 :raise ValueError:
564 If no remote with such a name exists.
565 """
566 r = Remote(self, name)
567 if not r.exists():
568 raise ValueError("Remote named '%s' didn't exist" % name)
569 return r
571 # { Submodules
573 @property
574 def submodules(self) -> "IterableList[Submodule]":
575 """
576 :return:
577 git.IterableList(Submodule, ...) of direct submodules available from the
578 current head
579 """
580 return Submodule.list_items(self)
582 def submodule(self, name: str) -> "Submodule":
583 """:return: The submodule with the given name
585 :raise ValueError:
586 If no such submodule exists.
587 """
588 try:
589 return self.submodules[name]
590 except IndexError as e:
591 raise ValueError("Didn't find submodule named %r" % name) from e
592 # END exception handling
594 def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule:
595 """Create a new submodule.
597 :note:
598 For a description of the applicable parameters, see the documentation of
599 :meth:`Submodule.add <git.objects.submodule.base.Submodule.add>`.
601 :return:
602 The created submodule.
603 """
604 return Submodule.add(self, *args, **kwargs)
606 def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]:
607 """An iterator yielding Submodule instances.
609 See the :class:`~git.objects.util.Traversable` interface for a description of `args`
610 and `kwargs`.
612 :return:
613 Iterator
614 """
615 return RootModule(self).traverse(*args, **kwargs)
617 def submodule_update(self, *args: Any, **kwargs: Any) -> RootModule:
618 """Update the submodules, keeping the repository consistent as it will
619 take the previous state into consideration.
621 :note:
622 For more information, please see the documentation of
623 :meth:`RootModule.update <git.objects.submodule.root.RootModule.update>`.
624 """
625 return RootModule(self).update(*args, **kwargs)
627 # }END submodules
629 @property
630 def tags(self) -> "IterableList[TagReference]":
631 """A list of :class:`~git.refs.tag.TagReference` objects that are available in
632 this repo.
634 :return:
635 ``git.IterableList(TagReference, ...)``
636 """
637 return TagReference.list_items(self)
639 def tag(self, path: PathLike) -> TagReference:
640 """
641 :return:
642 :class:`~git.refs.tag.TagReference` object, reference pointing to a
643 :class:`~git.objects.commit.Commit` or tag
645 :param path:
646 Path to the tag reference, e.g. ``0.1.5`` or ``tags/0.1.5``.
647 """
648 full_path = self._to_full_tag_path(path)
649 return TagReference(self, full_path)
651 @staticmethod
652 def _to_full_tag_path(path: PathLike) -> str:
653 path_str = str(path)
654 if path_str.startswith(TagReference._common_path_default + "/"):
655 return path_str
656 if path_str.startswith(TagReference._common_default + "/"):
657 return Reference._common_path_default + "/" + path_str
658 else:
659 return TagReference._common_path_default + "/" + path_str
661 def create_head(
662 self,
663 path: PathLike,
664 commit: Union["SymbolicReference", "str"] = "HEAD",
665 force: bool = False,
666 logmsg: Optional[str] = None,
667 ) -> "Head":
668 """Create a new head within the repository.
670 :note:
671 For more documentation, please see the
672 :meth:`Head.create <git.refs.head.Head.create>` method.
674 :return:
675 Newly created :class:`~git.refs.head.Head` Reference.
676 """
677 return Head.create(self, path, commit, logmsg, force)
679 def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None:
680 """Delete the given heads.
682 :param kwargs:
683 Additional keyword arguments to be passed to :manpage:`git-branch(1)`.
684 """
685 return Head.delete(self, *heads, **kwargs)
687 def create_tag(
688 self,
689 path: PathLike,
690 ref: Union[str, "SymbolicReference"] = "HEAD",
691 message: Optional[str] = None,
692 force: bool = False,
693 **kwargs: Any,
694 ) -> TagReference:
695 """Create a new tag reference.
697 :note:
698 For more documentation, please see the
699 :meth:`TagReference.create <git.refs.tag.TagReference.create>` method.
701 :return:
702 :class:`~git.refs.tag.TagReference` object
703 """
704 return TagReference.create(self, path, ref, message, force, **kwargs)
706 def delete_tag(self, *tags: TagReference) -> None:
707 """Delete the given tag references."""
708 return TagReference.delete(self, *tags)
710 def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote:
711 """Create a new remote.
713 For more information, please see the documentation of the
714 :meth:`Remote.create <git.remote.Remote.create>` method.
716 :return:
717 :class:`~git.remote.Remote` reference
718 """
719 return Remote.create(self, name, url, **kwargs)
721 def delete_remote(self, remote: "Remote") -> str:
722 """Delete the given remote."""
723 return Remote.remove(self, remote)
725 def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[PathLike] = None) -> str:
726 if git_dir is None:
727 git_dir = self.git_dir
728 # We do not support an absolute path of the gitconfig on Windows.
729 # Use the global config instead.
730 if sys.platform == "win32" and config_level == "system":
731 config_level = "global"
733 if config_level == "system":
734 return "/etc/gitconfig"
735 elif config_level == "user":
736 config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", "~"), ".config")
737 return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config")))
738 elif config_level == "global":
739 return osp.normpath(osp.expanduser("~/.gitconfig"))
740 elif config_level == "repository":
741 repo_dir = self._common_dir or git_dir
742 if not repo_dir:
743 raise NotADirectoryError
744 else:
745 return osp.normpath(osp.join(repo_dir, "config"))
746 else:
747 assert_never( # type: ignore[unreachable]
748 config_level,
749 ValueError(f"Invalid configuration level: {config_level!r}"),
750 )
752 def config_reader(
753 self,
754 config_level: Optional[Lit_config_levels] = None,
755 ) -> GitConfigParser:
756 """
757 :return:
758 :class:`~git.config.GitConfigParser` allowing to read the full git
759 configuration, but not to write it.
761 The configuration will include values from the system, user and repository
762 configuration files.
764 :param config_level:
765 For possible values, see the :meth:`config_writer` method. If ``None``, all
766 applicable levels will be used. Specify a level in case you know which file
767 you wish to read to prevent reading multiple files.
769 :note:
770 On Windows, system configuration cannot currently be read as the path is
771 unknown, instead the global path will be used.
772 """
773 return self._config_reader(config_level=config_level)
775 def _config_reader(
776 self,
777 config_level: Optional[Lit_config_levels] = None,
778 git_dir: Optional[PathLike] = None,
779 ) -> GitConfigParser:
780 if config_level is None:
781 files = [self._get_config_path(f, git_dir) for f in self.config_level if f]
782 else:
783 files = [self._get_config_path(config_level, git_dir)]
784 return GitConfigParser(files, read_only=True, repo=self)
786 def config_writer(self, config_level: Lit_config_levels = "repository") -> GitConfigParser:
787 """
788 :return:
789 A :class:`~git.config.GitConfigParser` allowing to write values of the
790 specified configuration file level. Config writers should be retrieved, used
791 to change the configuration, and written right away as they will lock the
792 configuration file in question and prevent other's to write it.
794 :param config_level:
795 One of the following values:
797 * ``"system"`` = system wide configuration file
798 * ``"global"`` = user level configuration file
799 * ``"`repository"`` = configuration file for this repository only
800 """
801 return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self, merge_includes=False)
803 def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit:
804 """The :class:`~git.objects.commit.Commit` object for the specified revision.
806 :param rev:
807 Revision specifier, see :manpage:`git-rev-parse(1)` for viable options.
809 :return:
810 :class:`~git.objects.commit.Commit`
811 """
812 if rev is None:
813 return self.head.commit
814 return self.rev_parse(str(rev) + "^0")
816 def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator["Tree"]:
817 """:return: Iterator yielding :class:`~git.objects.tree.Tree` objects
819 :note:
820 Accepts all arguments known to the :meth:`iter_commits` method.
821 """
822 return (c.tree for c in self.iter_commits(*args, **kwargs))
824 def tree(self, rev: Union[Tree_ish, str, None] = None) -> "Tree":
825 """The :class:`~git.objects.tree.Tree` object for the given tree-ish revision.
827 Examples::
829 repo.tree(repo.heads[0])
831 :param rev:
832 A revision pointing to a Treeish (being a commit or tree).
834 :return:
835 :class:`~git.objects.tree.Tree`
837 :note:
838 If you need a non-root level tree, find it by iterating the root tree.
839 Otherwise it cannot know about its path relative to the repository root and
840 subsequent operations might have unexpected results.
841 """
842 if rev is None:
843 return self.head.commit.tree
844 return self.rev_parse(str(rev) + "^{tree}")
846 def iter_commits(
847 self,
848 rev: Union[str, Commit, "SymbolicReference", None] = None,
849 paths: Union[PathLike, Sequence[PathLike]] = "",
850 allow_unsafe_options: bool = False,
851 **kwargs: Any,
852 ) -> Iterator[Commit]:
853 """An iterator of :class:`~git.objects.commit.Commit` objects representing the
854 history of a given ref/commit.
856 :param rev:
857 Revision specifier, see :manpage:`git-rev-parse(1)` for viable options.
858 If ``None``, the active branch will be used.
860 :param paths:
861 An optional path or a list of paths. If set, only commits that include the
862 path or paths will be returned.
864 :param kwargs:
865 Arguments to be passed to :manpage:`git-rev-list(1)`.
866 Common ones are ``max_count`` and ``skip``.
868 :param allow_unsafe_options:
869 Allow unsafe options in the revision argument, like ``--output``.
871 :note:
872 To receive only commits between two named revisions, use the
873 ``"revA...revB"`` revision specifier.
875 :return:
876 Iterator of :class:`~git.objects.commit.Commit` objects
877 """
878 if rev is None:
879 rev = self.head.commit
881 if not allow_unsafe_options:
882 Git.check_unsafe_options(
883 options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options
884 )
886 return Commit.iter_items(
887 self,
888 rev,
889 paths,
890 allow_unsafe_options=allow_unsafe_options,
891 **kwargs,
892 )
894 def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]:
895 R"""Find the closest common ancestor for the given revision
896 (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s,
897 :class:`~git.refs.reference.Reference`\s, etc.).
899 :param rev:
900 At least two revs to find the common ancestor for.
902 :param kwargs:
903 Additional arguments to be passed to the ``repo.git.merge_base()`` command
904 which does all the work.
906 :return:
907 A list of :class:`~git.objects.commit.Commit` objects. If ``--all`` was
908 not passed as a keyword argument, the list will have at max one
909 :class:`~git.objects.commit.Commit`, or is empty if no common merge base
910 exists.
912 :raise ValueError:
913 If fewer than two revisions are provided.
914 """
915 if len(rev) < 2:
916 raise ValueError("Please specify at least two revs, got only %i" % len(rev))
917 # END handle input
919 res: List[Commit] = []
920 try:
921 lines: List[str] = self.git.merge_base(*rev, **kwargs).splitlines()
922 except GitCommandError as err:
923 if err.status == 128:
924 raise
925 # END handle invalid rev
926 # Status code 1 is returned if there is no merge-base.
927 # (See: https://github.com/git/git/blob/v2.44.0/builtin/merge-base.c#L19)
928 return res
929 # END exception handling
931 for line in lines:
932 res.append(self.commit(line))
933 # END for each merge-base
935 return res
937 def is_ancestor(self, ancestor_rev: Commit, rev: Commit) -> bool:
938 """Check if a commit is an ancestor of another.
940 :param ancestor_rev:
941 Rev which should be an ancestor.
943 :param rev:
944 Rev to test against `ancestor_rev`.
946 :return:
947 ``True`` if `ancestor_rev` is an ancestor to `rev`.
948 """
949 try:
950 self.git.merge_base(ancestor_rev, rev, is_ancestor=True)
951 except GitCommandError as err:
952 if err.status == 1:
953 return False
954 raise
955 return True
957 def is_valid_object(self, sha: str, object_type: Union[str, None] = None) -> bool:
958 try:
959 complete_sha = self.odb.partial_to_complete_sha_hex(sha)
960 object_info = self.odb.info(complete_sha)
961 if object_type:
962 if object_info.type == object_type.encode():
963 return True
964 else:
965 _logger.debug(
966 "Commit hash points to an object of type '%s'. Requested were objects of type '%s'",
967 object_info.type.decode(),
968 object_type,
969 )
970 return False
971 else:
972 return True
973 except BadObject:
974 _logger.debug("Commit hash is invalid.")
975 return False
977 def _get_daemon_export(self) -> bool:
978 git_dir = getattr(self, "git_dir", None)
979 if git_dir is None:
980 return False
981 filename = osp.join(git_dir, self.DAEMON_EXPORT_FILE)
982 return osp.exists(filename)
984 def _set_daemon_export(self, value: object) -> None:
985 git_dir = getattr(self, "git_dir", None)
986 if git_dir is None:
987 return
988 filename = osp.join(git_dir, self.DAEMON_EXPORT_FILE)
989 fileexists = osp.exists(filename)
990 if value and not fileexists:
991 touch(filename)
992 elif not value and fileexists:
993 os.unlink(filename)
995 @property
996 def daemon_export(self) -> bool:
997 """If True, git-daemon may export this repository"""
998 return self._get_daemon_export()
1000 @daemon_export.setter
1001 def daemon_export(self, value: object) -> None:
1002 self._set_daemon_export(value)
1004 def _get_alternates(self) -> List[str]:
1005 """The list of alternates for this repo from which objects can be retrieved.
1007 :return:
1008 List of strings being pathnames of alternates
1009 """
1010 alternates_path = osp.join(self.odb.root_path(), "info", "alternates")
1012 if osp.exists(alternates_path):
1013 with open(alternates_path, "rb") as f:
1014 alts = f.read().decode(defenc)
1015 return alts.strip().splitlines()
1016 return []
1018 def _set_alternates(self, alts: List[str]) -> None:
1019 """Set the alternates.
1021 :param alts:
1022 The array of string paths representing the alternates at which git should
1023 look for objects, i.e. ``/home/user/repo/.git/objects``.
1025 :raise git.exc.NoSuchPathError:
1027 :note:
1028 The method does not check for the existence of the paths in `alts`, as the
1029 caller is responsible.
1030 """
1031 alternates_path = osp.join(self.odb.root_path(), "info", "alternates")
1032 if not alts:
1033 if osp.isfile(alternates_path):
1034 os.remove(alternates_path)
1035 else:
1036 with open(alternates_path, "wb") as f:
1037 f.write("\n".join(alts).encode(defenc))
1039 @property
1040 def alternates(self) -> List[str]:
1041 """Retrieve a list of alternates paths or set a list paths to be used as alternates"""
1042 return self._get_alternates()
1044 @alternates.setter
1045 def alternates(self, alts: List[str]) -> None:
1046 self._set_alternates(alts)
1048 def is_dirty(
1049 self,
1050 index: bool = True,
1051 working_tree: bool = True,
1052 untracked_files: bool = False,
1053 submodules: bool = True,
1054 path: Optional[PathLike] = None,
1055 ) -> bool:
1056 """
1057 :return:
1058 ``True`` if the repository is considered dirty. By default it will react
1059 like a :manpage:`git-status(1)` without untracked files, hence it is dirty
1060 if the index or the working copy have changes.
1061 """
1062 if self._bare:
1063 # Bare repositories with no associated working directory are
1064 # always considered to be clean.
1065 return False
1067 # Start from the one which is fastest to evaluate.
1068 default_args = ["--abbrev=40", "--full-index", "--raw"]
1069 if not submodules:
1070 default_args.append("--ignore-submodules")
1071 if path:
1072 default_args.extend(["--", os.fspath(path)])
1073 if index:
1074 # diff index against HEAD.
1075 if osp.isfile(self.index.path) and len(self.git.diff("--cached", *default_args)):
1076 return True
1077 # END index handling
1078 if working_tree:
1079 # diff index against working tree.
1080 if len(self.git.diff(*default_args)):
1081 return True
1082 # END working tree handling
1083 if untracked_files:
1084 if len(self._get_untracked_files(path, ignore_submodules=not submodules)):
1085 return True
1086 # END untracked files
1087 return False
1089 @property
1090 def untracked_files(self) -> List[str]:
1091 """
1092 :return:
1093 list(str,...)
1095 Files currently untracked as they have not been staged yet. Paths are
1096 relative to the current working directory of the git command.
1098 :note:
1099 Ignored files will not appear here, i.e. files mentioned in ``.gitignore``.
1101 :note:
1102 This property is expensive, as no cache is involved. To process the result,
1103 please consider caching it yourself.
1104 """
1105 return self._get_untracked_files()
1107 def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]:
1108 # Make sure we get all files, not only untracked directories.
1109 proc = self.git.status(*args, porcelain=True, untracked_files=True, as_process=True, **kwargs)
1110 # Untracked files prefix in porcelain mode
1111 prefix = "?? "
1112 untracked_files = []
1113 for line in proc.stdout:
1114 line = line.decode(defenc)
1115 if not line.startswith(prefix):
1116 continue
1117 filename = line[len(prefix) :].rstrip("\n")
1118 # Special characters are escaped
1119 if filename[0] == filename[-1] == '"':
1120 filename = filename[1:-1]
1121 # WHATEVER ... it's a mess, but works for me
1122 filename = filename.encode("ascii").decode("unicode_escape").encode("latin1").decode(defenc)
1123 untracked_files.append(filename)
1124 finalize_process(proc)
1125 return untracked_files
1127 def ignored(self, *paths: PathLike) -> List[str]:
1128 """Checks if paths are ignored via ``.gitignore``.
1130 This does so using the :manpage:`git-check-ignore(1)` method.
1132 :param paths:
1133 List of paths to check whether they are ignored or not.
1135 :return:
1136 Subset of those paths which are ignored
1137 """
1138 try:
1139 proc: str = self.git.check_ignore(*paths)
1140 except GitCommandError as err:
1141 if err.status == 1:
1142 # If return code is 1, this means none of the items in *paths are
1143 # ignored by Git, so return an empty list.
1144 return []
1145 else:
1146 # Raise the exception on all other return codes.
1147 raise
1149 return proc.replace("\\\\", "\\").replace('"', "").split("\n")
1151 @property
1152 def active_branch(self) -> Head:
1153 """The name of the currently active branch.
1155 :raise TypeError:
1156 If HEAD is detached.
1158 :raise ValueError:
1159 If HEAD points to the ``.invalid`` ref Git uses to mark refs as
1160 incompatible with older clients.
1162 :return:
1163 :class:`~git.refs.head.Head` to the active branch
1164 """
1165 active_branch = self.head.reference
1166 if active_branch.name == ".invalid":
1167 raise ValueError(
1168 "HEAD points to 'refs/heads/.invalid', which Git uses to mark refs as incompatible with older clients"
1169 )
1170 return active_branch
1172 def blame_incremental(
1173 self, rev: str | HEAD | None, file: str, allow_unsafe_options: bool = False, **kwargs: Any
1174 ) -> Iterator["BlameEntry"]:
1175 """Iterator for blame information for the given file at the given revision.
1177 Unlike :meth:`blame`, this does not return the actual file's contents, only a
1178 stream of :class:`BlameEntry` tuples.
1180 :param rev:
1181 Revision specifier. If ``None``, the blame will include all the latest
1182 uncommitted changes. Otherwise, anything successfully parsed by
1183 :manpage:`git-rev-parse(1)` is a valid option.
1185 :param allow_unsafe_options:
1186 Allow unsafe options in revision argument, like ``--output`` or ``--contents``.
1188 :return:
1189 Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the
1190 commit to blame for the line, and range indicates a span of line numbers in
1191 the resulting file.
1193 If you combine all line number ranges outputted by this command, you should get
1194 a continuous range spanning all line numbers in the file.
1195 """
1196 if not allow_unsafe_options:
1197 Git.check_unsafe_options(
1198 options=Git._option_candidates([rev], kwargs),
1199 unsafe_options=self.unsafe_git_blame_options,
1200 clusterable_short_options="46bceflnpqstvw",
1201 )
1203 data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs)
1204 commits: Dict[bytes, Commit] = {}
1206 stream = (line for line in data.split(b"\n") if line)
1207 while True:
1208 try:
1209 # When exhausted, causes a StopIteration, terminating this function.
1210 line = next(stream)
1211 except StopIteration:
1212 return
1213 split_line = line.split()
1214 hexsha, orig_lineno_b, lineno_b, num_lines_b = split_line
1215 lineno = int(lineno_b)
1216 num_lines = int(num_lines_b)
1217 orig_lineno = int(orig_lineno_b)
1218 if hexsha not in commits:
1219 # Now read the next few lines and build up a dict of properties for this
1220 # commit.
1221 props: Dict[bytes, bytes] = {}
1222 while True:
1223 try:
1224 line = next(stream)
1225 except StopIteration:
1226 return
1227 if line == b"boundary":
1228 # "boundary" indicates a root commit and occurs instead of the
1229 # "previous" tag.
1230 continue
1232 tag, value = line.split(b" ", 1)
1233 props[tag] = value
1234 if tag == b"filename":
1235 # "filename" formally terminates the entry for --incremental.
1236 orig_filename = value
1237 break
1239 c = Commit(
1240 self,
1241 hex_to_bin(hexsha),
1242 author=Actor(
1243 safe_decode(props[b"author"]),
1244 safe_decode(props[b"author-mail"].lstrip(b"<").rstrip(b">")),
1245 ),
1246 authored_date=int(props[b"author-time"]),
1247 committer=Actor(
1248 safe_decode(props[b"committer"]),
1249 safe_decode(props[b"committer-mail"].lstrip(b"<").rstrip(b">")),
1250 ),
1251 committed_date=int(props[b"committer-time"]),
1252 )
1253 commits[hexsha] = c
1254 else:
1255 # Discard all lines until we find "filename" which is guaranteed to be
1256 # the last line.
1257 while True:
1258 try:
1259 # Will fail if we reach the EOF unexpectedly.
1260 line = next(stream)
1261 except StopIteration:
1262 return
1263 tag, value = line.split(b" ", 1)
1264 if tag == b"filename":
1265 orig_filename = value
1266 break
1268 yield BlameEntry(
1269 commits[hexsha],
1270 range(lineno, lineno + num_lines),
1271 safe_decode(orig_filename),
1272 range(orig_lineno, orig_lineno + num_lines),
1273 )
1275 def blame(
1276 self,
1277 rev: Union[str, HEAD, None],
1278 file: str,
1279 incremental: bool = False,
1280 rev_opts: Optional[Sequence[str]] = None,
1281 allow_unsafe_options: bool = False,
1282 **kwargs: Any,
1283 ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None:
1284 """The blame information for the given file at the given revision.
1286 :param rev:
1287 Revision specifier. If ``None``, the blame will include all the latest
1288 uncommitted changes. Otherwise, anything successfully parsed by
1289 :manpage:`git-rev-parse(1)` is a valid option.
1291 :param allow_unsafe_options:
1292 Allow unsafe options in revision argument, like ``--output`` or ``--contents``.
1294 :return:
1295 list: [git.Commit, list: [<line>]]
1297 A list of lists associating a :class:`~git.objects.commit.Commit` object
1298 with a list of lines that changed within the given commit. The
1299 :class:`~git.objects.commit.Commit` objects will be given in order of
1300 appearance.
1301 """
1302 if incremental:
1303 return self.blame_incremental(rev, file, allow_unsafe_options=allow_unsafe_options, **kwargs)
1304 rev_opts_list = list(rev_opts or [])
1305 if not allow_unsafe_options:
1306 Git.check_unsafe_options(
1307 options=Git._option_candidates([rev, rev_opts_list], kwargs),
1308 unsafe_options=self.unsafe_git_blame_options,
1309 clusterable_short_options="46bceflnpqstvw",
1310 )
1311 data: bytes = self.git.blame(rev, *rev_opts_list, "--", file, p=True, stdout_as_string=False, **kwargs)
1312 commits: Dict[str, Commit] = {}
1313 blames: List[List[Commit | List[str | bytes] | None]] = []
1315 class InfoTD(TypedDict, total=False):
1316 sha: str
1317 id: str
1318 filename: str
1319 summary: str
1320 author: str
1321 author_email: str
1322 author_date: int
1323 committer: str
1324 committer_email: str
1325 committer_date: int
1327 info: InfoTD = {}
1329 keepends = True
1330 for line_bytes in data.splitlines(keepends):
1331 line_str = ""
1332 try:
1333 line_str = line_bytes.rstrip().decode(defenc)
1334 except UnicodeDecodeError:
1335 firstpart = ""
1336 parts = []
1337 is_binary = True
1338 else:
1339 # As we don't have an idea when the binary data ends, as it could
1340 # contain multiple newlines in the process. So we rely on being able to
1341 # decode to tell us what it is. This can absolutely fail even on text
1342 # files, but even if it does, we should be fine treating it as binary
1343 # instead.
1344 parts = self.re_whitespace.split(line_str, 1)
1345 firstpart = parts[0]
1346 is_binary = False
1347 # END handle decode of line
1349 if self.re_hexsha_only.search(firstpart):
1350 # handles
1351 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 1 1 7 - indicates blame-data start
1352 # 634396b2f541a9f2d58b00be1a07f0c358b999b3 2 2 - indicates
1353 # another line of blame with the same data
1354 digits = parts[-1].split(" ")
1355 if len(digits) == 3:
1356 info = {"id": firstpart}
1357 blames.append([None, []])
1358 elif info["id"] != firstpart:
1359 info = {"id": firstpart}
1360 blames.append([commits.get(firstpart), []])
1361 # END blame data initialization
1362 else:
1363 m = self.re_author_committer_start.search(firstpart)
1364 if m:
1365 # handles:
1366 # author Tom Preston-Werner
1367 # author-mail <tom@mojombo.com>
1368 # author-time 1192271832
1369 # author-tz -0700
1370 # committer Tom Preston-Werner
1371 # committer-mail <tom@mojombo.com>
1372 # committer-time 1192271832
1373 # committer-tz -0700 - IGNORED BY US
1374 role = m.group(0)
1375 if role == "author":
1376 if firstpart.endswith("-mail"):
1377 info["author_email"] = parts[-1]
1378 elif firstpart.endswith("-time"):
1379 info["author_date"] = int(parts[-1])
1380 elif role == firstpart:
1381 info["author"] = parts[-1]
1382 elif role == "committer":
1383 if firstpart.endswith("-mail"):
1384 info["committer_email"] = parts[-1]
1385 elif firstpart.endswith("-time"):
1386 info["committer_date"] = int(parts[-1])
1387 elif role == firstpart:
1388 info["committer"] = parts[-1]
1389 # END distinguish mail,time,name
1390 else:
1391 # handle
1392 # filename lib/grit.rb
1393 # summary add Blob
1394 # <and rest>
1395 if firstpart.startswith("filename"):
1396 info["filename"] = parts[-1]
1397 elif firstpart.startswith("summary"):
1398 info["summary"] = parts[-1]
1399 elif firstpart == "":
1400 if info:
1401 sha = info["id"]
1402 c = commits.get(sha)
1403 if c is None:
1404 c = Commit(
1405 self,
1406 hex_to_bin(sha),
1407 author=Actor._from_string(f"{info['author']} {info['author_email']}"),
1408 authored_date=info["author_date"],
1409 committer=Actor._from_string(f"{info['committer']} {info['committer_email']}"),
1410 committed_date=info["committer_date"],
1411 )
1412 commits[sha] = c
1413 blames[-1][0] = c
1414 # END if commit objects needs initial creation
1416 if blames[-1][1] is not None:
1417 line: str | bytes
1418 if not is_binary:
1419 if line_str and line_str[0] == "\t":
1420 line_str = line_str[1:]
1421 line = line_str
1422 else:
1423 line = line_bytes
1424 # NOTE: We are actually parsing lines out of binary
1425 # data, which can lead to the binary being split up
1426 # along the newline separator. We will append this
1427 # to the blame we are currently looking at, even
1428 # though it should be concatenated with the last
1429 # line we have seen.
1430 blames[-1][1].append(line)
1432 info = {"id": sha}
1433 # END if we collected commit info
1434 # END distinguish filename,summary,rest
1435 # END distinguish author|committer vs filename,summary,rest
1436 # END distinguish hexsha vs other information
1437 return blames
1439 @classmethod
1440 def init(
1441 cls,
1442 path: Union[PathLike, None] = None,
1443 mkdir: bool = True,
1444 odbt: Type[GitCmdObjectDB] = GitCmdObjectDB,
1445 expand_vars: bool = True,
1446 allow_unsafe_options: bool = False,
1447 **kwargs: Any,
1448 ) -> "Repo":
1449 """Initialize a git repository at the given path if specified.
1451 :param path:
1452 The full path to the repo (traditionally ends with ``/<name>.git``). Or
1453 ``None``, in which case the repository will be created in the current
1454 working directory.
1456 :param mkdir:
1457 If specified, will create the repository directory if it doesn't already
1458 exist. Creates the directory with a mode=0755.
1459 Only effective if a path is explicitly given.
1461 :param odbt:
1462 Object DataBase type - a type which is constructed by providing the
1463 directory containing the database objects, i.e. ``.git/objects``. It will be
1464 used to access all object data.
1466 :param expand_vars:
1467 If specified, environment variables will not be escaped. This can lead to
1468 information disclosure, allowing attackers to access the contents of
1469 environment variables.
1471 :param allow_unsafe_options:
1472 Allow unsafe options to be used, such as ``--template`` and
1473 ``--separate-git-dir``.
1475 :param kwargs:
1476 Keyword arguments serving as additional options to the
1477 :manpage:`git-init(1)` command.
1479 :return:
1480 :class:`Repo` (the newly created repo)
1481 """
1482 if not allow_unsafe_options:
1483 Git.check_unsafe_options(
1484 options=Git._option_candidates([], kwargs),
1485 unsafe_options=cls.unsafe_git_init_options,
1486 )
1487 if path:
1488 path = expand_path(path, expand_vars)
1489 if mkdir and path and not osp.exists(path):
1490 os.makedirs(path, 0o755)
1492 # git command automatically chdir into the directory
1493 git = cls.GitCommandWrapperType(path)
1494 git.init(**kwargs)
1495 return cls(path, odbt=odbt)
1497 @classmethod
1498 def _clone(
1499 cls,
1500 git: "Git",
1501 url: PathLike,
1502 path: PathLike,
1503 odb_default_type: Type[GitCmdObjectDB],
1504 progress: Union["RemoteProgress", "UpdateProgress", Callable[..., "RemoteProgress"], None] = None,
1505 multi_options: Optional[List[str]] = None,
1506 allow_unsafe_protocols: bool = False,
1507 allow_unsafe_options: bool = False,
1508 **kwargs: Any,
1509 ) -> "Repo":
1510 odbt = kwargs.pop("odbt", odb_default_type)
1512 # url may be a path and this has no effect if it is a string
1513 url = os.fspath(url)
1514 path = os.fspath(path)
1516 ## A bug win cygwin's Git, when `--bare` or `--separate-git-dir`
1517 # it prepends the cwd or(?) the `url` into the `path, so::
1518 # git clone --bare /cygwin/d/foo.git C:\\Work
1519 # becomes::
1520 # git clone --bare /cygwin/d/foo.git /cygwin/d/C:\\Work
1521 #
1522 clone_path = Git.polish_url(path) if Git.is_cygwin() and "bare" in kwargs else path
1523 sep_dir = kwargs.get("separate_git_dir")
1524 if sep_dir:
1525 kwargs["separate_git_dir"] = Git.polish_url(sep_dir)
1526 multi = None
1527 if multi_options:
1528 multi = shlex.split(" ".join(multi_options))
1530 clone_url = Git.polish_url(url, expand_vars=False)
1531 if not allow_unsafe_protocols:
1532 Git.check_unsafe_protocols(clone_url)
1533 if not allow_unsafe_options:
1534 Git.check_unsafe_options(
1535 options=Git._option_candidates([], kwargs),
1536 unsafe_options=cls.unsafe_git_clone_options,
1537 )
1538 if not allow_unsafe_options and multi:
1539 Git.check_unsafe_options(options=multi, unsafe_options=cls.unsafe_git_clone_options)
1541 proc = git.clone(
1542 multi,
1543 "--",
1544 clone_url,
1545 clone_path,
1546 with_extended_output=True,
1547 as_process=True,
1548 v=True,
1549 universal_newlines=True,
1550 **add_progress(kwargs, git, progress),
1551 )
1552 if progress:
1553 handle_process_output(
1554 proc,
1555 None,
1556 to_progress_instance(progress).new_message_handler(),
1557 finalize_process,
1558 decode_streams=False,
1559 )
1560 else:
1561 (stdout, stderr) = proc.communicate()
1562 cmdline = getattr(proc, "args", "")
1563 cmdline = remove_password_if_present(cmdline)
1565 _logger.debug("Cmd(%s)'s unused stdout: %s", cmdline, stdout)
1566 finalize_process(proc, stderr=stderr)
1568 # Our git command could have a different working dir than our actual
1569 # environment, hence we prepend its working dir if required.
1570 if not osp.isabs(path):
1571 path = osp.join(git._working_dir, path) if git._working_dir is not None else path
1573 repo = cls(path, odbt=odbt)
1575 # Retain env values that were passed to _clone().
1576 repo.git.update_environment(**git.environment())
1578 # Adjust remotes - there may be operating systems which use backslashes, These
1579 # might be given as initial paths, but when handling the config file that
1580 # contains the remote from which we were clones, git stops liking it as it will
1581 # escape the backslashes. Hence we undo the escaping just to be sure.
1582 if repo.remotes:
1583 with repo.remotes[0].config_writer as writer:
1584 writer.set_value("url", Git.polish_url(repo.remotes[0].url, expand_vars=False))
1585 # END handle remote repo
1586 return repo
1588 def clone(
1589 self,
1590 path: PathLike,
1591 progress: Optional[CallableProgress] = None,
1592 multi_options: Optional[List[str]] = None,
1593 allow_unsafe_protocols: bool = False,
1594 allow_unsafe_options: bool = False,
1595 **kwargs: Any,
1596 ) -> "Repo":
1597 """Create a clone from this repository.
1599 :param path:
1600 The full path of the new repo (traditionally ends with ``./<name>.git``).
1602 :param progress:
1603 See :meth:`Remote.push <git.remote.Remote.push>`.
1605 :param multi_options:
1606 A list of :manpage:`git-clone(1)` options that can be provided multiple
1607 times.
1609 One option per list item which is passed exactly as specified to clone.
1610 For example::
1612 [
1613 "--config core.filemode=false",
1614 "--config core.ignorecase",
1615 "--recurse-submodule=repo1_path",
1616 "--recurse-submodule=repo2_path",
1617 ]
1619 :param allow_unsafe_protocols:
1620 Allow unsafe protocols to be used, like ``ext``.
1622 :param allow_unsafe_options:
1623 Allow unsafe options to be used, like ``--upload-pack``.
1625 :param kwargs:
1626 * ``odbt`` = ObjectDatabase Type, allowing to determine the object database
1627 implementation used by the returned :class:`Repo` instance.
1628 * All remaining keyword arguments are given to the :manpage:`git-clone(1)`
1629 command.
1631 :return:
1632 :class:`Repo` (the newly cloned repo)
1633 """
1634 return self._clone(
1635 self.git,
1636 self.common_dir,
1637 path,
1638 type(self.odb),
1639 progress, # type: ignore[arg-type]
1640 multi_options,
1641 allow_unsafe_protocols=allow_unsafe_protocols,
1642 allow_unsafe_options=allow_unsafe_options,
1643 **kwargs,
1644 )
1646 @classmethod
1647 def clone_from(
1648 cls,
1649 url: PathLike,
1650 to_path: PathLike,
1651 progress: CallableProgress = None,
1652 env: Optional[Mapping[str, str]] = None,
1653 multi_options: Optional[List[str]] = None,
1654 allow_unsafe_protocols: bool = False,
1655 allow_unsafe_options: bool = False,
1656 **kwargs: Any,
1657 ) -> "Repo":
1658 """Create a clone from the given URL.
1660 :param url:
1661 Valid git url, see: https://git-scm.com/docs/git-clone#URLS
1663 :param to_path:
1664 Path to which the repository should be cloned to.
1666 :param progress:
1667 See :meth:`Remote.push <git.remote.Remote.push>`.
1669 :param env:
1670 Optional dictionary containing the desired environment variables.
1672 Note: Provided variables will be used to update the execution environment
1673 for ``git``. If some variable is not specified in `env` and is defined in
1674 :attr:`os.environ`, value from :attr:`os.environ` will be used. If you want
1675 to unset some variable, consider providing empty string as its value.
1677 :param multi_options:
1678 See the :meth:`clone` method.
1680 :param allow_unsafe_protocols:
1681 Allow unsafe protocols to be used, like ``ext``.
1683 :param allow_unsafe_options:
1684 Allow unsafe options to be used, like ``--upload-pack``.
1686 :param kwargs:
1687 See the :meth:`clone` method.
1689 :return:
1690 :class:`Repo` instance pointing to the cloned directory.
1691 """
1692 git = cls.GitCommandWrapperType(os.getcwd())
1693 if env is not None:
1694 git.update_environment(**env)
1695 return cls._clone(
1696 git,
1697 url,
1698 to_path,
1699 GitCmdObjectDB,
1700 progress, # type: ignore[arg-type]
1701 multi_options,
1702 allow_unsafe_protocols=allow_unsafe_protocols,
1703 allow_unsafe_options=allow_unsafe_options,
1704 **kwargs,
1705 )
1707 def archive(
1708 self,
1709 ostream: Union[TextIO, BinaryIO],
1710 treeish: Optional[str] = None,
1711 prefix: Optional[str] = None,
1712 allow_unsafe_options: bool = False,
1713 allow_unsafe_protocols: bool = False,
1714 **kwargs: Any,
1715 ) -> Repo:
1716 """Archive the tree at the given revision.
1718 :param ostream:
1719 File-compatible stream object to which the archive will be written as bytes.
1721 :param treeish:
1722 The treeish name/id, defaults to active branch.
1724 :param prefix:
1725 The optional prefix to prepend to each filename in the archive.
1727 :param kwargs:
1728 Additional arguments passed to :manpage:`git-archive(1)`:
1730 * Use the ``format`` argument to define the kind of format. Use specialized
1731 ostreams to write any format supported by Python.
1732 * You may specify the special ``path`` keyword, which may either be a
1733 repository-relative path to a directory or file to place into the archive,
1734 or a list or tuple of multiple paths.
1736 :param allow_unsafe_options:
1737 Allow unsafe options, like ``--exec`` or ``--output``.
1739 :param allow_unsafe_protocols:
1740 Allow unsafe protocols to be used in ``remote``, like ``ext``.
1742 :raise git.exc.GitCommandError:
1743 If something went wrong.
1745 :return:
1746 self
1747 """
1748 if treeish is None:
1749 treeish = self.head.commit
1750 if prefix and "prefix" not in kwargs:
1751 kwargs["prefix"] = prefix
1752 remote = kwargs.get("remote")
1753 if not allow_unsafe_protocols and remote is not None:
1754 Git.check_unsafe_protocols(str(remote))
1755 if not allow_unsafe_options:
1756 Git.check_unsafe_options(
1757 options=Git._option_candidates([], kwargs),
1758 unsafe_options=self.unsafe_git_archive_options,
1759 )
1760 kwargs["output_stream"] = ostream
1761 path = kwargs.pop("path", [])
1762 path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path)
1763 if not isinstance(path, (tuple, list)):
1764 path = [path]
1765 # END ensure paths is list (or tuple)
1766 self.git.archive("--", treeish, *path, **kwargs)
1767 return self
1769 def has_separate_working_tree(self) -> bool:
1770 """
1771 :return:
1772 True if our :attr:`git_dir` is not at the root of our
1773 :attr:`working_tree_dir`, but a ``.git`` file with a platform-agnostic
1774 symbolic link. Our :attr:`git_dir` will be wherever the ``.git`` file points
1775 to.
1777 :note:
1778 Bare repositories will always return ``False`` here.
1779 """
1780 if self.bare:
1781 return False
1782 if self.working_tree_dir:
1783 return osp.isfile(osp.join(self.working_tree_dir, ".git"))
1784 else:
1785 return False # Or raise Error?
1787 rev_parse = rev_parse
1789 def __repr__(self) -> str:
1790 clazz = self.__class__
1791 return "<%s.%s %r>" % (clazz.__module__, clazz.__name__, self.git_dir)
1793 def currently_rebasing_on(self) -> Commit | None:
1794 """
1795 :return:
1796 The commit which is currently being replayed while rebasing.
1798 ``None`` if we are not currently rebasing.
1799 """
1800 if not self.git_dir:
1801 return None
1802 rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
1803 if not osp.isfile(rebase_head_file):
1804 return None
1805 with open(rebase_head_file, "rt") as f:
1806 content = f.readline().strip()
1807 return self.commit(content)