objectspec.py 7.4 KB

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