| |
| |
|
|
| """General repository-related functions.""" |
|
|
| from __future__ import annotations |
|
|
| __all__ = [ |
| "rev_parse", |
| "is_git_dir", |
| "touch", |
| "find_submodule_git_dir", |
| "name_to_object", |
| "short_to_long", |
| "deref_tag", |
| "to_commit", |
| "find_worktree_git_dir", |
| ] |
|
|
| import os |
| import os.path as osp |
| from pathlib import Path |
| import stat |
| from string import digits |
|
|
| from gitdb.exc import BadName, BadObject |
|
|
| from git.cmd import Git |
| from git.exc import WorkTreeRepositoryUnsupported |
| from git.objects import Object |
| from git.refs import SymbolicReference |
| from git.util import cygpath, bin_to_hex, hex_to_bin |
|
|
| |
|
|
| from typing import Optional, TYPE_CHECKING, Union, cast, overload |
|
|
| from git.types import AnyGitObject, Literal, PathLike |
|
|
| if TYPE_CHECKING: |
| from git.db import GitCmdObjectDB |
| from git.objects import Commit, TagObject |
| from git.refs.reference import Reference |
| from git.refs.tag import Tag |
|
|
| from .base import Repo |
|
|
| |
|
|
|
|
| def touch(filename: str) -> str: |
| with open(filename, "ab"): |
| pass |
| return filename |
|
|
|
|
| def is_git_dir(d: PathLike) -> bool: |
| """This is taken from the git setup.c:is_git_directory function. |
| |
| :raise git.exc.WorkTreeRepositoryUnsupported: |
| If it sees a worktree directory. It's quite hacky to do that here, but at least |
| clearly indicates that we don't support it. There is the unlikely danger to |
| throw if we see directories which just look like a worktree dir, but are none. |
| """ |
| if osp.isdir(d): |
| if (osp.isdir(osp.join(d, "objects")) or "GIT_OBJECT_DIRECTORY" in os.environ) and osp.isdir( |
| osp.join(d, "refs") |
| ): |
| headref = osp.join(d, "HEAD") |
| return osp.isfile(headref) or (osp.islink(headref) and os.readlink(headref).startswith("refs")) |
| elif ( |
| osp.isfile(osp.join(d, "gitdir")) |
| and osp.isfile(osp.join(d, "commondir")) |
| and osp.isfile(osp.join(d, "gitfile")) |
| ): |
| raise WorkTreeRepositoryUnsupported(d) |
| return False |
|
|
|
|
| def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]: |
| """Search for a gitdir for this worktree.""" |
| try: |
| statbuf = os.stat(dotgit) |
| except OSError: |
| return None |
| if not stat.S_ISREG(statbuf.st_mode): |
| return None |
|
|
| try: |
| lines = Path(dotgit).read_text().splitlines() |
| for key, value in [line.strip().split(": ") for line in lines]: |
| if key == "gitdir": |
| return value |
| except ValueError: |
| pass |
| return None |
|
|
|
|
| def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]: |
| """Search for a submodule repo.""" |
| if is_git_dir(d): |
| return d |
|
|
| try: |
| with open(d) as fp: |
| content = fp.read().rstrip() |
| except IOError: |
| |
| pass |
| else: |
| if content.startswith("gitdir: "): |
| path = content[8:] |
|
|
| if Git.is_cygwin(): |
| |
| |
| |
| path = cygpath(path) |
| if not osp.isabs(path): |
| path = osp.normpath(osp.join(osp.dirname(d), path)) |
| return find_submodule_git_dir(path) |
| |
| return None |
|
|
|
|
| def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]: |
| """ |
| :return: |
| Long hexadecimal sha1 from the given less than 40 byte hexsha, or ``None`` if no |
| candidate could be found. |
| |
| :param hexsha: |
| hexsha with less than 40 bytes. |
| """ |
| try: |
| return bin_to_hex(odb.partial_to_complete_sha_hex(hexsha)) |
| except BadObject: |
| return None |
| |
|
|
|
|
| @overload |
| def name_to_object(repo: "Repo", name: str, return_ref: Literal[False] = ...) -> AnyGitObject: ... |
|
|
|
|
| @overload |
| def name_to_object(repo: "Repo", name: str, return_ref: Literal[True]) -> Union[AnyGitObject, SymbolicReference]: ... |
|
|
|
|
| def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[AnyGitObject, SymbolicReference]: |
| """ |
| :return: |
| Object specified by the given name - hexshas (short and long) as well as |
| references are supported. |
| |
| :param return_ref: |
| If ``True``, and name specifies a reference, we will return the reference |
| instead of the object. Otherwise it will raise :exc:`~gitdb.exc.BadObject` or |
| :exc:`~gitdb.exc.BadName`. |
| """ |
| hexsha: Union[None, str, bytes] = None |
|
|
| |
| if repo.re_hexsha_shortened.match(name): |
| if len(name) != 40: |
| |
| hexsha = short_to_long(repo.odb, name) |
| else: |
| hexsha = name |
| |
| |
|
|
| |
| |
| if hexsha is None: |
| for base in ( |
| "%s", |
| "refs/%s", |
| "refs/tags/%s", |
| "refs/heads/%s", |
| "refs/remotes/%s", |
| "refs/remotes/%s/HEAD", |
| ): |
| try: |
| hexsha = SymbolicReference.dereference_recursive(repo, base % name) |
| if return_ref: |
| return SymbolicReference(repo, base % name) |
| |
| break |
| except ValueError: |
| pass |
| |
| |
|
|
| |
| if return_ref: |
| raise BadObject("Couldn't find reference named %r" % name) |
| |
|
|
| |
| if hexsha is None: |
| raise BadName(name) |
| |
|
|
| return Object.new_from_sha(repo, hex_to_bin(hexsha)) |
|
|
|
|
| def deref_tag(tag: "Tag") -> AnyGitObject: |
| """Recursively dereference a tag and return the resulting object.""" |
| while True: |
| try: |
| tag = tag.object |
| except AttributeError: |
| break |
| |
| return tag |
|
|
|
|
| def to_commit(obj: Object) -> "Commit": |
| """Convert the given object to a commit if possible and return it.""" |
| if obj.type == "tag": |
| obj = deref_tag(obj) |
|
|
| if obj.type != "commit": |
| raise ValueError("Cannot convert object %r to type commit" % obj) |
| |
| return obj |
|
|
|
|
| def rev_parse(repo: "Repo", rev: str) -> AnyGitObject: |
| """Parse a revision string. Like :manpage:`git-rev-parse(1)`. |
| |
| :return: |
| `~git.objects.base.Object` at the given revision. |
| |
| This may be any type of git object: |
| |
| * :class:`Commit <git.objects.commit.Commit>` |
| * :class:`TagObject <git.objects.tag.TagObject>` |
| * :class:`Tree <git.objects.tree.Tree>` |
| * :class:`Blob <git.objects.blob.Blob>` |
| |
| :param rev: |
| :manpage:`git-rev-parse(1)`-compatible revision specification as string. |
| Please see :manpage:`git-rev-parse(1)` for details. |
| |
| :raise gitdb.exc.BadObject: |
| If the given revision could not be found. |
| |
| :raise ValueError: |
| If `rev` couldn't be parsed. |
| |
| :raise IndexError: |
| If an invalid reflog index is specified. |
| """ |
| |
| if rev.startswith(":/"): |
| |
| raise NotImplementedError("commit by message search (regex)") |
| |
|
|
| obj: Optional[AnyGitObject] = None |
| ref = None |
| output_type = "commit" |
| start = 0 |
| parsed_to = 0 |
| lr = len(rev) |
| while start < lr: |
| if rev[start] not in "^~:@": |
| start += 1 |
| continue |
| |
|
|
| token = rev[start] |
|
|
| if obj is None: |
| |
| if start == 0: |
| ref = repo.head.ref |
| else: |
| if token == "@": |
| ref = cast("Reference", name_to_object(repo, rev[:start], return_ref=True)) |
| else: |
| obj = name_to_object(repo, rev[:start]) |
| |
| |
| else: |
| if ref is not None: |
| obj = cast("Commit", ref.commit) |
| |
| |
|
|
| start += 1 |
|
|
| |
| if start < lr and rev[start] == "{": |
| end = rev.find("}", start) |
| if end == -1: |
| raise ValueError("Missing closing brace to define type in %s" % rev) |
| output_type = rev[start + 1 : end] |
|
|
| |
| if output_type == "commit": |
| pass |
| elif output_type == "tree": |
| try: |
| obj = cast(AnyGitObject, obj) |
| obj = to_commit(obj).tree |
| except (AttributeError, ValueError): |
| pass |
| |
| elif output_type in ("", "blob"): |
| obj = cast("TagObject", obj) |
| if obj and obj.type == "tag": |
| obj = deref_tag(obj) |
| else: |
| |
| pass |
| |
| elif token == "@": |
| |
| assert ref is not None, "Require Reference to access reflog" |
| revlog_index = None |
| try: |
| |
| revlog_index = -(int(output_type) + 1) |
| except ValueError as e: |
| |
| raise NotImplementedError("Support for additional @{...} modes not implemented") from e |
| |
|
|
| try: |
| entry = ref.log_entry(revlog_index) |
| except IndexError as e: |
| raise IndexError("Invalid revlog index: %i" % revlog_index) from e |
| |
|
|
| obj = Object.new_from_sha(repo, hex_to_bin(entry.newhexsha)) |
|
|
| |
| output_type = "" |
| else: |
| raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev)) |
| |
|
|
| |
| |
| if output_type and obj and obj.type != output_type: |
| raise ValueError("Could not accommodate requested object type %r, got %s" % (output_type, obj.type)) |
| |
|
|
| start = end + 1 |
| parsed_to = start |
| continue |
| |
|
|
| |
| num = 0 |
| if token != ":": |
| found_digit = False |
| while start < lr: |
| if rev[start] in digits: |
| num = num * 10 + int(rev[start]) |
| start += 1 |
| found_digit = True |
| else: |
| break |
| |
| |
|
|
| |
| if not found_digit: |
| num = 1 |
| |
| |
|
|
| parsed_to = start |
| |
| try: |
| obj = cast(AnyGitObject, obj) |
| if token == "~": |
| obj = to_commit(obj) |
| for _ in range(num): |
| obj = obj.parents[0] |
| |
| elif token == "^": |
| obj = to_commit(obj) |
| |
| if num: |
| obj = obj.parents[num - 1] |
| elif token == ":": |
| if obj.type != "tree": |
| obj = obj.tree |
| |
| obj = obj[rev[start:]] |
| parsed_to = lr |
| else: |
| raise ValueError("Invalid token: %r" % token) |
| |
| except (IndexError, AttributeError) as e: |
| raise BadName( |
| f"Invalid revision spec '{rev}' - not enough " f"parent commits to reach '{token}{int(num)}'" |
| ) from e |
| |
| |
|
|
| |
| if obj is None: |
| obj = name_to_object(repo, rev) |
| parsed_to = lr |
| |
|
|
| if obj is None: |
| raise ValueError("Revision specifier could not be parsed: %s" % rev) |
|
|
| if parsed_to != lr: |
| raise ValueError("Didn't consume complete rev spec %s, consumed part: %s" % (rev, rev[:parsed_to])) |
|
|
| return obj |
|
|