test_bundle.py 20 KB

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