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