fun.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. # This module is part of GitPython and is released under the
  2. # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
  3. """General repository-related functions."""
  4. from __future__ import annotations
  5. __all__ = [
  6. "rev_parse",
  7. "is_git_dir",
  8. "touch",
  9. "find_submodule_git_dir",
  10. "name_to_object",
  11. "short_to_long",
  12. "deref_tag",
  13. "to_commit",
  14. "find_worktree_git_dir",
  15. ]
  16. import os
  17. import os.path as osp
  18. from pathlib import Path
  19. import stat
  20. from string import digits
  21. from gitdb.exc import BadName, BadObject
  22. from git.cmd import Git
  23. from git.exc import WorkTreeRepositoryUnsupported
  24. from git.objects import Object
  25. from git.refs import SymbolicReference
  26. from git.util import cygpath, bin_to_hex, hex_to_bin
  27. # Typing ----------------------------------------------------------------------
  28. from typing import Optional, TYPE_CHECKING, Union, cast, overload
  29. from git.types import AnyGitObject, Literal, PathLike
  30. if TYPE_CHECKING:
  31. from git.db import GitCmdObjectDB
  32. from git.objects import Commit, TagObject
  33. from git.refs.reference import Reference
  34. from git.refs.tag import Tag
  35. from .base import Repo
  36. # ----------------------------------------------------------------------------
  37. def touch(filename: str) -> str:
  38. with open(filename, "ab"):
  39. pass
  40. return filename
  41. def is_git_dir(d: PathLike) -> bool:
  42. """This is taken from the git setup.c:is_git_directory function.
  43. :raise git.exc.WorkTreeRepositoryUnsupported:
  44. If it sees a worktree directory. It's quite hacky to do that here, but at least
  45. clearly indicates that we don't support it. There is the unlikely danger to
  46. throw if we see directories which just look like a worktree dir, but are none.
  47. """
  48. if osp.isdir(d):
  49. if (osp.isdir(osp.join(d, "objects")) or "GIT_OBJECT_DIRECTORY" in os.environ) and osp.isdir(
  50. osp.join(d, "refs")
  51. ):
  52. headref = osp.join(d, "HEAD")
  53. return osp.isfile(headref) or (osp.islink(headref) and os.readlink(headref).startswith("refs"))
  54. elif (
  55. osp.isfile(osp.join(d, "gitdir"))
  56. and osp.isfile(osp.join(d, "commondir"))
  57. and osp.isfile(osp.join(d, "gitfile"))
  58. ):
  59. raise WorkTreeRepositoryUnsupported(d)
  60. return False
  61. def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]:
  62. """Search for a gitdir for this worktree."""
  63. try:
  64. statbuf = os.stat(dotgit)
  65. except OSError:
  66. return None
  67. if not stat.S_ISREG(statbuf.st_mode):
  68. return None
  69. try:
  70. lines = Path(dotgit).read_text().splitlines()
  71. for key, value in [line.strip().split(": ") for line in lines]:
  72. if key == "gitdir":
  73. return value
  74. except ValueError:
  75. pass
  76. return None
  77. def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
  78. """Search for a submodule repo."""
  79. if is_git_dir(d):
  80. return d
  81. try:
  82. with open(d) as fp:
  83. content = fp.read().rstrip()
  84. except IOError:
  85. # It's probably not a file.
  86. pass
  87. else:
  88. if content.startswith("gitdir: "):
  89. path = content[8:]
  90. if Git.is_cygwin():
  91. # Cygwin creates submodules prefixed with `/cygdrive/...`.
  92. # Cygwin git understands Cygwin paths much better than Windows ones.
  93. # Also the Cygwin tests are assuming Cygwin paths.
  94. path = cygpath(path)
  95. if not osp.isabs(path):
  96. path = osp.normpath(osp.join(osp.dirname(d), path))
  97. return find_submodule_git_dir(path)
  98. # END handle exception
  99. return None
  100. def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:
  101. """
  102. :return:
  103. Long hexadecimal sha1 from the given less than 40 byte hexsha, or ``None`` if no
  104. candidate could be found.
  105. :param hexsha:
  106. hexsha with less than 40 bytes.
  107. """
  108. try:
  109. return bin_to_hex(odb.partial_to_complete_sha_hex(hexsha))
  110. except BadObject:
  111. return None
  112. # END exception handling
  113. @overload
  114. def name_to_object(repo: "Repo", name: str, return_ref: Literal[False] = ...) -> AnyGitObject: ...
  115. @overload
  116. def name_to_object(repo: "Repo", name: str, return_ref: Literal[True]) -> Union[AnyGitObject, SymbolicReference]: ...
  117. def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[AnyGitObject, SymbolicReference]:
  118. """
  119. :return:
  120. Object specified by the given name - hexshas (short and long) as well as
  121. references are supported.
  122. :param return_ref:
  123. If ``True``, and name specifies a reference, we will return the reference
  124. instead of the object. Otherwise it will raise :exc:`~gitdb.exc.BadObject` or
  125. :exc:`~gitdb.exc.BadName`.
  126. """
  127. hexsha: Union[None, str, bytes] = None
  128. # Is it a hexsha? Try the most common ones, which is 7 to 40.
  129. if repo.re_hexsha_shortened.match(name):
  130. if len(name) != 40:
  131. # Find long sha for short sha.
  132. hexsha = short_to_long(repo.odb, name)
  133. else:
  134. hexsha = name
  135. # END handle short shas
  136. # END find sha if it matches
  137. # If we couldn't find an object for what seemed to be a short hexsha, try to find it
  138. # as reference anyway, it could be named 'aaa' for instance.
  139. if hexsha is None:
  140. for base in (
  141. "%s",
  142. "refs/%s",
  143. "refs/tags/%s",
  144. "refs/heads/%s",
  145. "refs/remotes/%s",
  146. "refs/remotes/%s/HEAD",
  147. ):
  148. try:
  149. hexsha = SymbolicReference.dereference_recursive(repo, base % name)
  150. if return_ref:
  151. return SymbolicReference(repo, base % name)
  152. # END handle symbolic ref
  153. break
  154. except ValueError:
  155. pass
  156. # END for each base
  157. # END handle hexsha
  158. # Didn't find any ref, this is an error.
  159. if return_ref:
  160. raise BadObject("Couldn't find reference named %r" % name)
  161. # END handle return ref
  162. # Tried everything ? fail.
  163. if hexsha is None:
  164. raise BadName(name)
  165. # END assert hexsha was found
  166. return Object.new_from_sha(repo, hex_to_bin(hexsha))
  167. def deref_tag(tag: "Tag") -> AnyGitObject:
  168. """Recursively dereference a tag and return the resulting object."""
  169. while True:
  170. try:
  171. tag = tag.object
  172. except AttributeError:
  173. break
  174. # END dereference tag
  175. return tag
  176. def to_commit(obj: Object) -> "Commit":
  177. """Convert the given object to a commit if possible and return it."""
  178. if obj.type == "tag":
  179. obj = deref_tag(obj)
  180. if obj.type != "commit":
  181. raise ValueError("Cannot convert object %r to type commit" % obj)
  182. # END verify type
  183. return obj
  184. def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
  185. """Parse a revision string. Like :manpage:`git-rev-parse(1)`.
  186. :return:
  187. `~git.objects.base.Object` at the given revision.
  188. This may be any type of git object:
  189. * :class:`Commit <git.objects.commit.Commit>`
  190. * :class:`TagObject <git.objects.tag.TagObject>`
  191. * :class:`Tree <git.objects.tree.Tree>`
  192. * :class:`Blob <git.objects.blob.Blob>`
  193. :param rev:
  194. :manpage:`git-rev-parse(1)`-compatible revision specification as string.
  195. Please see :manpage:`git-rev-parse(1)` for details.
  196. :raise gitdb.exc.BadObject:
  197. If the given revision could not be found.
  198. :raise ValueError:
  199. If `rev` couldn't be parsed.
  200. :raise IndexError:
  201. If an invalid reflog index is specified.
  202. """
  203. # Are we in colon search mode?
  204. if rev.startswith(":/"):
  205. # Colon search mode
  206. raise NotImplementedError("commit by message search (regex)")
  207. # END handle search
  208. obj: Optional[AnyGitObject] = None
  209. ref = None
  210. output_type = "commit"
  211. start = 0
  212. parsed_to = 0
  213. lr = len(rev)
  214. while start < lr:
  215. if rev[start] not in "^~:@":
  216. start += 1
  217. continue
  218. # END handle start
  219. token = rev[start]
  220. if obj is None:
  221. # token is a rev name.
  222. if start == 0:
  223. ref = repo.head.ref
  224. else:
  225. if token == "@":
  226. ref = cast("Reference", name_to_object(repo, rev[:start], return_ref=True))
  227. else:
  228. obj = name_to_object(repo, rev[:start])
  229. # END handle token
  230. # END handle refname
  231. else:
  232. if ref is not None:
  233. obj = ref.commit
  234. # END handle ref
  235. # END initialize obj on first token
  236. start += 1
  237. # Try to parse {type}.
  238. if start < lr and rev[start] == "{":
  239. end = rev.find("}", start)
  240. if end == -1:
  241. raise ValueError("Missing closing brace to define type in %s" % rev)
  242. output_type = rev[start + 1 : end] # Exclude brace.
  243. # Handle type.
  244. if output_type == "commit":
  245. obj = cast("TagObject", obj)
  246. if obj and obj.type == "tag":
  247. obj = deref_tag(obj)
  248. else:
  249. # Cannot do anything for non-tags.
  250. pass
  251. # END handle tag
  252. elif output_type == "tree":
  253. try:
  254. obj = cast(AnyGitObject, obj)
  255. obj = to_commit(obj).tree
  256. except (AttributeError, ValueError):
  257. pass # Error raised later.
  258. # END exception handling
  259. elif output_type in ("", "blob"):
  260. obj = cast("TagObject", obj)
  261. if obj and obj.type == "tag":
  262. obj = deref_tag(obj)
  263. else:
  264. # Cannot do anything for non-tags.
  265. pass
  266. # END handle tag
  267. elif token == "@":
  268. # try single int
  269. assert ref is not None, "Require Reference to access reflog"
  270. revlog_index = None
  271. try:
  272. # Transform reversed index into the format of our revlog.
  273. revlog_index = -(int(output_type) + 1)
  274. except ValueError as e:
  275. # TODO: Try to parse the other date options, using parse_date maybe.
  276. raise NotImplementedError("Support for additional @{...} modes not implemented") from e
  277. # END handle revlog index
  278. try:
  279. entry = ref.log_entry(revlog_index)
  280. except IndexError as e:
  281. raise IndexError("Invalid revlog index: %i" % revlog_index) from e
  282. # END handle index out of bound
  283. obj = Object.new_from_sha(repo, hex_to_bin(entry.newhexsha))
  284. # Make it pass the following checks.
  285. output_type = ""
  286. else:
  287. raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev))
  288. # END handle output type
  289. # Empty output types don't require any specific type, its just about
  290. # dereferencing tags.
  291. if output_type and obj and obj.type != output_type:
  292. raise ValueError("Could not accommodate requested object type %r, got %s" % (output_type, obj.type))
  293. # END verify output type
  294. start = end + 1 # Skip brace.
  295. parsed_to = start
  296. continue
  297. # END parse type
  298. # Try to parse a number.
  299. num = 0
  300. if token != ":":
  301. found_digit = False
  302. while start < lr:
  303. if rev[start] in digits:
  304. num = num * 10 + int(rev[start])
  305. start += 1
  306. found_digit = True
  307. else:
  308. break
  309. # END handle number
  310. # END number parse loop
  311. # No explicit number given, 1 is the default. It could be 0 though.
  312. if not found_digit:
  313. num = 1
  314. # END set default num
  315. # END number parsing only if non-blob mode
  316. parsed_to = start
  317. # Handle hierarchy walk.
  318. try:
  319. obj = cast(AnyGitObject, obj)
  320. if token == "~":
  321. obj = to_commit(obj)
  322. for _ in range(num):
  323. obj = obj.parents[0]
  324. # END for each history item to walk
  325. elif token == "^":
  326. obj = to_commit(obj)
  327. # Must be n'th parent.
  328. if num:
  329. obj = obj.parents[num - 1]
  330. elif token == ":":
  331. if obj.type != "tree":
  332. obj = obj.tree
  333. # END get tree type
  334. obj = obj[rev[start:]]
  335. parsed_to = lr
  336. else:
  337. raise ValueError("Invalid token: %r" % token)
  338. # END end handle tag
  339. except (IndexError, AttributeError) as e:
  340. raise BadName(
  341. f"Invalid revision spec '{rev}' - not enough parent commits to reach '{token}{int(num)}'"
  342. ) from e
  343. # END exception handling
  344. # END parse loop
  345. # Still no obj? It's probably a simple name.
  346. if obj is None:
  347. obj = name_to_object(repo, rev)
  348. parsed_to = lr
  349. # END handle simple name
  350. if obj is None:
  351. raise ValueError("Revision specifier could not be parsed: %s" % rev)
  352. if parsed_to != lr:
  353. raise ValueError("Didn't consume complete rev spec %s, consumed part: %s" % (rev, rev[:parsed_to]))
  354. return obj