objectspec.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. # objectspec.py -- Object specification
  2. # Copyright (C) 2014 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as public by the Free Software Foundation; version 2.0
  6. # or (at your option) any later version. You can redistribute it and/or
  7. # modify it under the terms of either of these two licenses.
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # You should have received a copy of the licenses; if not, see
  16. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  17. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  18. # License, Version 2.0.
  19. #
  20. """Object specification."""
  21. from typing import TYPE_CHECKING, Iterator, List, Optional, Tuple, Union
  22. if TYPE_CHECKING:
  23. from .objects import Commit, ShaFile, Tree
  24. from .refs import Ref, RefsContainer
  25. from .repo import Repo
  26. def to_bytes(text: Union[str, bytes]) -> bytes:
  27. if getattr(text, "encode", None) is not None:
  28. text = text.encode("ascii") # type: ignore
  29. return text # type: ignore
  30. def parse_object(repo: "Repo", objectish: Union[bytes, str]) -> "ShaFile":
  31. """Parse a string referring to an object.
  32. Args:
  33. repo: A `Repo` object
  34. objectish: A string referring to an object
  35. Returns: A git object
  36. Raises:
  37. KeyError: If the object can not be found
  38. """
  39. objectish = to_bytes(objectish)
  40. return repo[objectish]
  41. def parse_tree(repo: "Repo", treeish: Union[bytes, str]) -> "Tree":
  42. """Parse a string referring to a tree.
  43. Args:
  44. repo: A `Repo` object
  45. treeish: A string referring to a tree
  46. Returns: A git object
  47. Raises:
  48. KeyError: If the object can not be found
  49. """
  50. treeish = to_bytes(treeish)
  51. try:
  52. treeish = parse_ref(repo, treeish)
  53. except KeyError: # treeish is commit sha
  54. pass
  55. o = repo[treeish]
  56. if o.type_name == b"commit":
  57. return repo[o.tree]
  58. return o
  59. def parse_ref(
  60. container: Union["Repo", "RefsContainer"], refspec: Union[str, bytes]
  61. ) -> "Ref":
  62. """Parse a string referring to a reference.
  63. Args:
  64. container: A RefsContainer object
  65. refspec: A string referring to a ref
  66. Returns: A ref
  67. Raises:
  68. KeyError: If the ref can not be found
  69. """
  70. refspec = to_bytes(refspec)
  71. possible_refs = [
  72. refspec,
  73. b"refs/" + refspec,
  74. b"refs/tags/" + refspec,
  75. b"refs/heads/" + refspec,
  76. b"refs/remotes/" + refspec,
  77. b"refs/remotes/" + refspec + b"/HEAD",
  78. ]
  79. for ref in possible_refs:
  80. if ref in container:
  81. return ref
  82. raise KeyError(refspec)
  83. def parse_reftuple(
  84. lh_container: Union["Repo", "RefsContainer"],
  85. rh_container: Union["Repo", "RefsContainer"],
  86. refspec: Union[str, bytes],
  87. force: bool = False,
  88. ) -> Tuple[Optional["Ref"], Optional["Ref"], bool]:
  89. """Parse a reftuple spec.
  90. Args:
  91. lh_container: A RefsContainer object
  92. rh_container: A RefsContainer object
  93. refspec: A string
  94. Returns: A tuple with left and right ref
  95. Raises:
  96. KeyError: If one of the refs can not be found
  97. """
  98. refspec = to_bytes(refspec)
  99. if refspec.startswith(b"+"):
  100. force = True
  101. refspec = refspec[1:]
  102. lh: Optional[bytes]
  103. rh: Optional[bytes]
  104. if b":" in refspec:
  105. (lh, rh) = refspec.split(b":")
  106. else:
  107. lh = rh = refspec
  108. if lh == b"":
  109. lh = None
  110. else:
  111. lh = parse_ref(lh_container, lh)
  112. if rh == b"":
  113. rh = None
  114. else:
  115. try:
  116. rh = parse_ref(rh_container, rh)
  117. except KeyError:
  118. # TODO: check force?
  119. if b"/" not in rh:
  120. rh = b"refs/heads/" + rh
  121. return (lh, rh, force)
  122. def parse_reftuples(
  123. lh_container: Union["Repo", "RefsContainer"],
  124. rh_container: Union["Repo", "RefsContainer"],
  125. refspecs: Union[bytes, List[bytes]],
  126. force: bool = False,
  127. ):
  128. """Parse a list of reftuple specs to a list of reftuples.
  129. Args:
  130. lh_container: A RefsContainer object
  131. rh_container: A RefsContainer object
  132. refspecs: A list of refspecs or a string
  133. force: Force overwriting for all reftuples
  134. Returns: A list of refs
  135. Raises:
  136. KeyError: If one of the refs can not be found
  137. """
  138. if not isinstance(refspecs, list):
  139. refspecs = [refspecs]
  140. ret = []
  141. # TODO: Support * in refspecs
  142. for refspec in refspecs:
  143. ret.append(parse_reftuple(lh_container, rh_container, refspec, force=force))
  144. return ret
  145. def parse_refs(container, refspecs):
  146. """Parse a list of refspecs to a list of refs.
  147. Args:
  148. container: A RefsContainer object
  149. refspecs: A list of refspecs or a string
  150. Returns: A list of refs
  151. Raises:
  152. KeyError: If one of the refs can not be found
  153. """
  154. # TODO: Support * in refspecs
  155. if not isinstance(refspecs, list):
  156. refspecs = [refspecs]
  157. ret = []
  158. for refspec in refspecs:
  159. ret.append(parse_ref(container, refspec))
  160. return ret
  161. def parse_commit_range(
  162. repo: "Repo", committishs: Union[str, bytes]
  163. ) -> Iterator["Commit"]:
  164. """Parse a string referring to a range of commits.
  165. Args:
  166. repo: A `Repo` object
  167. committishs: A string referring to a range of commits.
  168. Returns: An iterator over `Commit` objects
  169. Raises:
  170. KeyError: When the reference commits can not be found
  171. ValueError: If the range can not be parsed
  172. """
  173. committishs = to_bytes(committishs)
  174. # TODO(jelmer): Support more than a single commit..
  175. return iter([parse_commit(repo, committishs)])
  176. class AmbiguousShortId(Exception):
  177. """The short id is ambiguous."""
  178. def __init__(self, prefix, options) -> None:
  179. self.prefix = prefix
  180. self.options = options
  181. def scan_for_short_id(object_store, prefix):
  182. """Scan an object store for a short id."""
  183. # TODO(jelmer): This could short-circuit looking for objects
  184. # starting with a certain prefix.
  185. ret = []
  186. for object_id in object_store:
  187. if object_id.startswith(prefix):
  188. ret.append(object_store[object_id])
  189. if not ret:
  190. raise KeyError(prefix)
  191. if len(ret) == 1:
  192. return ret[0]
  193. raise AmbiguousShortId(prefix, ret)
  194. def parse_commit(repo: "Repo", committish: Union[str, bytes]) -> "Commit":
  195. """Parse a string referring to a single commit.
  196. Args:
  197. repo: A` Repo` object
  198. committish: A string referring to a single commit.
  199. Returns: A Commit object
  200. Raises:
  201. KeyError: When the reference commits can not be found
  202. ValueError: If the range can not be parsed
  203. """
  204. committish = to_bytes(committish)
  205. try:
  206. return repo[committish]
  207. except KeyError:
  208. pass
  209. try:
  210. return repo[parse_ref(repo, committish)]
  211. except KeyError:
  212. pass
  213. if len(committish) >= 4 and len(committish) < 40:
  214. try:
  215. int(committish, 16)
  216. except ValueError:
  217. pass
  218. else:
  219. try:
  220. return scan_for_short_id(repo.object_store, committish)
  221. except KeyError:
  222. pass
  223. raise KeyError(committish)
  224. # TODO: parse_path_in_tree(), which handles e.g. v1.0:Documentation