test_objectspec.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. # test_objectspec.py -- tests for objectspec.py
  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. """Tests for revision spec parsing."""
  22. # TODO: Round-trip parse-serialize-parse and serialize-parse-serialize tests.
  23. from dulwich.objects import Blob, Commit, Tag
  24. from dulwich.objectspec import (
  25. parse_commit,
  26. parse_commit_range,
  27. parse_object,
  28. parse_ref,
  29. parse_refs,
  30. parse_reftuple,
  31. parse_reftuples,
  32. parse_tree,
  33. )
  34. from dulwich.repo import MemoryRepo
  35. from dulwich.tests.utils import build_commit_graph
  36. from . import TestCase
  37. class ParseObjectTests(TestCase):
  38. """Test parse_object."""
  39. def test_nonexistent(self) -> None:
  40. r = MemoryRepo()
  41. self.assertRaises(KeyError, parse_object, r, "thisdoesnotexist")
  42. def test_blob_by_sha(self) -> None:
  43. r = MemoryRepo()
  44. b = Blob.from_string(b"Blah")
  45. r.object_store.add_object(b)
  46. self.assertEqual(b, parse_object(r, b.id))
  47. def test_parent_caret(self) -> None:
  48. r = MemoryRepo()
  49. c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 1, 2]])
  50. # c3's parents are [c1, c2]
  51. self.assertEqual(c1, parse_object(r, c3.id + b"^1"))
  52. self.assertEqual(c1, parse_object(r, c3.id + b"^")) # ^ defaults to ^1
  53. self.assertEqual(c2, parse_object(r, c3.id + b"^2"))
  54. def test_parent_tilde(self) -> None:
  55. r = MemoryRepo()
  56. c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 2]])
  57. self.assertEqual(c2, parse_object(r, c3.id + b"~"))
  58. self.assertEqual(c2, parse_object(r, c3.id + b"~1"))
  59. self.assertEqual(c1, parse_object(r, c3.id + b"~2"))
  60. def test_combined_operators(self) -> None:
  61. r = MemoryRepo()
  62. c1, c2, c3, c4 = build_commit_graph(
  63. r.object_store, [[1], [2, 1], [3, 1, 2], [4, 3]]
  64. )
  65. # c4~1^2 means: go back 1 generation from c4 (to c3), then take its 2nd parent
  66. # c3's parents are [c1, c2], so ^2 is c2
  67. self.assertEqual(c2, parse_object(r, c4.id + b"~1^2"))
  68. self.assertEqual(c1, parse_object(r, c4.id + b"~^"))
  69. def test_with_ref(self) -> None:
  70. r = MemoryRepo()
  71. c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 2]])
  72. r.refs[b"refs/heads/master"] = c3.id
  73. self.assertEqual(c2, parse_object(r, b"master~"))
  74. self.assertEqual(c1, parse_object(r, b"master~2"))
  75. def test_caret_zero(self) -> None:
  76. r = MemoryRepo()
  77. c1, c2 = build_commit_graph(r.object_store, [[1], [2, 1]])
  78. # ^0 means the commit itself
  79. self.assertEqual(c2, parse_object(r, c2.id + b"^0"))
  80. self.assertEqual(c1, parse_object(r, c2.id + b"~^0"))
  81. def test_missing_parent(self) -> None:
  82. r = MemoryRepo()
  83. c1, c2 = build_commit_graph(r.object_store, [[1], [2, 1]])
  84. # c2 only has 1 parent, so ^2 should fail
  85. self.assertRaises(ValueError, parse_object, r, c2.id + b"^2")
  86. # c1 has no parents, so ~ should fail
  87. self.assertRaises(ValueError, parse_object, r, c1.id + b"~")
  88. def test_empty_base(self) -> None:
  89. r = MemoryRepo()
  90. self.assertRaises(ValueError, parse_object, r, b"~1")
  91. self.assertRaises(ValueError, parse_object, r, b"^1")
  92. def test_non_commit_with_operators(self) -> None:
  93. r = MemoryRepo()
  94. b = Blob.from_string(b"Blah")
  95. r.object_store.add_object(b)
  96. # Can't apply ~ or ^ to a blob
  97. self.assertRaises(ValueError, parse_object, r, b.id + b"~1")
  98. class ParseCommitRangeTests(TestCase):
  99. """Test parse_commit_range."""
  100. def test_nonexistent(self) -> None:
  101. r = MemoryRepo()
  102. self.assertRaises(KeyError, parse_commit_range, r, "thisdoesnotexist")
  103. def test_commit_by_sha(self) -> None:
  104. r = MemoryRepo()
  105. c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 1, 2]])
  106. self.assertEqual([c1], list(parse_commit_range(r, c1.id)))
  107. class ParseCommitTests(TestCase):
  108. """Test parse_commit."""
  109. def test_nonexistent(self) -> None:
  110. r = MemoryRepo()
  111. self.assertRaises(KeyError, parse_commit, r, "thisdoesnotexist")
  112. def test_commit_by_sha(self) -> None:
  113. r = MemoryRepo()
  114. [c1] = build_commit_graph(r.object_store, [[1]])
  115. self.assertEqual(c1, parse_commit(r, c1.id))
  116. def test_commit_by_short_sha(self) -> None:
  117. r = MemoryRepo()
  118. [c1] = build_commit_graph(r.object_store, [[1]])
  119. self.assertEqual(c1, parse_commit(r, c1.id[:10]))
  120. def test_annotated_tag(self) -> None:
  121. r = MemoryRepo()
  122. [c1] = build_commit_graph(r.object_store, [[1]])
  123. # Create an annotated tag pointing to the commit
  124. tag = Tag()
  125. tag.name = b"v1.0"
  126. tag.message = b"Test tag"
  127. tag.tag_time = 1234567890
  128. tag.tag_timezone = 0
  129. tag.object = (Commit, c1.id)
  130. tag.tagger = b"Test Tagger <test@example.com>"
  131. r.object_store.add_object(tag)
  132. # parse_commit should follow the tag to the commit
  133. self.assertEqual(c1, parse_commit(r, tag.id))
  134. def test_nested_tags(self) -> None:
  135. r = MemoryRepo()
  136. [c1] = build_commit_graph(r.object_store, [[1]])
  137. # Create an annotated tag pointing to the commit
  138. tag1 = Tag()
  139. tag1.name = b"v1.0"
  140. tag1.message = b"Test tag"
  141. tag1.tag_time = 1234567890
  142. tag1.tag_timezone = 0
  143. tag1.object = (Commit, c1.id)
  144. tag1.tagger = b"Test Tagger <test@example.com>"
  145. r.object_store.add_object(tag1)
  146. # Create another tag pointing to the first tag
  147. tag2 = Tag()
  148. tag2.name = b"v1.0-release"
  149. tag2.message = b"Release tag"
  150. tag2.tag_time = 1234567900
  151. tag2.tag_timezone = 0
  152. tag2.object = (Tag, tag1.id)
  153. tag2.tagger = b"Test Tagger <test@example.com>"
  154. r.object_store.add_object(tag2)
  155. # parse_commit should follow both tags to the commit
  156. self.assertEqual(c1, parse_commit(r, tag2.id))
  157. def test_tag_to_missing_commit(self) -> None:
  158. r = MemoryRepo()
  159. # Create a tag pointing to a non-existent commit
  160. missing_sha = b"1234567890123456789012345678901234567890"
  161. tag = Tag()
  162. tag.name = b"v1.0"
  163. tag.message = b"Test tag"
  164. tag.tag_time = 1234567890
  165. tag.tag_timezone = 0
  166. tag.object = (Commit, missing_sha)
  167. tag.tagger = b"Test Tagger <test@example.com>"
  168. r.object_store.add_object(tag)
  169. # Should raise KeyError for missing commit
  170. self.assertRaises(KeyError, parse_commit, r, tag.id)
  171. def test_tag_to_blob(self) -> None:
  172. r = MemoryRepo()
  173. # Create a blob
  174. blob = Blob.from_string(b"Test content")
  175. r.object_store.add_object(blob)
  176. # Create a tag pointing to the blob
  177. tag = Tag()
  178. tag.name = b"blob-tag"
  179. tag.message = b"Tag pointing to blob"
  180. tag.tag_time = 1234567890
  181. tag.tag_timezone = 0
  182. tag.object = (Blob, blob.id)
  183. tag.tagger = b"Test Tagger <test@example.com>"
  184. r.object_store.add_object(tag)
  185. # Should raise ValueError as it's not a commit
  186. self.assertRaises(ValueError, parse_commit, r, tag.id)
  187. def test_commit_object(self) -> None:
  188. r = MemoryRepo()
  189. [c1] = build_commit_graph(r.object_store, [[1]])
  190. # Test that passing a Commit object directly returns the same object
  191. self.assertEqual(c1, parse_commit(r, c1))
  192. class ParseRefTests(TestCase):
  193. def test_nonexistent(self) -> None:
  194. r = {}
  195. self.assertRaises(KeyError, parse_ref, r, b"thisdoesnotexist")
  196. def test_ambiguous_ref(self) -> None:
  197. r = {
  198. b"ambig1": "bla",
  199. b"refs/ambig1": "bla",
  200. b"refs/tags/ambig1": "bla",
  201. b"refs/heads/ambig1": "bla",
  202. b"refs/remotes/ambig1": "bla",
  203. b"refs/remotes/ambig1/HEAD": "bla",
  204. }
  205. self.assertEqual(b"ambig1", parse_ref(r, b"ambig1"))
  206. def test_ambiguous_ref2(self) -> None:
  207. r = {
  208. b"refs/ambig2": "bla",
  209. b"refs/tags/ambig2": "bla",
  210. b"refs/heads/ambig2": "bla",
  211. b"refs/remotes/ambig2": "bla",
  212. b"refs/remotes/ambig2/HEAD": "bla",
  213. }
  214. self.assertEqual(b"refs/ambig2", parse_ref(r, b"ambig2"))
  215. def test_ambiguous_tag(self) -> None:
  216. r = {
  217. b"refs/tags/ambig3": "bla",
  218. b"refs/heads/ambig3": "bla",
  219. b"refs/remotes/ambig3": "bla",
  220. b"refs/remotes/ambig3/HEAD": "bla",
  221. }
  222. self.assertEqual(b"refs/tags/ambig3", parse_ref(r, b"ambig3"))
  223. def test_ambiguous_head(self) -> None:
  224. r = {
  225. b"refs/heads/ambig4": "bla",
  226. b"refs/remotes/ambig4": "bla",
  227. b"refs/remotes/ambig4/HEAD": "bla",
  228. }
  229. self.assertEqual(b"refs/heads/ambig4", parse_ref(r, b"ambig4"))
  230. def test_ambiguous_remote(self) -> None:
  231. r = {b"refs/remotes/ambig5": "bla", b"refs/remotes/ambig5/HEAD": "bla"}
  232. self.assertEqual(b"refs/remotes/ambig5", parse_ref(r, b"ambig5"))
  233. def test_ambiguous_remote_head(self) -> None:
  234. r = {b"refs/remotes/ambig6/HEAD": "bla"}
  235. self.assertEqual(b"refs/remotes/ambig6/HEAD", parse_ref(r, b"ambig6"))
  236. def test_heads_full(self) -> None:
  237. r = {b"refs/heads/foo": "bla"}
  238. self.assertEqual(b"refs/heads/foo", parse_ref(r, b"refs/heads/foo"))
  239. def test_heads_partial(self) -> None:
  240. r = {b"refs/heads/foo": "bla"}
  241. self.assertEqual(b"refs/heads/foo", parse_ref(r, b"heads/foo"))
  242. def test_tags_partial(self) -> None:
  243. r = {b"refs/tags/foo": "bla"}
  244. self.assertEqual(b"refs/tags/foo", parse_ref(r, b"tags/foo"))
  245. class ParseRefsTests(TestCase):
  246. def test_nonexistent(self) -> None:
  247. r = {}
  248. self.assertRaises(KeyError, parse_refs, r, [b"thisdoesnotexist"])
  249. def test_head(self) -> None:
  250. r = {b"refs/heads/foo": "bla"}
  251. self.assertEqual([b"refs/heads/foo"], parse_refs(r, [b"foo"]))
  252. def test_full(self) -> None:
  253. r = {b"refs/heads/foo": "bla"}
  254. self.assertEqual([b"refs/heads/foo"], parse_refs(r, b"refs/heads/foo"))
  255. class ParseReftupleTests(TestCase):
  256. def test_nonexistent(self) -> None:
  257. r = {}
  258. self.assertRaises(KeyError, parse_reftuple, r, r, b"thisdoesnotexist")
  259. def test_head(self) -> None:
  260. r = {b"refs/heads/foo": "bla"}
  261. self.assertEqual(
  262. (b"refs/heads/foo", b"refs/heads/foo", False),
  263. parse_reftuple(r, r, b"foo"),
  264. )
  265. self.assertEqual(
  266. (b"refs/heads/foo", b"refs/heads/foo", True),
  267. parse_reftuple(r, r, b"+foo"),
  268. )
  269. self.assertEqual(
  270. (b"refs/heads/foo", b"refs/heads/foo", True),
  271. parse_reftuple(r, {}, b"+foo"),
  272. )
  273. self.assertEqual(
  274. (b"refs/heads/foo", b"refs/heads/foo", True),
  275. parse_reftuple(r, {}, b"foo", True),
  276. )
  277. def test_full(self) -> None:
  278. r = {b"refs/heads/foo": "bla"}
  279. self.assertEqual(
  280. (b"refs/heads/foo", b"refs/heads/foo", False),
  281. parse_reftuple(r, r, b"refs/heads/foo"),
  282. )
  283. def test_no_left_ref(self) -> None:
  284. r = {b"refs/heads/foo": "bla"}
  285. self.assertEqual(
  286. (None, b"refs/heads/foo", False),
  287. parse_reftuple(r, r, b":refs/heads/foo"),
  288. )
  289. def test_no_right_ref(self) -> None:
  290. r = {b"refs/heads/foo": "bla"}
  291. self.assertEqual(
  292. (b"refs/heads/foo", None, False),
  293. parse_reftuple(r, r, b"refs/heads/foo:"),
  294. )
  295. def test_default_with_string(self) -> None:
  296. r = {b"refs/heads/foo": "bla"}
  297. self.assertEqual(
  298. (b"refs/heads/foo", b"refs/heads/foo", False),
  299. parse_reftuple(r, r, "foo"),
  300. )
  301. class ParseReftuplesTests(TestCase):
  302. def test_nonexistent(self) -> None:
  303. r = {}
  304. self.assertRaises(KeyError, parse_reftuples, r, r, [b"thisdoesnotexist"])
  305. def test_head(self) -> None:
  306. r = {b"refs/heads/foo": "bla"}
  307. self.assertEqual(
  308. [(b"refs/heads/foo", b"refs/heads/foo", False)],
  309. parse_reftuples(r, r, [b"foo"]),
  310. )
  311. def test_full(self) -> None:
  312. r = {b"refs/heads/foo": "bla"}
  313. self.assertEqual(
  314. [(b"refs/heads/foo", b"refs/heads/foo", False)],
  315. parse_reftuples(r, r, b"refs/heads/foo"),
  316. )
  317. r = {b"refs/heads/foo": "bla"}
  318. self.assertEqual(
  319. [(b"refs/heads/foo", b"refs/heads/foo", True)],
  320. parse_reftuples(r, r, b"refs/heads/foo", True),
  321. )
  322. class ParseTreeTests(TestCase):
  323. """Test parse_tree."""
  324. def test_nonexistent(self) -> None:
  325. r = MemoryRepo()
  326. self.assertRaises(KeyError, parse_tree, r, "thisdoesnotexist")
  327. def test_from_commit(self) -> None:
  328. r = MemoryRepo()
  329. c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 1, 2]])
  330. self.assertEqual(r[c1.tree], parse_tree(r, c1.id))
  331. self.assertEqual(r[c1.tree], parse_tree(r, c1.tree))
  332. def test_from_ref(self) -> None:
  333. r = MemoryRepo()
  334. c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 1, 2]])
  335. r.refs[b"refs/heads/foo"] = c1.id
  336. self.assertEqual(r[c1.tree], parse_tree(r, b"foo"))
  337. def test_tree_object(self) -> None:
  338. r = MemoryRepo()
  339. [c1] = build_commit_graph(r.object_store, [[1]])
  340. tree = r[c1.tree]
  341. # Test that passing a Tree object directly returns the same object
  342. self.assertEqual(tree, parse_tree(r, tree))
  343. def test_commit_object(self) -> None:
  344. r = MemoryRepo()
  345. [c1] = build_commit_graph(r.object_store, [[1]])
  346. # Test that passing a Commit object returns its tree
  347. self.assertEqual(r[c1.tree], parse_tree(r, c1))
  348. def test_tag_object(self) -> None:
  349. r = MemoryRepo()
  350. [c1] = build_commit_graph(r.object_store, [[1]])
  351. # Create an annotated tag pointing to the commit
  352. tag = Tag()
  353. tag.name = b"v1.0"
  354. tag.message = b"Test tag"
  355. tag.tag_time = 1234567890
  356. tag.tag_timezone = 0
  357. tag.object = (Commit, c1.id)
  358. tag.tagger = b"Test Tagger <test@example.com>"
  359. r.object_store.add_object(tag)
  360. # parse_tree should follow the tag to the commit's tree
  361. self.assertEqual(r[c1.tree], parse_tree(r, tag))