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