objectspec.py 8.4 KB

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