| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- # This module is part of GitPython and is released under the
- # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
- """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
- # Typing ----------------------------------------------------------------------
- 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:
- # It's probably not a file.
- pass
- else:
- if content.startswith("gitdir: "):
- path = content[8:]
- if Git.is_cygwin():
- # Cygwin creates submodules prefixed with `/cygdrive/...`.
- # Cygwin git understands Cygwin paths much better than Windows ones.
- # Also the Cygwin tests are assuming Cygwin paths.
- path = cygpath(path)
- if not osp.isabs(path):
- path = osp.normpath(osp.join(osp.dirname(d), path))
- return find_submodule_git_dir(path)
- # END handle exception
- 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
- # END exception handling
- @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
- # Is it a hexsha? Try the most common ones, which is 7 to 40.
- if repo.re_hexsha_shortened.match(name):
- if len(name) != 40:
- # Find long sha for short sha.
- hexsha = short_to_long(repo.odb, name)
- else:
- hexsha = name
- # END handle short shas
- # END find sha if it matches
- # If we couldn't find an object for what seemed to be a short hexsha, try to find it
- # as reference anyway, it could be named 'aaa' for instance.
- 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)
- # END handle symbolic ref
- break
- except ValueError:
- pass
- # END for each base
- # END handle hexsha
- # Didn't find any ref, this is an error.
- if return_ref:
- raise BadObject("Couldn't find reference named %r" % name)
- # END handle return ref
- # Tried everything ? fail.
- if hexsha is None:
- raise BadName(name)
- # END assert hexsha was found
- 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
- # END dereference tag
- 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)
- # END verify type
- 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.
- """
- # Are we in colon search mode?
- if rev.startswith(":/"):
- # Colon search mode
- raise NotImplementedError("commit by message search (regex)")
- # END handle search
- 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
- # END handle start
- token = rev[start]
- if obj is None:
- # token is a rev name.
- 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])
- # END handle token
- # END handle refname
- else:
- if ref is not None:
- obj = ref.commit
- # END handle ref
- # END initialize obj on first token
- start += 1
- # Try to parse {type}.
- 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] # Exclude brace.
- # Handle type.
- if output_type == "commit":
- obj = cast("TagObject", obj)
- if obj and obj.type == "tag":
- obj = deref_tag(obj)
- else:
- # Cannot do anything for non-tags.
- pass
- # END handle tag
- elif output_type == "tree":
- try:
- obj = cast(AnyGitObject, obj)
- obj = to_commit(obj).tree
- except (AttributeError, ValueError):
- pass # Error raised later.
- # END exception handling
- elif output_type in ("", "blob"):
- obj = cast("TagObject", obj)
- if obj and obj.type == "tag":
- obj = deref_tag(obj)
- else:
- # Cannot do anything for non-tags.
- pass
- # END handle tag
- elif token == "@":
- # try single int
- assert ref is not None, "Require Reference to access reflog"
- revlog_index = None
- try:
- # Transform reversed index into the format of our revlog.
- revlog_index = -(int(output_type) + 1)
- except ValueError as e:
- # TODO: Try to parse the other date options, using parse_date maybe.
- raise NotImplementedError("Support for additional @{...} modes not implemented") from e
- # END handle revlog index
- try:
- entry = ref.log_entry(revlog_index)
- except IndexError as e:
- raise IndexError("Invalid revlog index: %i" % revlog_index) from e
- # END handle index out of bound
- obj = Object.new_from_sha(repo, hex_to_bin(entry.newhexsha))
- # Make it pass the following checks.
- output_type = ""
- else:
- raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev))
- # END handle output type
- # Empty output types don't require any specific type, its just about
- # dereferencing tags.
- 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))
- # END verify output type
- start = end + 1 # Skip brace.
- parsed_to = start
- continue
- # END parse type
- # Try to parse a number.
- 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
- # END handle number
- # END number parse loop
- # No explicit number given, 1 is the default. It could be 0 though.
- if not found_digit:
- num = 1
- # END set default num
- # END number parsing only if non-blob mode
- parsed_to = start
- # Handle hierarchy walk.
- try:
- obj = cast(AnyGitObject, obj)
- if token == "~":
- obj = to_commit(obj)
- for _ in range(num):
- obj = obj.parents[0]
- # END for each history item to walk
- elif token == "^":
- obj = to_commit(obj)
- # Must be n'th parent.
- if num:
- obj = obj.parents[num - 1]
- elif token == ":":
- if obj.type != "tree":
- obj = obj.tree
- # END get tree type
- obj = obj[rev[start:]]
- parsed_to = lr
- else:
- raise ValueError("Invalid token: %r" % token)
- # END end handle tag
- except (IndexError, AttributeError) as e:
- raise BadName(
- f"Invalid revision spec '{rev}' - not enough parent commits to reach '{token}{int(num)}'"
- ) from e
- # END exception handling
- # END parse loop
- # Still no obj? It's probably a simple name.
- if obj is None:
- obj = name_to_object(repo, rev)
- parsed_to = lr
- # END handle simple name
- 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
|