test_bundle.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. # test_bundle.py -- test bundle compatibility with CGit
  2. # Copyright (C) 2025 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 compatibility with CGit."""
  22. import os
  23. import tempfile
  24. from dulwich.bundle import create_bundle_from_repo, read_bundle, write_bundle
  25. from dulwich.objects import Commit, Tree
  26. from dulwich.repo import Repo
  27. from .utils import CompatTestCase, rmtree_ro, run_git_or_fail
  28. class CompatBundleTestCase(CompatTestCase):
  29. def setUp(self) -> None:
  30. super().setUp()
  31. self.test_dir = tempfile.mkdtemp()
  32. self.addCleanup(rmtree_ro, self.test_dir)
  33. self.repo_path = os.path.join(self.test_dir, "repo")
  34. self.repo = Repo.init(self.repo_path, mkdir=True)
  35. self.addCleanup(self.repo.close)
  36. def test_create_bundle_git_compat(self) -> None:
  37. """Test creating a bundle that git can read."""
  38. # Create a commit
  39. commit = Commit()
  40. commit.committer = commit.author = b"Test User <test@example.com>"
  41. commit.commit_time = commit.author_time = 1234567890
  42. commit.commit_timezone = commit.author_timezone = 0
  43. commit.message = b"Test commit"
  44. tree = Tree()
  45. self.repo.object_store.add_object(tree)
  46. commit.tree = tree.id
  47. self.repo.object_store.add_object(commit)
  48. # Update ref
  49. self.repo.refs[b"refs/heads/master"] = commit.id
  50. # Create bundle using dulwich
  51. bundle_path = os.path.join(self.test_dir, "test.bundle")
  52. # Use create_bundle_from_repo helper
  53. bundle = create_bundle_from_repo(self.repo)
  54. self.addCleanup(bundle.close)
  55. with open(bundle_path, "wb") as f:
  56. write_bundle(f, bundle)
  57. # Verify git can read the bundle (must run from a repo directory)
  58. output = run_git_or_fail(["bundle", "verify", bundle_path], cwd=self.repo_path)
  59. self.assertIn(b"The bundle contains", output)
  60. self.assertIn(b"refs/heads/master", output)
  61. def test_read_git_bundle(self) -> None:
  62. """Test reading a bundle created by git."""
  63. # Create a commit using git
  64. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  65. run_git_or_fail(
  66. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  67. )
  68. # Create a file and commit
  69. test_file = os.path.join(self.repo_path, "test.txt")
  70. with open(test_file, "w") as f:
  71. f.write("test content\n")
  72. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  73. run_git_or_fail(["commit", "-m", "Test commit"], cwd=self.repo_path)
  74. # Create bundle using git
  75. bundle_path = os.path.join(self.test_dir, "git.bundle")
  76. run_git_or_fail(["bundle", "create", bundle_path, "HEAD"], cwd=self.repo_path)
  77. # Read bundle using dulwich
  78. with open(bundle_path, "rb") as f:
  79. bundle = read_bundle(f)
  80. self.addCleanup(bundle.close)
  81. # Verify bundle contents
  82. self.assertEqual(2, bundle.version)
  83. self.assertIn(b"HEAD", bundle.references)
  84. self.assertEqual({}, bundle.capabilities)
  85. self.assertEqual([], bundle.prerequisites)
  86. def test_read_git_bundle_multiple_refs(self) -> None:
  87. """Test reading a bundle with multiple references created by git."""
  88. # Create commits and branches using git
  89. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  90. run_git_or_fail(
  91. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  92. )
  93. # Create initial commit
  94. test_file = os.path.join(self.repo_path, "test.txt")
  95. with open(test_file, "w") as f:
  96. f.write("initial content\n")
  97. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  98. run_git_or_fail(["commit", "-m", "Initial commit"], cwd=self.repo_path)
  99. # Create feature branch
  100. run_git_or_fail(["checkout", "-b", "feature"], cwd=self.repo_path)
  101. with open(test_file, "w") as f:
  102. f.write("feature content\n")
  103. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  104. run_git_or_fail(["commit", "-m", "Feature commit"], cwd=self.repo_path)
  105. # Create another branch
  106. run_git_or_fail(["checkout", "-b", "develop", "master"], cwd=self.repo_path)
  107. dev_file = os.path.join(self.repo_path, "dev.txt")
  108. with open(dev_file, "w") as f:
  109. f.write("dev content\n")
  110. run_git_or_fail(["add", "dev.txt"], cwd=self.repo_path)
  111. run_git_or_fail(["commit", "-m", "Dev commit"], cwd=self.repo_path)
  112. # Create bundle with all branches
  113. bundle_path = os.path.join(self.test_dir, "multi_ref.bundle")
  114. run_git_or_fail(["bundle", "create", bundle_path, "--all"], cwd=self.repo_path)
  115. # Read bundle using dulwich
  116. with open(bundle_path, "rb") as f:
  117. bundle = read_bundle(f)
  118. self.addCleanup(bundle.close)
  119. # Verify bundle contains all refs
  120. self.assertIn(b"refs/heads/master", bundle.references)
  121. self.assertIn(b"refs/heads/feature", bundle.references)
  122. self.assertIn(b"refs/heads/develop", bundle.references)
  123. def test_read_git_bundle_with_prerequisites(self) -> None:
  124. """Test reading a bundle with prerequisites created by git."""
  125. # Create initial commits using git
  126. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  127. run_git_or_fail(
  128. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  129. )
  130. # Create base commits
  131. test_file = os.path.join(self.repo_path, "test.txt")
  132. with open(test_file, "w") as f:
  133. f.write("content 1\n")
  134. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  135. run_git_or_fail(["commit", "-m", "Commit 1"], cwd=self.repo_path)
  136. with open(test_file, "a") as f:
  137. f.write("content 2\n")
  138. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  139. run_git_or_fail(["commit", "-m", "Commit 2"], cwd=self.repo_path)
  140. # Get the first commit hash to use as base
  141. first_commit = run_git_or_fail(
  142. ["rev-parse", "HEAD~1"], cwd=self.repo_path
  143. ).strip()
  144. # Create more commits
  145. with open(test_file, "a") as f:
  146. f.write("content 3\n")
  147. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  148. run_git_or_fail(["commit", "-m", "Commit 3"], cwd=self.repo_path)
  149. # Create bundle with prerequisites (only commits after first_commit)
  150. bundle_path = os.path.join(self.test_dir, "prereq.bundle")
  151. run_git_or_fail(
  152. ["bundle", "create", bundle_path, f"{first_commit.decode()}..HEAD"],
  153. cwd=self.repo_path,
  154. )
  155. # Read bundle using dulwich
  156. with open(bundle_path, "rb") as f:
  157. bundle = read_bundle(f)
  158. self.addCleanup(bundle.close)
  159. # Verify bundle has prerequisites
  160. self.assertGreater(len(bundle.prerequisites), 0)
  161. # The prerequisite should be the first commit
  162. prereq_ids = [p[0] for p in bundle.prerequisites]
  163. self.assertIn(first_commit, prereq_ids)
  164. def test_read_git_bundle_complex_pack(self) -> None:
  165. """Test reading a bundle with complex pack data (multiple objects) created by git."""
  166. # Create a more complex repository structure
  167. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  168. run_git_or_fail(
  169. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  170. )
  171. # Create multiple files in subdirectories
  172. os.makedirs(os.path.join(self.repo_path, "src", "main"), exist_ok=True)
  173. os.makedirs(os.path.join(self.repo_path, "tests"), exist_ok=True)
  174. # Add various file types
  175. files = [
  176. ("README.md", "# Test Project\n\nThis is a test."),
  177. ("src/main/app.py", "def main():\n print('Hello')\n"),
  178. ("src/main/utils.py", "def helper():\n return 42\n"),
  179. ("tests/test_app.py", "def test_main():\n pass\n"),
  180. (".gitignore", "*.pyc\n__pycache__/\n"),
  181. ]
  182. for filepath, content in files:
  183. full_path = os.path.join(self.repo_path, filepath)
  184. with open(full_path, "w") as f:
  185. f.write(content)
  186. run_git_or_fail(["add", filepath], cwd=self.repo_path)
  187. run_git_or_fail(["commit", "-m", "Initial complex commit"], cwd=self.repo_path)
  188. # Make additional changes
  189. with open(os.path.join(self.repo_path, "src/main/app.py"), "a") as f:
  190. f.write("\nif __name__ == '__main__':\n main()\n")
  191. run_git_or_fail(["add", "src/main/app.py"], cwd=self.repo_path)
  192. run_git_or_fail(["commit", "-m", "Update app.py"], cwd=self.repo_path)
  193. # Create bundle
  194. bundle_path = os.path.join(self.test_dir, "complex.bundle")
  195. run_git_or_fail(["bundle", "create", bundle_path, "HEAD"], cwd=self.repo_path)
  196. # Read bundle using dulwich
  197. with open(bundle_path, "rb") as f:
  198. bundle = read_bundle(f)
  199. self.addCleanup(bundle.close)
  200. # Verify bundle contents
  201. self.assertEqual(2, bundle.version)
  202. self.assertIn(b"HEAD", bundle.references)
  203. # Verify pack data exists
  204. self.assertIsNotNone(bundle.pack_data)
  205. self.assertGreater(len(bundle.pack_data), 0)
  206. def test_clone_from_git_bundle(self) -> None:
  207. """Test cloning from a bundle created by git."""
  208. # Create a repository with some history
  209. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  210. run_git_or_fail(
  211. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  212. )
  213. # Create commits
  214. test_file = os.path.join(self.repo_path, "test.txt")
  215. for i in range(3):
  216. with open(test_file, "a") as f:
  217. f.write(f"Line {i}\n")
  218. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  219. run_git_or_fail(["commit", "-m", f"Commit {i}"], cwd=self.repo_path)
  220. # Create bundle
  221. bundle_path = os.path.join(self.test_dir, "clone.bundle")
  222. run_git_or_fail(["bundle", "create", bundle_path, "HEAD"], cwd=self.repo_path)
  223. # Read bundle using dulwich
  224. with open(bundle_path, "rb") as f:
  225. bundle = read_bundle(f)
  226. self.addCleanup(bundle.close)
  227. # Verify bundle was read correctly
  228. self.assertEqual(2, bundle.version)
  229. self.assertIn(b"HEAD", bundle.references)
  230. self.assertEqual([], bundle.prerequisites)
  231. # Verify pack data exists
  232. self.assertIsNotNone(bundle.pack_data)
  233. self.assertGreater(len(bundle.pack_data), 0)
  234. # Use git to verify the bundle can be used for cloning
  235. clone_path = os.path.join(self.test_dir, "cloned_repo")
  236. run_git_or_fail(["clone", bundle_path, clone_path])
  237. # Verify the cloned repository exists and has content
  238. self.assertTrue(os.path.exists(clone_path))
  239. self.assertTrue(os.path.exists(os.path.join(clone_path, "test.txt")))
  240. def test_unbundle_git_bundle(self) -> None:
  241. """Test unbundling a bundle created by git using dulwich CLI."""
  242. # Create a repository with commits using git
  243. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  244. run_git_or_fail(
  245. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  246. )
  247. # Create commits
  248. test_file = os.path.join(self.repo_path, "test.txt")
  249. with open(test_file, "w") as f:
  250. f.write("content 1\n")
  251. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  252. run_git_or_fail(["commit", "-m", "Commit 1"], cwd=self.repo_path)
  253. with open(test_file, "a") as f:
  254. f.write("content 2\n")
  255. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  256. run_git_or_fail(["commit", "-m", "Commit 2"], cwd=self.repo_path)
  257. # Get commit SHA for verification
  258. head_sha = run_git_or_fail(["rev-parse", "HEAD"], cwd=self.repo_path).strip()
  259. # Create bundle using git
  260. bundle_path = os.path.join(self.test_dir, "unbundle_test.bundle")
  261. run_git_or_fail(["bundle", "create", bundle_path, "master"], cwd=self.repo_path)
  262. # Create a new empty repository to unbundle into
  263. unbundle_repo_path = os.path.join(self.test_dir, "unbundle_repo")
  264. unbundle_repo = Repo.init(unbundle_repo_path, mkdir=True)
  265. self.addCleanup(unbundle_repo.close)
  266. # Read the bundle and store objects using dulwich
  267. with open(bundle_path, "rb") as f:
  268. bundle = read_bundle(f)
  269. self.addCleanup(bundle.close)
  270. # Use the bundle's store_objects method to unbundle
  271. bundle.store_objects(unbundle_repo.object_store)
  272. # Verify objects are now in the repository
  273. # Check that the HEAD commit exists
  274. self.assertIn(head_sha, unbundle_repo.object_store)
  275. # Verify we can retrieve the commit
  276. commit = unbundle_repo.object_store[head_sha]
  277. self.assertEqual(b"Commit 2\n", commit.message)