test_objectspec.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  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, Tree
  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. def test_tag_dereference(self) -> None:
  99. r = MemoryRepo()
  100. [c1] = build_commit_graph(r.object_store, [[1]])
  101. # Create an annotated tag
  102. tag = Tag()
  103. tag.name = b"v1.0"
  104. tag.message = b"Test tag"
  105. tag.tag_time = 1234567890
  106. tag.tag_timezone = 0
  107. tag.object = (Commit, c1.id)
  108. tag.tagger = b"Test Tagger <test@example.com>"
  109. r.object_store.add_object(tag)
  110. # ^{} dereferences the tag
  111. self.assertEqual(c1, parse_object(r, tag.id + b"^{}"))
  112. def test_nested_tag_dereference(self) -> None:
  113. r = MemoryRepo()
  114. [c1] = build_commit_graph(r.object_store, [[1]])
  115. # Create a tag pointing to a commit
  116. tag1 = Tag()
  117. tag1.name = b"v1.0"
  118. tag1.message = b"Test tag"
  119. tag1.tag_time = 1234567890
  120. tag1.tag_timezone = 0
  121. tag1.object = (Commit, c1.id)
  122. tag1.tagger = b"Test Tagger <test@example.com>"
  123. r.object_store.add_object(tag1)
  124. # Create another tag pointing to the first tag
  125. tag2 = Tag()
  126. tag2.name = b"v1.0-release"
  127. tag2.message = b"Release tag"
  128. tag2.tag_time = 1234567900
  129. tag2.tag_timezone = 0
  130. tag2.object = (Tag, tag1.id)
  131. tag2.tagger = b"Test Tagger <test@example.com>"
  132. r.object_store.add_object(tag2)
  133. # ^{} should recursively dereference to the commit
  134. self.assertEqual(c1, parse_object(r, tag2.id + b"^{}"))
  135. def test_path_in_tree(self) -> None:
  136. r = MemoryRepo()
  137. # Create a blob
  138. b = Blob.from_string(b"Test content")
  139. # Create a commit with the blob in its tree
  140. [c1] = build_commit_graph(r.object_store, [[1]], trees={1: [(b"test.txt", b)]})
  141. # HEAD:test.txt should return the blob
  142. r.refs[b"HEAD"] = c1.id
  143. result = parse_object(r, b"HEAD:test.txt")
  144. self.assertEqual(b"Test content", result.data)
  145. def test_path_in_tree_nested(self) -> None:
  146. r = MemoryRepo()
  147. # Create blobs
  148. b1 = Blob.from_string(b"Content 1")
  149. b2 = Blob.from_string(b"Content 2")
  150. # For nested trees, we need to create them manually
  151. # Create subtree
  152. subtree = Tree()
  153. subtree.add(b"file.txt", 0o100644, b1.id)
  154. r.object_store.add_object(b1)
  155. r.object_store.add_object(subtree)
  156. # Create main tree
  157. main_tree = Tree()
  158. main_tree.add(b"README", 0o100644, b2.id)
  159. main_tree.add(b"subdir", 0o040000, subtree.id)
  160. r.object_store.add_object(b2)
  161. r.object_store.add_object(main_tree)
  162. # Create commit with our tree
  163. c = Commit()
  164. c.tree = main_tree.id
  165. c.author = c.committer = b"Test User <test@example.com>"
  166. c.author_time = c.commit_time = 1234567890
  167. c.author_timezone = c.commit_timezone = 0
  168. c.message = b"Test commit"
  169. r.object_store.add_object(c)
  170. # Lookup nested path
  171. result = parse_object(r, c.id + b":subdir/file.txt")
  172. self.assertEqual(b"Content 1", result.data)
  173. def test_reflog_lookup(self) -> None:
  174. # Use a real repo for reflog testing
  175. import tempfile
  176. from dulwich.repo import Repo
  177. with tempfile.TemporaryDirectory() as tmpdir:
  178. r = Repo.init_bare(tmpdir)
  179. c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 2]])
  180. # Write reflog entries using the repo's _write_reflog method
  181. # These are written in chronological order (oldest first)
  182. r._write_reflog(
  183. b"HEAD",
  184. None,
  185. c1.id,
  186. b"Test User <test@example.com>",
  187. 1234567890,
  188. 0,
  189. b"commit: Initial commit",
  190. )
  191. r._write_reflog(
  192. b"HEAD",
  193. c1.id,
  194. c2.id,
  195. b"Test User <test@example.com>",
  196. 1234567891,
  197. 0,
  198. b"commit: Second commit",
  199. )
  200. r._write_reflog(
  201. b"HEAD",
  202. c2.id,
  203. c3.id,
  204. b"Test User <test@example.com>",
  205. 1234567892,
  206. 0,
  207. b"commit: Third commit",
  208. )
  209. # HEAD@{0} is the most recent (c3)
  210. self.assertEqual(c3, parse_object(r, b"HEAD@{0}"))
  211. # HEAD@{1} is the second most recent (c2)
  212. self.assertEqual(c2, parse_object(r, b"HEAD@{1}"))
  213. # HEAD@{2} is the third/oldest (c1)
  214. self.assertEqual(c1, parse_object(r, b"HEAD@{2}"))
  215. def test_reflog_time_lookup(self) -> None:
  216. # Use a real repo for reflog testing with time specifications
  217. import tempfile
  218. from dulwich.repo import Repo
  219. with tempfile.TemporaryDirectory() as tmpdir:
  220. r = Repo.init_bare(tmpdir)
  221. c1, c2, c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 2]])
  222. # Write reflog entries with specific timestamps
  223. # 1234567890 = 2009-02-13 23:31:30 UTC
  224. r._write_reflog(
  225. b"HEAD",
  226. None,
  227. c1.id,
  228. b"Test User <test@example.com>",
  229. 1234567890,
  230. 0,
  231. b"commit: Initial commit",
  232. )
  233. # 1234657890 = 2009-02-14 23:31:30 UTC (1 day + 1 second later)
  234. r._write_reflog(
  235. b"HEAD",
  236. c1.id,
  237. c2.id,
  238. b"Test User <test@example.com>",
  239. 1234657890,
  240. 0,
  241. b"commit: Second commit",
  242. )
  243. # 1235000000 = 2009-02-18 19:33:20 UTC
  244. r._write_reflog(
  245. b"HEAD",
  246. c2.id,
  247. c3.id,
  248. b"Test User <test@example.com>",
  249. 1235000000,
  250. 0,
  251. b"commit: Third commit",
  252. )
  253. # Lookup by timestamp - should get the most recent entry at or before time
  254. self.assertEqual(c1, parse_object(r, b"HEAD@{1234567890}"))
  255. self.assertEqual(c2, parse_object(r, b"HEAD@{1234657890}"))
  256. self.assertEqual(c3, parse_object(r, b"HEAD@{1235000000}"))
  257. # Future timestamp should get latest entry
  258. self.assertEqual(c3, parse_object(r, b"HEAD@{9999999999}"))
  259. def test_index_path_lookup_stage0(self) -> None:
  260. # Test index path lookup for stage 0 (normal files)
  261. import tempfile
  262. from dulwich.repo import Repo
  263. with tempfile.TemporaryDirectory() as tmpdir:
  264. r = Repo.init(tmpdir)
  265. # Create a blob and add it to the index
  266. b = Blob.from_string(b"Test content")
  267. r.object_store.add_object(b)
  268. # Add to index
  269. index = r.open_index()
  270. from dulwich.index import IndexEntry
  271. index[b"test.txt"] = IndexEntry(
  272. ctime=(0, 0),
  273. mtime=(0, 0),
  274. dev=0,
  275. ino=0,
  276. mode=0o100644,
  277. uid=0,
  278. gid=0,
  279. size=len(b.data),
  280. sha=b.id,
  281. )
  282. index.write()
  283. # Test :path syntax (defaults to stage 0)
  284. result = parse_object(r, b":test.txt")
  285. self.assertEqual(b"Test content", result.data)
  286. # Test :0:path syntax (explicit stage 0)
  287. result = parse_object(r, b":0:test.txt")
  288. self.assertEqual(b"Test content", result.data)
  289. def test_index_path_lookup_conflicts(self) -> None:
  290. # Test index path lookup with merge conflicts (stages 1-3)
  291. import tempfile
  292. from dulwich.index import ConflictedIndexEntry, IndexEntry
  293. from dulwich.repo import Repo
  294. with tempfile.TemporaryDirectory() as tmpdir:
  295. r = Repo.init(tmpdir)
  296. # Create three different versions of a file
  297. b_ancestor = Blob.from_string(b"Ancestor content")
  298. b_this = Blob.from_string(b"This content")
  299. b_other = Blob.from_string(b"Other content")
  300. r.object_store.add_object(b_ancestor)
  301. r.object_store.add_object(b_this)
  302. r.object_store.add_object(b_other)
  303. # Add conflicted entry to index
  304. index = r.open_index()
  305. index[b"conflict.txt"] = ConflictedIndexEntry(
  306. ancestor=IndexEntry(
  307. ctime=(0, 0),
  308. mtime=(0, 0),
  309. dev=0,
  310. ino=0,
  311. mode=0o100644,
  312. uid=0,
  313. gid=0,
  314. size=len(b_ancestor.data),
  315. sha=b_ancestor.id,
  316. ),
  317. this=IndexEntry(
  318. ctime=(0, 0),
  319. mtime=(0, 0),
  320. dev=0,
  321. ino=0,
  322. mode=0o100644,
  323. uid=0,
  324. gid=0,
  325. size=len(b_this.data),
  326. sha=b_this.id,
  327. ),
  328. other=IndexEntry(
  329. ctime=(0, 0),
  330. mtime=(0, 0),
  331. dev=0,
  332. ino=0,
  333. mode=0o100644,
  334. uid=0,
  335. gid=0,
  336. size=len(b_other.data),
  337. sha=b_other.id,
  338. ),
  339. )
  340. index.write()
  341. # Test stage 1 (ancestor)
  342. result = parse_object(r, b":1:conflict.txt")
  343. self.assertEqual(b"Ancestor content", result.data)
  344. # Test stage 2 (this)
  345. result = parse_object(r, b":2:conflict.txt")
  346. self.assertEqual(b"This content", result.data)
  347. # Test stage 3 (other)
  348. result = parse_object(r, b":3:conflict.txt")
  349. self.assertEqual(b"Other content", result.data)
  350. # Test that :conflict.txt raises an error for conflicted files
  351. self.assertRaises(ValueError, parse_object, r, b":conflict.txt")
  352. def test_index_path_not_found(self) -> None:
  353. # Test error when path not in index
  354. import tempfile
  355. from dulwich.repo import Repo
  356. with tempfile.TemporaryDirectory() as tmpdir:
  357. r = Repo.init(tmpdir)
  358. # Try to lookup non-existent path
  359. self.assertRaises(KeyError, parse_object, r, b":nonexistent.txt")
  360. class ParseCommitRangeTests(TestCase):
  361. """Test parse_commit_range."""
  362. def test_nonexistent(self) -> None:
  363. r = MemoryRepo()
  364. self.assertRaises(KeyError, parse_commit_range, r, "thisdoesnotexist..HEAD")
  365. def test_commit_by_sha(self) -> None:
  366. r = MemoryRepo()
  367. c1, _c2, _c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 1, 2]])
  368. self.assertIsNone(parse_commit_range(r, c1.id))
  369. def test_commit_range(self) -> None:
  370. r = MemoryRepo()
  371. c1, c2, _c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 1, 2]])
  372. result = parse_commit_range(r, f"{c1.id.decode()}..{c2.id.decode()}")
  373. self.assertIsNotNone(result)
  374. start_commit, end_commit = result
  375. self.assertEqual(c1, start_commit)
  376. self.assertEqual(c2, end_commit)
  377. class ParseCommitTests(TestCase):
  378. """Test parse_commit."""
  379. def test_nonexistent(self) -> None:
  380. r = MemoryRepo()
  381. self.assertRaises(KeyError, parse_commit, r, "thisdoesnotexist")
  382. def test_commit_by_sha(self) -> None:
  383. r = MemoryRepo()
  384. [c1] = build_commit_graph(r.object_store, [[1]])
  385. self.assertEqual(c1, parse_commit(r, c1.id))
  386. def test_commit_by_short_sha(self) -> None:
  387. r = MemoryRepo()
  388. [c1] = build_commit_graph(r.object_store, [[1]])
  389. self.assertEqual(c1, parse_commit(r, c1.id[:10]))
  390. def test_annotated_tag(self) -> None:
  391. r = MemoryRepo()
  392. [c1] = build_commit_graph(r.object_store, [[1]])
  393. # Create an annotated tag pointing to the commit
  394. tag = Tag()
  395. tag.name = b"v1.0"
  396. tag.message = b"Test tag"
  397. tag.tag_time = 1234567890
  398. tag.tag_timezone = 0
  399. tag.object = (Commit, c1.id)
  400. tag.tagger = b"Test Tagger <test@example.com>"
  401. r.object_store.add_object(tag)
  402. # parse_commit should follow the tag to the commit
  403. self.assertEqual(c1, parse_commit(r, tag.id))
  404. def test_nested_tags(self) -> None:
  405. r = MemoryRepo()
  406. [c1] = build_commit_graph(r.object_store, [[1]])
  407. # Create an annotated tag pointing to the commit
  408. tag1 = Tag()
  409. tag1.name = b"v1.0"
  410. tag1.message = b"Test tag"
  411. tag1.tag_time = 1234567890
  412. tag1.tag_timezone = 0
  413. tag1.object = (Commit, c1.id)
  414. tag1.tagger = b"Test Tagger <test@example.com>"
  415. r.object_store.add_object(tag1)
  416. # Create another tag pointing to the first tag
  417. tag2 = Tag()
  418. tag2.name = b"v1.0-release"
  419. tag2.message = b"Release tag"
  420. tag2.tag_time = 1234567900
  421. tag2.tag_timezone = 0
  422. tag2.object = (Tag, tag1.id)
  423. tag2.tagger = b"Test Tagger <test@example.com>"
  424. r.object_store.add_object(tag2)
  425. # parse_commit should follow both tags to the commit
  426. self.assertEqual(c1, parse_commit(r, tag2.id))
  427. def test_tag_to_missing_commit(self) -> None:
  428. r = MemoryRepo()
  429. # Create a tag pointing to a non-existent commit
  430. missing_sha = b"1234567890123456789012345678901234567890"
  431. tag = Tag()
  432. tag.name = b"v1.0"
  433. tag.message = b"Test tag"
  434. tag.tag_time = 1234567890
  435. tag.tag_timezone = 0
  436. tag.object = (Commit, missing_sha)
  437. tag.tagger = b"Test Tagger <test@example.com>"
  438. r.object_store.add_object(tag)
  439. # Should raise KeyError for missing commit
  440. self.assertRaises(KeyError, parse_commit, r, tag.id)
  441. def test_tag_to_blob(self) -> None:
  442. r = MemoryRepo()
  443. # Create a blob
  444. blob = Blob.from_string(b"Test content")
  445. r.object_store.add_object(blob)
  446. # Create a tag pointing to the blob
  447. tag = Tag()
  448. tag.name = b"blob-tag"
  449. tag.message = b"Tag pointing to blob"
  450. tag.tag_time = 1234567890
  451. tag.tag_timezone = 0
  452. tag.object = (Blob, blob.id)
  453. tag.tagger = b"Test Tagger <test@example.com>"
  454. r.object_store.add_object(tag)
  455. # Should raise ValueError as it's not a commit
  456. self.assertRaises(ValueError, parse_commit, r, tag.id)
  457. def test_commit_object(self) -> None:
  458. r = MemoryRepo()
  459. [c1] = build_commit_graph(r.object_store, [[1]])
  460. # Test that passing a Commit object directly returns the same object
  461. self.assertEqual(c1, parse_commit(r, c1))
  462. class ParseRefTests(TestCase):
  463. def test_nonexistent(self) -> None:
  464. r = {}
  465. self.assertRaises(KeyError, parse_ref, r, b"thisdoesnotexist")
  466. def test_ambiguous_ref(self) -> None:
  467. r = {
  468. b"ambig1": "bla",
  469. b"refs/ambig1": "bla",
  470. b"refs/tags/ambig1": "bla",
  471. b"refs/heads/ambig1": "bla",
  472. b"refs/remotes/ambig1": "bla",
  473. b"refs/remotes/ambig1/HEAD": "bla",
  474. }
  475. self.assertEqual(b"ambig1", parse_ref(r, b"ambig1"))
  476. def test_ambiguous_ref2(self) -> None:
  477. r = {
  478. b"refs/ambig2": "bla",
  479. b"refs/tags/ambig2": "bla",
  480. b"refs/heads/ambig2": "bla",
  481. b"refs/remotes/ambig2": "bla",
  482. b"refs/remotes/ambig2/HEAD": "bla",
  483. }
  484. self.assertEqual(b"refs/ambig2", parse_ref(r, b"ambig2"))
  485. def test_ambiguous_tag(self) -> None:
  486. r = {
  487. b"refs/tags/ambig3": "bla",
  488. b"refs/heads/ambig3": "bla",
  489. b"refs/remotes/ambig3": "bla",
  490. b"refs/remotes/ambig3/HEAD": "bla",
  491. }
  492. self.assertEqual(b"refs/tags/ambig3", parse_ref(r, b"ambig3"))
  493. def test_ambiguous_head(self) -> None:
  494. r = {
  495. b"refs/heads/ambig4": "bla",
  496. b"refs/remotes/ambig4": "bla",
  497. b"refs/remotes/ambig4/HEAD": "bla",
  498. }
  499. self.assertEqual(b"refs/heads/ambig4", parse_ref(r, b"ambig4"))
  500. def test_ambiguous_remote(self) -> None:
  501. r = {b"refs/remotes/ambig5": "bla", b"refs/remotes/ambig5/HEAD": "bla"}
  502. self.assertEqual(b"refs/remotes/ambig5", parse_ref(r, b"ambig5"))
  503. def test_ambiguous_remote_head(self) -> None:
  504. r = {b"refs/remotes/ambig6/HEAD": "bla"}
  505. self.assertEqual(b"refs/remotes/ambig6/HEAD", parse_ref(r, b"ambig6"))
  506. def test_heads_full(self) -> None:
  507. r = {b"refs/heads/foo": "bla"}
  508. self.assertEqual(b"refs/heads/foo", parse_ref(r, b"refs/heads/foo"))
  509. def test_heads_partial(self) -> None:
  510. r = {b"refs/heads/foo": "bla"}
  511. self.assertEqual(b"refs/heads/foo", parse_ref(r, b"heads/foo"))
  512. def test_tags_partial(self) -> None:
  513. r = {b"refs/tags/foo": "bla"}
  514. self.assertEqual(b"refs/tags/foo", parse_ref(r, b"tags/foo"))
  515. class ParseRefsTests(TestCase):
  516. def test_nonexistent(self) -> None:
  517. r = {}
  518. self.assertRaises(KeyError, parse_refs, r, [b"thisdoesnotexist"])
  519. def test_head(self) -> None:
  520. r = {b"refs/heads/foo": "bla"}
  521. self.assertEqual([b"refs/heads/foo"], parse_refs(r, [b"foo"]))
  522. def test_full(self) -> None:
  523. r = {b"refs/heads/foo": "bla"}
  524. self.assertEqual([b"refs/heads/foo"], parse_refs(r, b"refs/heads/foo"))
  525. class ParseReftupleTests(TestCase):
  526. def test_nonexistent(self) -> None:
  527. r = {}
  528. self.assertRaises(KeyError, parse_reftuple, r, r, b"thisdoesnotexist")
  529. def test_head(self) -> None:
  530. r = {b"refs/heads/foo": "bla"}
  531. self.assertEqual(
  532. (b"refs/heads/foo", b"refs/heads/foo", False),
  533. parse_reftuple(r, r, b"foo"),
  534. )
  535. self.assertEqual(
  536. (b"refs/heads/foo", b"refs/heads/foo", True),
  537. parse_reftuple(r, r, b"+foo"),
  538. )
  539. self.assertEqual(
  540. (b"refs/heads/foo", b"refs/heads/foo", True),
  541. parse_reftuple(r, {}, b"+foo"),
  542. )
  543. self.assertEqual(
  544. (b"refs/heads/foo", b"refs/heads/foo", True),
  545. parse_reftuple(r, {}, b"foo", True),
  546. )
  547. def test_full(self) -> None:
  548. r = {b"refs/heads/foo": "bla"}
  549. self.assertEqual(
  550. (b"refs/heads/foo", b"refs/heads/foo", False),
  551. parse_reftuple(r, r, b"refs/heads/foo"),
  552. )
  553. def test_no_left_ref(self) -> None:
  554. r = {b"refs/heads/foo": "bla"}
  555. self.assertEqual(
  556. (None, b"refs/heads/foo", False),
  557. parse_reftuple(r, r, b":refs/heads/foo"),
  558. )
  559. def test_no_right_ref(self) -> None:
  560. r = {b"refs/heads/foo": "bla"}
  561. self.assertEqual(
  562. (b"refs/heads/foo", None, False),
  563. parse_reftuple(r, r, b"refs/heads/foo:"),
  564. )
  565. def test_default_with_string(self) -> None:
  566. r = {b"refs/heads/foo": "bla"}
  567. self.assertEqual(
  568. (b"refs/heads/foo", b"refs/heads/foo", False),
  569. parse_reftuple(r, r, "foo"),
  570. )
  571. class ParseReftuplesTests(TestCase):
  572. def test_nonexistent(self) -> None:
  573. r = {}
  574. self.assertRaises(KeyError, parse_reftuples, r, r, [b"thisdoesnotexist"])
  575. def test_head(self) -> None:
  576. r = {b"refs/heads/foo": "bla"}
  577. self.assertEqual(
  578. [(b"refs/heads/foo", b"refs/heads/foo", False)],
  579. parse_reftuples(r, r, [b"foo"]),
  580. )
  581. def test_full(self) -> None:
  582. r = {b"refs/heads/foo": "bla"}
  583. self.assertEqual(
  584. [(b"refs/heads/foo", b"refs/heads/foo", False)],
  585. parse_reftuples(r, r, b"refs/heads/foo"),
  586. )
  587. r = {b"refs/heads/foo": "bla"}
  588. self.assertEqual(
  589. [(b"refs/heads/foo", b"refs/heads/foo", True)],
  590. parse_reftuples(r, r, b"refs/heads/foo", True),
  591. )
  592. class ParseTreeTests(TestCase):
  593. """Test parse_tree."""
  594. def test_nonexistent(self) -> None:
  595. r = MemoryRepo()
  596. self.assertRaises(KeyError, parse_tree, r, "thisdoesnotexist")
  597. def test_from_commit(self) -> None:
  598. r = MemoryRepo()
  599. c1, _c2, _c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 1, 2]])
  600. self.assertEqual(r[c1.tree], parse_tree(r, c1.id))
  601. self.assertEqual(r[c1.tree], parse_tree(r, c1.tree))
  602. def test_from_ref(self) -> None:
  603. r = MemoryRepo()
  604. c1, _c2, _c3 = build_commit_graph(r.object_store, [[1], [2, 1], [3, 1, 2]])
  605. r.refs[b"refs/heads/foo"] = c1.id
  606. self.assertEqual(r[c1.tree], parse_tree(r, b"foo"))
  607. def test_tree_object(self) -> None:
  608. r = MemoryRepo()
  609. [c1] = build_commit_graph(r.object_store, [[1]])
  610. tree = r[c1.tree]
  611. # Test that passing a Tree object directly returns the same object
  612. self.assertEqual(tree, parse_tree(r, tree))
  613. def test_commit_object(self) -> None:
  614. r = MemoryRepo()
  615. [c1] = build_commit_graph(r.object_store, [[1]])
  616. # Test that passing a Commit object returns its tree
  617. self.assertEqual(r[c1.tree], parse_tree(r, c1))
  618. def test_tag_object(self) -> None:
  619. r = MemoryRepo()
  620. [c1] = build_commit_graph(r.object_store, [[1]])
  621. # Create an annotated tag pointing to the commit
  622. tag = Tag()
  623. tag.name = b"v1.0"
  624. tag.message = b"Test tag"
  625. tag.tag_time = 1234567890
  626. tag.tag_timezone = 0
  627. tag.object = (Commit, c1.id)
  628. tag.tagger = b"Test Tagger <test@example.com>"
  629. r.object_store.add_object(tag)
  630. # parse_tree should follow the tag to the commit's tree
  631. self.assertEqual(r[c1.tree], parse_tree(r, tag))