2
0

objectspec.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. # objectspec.py -- Object specification
  2. # Copyright (C) 2014 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as published by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Object specification."""
  22. from typing import TYPE_CHECKING, Optional, Union
  23. from .objects import Commit, ShaFile, Tag, Tree
  24. if TYPE_CHECKING:
  25. from .refs import Ref, RefsContainer
  26. from .repo import Repo
  27. def to_bytes(text: Union[str, bytes]) -> bytes:
  28. if getattr(text, "encode", None) is not None:
  29. text = text.encode("ascii") # type: ignore
  30. return text # type: ignore
  31. def parse_object(repo: "Repo", objectish: Union[bytes, str]) -> "ShaFile":
  32. """Parse a string referring to an object.
  33. Args:
  34. repo: A `Repo` object
  35. objectish: A string referring to an object
  36. Returns: A git object
  37. Raises:
  38. KeyError: If the object can not be found
  39. """
  40. objectish = to_bytes(objectish)
  41. return repo[objectish]
  42. def parse_tree(repo: "Repo", treeish: Union[bytes, str, Tree, Commit, Tag]) -> "Tree":
  43. """Parse a string referring to a tree.
  44. Args:
  45. repo: A `Repo` object
  46. treeish: A string referring to a tree, or a Tree, Commit, or Tag object
  47. Returns: A Tree object
  48. Raises:
  49. KeyError: If the object can not be found
  50. """
  51. # If already a Tree, return it directly
  52. if isinstance(treeish, Tree):
  53. return treeish
  54. # If it's a Commit, return its tree
  55. if isinstance(treeish, Commit):
  56. return repo[treeish.tree]
  57. # For Tag objects or strings, use the existing logic
  58. if isinstance(treeish, Tag):
  59. treeish = treeish.id
  60. else:
  61. treeish = to_bytes(treeish)
  62. try:
  63. treeish = parse_ref(repo, treeish)
  64. except KeyError: # treeish is commit sha
  65. pass
  66. try:
  67. o = repo[treeish]
  68. except KeyError:
  69. # Try parsing as commit (handles short hashes)
  70. try:
  71. commit = parse_commit(repo, treeish)
  72. return repo[commit.tree]
  73. except KeyError:
  74. raise KeyError(treeish)
  75. if o.type_name == b"commit":
  76. return repo[o.tree]
  77. elif o.type_name == b"tag":
  78. # Tag handling - dereference and recurse
  79. obj_type, obj_sha = o.object
  80. return parse_tree(repo, obj_sha)
  81. return o
  82. def parse_ref(
  83. container: Union["Repo", "RefsContainer"], refspec: Union[str, bytes]
  84. ) -> "Ref":
  85. """Parse a string referring to a reference.
  86. Args:
  87. container: A RefsContainer object
  88. refspec: A string referring to a ref
  89. Returns: A ref
  90. Raises:
  91. KeyError: If the ref can not be found
  92. """
  93. refspec = to_bytes(refspec)
  94. possible_refs = [
  95. refspec,
  96. b"refs/" + refspec,
  97. b"refs/tags/" + refspec,
  98. b"refs/heads/" + refspec,
  99. b"refs/remotes/" + refspec,
  100. b"refs/remotes/" + refspec + b"/HEAD",
  101. ]
  102. for ref in possible_refs:
  103. if ref in container:
  104. return ref
  105. raise KeyError(refspec)
  106. def parse_reftuple(
  107. lh_container: Union["Repo", "RefsContainer"],
  108. rh_container: Union["Repo", "RefsContainer"],
  109. refspec: Union[str, bytes],
  110. force: bool = False,
  111. ) -> tuple[Optional["Ref"], Optional["Ref"], bool]:
  112. """Parse a reftuple spec.
  113. Args:
  114. lh_container: A RefsContainer object
  115. rh_container: A RefsContainer object
  116. refspec: A string
  117. Returns: A tuple with left and right ref
  118. Raises:
  119. KeyError: If one of the refs can not be found
  120. """
  121. refspec = to_bytes(refspec)
  122. if refspec.startswith(b"+"):
  123. force = True
  124. refspec = refspec[1:]
  125. lh: Optional[bytes]
  126. rh: Optional[bytes]
  127. if b":" in refspec:
  128. (lh, rh) = refspec.split(b":")
  129. else:
  130. lh = rh = refspec
  131. if lh == b"":
  132. lh = None
  133. else:
  134. lh = parse_ref(lh_container, lh)
  135. if rh == b"":
  136. rh = None
  137. else:
  138. try:
  139. rh = parse_ref(rh_container, rh)
  140. except KeyError:
  141. # TODO: check force?
  142. if b"/" not in rh:
  143. rh = b"refs/heads/" + rh
  144. return (lh, rh, force)
  145. def parse_reftuples(
  146. lh_container: Union["Repo", "RefsContainer"],
  147. rh_container: Union["Repo", "RefsContainer"],
  148. refspecs: Union[bytes, list[bytes]],
  149. force: bool = False,
  150. ):
  151. """Parse a list of reftuple specs to a list of reftuples.
  152. Args:
  153. lh_container: A RefsContainer object
  154. rh_container: A RefsContainer object
  155. refspecs: A list of refspecs or a string
  156. force: Force overwriting for all reftuples
  157. Returns: A list of refs
  158. Raises:
  159. KeyError: If one of the refs can not be found
  160. """
  161. if not isinstance(refspecs, list):
  162. refspecs = [refspecs]
  163. ret = []
  164. # TODO: Support * in refspecs
  165. for refspec in refspecs:
  166. ret.append(parse_reftuple(lh_container, rh_container, refspec, force=force))
  167. return ret
  168. def parse_refs(container, refspecs):
  169. """Parse a list of refspecs to a list of refs.
  170. Args:
  171. container: A RefsContainer object
  172. refspecs: A list of refspecs or a string
  173. Returns: A list of refs
  174. Raises:
  175. KeyError: If one of the refs can not be found
  176. """
  177. # TODO: Support * in refspecs
  178. if not isinstance(refspecs, list):
  179. refspecs = [refspecs]
  180. ret = []
  181. for refspec in refspecs:
  182. ret.append(parse_ref(container, refspec))
  183. return ret
  184. def parse_commit_range(
  185. repo: "Repo", committish: Union[str, bytes]
  186. ) -> Optional[tuple["Commit", "Commit"]]:
  187. """Parse a string referring to a commit range.
  188. Args:
  189. repo: A `Repo` object
  190. committish: A string referring to a commit or range (e.g., "HEAD~3..HEAD")
  191. Returns:
  192. None if committish is a single commit reference
  193. A tuple of (start_commit, end_commit) if it's a range
  194. Raises:
  195. KeyError: When the commits can not be found
  196. ValueError: If the range can not be parsed
  197. """
  198. committish = to_bytes(committish)
  199. if b".." not in committish:
  200. return None
  201. parts = committish.split(b"..", 1)
  202. if len(parts) != 2:
  203. raise ValueError(f"Invalid commit range: {committish.decode('utf-8')}")
  204. start_ref = parts[0]
  205. end_ref = parts[1] if parts[1] else b"HEAD"
  206. start_commit = parse_commit(repo, start_ref)
  207. end_commit = parse_commit(repo, end_ref)
  208. return (start_commit, end_commit)
  209. class AmbiguousShortId(Exception):
  210. """The short id is ambiguous."""
  211. def __init__(self, prefix, options) -> None:
  212. self.prefix = prefix
  213. self.options = options
  214. def scan_for_short_id(object_store, prefix, tp):
  215. """Scan an object store for a short id."""
  216. ret = []
  217. for object_id in object_store.iter_prefix(prefix):
  218. o = object_store[object_id]
  219. if isinstance(o, tp):
  220. ret.append(o)
  221. if not ret:
  222. raise KeyError(prefix)
  223. if len(ret) == 1:
  224. return ret[0]
  225. raise AmbiguousShortId(prefix, ret)
  226. def parse_commit(repo: "Repo", committish: Union[str, bytes, Commit, Tag]) -> "Commit":
  227. """Parse a string referring to a single commit.
  228. Args:
  229. repo: A` Repo` object
  230. committish: A string referring to a single commit, or a Commit or Tag object.
  231. Returns: A Commit object
  232. Raises:
  233. KeyError: When the reference commits can not be found
  234. ValueError: If the range can not be parsed
  235. """
  236. def dereference_tag(obj):
  237. """Follow tag references until we reach a non-tag object."""
  238. while isinstance(obj, Tag):
  239. obj_type, obj_sha = obj.object
  240. try:
  241. obj = repo.object_store[obj_sha]
  242. except KeyError:
  243. # Tag points to a missing object
  244. raise KeyError(obj_sha)
  245. if not isinstance(obj, Commit):
  246. raise ValueError(f"Expected commit, got {obj.type_name}")
  247. return obj
  248. # If already a Commit object, return it directly
  249. if isinstance(committish, Commit):
  250. return committish
  251. # If it's a Tag object, dereference it
  252. if isinstance(committish, Tag):
  253. return dereference_tag(committish)
  254. committish = to_bytes(committish)
  255. try:
  256. obj = repo[committish]
  257. except KeyError:
  258. pass
  259. else:
  260. return dereference_tag(obj)
  261. try:
  262. obj = repo[parse_ref(repo, committish)]
  263. except KeyError:
  264. pass
  265. else:
  266. return dereference_tag(obj)
  267. if len(committish) >= 4 and len(committish) < 40:
  268. try:
  269. int(committish, 16)
  270. except ValueError:
  271. pass
  272. else:
  273. try:
  274. obj = scan_for_short_id(repo.object_store, committish, Commit)
  275. except KeyError:
  276. pass
  277. else:
  278. return dereference_tag(obj)
  279. raise KeyError(committish)
  280. # TODO: parse_path_in_tree(), which handles e.g. v1.0:Documentation