test_bundle.py 16 KB

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