2
0

test_bundle.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. # test_bundle.py -- tests for bundle
  2. # Copyright (C) 2020 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 bundle support."""
  22. import os
  23. import tempfile
  24. from io import BytesIO
  25. from dulwich.bundle import Bundle, create_bundle_from_repo, read_bundle, write_bundle
  26. from dulwich.object_format import DEFAULT_OBJECT_FORMAT
  27. from dulwich.objects import Blob, Commit, Tree
  28. from dulwich.pack import PackData, write_pack_objects
  29. from dulwich.repo import MemoryRepo
  30. from . import TestCase
  31. class BundleTests(TestCase):
  32. def setUp(self):
  33. super().setUp()
  34. self.tempdir = tempfile.mkdtemp()
  35. self.addCleanup(os.rmdir, self.tempdir)
  36. def test_bundle_repr(self) -> None:
  37. """Test the Bundle.__repr__ method."""
  38. bundle = Bundle()
  39. bundle.version = 3
  40. bundle.capabilities = {"foo": "bar"}
  41. bundle.prerequisites = [(b"cc" * 20, "comment")]
  42. bundle.references = {b"refs/heads/master": b"ab" * 20}
  43. # Create a simple pack data
  44. b = BytesIO()
  45. write_pack_objects(b.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  46. b.seek(0)
  47. bundle.pack_data = PackData.from_file(b, object_format=DEFAULT_OBJECT_FORMAT)
  48. # Check the repr output
  49. rep = repr(bundle)
  50. self.assertIn("Bundle(version=3", rep)
  51. self.assertIn("capabilities={'foo': 'bar'}", rep)
  52. self.assertIn("prerequisites=[(", rep)
  53. self.assertIn("references={", rep)
  54. def test_bundle_equality(self) -> None:
  55. """Test the Bundle.__eq__ method."""
  56. # Create two identical bundles
  57. bundle1 = Bundle()
  58. bundle1.version = 3
  59. bundle1.capabilities = {"foo": "bar"}
  60. bundle1.prerequisites = [(b"cc" * 20, "comment")]
  61. bundle1.references = {b"refs/heads/master": b"ab" * 20}
  62. b1 = BytesIO()
  63. write_pack_objects(b1.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  64. b1.seek(0)
  65. bundle1.pack_data = PackData.from_file(b1, object_format=DEFAULT_OBJECT_FORMAT)
  66. self.addCleanup(bundle1.pack_data.close)
  67. bundle2 = Bundle()
  68. bundle2.version = 3
  69. bundle2.capabilities = {"foo": "bar"}
  70. bundle2.prerequisites = [(b"cc" * 20, "comment")]
  71. bundle2.references = {b"refs/heads/master": b"ab" * 20}
  72. b2 = BytesIO()
  73. write_pack_objects(b2.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  74. b2.seek(0)
  75. bundle2.pack_data = PackData.from_file(b2, object_format=DEFAULT_OBJECT_FORMAT)
  76. self.addCleanup(bundle2.pack_data.close)
  77. # Test equality
  78. self.assertEqual(bundle1, bundle2)
  79. # Test inequality by changing different attributes
  80. bundle3 = Bundle()
  81. bundle3.version = 2 # Different version
  82. bundle3.capabilities = {"foo": "bar"}
  83. bundle3.prerequisites = [(b"cc" * 20, "comment")]
  84. bundle3.references = {b"refs/heads/master": b"ab" * 20}
  85. b3 = BytesIO()
  86. write_pack_objects(b3.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  87. b3.seek(0)
  88. bundle3.pack_data = PackData.from_file(b3, object_format=DEFAULT_OBJECT_FORMAT)
  89. self.addCleanup(bundle3.pack_data.close)
  90. self.assertNotEqual(bundle1, bundle3)
  91. bundle4 = Bundle()
  92. bundle4.version = 3
  93. bundle4.capabilities = {"different": "value"} # Different capabilities
  94. bundle4.prerequisites = [(b"cc" * 20, "comment")]
  95. bundle4.references = {b"refs/heads/master": b"ab" * 20}
  96. b4 = BytesIO()
  97. write_pack_objects(b4.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  98. b4.seek(0)
  99. bundle4.pack_data = PackData.from_file(b4, object_format=DEFAULT_OBJECT_FORMAT)
  100. self.addCleanup(bundle4.pack_data.close)
  101. self.assertNotEqual(bundle1, bundle4)
  102. bundle5 = Bundle()
  103. bundle5.version = 3
  104. bundle5.capabilities = {"foo": "bar"}
  105. bundle5.prerequisites = [(b"dd" * 20, "different")] # Different prerequisites
  106. bundle5.references = {b"refs/heads/master": b"ab" * 20}
  107. b5 = BytesIO()
  108. write_pack_objects(b5.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  109. b5.seek(0)
  110. bundle5.pack_data = PackData.from_file(b5, object_format=DEFAULT_OBJECT_FORMAT)
  111. self.addCleanup(bundle5.pack_data.close)
  112. self.assertNotEqual(bundle1, bundle5)
  113. bundle6 = Bundle()
  114. bundle6.version = 3
  115. bundle6.capabilities = {"foo": "bar"}
  116. bundle6.prerequisites = [(b"cc" * 20, "comment")]
  117. bundle6.references = {
  118. b"refs/heads/different": b"ab" * 20
  119. } # Different references
  120. b6 = BytesIO()
  121. write_pack_objects(b6.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  122. b6.seek(0)
  123. bundle6.pack_data = PackData.from_file(b6, object_format=DEFAULT_OBJECT_FORMAT)
  124. self.addCleanup(bundle6.pack_data.close)
  125. self.assertNotEqual(bundle1, bundle6)
  126. # Test inequality with different type
  127. self.assertNotEqual(bundle1, "not a bundle")
  128. def test_read_bundle_v2(self) -> None:
  129. """Test reading a v2 bundle."""
  130. f = BytesIO()
  131. f.write(b"# v2 git bundle\n")
  132. f.write(b"-" + b"cc" * 20 + b" prerequisite comment\n")
  133. f.write(b"ab" * 20 + b" refs/heads/master\n")
  134. f.write(b"\n")
  135. # Add pack data
  136. b = BytesIO()
  137. write_pack_objects(b.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  138. f.write(b.getvalue())
  139. f.seek(0)
  140. bundle = read_bundle(f)
  141. self.assertEqual(2, bundle.version)
  142. self.assertEqual({}, bundle.capabilities)
  143. self.assertEqual([(b"cc" * 20, b"prerequisite comment")], bundle.prerequisites)
  144. self.assertEqual({b"refs/heads/master": b"ab" * 20}, bundle.references)
  145. def test_read_bundle_v3(self) -> None:
  146. """Test reading a v3 bundle with capabilities."""
  147. f = BytesIO()
  148. f.write(b"# v3 git bundle\n")
  149. f.write(b"@capability1\n")
  150. f.write(b"@capability2=value2\n")
  151. f.write(b"-" + b"cc" * 20 + b" prerequisite comment\n")
  152. f.write(b"ab" * 20 + b" refs/heads/master\n")
  153. f.write(b"\n")
  154. # Add pack data
  155. b = BytesIO()
  156. write_pack_objects(b.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  157. f.write(b.getvalue())
  158. f.seek(0)
  159. bundle = read_bundle(f)
  160. self.assertEqual(3, bundle.version)
  161. self.assertEqual(
  162. {"capability1": None, "capability2": "value2"}, bundle.capabilities
  163. )
  164. self.assertEqual([(b"cc" * 20, b"prerequisite comment")], bundle.prerequisites)
  165. self.assertEqual({b"refs/heads/master": b"ab" * 20}, bundle.references)
  166. def test_read_bundle_invalid_format(self) -> None:
  167. """Test reading a bundle with invalid format."""
  168. f = BytesIO()
  169. f.write(b"invalid bundle format\n")
  170. f.seek(0)
  171. with self.assertRaises(AssertionError):
  172. read_bundle(f)
  173. def test_write_bundle_v2(self) -> None:
  174. """Test writing a v2 bundle."""
  175. bundle = Bundle()
  176. bundle.version = 2
  177. bundle.capabilities = {}
  178. bundle.prerequisites = [(b"cc" * 20, b"prerequisite comment")]
  179. bundle.references = {b"refs/heads/master": b"ab" * 20}
  180. # Create a simple pack data
  181. b = BytesIO()
  182. write_pack_objects(b.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  183. b.seek(0)
  184. bundle.pack_data = PackData.from_file(b, object_format=DEFAULT_OBJECT_FORMAT)
  185. # Write the bundle
  186. f = BytesIO()
  187. write_bundle(f, bundle)
  188. f.seek(0)
  189. # Verify the written content
  190. self.assertEqual(b"# v2 git bundle\n", f.readline())
  191. self.assertEqual(b"-" + b"cc" * 20 + b" prerequisite comment\n", f.readline())
  192. self.assertEqual(b"ab" * 20 + b" refs/heads/master\n", f.readline())
  193. self.assertEqual(b"\n", f.readline())
  194. # The rest is pack data which we don't validate in detail
  195. def test_write_bundle_v3(self) -> None:
  196. """Test writing a v3 bundle with capabilities."""
  197. bundle = Bundle()
  198. bundle.version = 3
  199. bundle.capabilities = {"capability1": None, "capability2": "value2"}
  200. bundle.prerequisites = [(b"cc" * 20, b"prerequisite comment")]
  201. bundle.references = {b"refs/heads/master": b"ab" * 20}
  202. # Create a simple pack data
  203. b = BytesIO()
  204. write_pack_objects(b.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  205. b.seek(0)
  206. bundle.pack_data = PackData.from_file(b, object_format=DEFAULT_OBJECT_FORMAT)
  207. # Write the bundle
  208. f = BytesIO()
  209. write_bundle(f, bundle)
  210. f.seek(0)
  211. # Verify the written content
  212. self.assertEqual(b"# v3 git bundle\n", f.readline())
  213. self.assertEqual(b"@capability1\n", f.readline())
  214. self.assertEqual(b"@capability2=value2\n", f.readline())
  215. self.assertEqual(b"-" + b"cc" * 20 + b" prerequisite comment\n", f.readline())
  216. self.assertEqual(b"ab" * 20 + b" refs/heads/master\n", f.readline())
  217. self.assertEqual(b"\n", f.readline())
  218. # The rest is pack data which we don't validate in detail
  219. def test_write_bundle_auto_version(self) -> None:
  220. """Test writing a bundle with auto-detected version."""
  221. # Create a bundle with no explicit version but capabilities
  222. bundle1 = Bundle()
  223. bundle1.version = None
  224. bundle1.capabilities = {"capability1": "value1"}
  225. bundle1.prerequisites = [(b"cc" * 20, b"prerequisite comment")]
  226. bundle1.references = {b"refs/heads/master": b"ab" * 20}
  227. b1 = BytesIO()
  228. write_pack_objects(b1.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  229. b1.seek(0)
  230. bundle1.pack_data = PackData.from_file(b1, object_format=DEFAULT_OBJECT_FORMAT)
  231. f1 = BytesIO()
  232. write_bundle(f1, bundle1)
  233. f1.seek(0)
  234. # Should use v3 format since capabilities are present
  235. self.assertEqual(b"# v3 git bundle\n", f1.readline())
  236. # Create a bundle with no explicit version and no capabilities
  237. bundle2 = Bundle()
  238. bundle2.version = None
  239. bundle2.capabilities = {}
  240. bundle2.prerequisites = [(b"cc" * 20, b"prerequisite comment")]
  241. bundle2.references = {b"refs/heads/master": b"ab" * 20}
  242. b2 = BytesIO()
  243. write_pack_objects(b2.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  244. b2.seek(0)
  245. bundle2.pack_data = PackData.from_file(b2, object_format=DEFAULT_OBJECT_FORMAT)
  246. f2 = BytesIO()
  247. write_bundle(f2, bundle2)
  248. f2.seek(0)
  249. # Should use v2 format since no capabilities are present
  250. self.assertEqual(b"# v2 git bundle\n", f2.readline())
  251. def test_write_bundle_invalid_version(self) -> None:
  252. """Test writing a bundle with an invalid version."""
  253. bundle = Bundle()
  254. bundle.version = 4 # Invalid version
  255. bundle.capabilities = {}
  256. bundle.prerequisites = []
  257. bundle.references = {}
  258. b = BytesIO()
  259. write_pack_objects(b.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  260. b.seek(0)
  261. bundle.pack_data = PackData.from_file(b, object_format=DEFAULT_OBJECT_FORMAT)
  262. f = BytesIO()
  263. with self.assertRaises(AssertionError):
  264. write_bundle(f, bundle)
  265. def test_roundtrip_bundle(self) -> None:
  266. origbundle = Bundle()
  267. origbundle.version = 3
  268. origbundle.capabilities = {"foo": None}
  269. origbundle.references = {b"refs/heads/master": b"ab" * 20}
  270. origbundle.prerequisites = [(b"cc" * 20, b"comment")]
  271. b = BytesIO()
  272. write_pack_objects(b.write, [], object_format=DEFAULT_OBJECT_FORMAT)
  273. b.seek(0)
  274. origbundle.pack_data = PackData.from_file(
  275. b, object_format=DEFAULT_OBJECT_FORMAT
  276. )
  277. with tempfile.TemporaryDirectory() as td:
  278. with open(os.path.join(td, "foo"), "wb") as f:
  279. write_bundle(f, origbundle)
  280. with open(os.path.join(td, "foo"), "rb") as f:
  281. newbundle = read_bundle(f)
  282. self.assertEqual(origbundle, newbundle)
  283. def test_create_bundle_from_repo(self) -> None:
  284. """Test creating a bundle from a repository."""
  285. # Create a simple repository
  286. repo = MemoryRepo()
  287. self.addCleanup(repo.close)
  288. # Create a blob
  289. blob = Blob.from_string(b"Hello world")
  290. repo.object_store.add_object(blob)
  291. # Create a tree
  292. tree = Tree()
  293. tree.add(b"hello.txt", 0o100644, blob.id)
  294. repo.object_store.add_object(tree)
  295. # Create a commit
  296. commit = Commit()
  297. commit.tree = tree.id
  298. commit.message = b"Initial commit"
  299. commit.author = commit.committer = b"Test User <test@example.com>"
  300. commit.commit_time = commit.author_time = 1234567890
  301. commit.commit_timezone = commit.author_timezone = 0
  302. repo.object_store.add_object(commit)
  303. # Add a reference
  304. repo.refs[b"refs/heads/master"] = commit.id
  305. # Create bundle from repository
  306. bundle = create_bundle_from_repo(repo)
  307. # Verify bundle contents
  308. self.assertEqual(bundle.references, {b"refs/heads/master": commit.id})
  309. self.assertEqual(bundle.prerequisites, [])
  310. self.assertEqual(bundle.capabilities, {})
  311. self.assertIsNotNone(bundle.pack_data)
  312. # Verify the bundle contains the right objects
  313. objects = list(bundle.pack_data.iter_unpacked())
  314. object_ids = {obj.sha().hex().encode("ascii") for obj in objects}
  315. self.assertIn(blob.id, object_ids)
  316. self.assertIn(tree.id, object_ids)
  317. self.assertIn(commit.id, object_ids)
  318. def test_create_bundle_with_prerequisites(self) -> None:
  319. """Test creating a bundle with prerequisites."""
  320. repo = MemoryRepo()
  321. # Create some objects
  322. blob = Blob.from_string(b"Hello world")
  323. repo.object_store.add_object(blob)
  324. tree = Tree()
  325. tree.add(b"hello.txt", 0o100644, blob.id)
  326. repo.object_store.add_object(tree)
  327. commit = Commit()
  328. commit.tree = tree.id
  329. commit.message = b"Initial commit"
  330. commit.author = commit.committer = b"Test User <test@example.com>"
  331. commit.commit_time = commit.author_time = 1234567890
  332. commit.commit_timezone = commit.author_timezone = 0
  333. repo.object_store.add_object(commit)
  334. repo.refs[b"refs/heads/master"] = commit.id
  335. # Create bundle with prerequisites
  336. prereq_id = b"aa" * 20 # hex string like other object ids
  337. bundle = create_bundle_from_repo(repo, prerequisites=[prereq_id])
  338. # Verify prerequisites are included
  339. self.assertEqual(len(bundle.prerequisites), 1)
  340. self.assertEqual(bundle.prerequisites[0][0], prereq_id)
  341. def test_create_bundle_with_specific_refs(self) -> None:
  342. """Test creating a bundle with specific refs."""
  343. repo = MemoryRepo()
  344. # Create objects and refs
  345. blob = Blob.from_string(b"Hello world")
  346. repo.object_store.add_object(blob)
  347. tree = Tree()
  348. tree.add(b"hello.txt", 0o100644, blob.id)
  349. repo.object_store.add_object(tree)
  350. commit = Commit()
  351. commit.tree = tree.id
  352. commit.message = b"Initial commit"
  353. commit.author = commit.committer = b"Test User <test@example.com>"
  354. commit.commit_time = commit.author_time = 1234567890
  355. commit.commit_timezone = commit.author_timezone = 0
  356. repo.object_store.add_object(commit)
  357. repo.refs[b"refs/heads/master"] = commit.id
  358. repo.refs[b"refs/heads/feature"] = commit.id
  359. # Create bundle with only master ref
  360. from dulwich.refs import Ref
  361. bundle = create_bundle_from_repo(repo, refs=[Ref(b"refs/heads/master")])
  362. # Verify only master ref is included
  363. self.assertEqual(len(bundle.references), 1)
  364. self.assertIn(b"refs/heads/master", bundle.references)
  365. self.assertNotIn(b"refs/heads/feature", bundle.references)
  366. def test_create_bundle_with_capabilities(self) -> None:
  367. """Test creating a bundle with capabilities."""
  368. repo = MemoryRepo()
  369. # Create minimal objects
  370. blob = Blob.from_string(b"Hello world")
  371. repo.object_store.add_object(blob)
  372. tree = Tree()
  373. tree.add(b"hello.txt", 0o100644, blob.id)
  374. repo.object_store.add_object(tree)
  375. commit = Commit()
  376. commit.tree = tree.id
  377. commit.message = b"Initial commit"
  378. commit.author = commit.committer = b"Test User <test@example.com>"
  379. commit.commit_time = commit.author_time = 1234567890
  380. commit.commit_timezone = commit.author_timezone = 0
  381. repo.object_store.add_object(commit)
  382. repo.refs[b"refs/heads/master"] = commit.id
  383. # Create bundle with capabilities
  384. capabilities = {"object-format": "sha1"}
  385. bundle = create_bundle_from_repo(repo, capabilities=capabilities, version=3)
  386. # Verify capabilities are included
  387. self.assertEqual(bundle.capabilities, capabilities)
  388. self.assertEqual(bundle.version, 3)
  389. def test_create_bundle_with_hex_bytestring_prerequisite(self) -> None:
  390. """Test creating a bundle with prerequisite as 40-byte hex bytestring."""
  391. repo = MemoryRepo()
  392. # Create minimal objects
  393. blob = Blob.from_string(b"Hello world")
  394. repo.object_store.add_object(blob)
  395. tree = Tree()
  396. tree.add(b"hello.txt", 0o100644, blob.id)
  397. repo.object_store.add_object(tree)
  398. commit = Commit()
  399. commit.tree = tree.id
  400. commit.message = b"Initial commit"
  401. commit.author = commit.committer = b"Test User <test@example.com>"
  402. commit.commit_time = commit.author_time = 1234567890
  403. commit.commit_timezone = commit.author_timezone = 0
  404. repo.object_store.add_object(commit)
  405. repo.refs[b"refs/heads/master"] = commit.id
  406. # Create another blob to use as prerequisite
  407. prereq_blob = Blob.from_string(b"prerequisite")
  408. # Use blob.id directly (40-byte hex bytestring)
  409. bundle = create_bundle_from_repo(repo, prerequisites=[prereq_blob.id])
  410. # Verify the prerequisite was added correctly
  411. self.assertEqual(len(bundle.prerequisites), 1)
  412. self.assertEqual(bundle.prerequisites[0][0], prereq_blob.id)
  413. def test_create_bundle_with_hex_bytestring_prerequisite_simple(self) -> None:
  414. """Test creating a bundle with prerequisite as 40-byte hex bytestring."""
  415. repo = MemoryRepo()
  416. # Create minimal objects
  417. blob = Blob.from_string(b"Hello world")
  418. repo.object_store.add_object(blob)
  419. tree = Tree()
  420. tree.add(b"hello.txt", 0o100644, blob.id)
  421. repo.object_store.add_object(tree)
  422. commit = Commit()
  423. commit.tree = tree.id
  424. commit.message = b"Initial commit"
  425. commit.author = commit.committer = b"Test User <test@example.com>"
  426. commit.commit_time = commit.author_time = 1234567890
  427. commit.commit_timezone = commit.author_timezone = 0
  428. repo.object_store.add_object(commit)
  429. repo.refs[b"refs/heads/master"] = commit.id
  430. # Use a 40-byte hex bytestring as prerequisite
  431. prereq_hex = b"aa" * 20
  432. bundle = create_bundle_from_repo(repo, prerequisites=[prereq_hex])
  433. # Verify the prerequisite was added correctly
  434. self.assertEqual(len(bundle.prerequisites), 1)
  435. self.assertEqual(bundle.prerequisites[0][0], prereq_hex)