objectspec.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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 public 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 collections.abc import Iterator
  23. from typing import TYPE_CHECKING, Optional, Union
  24. from .objects import Commit, ShaFile, Tag, Tree
  25. if TYPE_CHECKING:
  26. from .refs import Ref, RefsContainer
  27. from .repo import Repo
  28. def to_bytes(text: Union[str, bytes]) -> bytes:
  29. if getattr(text, "encode", None) is not None:
  30. text = text.encode("ascii") # type: ignore
  31. return text # type: ignore
  32. def parse_object(repo: "Repo", objectish: Union[bytes, str]) -> "ShaFile":
  33. """Parse a string referring to an object.
  34. Args:
  35. repo: A `Repo` object
  36. objectish: A string referring to an object
  37. Returns: A git object
  38. Raises:
  39. KeyError: If the object can not be found
  40. """
  41. objectish = to_bytes(objectish)
  42. return repo[objectish]
  43. def parse_tree(repo: "Repo", treeish: Union[bytes, str, Tree, Commit, Tag]) -> "Tree":
  44. """Parse a string referring to a tree.
  45. Args:
  46. repo: A `Repo` object
  47. treeish: A string referring to a tree, or a Tree, Commit, or Tag object
  48. Returns: A Tree object
  49. Raises:
  50. KeyError: If the object can not be found
  51. """
  52. # If already a Tree, return it directly
  53. if isinstance(treeish, Tree):
  54. return treeish
  55. # If it's a Commit, return its tree
  56. if isinstance(treeish, Commit):
  57. return repo[treeish.tree]
  58. # For Tag objects or strings, use the existing logic
  59. if isinstance(treeish, Tag):
  60. treeish = treeish.id
  61. else:
  62. treeish = to_bytes(treeish)
  63. try:
  64. treeish = parse_ref(repo, treeish)
  65. except KeyError: # treeish is commit sha
  66. pass
  67. try:
  68. o = repo[treeish]
  69. except KeyError:
  70. # Try parsing as commit (handles short hashes)
  71. try:
  72. commit = parse_commit(repo, treeish)
  73. return repo[commit.tree]
  74. except KeyError:
  75. raise KeyError(treeish)
  76. if o.type_name == b"commit":
  77. return repo[o.tree]
  78. elif o.type_name == b"tag":
  79. # Tag handling - dereference and recurse
  80. obj_type, obj_sha = o.object
  81. return parse_tree(repo, obj_sha)
  82. return o
  83. def parse_ref(
  84. container: Union["Repo", "RefsContainer"], refspec: Union[str, bytes]
  85. ) -> "Ref":
  86. """Parse a string referring to a reference.
  87. Args:
  88. container: A RefsContainer object
  89. refspec: A string referring to a ref
  90. Returns: A ref
  91. Raises:
  92. KeyError: If the ref can not be found
  93. """
  94. refspec = to_bytes(refspec)
  95. possible_refs = [
  96. refspec,
  97. b"refs/" + refspec,
  98. b"refs/tags/" + refspec,
  99. b"refs/heads/" + refspec,
  100. b"refs/remotes/" + refspec,
  101. b"refs/remotes/" + refspec + b"/HEAD",
  102. ]
  103. for ref in possible_refs:
  104. if ref in container:
  105. return ref
  106. raise KeyError(refspec)
  107. def parse_reftuple(
  108. lh_container: Union["Repo", "RefsContainer"],
  109. rh_container: Union["Repo", "RefsContainer"],
  110. refspec: Union[str, bytes],
  111. force: bool = False,
  112. ) -> tuple[Optional["Ref"], Optional["Ref"], bool]:
  113. """Parse a reftuple spec.
  114. Args:
  115. lh_container: A RefsContainer object
  116. rh_container: A RefsContainer object
  117. refspec: A string
  118. Returns: A tuple with left and right ref
  119. Raises:
  120. KeyError: If one of the refs can not be found
  121. """
  122. refspec = to_bytes(refspec)
  123. if refspec.startswith(b"+"):
  124. force = True
  125. refspec = refspec[1:]
  126. lh: Optional[bytes]
  127. rh: Optional[bytes]
  128. if b":" in refspec:
  129. (lh, rh) = refspec.split(b":")
  130. else:
  131. lh = rh = refspec
  132. if lh == b"":
  133. lh = None
  134. else:
  135. lh = parse_ref(lh_container, lh)
  136. if rh == b"":
  137. rh = None
  138. else:
  139. try:
  140. rh = parse_ref(rh_container, rh)
  141. except KeyError:
  142. # TODO: check force?
  143. if b"/" not in rh:
  144. rh = b"refs/heads/" + rh
  145. return (lh, rh, force)
  146. def parse_reftuples(
  147. lh_container: Union["Repo", "RefsContainer"],
  148. rh_container: Union["Repo", "RefsContainer"],
  149. refspecs: Union[bytes, list[bytes]],
  150. force: bool = False,
  151. ):
  152. """Parse a list of reftuple specs to a list of reftuples.
  153. Args:
  154. lh_container: A RefsContainer object
  155. rh_container: A RefsContainer object
  156. refspecs: A list of refspecs or a string
  157. force: Force overwriting for all reftuples
  158. Returns: A list of refs
  159. Raises:
  160. KeyError: If one of the refs can not be found
  161. """
  162. if not isinstance(refspecs, list):
  163. refspecs = [refspecs]
  164. ret = []
  165. # TODO: Support * in refspecs
  166. for refspec in refspecs:
  167. ret.append(parse_reftuple(lh_container, rh_container, refspec, force=force))
  168. return ret
  169. def parse_refs(container, refspecs):
  170. """Parse a list of refspecs to a list of refs.
  171. Args:
  172. container: A RefsContainer object
  173. refspecs: A list of refspecs or a string
  174. Returns: A list of refs
  175. Raises:
  176. KeyError: If one of the refs can not be found
  177. """
  178. # TODO: Support * in refspecs
  179. if not isinstance(refspecs, list):
  180. refspecs = [refspecs]
  181. ret = []
  182. for refspec in refspecs:
  183. ret.append(parse_ref(container, refspec))
  184. return ret
  185. def parse_commit_range(
  186. repo: "Repo", committishs: Union[str, bytes]
  187. ) -> Iterator["Commit"]:
  188. """Parse a string referring to a range of commits.
  189. Args:
  190. repo: A `Repo` object
  191. committishs: A string referring to a range of commits.
  192. Returns: An iterator over `Commit` objects
  193. Raises:
  194. KeyError: When the reference commits can not be found
  195. ValueError: If the range can not be parsed
  196. """
  197. committishs = to_bytes(committishs)
  198. # TODO(jelmer): Support more than a single commit..
  199. return iter([parse_commit(repo, committishs)])
  200. class AmbiguousShortId(Exception):
  201. """The short id is ambiguous."""
  202. def __init__(self, prefix, options) -> None:
  203. self.prefix = prefix
  204. self.options = options
  205. def scan_for_short_id(object_store, prefix, tp):
  206. """Scan an object store for a short id."""
  207. ret = []
  208. for object_id in object_store.iter_prefix(prefix):
  209. o = object_store[object_id]
  210. if isinstance(o, tp):
  211. ret.append(o)
  212. if not ret:
  213. raise KeyError(prefix)
  214. if len(ret) == 1:
  215. return ret[0]
  216. raise AmbiguousShortId(prefix, ret)
  217. def parse_commit(repo: "Repo", committish: Union[str, bytes, Commit]) -> "Commit":
  218. """Parse a string referring to a single commit.
  219. Args:
  220. repo: A` Repo` object
  221. committish: A string referring to a single commit, or a Commit object.
  222. Returns: A Commit object
  223. Raises:
  224. KeyError: When the reference commits can not be found
  225. ValueError: If the range can not be parsed
  226. """
  227. def dereference_tag(obj):
  228. """Follow tag references until we reach a non-tag object."""
  229. while isinstance(obj, Tag):
  230. obj_type, obj_sha = obj.object
  231. try:
  232. obj = repo.object_store[obj_sha]
  233. except KeyError:
  234. # Tag points to a missing object
  235. raise KeyError(obj_sha)
  236. if not isinstance(obj, Commit):
  237. raise ValueError(f"Expected commit, got {obj.type_name}")
  238. return obj
  239. # If already a Commit object, return it directly
  240. if isinstance(committish, Commit):
  241. return committish
  242. committish = to_bytes(committish)
  243. try:
  244. obj = repo[committish]
  245. except KeyError:
  246. pass
  247. else:
  248. return dereference_tag(obj)
  249. try:
  250. obj = repo[parse_ref(repo, committish)]
  251. except KeyError:
  252. pass
  253. else:
  254. return dereference_tag(obj)
  255. if len(committish) >= 4 and len(committish) < 40:
  256. try:
  257. int(committish, 16)
  258. except ValueError:
  259. pass
  260. else:
  261. try:
  262. obj = scan_for_short_id(repo.object_store, committish, Commit)
  263. except KeyError:
  264. pass
  265. else:
  266. return dereference_tag(obj)
  267. raise KeyError(committish)
  268. # TODO: parse_path_in_tree(), which handles e.g. v1.0:Documentation