test_bundle.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  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. with open(bundle_path, "wb") as f:
  55. write_bundle(f, bundle)
  56. # Verify git can read the bundle (must run from a repo directory)
  57. output = run_git_or_fail(["bundle", "verify", bundle_path], cwd=self.repo_path)
  58. self.assertIn(b"The bundle contains", output)
  59. self.assertIn(b"refs/heads/master", output)
  60. def test_read_git_bundle(self) -> None:
  61. """Test reading a bundle created by git."""
  62. # Create a commit using git
  63. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  64. run_git_or_fail(
  65. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  66. )
  67. # Create a file and commit
  68. test_file = os.path.join(self.repo_path, "test.txt")
  69. with open(test_file, "w") as f:
  70. f.write("test content\n")
  71. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  72. run_git_or_fail(["commit", "-m", "Test commit"], cwd=self.repo_path)
  73. # Create bundle using git
  74. bundle_path = os.path.join(self.test_dir, "git.bundle")
  75. run_git_or_fail(["bundle", "create", bundle_path, "HEAD"], cwd=self.repo_path)
  76. # Read bundle using dulwich
  77. with open(bundle_path, "rb") as f:
  78. bundle = read_bundle(f)
  79. # Verify bundle contents
  80. self.assertEqual(2, bundle.version)
  81. self.assertIn(b"HEAD", bundle.references)
  82. self.assertEqual({}, bundle.capabilities)
  83. self.assertEqual([], bundle.prerequisites)
  84. def test_read_git_bundle_multiple_refs(self) -> None:
  85. """Test reading a bundle with multiple references created by git."""
  86. # Create commits and branches using git
  87. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  88. run_git_or_fail(
  89. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  90. )
  91. # Create initial commit
  92. test_file = os.path.join(self.repo_path, "test.txt")
  93. with open(test_file, "w") as f:
  94. f.write("initial content\n")
  95. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  96. run_git_or_fail(["commit", "-m", "Initial commit"], cwd=self.repo_path)
  97. # Create feature branch
  98. run_git_or_fail(["checkout", "-b", "feature"], cwd=self.repo_path)
  99. with open(test_file, "w") as f:
  100. f.write("feature content\n")
  101. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  102. run_git_or_fail(["commit", "-m", "Feature commit"], cwd=self.repo_path)
  103. # Create another branch
  104. run_git_or_fail(["checkout", "-b", "develop", "master"], cwd=self.repo_path)
  105. dev_file = os.path.join(self.repo_path, "dev.txt")
  106. with open(dev_file, "w") as f:
  107. f.write("dev content\n")
  108. run_git_or_fail(["add", "dev.txt"], cwd=self.repo_path)
  109. run_git_or_fail(["commit", "-m", "Dev commit"], cwd=self.repo_path)
  110. # Create bundle with all branches
  111. bundle_path = os.path.join(self.test_dir, "multi_ref.bundle")
  112. run_git_or_fail(["bundle", "create", bundle_path, "--all"], cwd=self.repo_path)
  113. # Read bundle using dulwich
  114. with open(bundle_path, "rb") as f:
  115. bundle = read_bundle(f)
  116. # Verify bundle contains all refs
  117. self.assertIn(b"refs/heads/master", bundle.references)
  118. self.assertIn(b"refs/heads/feature", bundle.references)
  119. self.assertIn(b"refs/heads/develop", bundle.references)
  120. def test_read_git_bundle_with_prerequisites(self) -> None:
  121. """Test reading a bundle with prerequisites created by git."""
  122. # Create initial commits using git
  123. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  124. run_git_or_fail(
  125. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  126. )
  127. # Create base commits
  128. test_file = os.path.join(self.repo_path, "test.txt")
  129. with open(test_file, "w") as f:
  130. f.write("content 1\n")
  131. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  132. run_git_or_fail(["commit", "-m", "Commit 1"], cwd=self.repo_path)
  133. with open(test_file, "a") as f:
  134. f.write("content 2\n")
  135. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  136. run_git_or_fail(["commit", "-m", "Commit 2"], cwd=self.repo_path)
  137. # Get the first commit hash to use as base
  138. first_commit = run_git_or_fail(
  139. ["rev-parse", "HEAD~1"], cwd=self.repo_path
  140. ).strip()
  141. # Create more commits
  142. with open(test_file, "a") as f:
  143. f.write("content 3\n")
  144. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  145. run_git_or_fail(["commit", "-m", "Commit 3"], cwd=self.repo_path)
  146. # Create bundle with prerequisites (only commits after first_commit)
  147. bundle_path = os.path.join(self.test_dir, "prereq.bundle")
  148. run_git_or_fail(
  149. ["bundle", "create", bundle_path, f"{first_commit.decode()}..HEAD"],
  150. cwd=self.repo_path,
  151. )
  152. # Read bundle using dulwich
  153. with open(bundle_path, "rb") as f:
  154. bundle = read_bundle(f)
  155. # Verify bundle has prerequisites
  156. self.assertGreater(len(bundle.prerequisites), 0)
  157. # The prerequisite should be the first commit
  158. prereq_ids = [p[0] for p in bundle.prerequisites]
  159. self.assertIn(first_commit, prereq_ids)
  160. def test_read_git_bundle_complex_pack(self) -> None:
  161. """Test reading a bundle with complex pack data (multiple objects) created by git."""
  162. # Create a more complex repository structure
  163. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  164. run_git_or_fail(
  165. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  166. )
  167. # Create multiple files in subdirectories
  168. os.makedirs(os.path.join(self.repo_path, "src", "main"), exist_ok=True)
  169. os.makedirs(os.path.join(self.repo_path, "tests"), exist_ok=True)
  170. # Add various file types
  171. files = [
  172. ("README.md", "# Test Project\n\nThis is a test."),
  173. ("src/main/app.py", "def main():\n print('Hello')\n"),
  174. ("src/main/utils.py", "def helper():\n return 42\n"),
  175. ("tests/test_app.py", "def test_main():\n pass\n"),
  176. (".gitignore", "*.pyc\n__pycache__/\n"),
  177. ]
  178. for filepath, content in files:
  179. full_path = os.path.join(self.repo_path, filepath)
  180. with open(full_path, "w") as f:
  181. f.write(content)
  182. run_git_or_fail(["add", filepath], cwd=self.repo_path)
  183. run_git_or_fail(["commit", "-m", "Initial complex commit"], cwd=self.repo_path)
  184. # Make additional changes
  185. with open(os.path.join(self.repo_path, "src/main/app.py"), "a") as f:
  186. f.write("\nif __name__ == '__main__':\n main()\n")
  187. run_git_or_fail(["add", "src/main/app.py"], cwd=self.repo_path)
  188. run_git_or_fail(["commit", "-m", "Update app.py"], cwd=self.repo_path)
  189. # Create bundle
  190. bundle_path = os.path.join(self.test_dir, "complex.bundle")
  191. run_git_or_fail(["bundle", "create", bundle_path, "HEAD"], cwd=self.repo_path)
  192. # Read bundle using dulwich
  193. with open(bundle_path, "rb") as f:
  194. bundle = read_bundle(f)
  195. # Verify bundle contents
  196. self.assertEqual(2, bundle.version)
  197. self.assertIn(b"HEAD", bundle.references)
  198. # Verify pack data exists
  199. self.assertIsNotNone(bundle.pack_data)
  200. self.assertGreater(len(bundle.pack_data), 0)
  201. def test_clone_from_git_bundle(self) -> None:
  202. """Test cloning from a bundle created by git."""
  203. # Create a repository with some history
  204. run_git_or_fail(["config", "user.name", "Test User"], cwd=self.repo_path)
  205. run_git_or_fail(
  206. ["config", "user.email", "test@example.com"], cwd=self.repo_path
  207. )
  208. # Create commits
  209. test_file = os.path.join(self.repo_path, "test.txt")
  210. for i in range(3):
  211. with open(test_file, "a") as f:
  212. f.write(f"Line {i}\n")
  213. run_git_or_fail(["add", "test.txt"], cwd=self.repo_path)
  214. run_git_or_fail(["commit", "-m", f"Commit {i}"], cwd=self.repo_path)
  215. # Create bundle
  216. bundle_path = os.path.join(self.test_dir, "clone.bundle")
  217. run_git_or_fail(["bundle", "create", bundle_path, "HEAD"], cwd=self.repo_path)
  218. # Read bundle using dulwich
  219. with open(bundle_path, "rb") as f:
  220. bundle = read_bundle(f)
  221. # Verify bundle was read correctly
  222. self.assertEqual(2, bundle.version)
  223. self.assertIn(b"HEAD", bundle.references)
  224. self.assertEqual([], bundle.prerequisites)
  225. # Verify pack data exists
  226. self.assertIsNotNone(bundle.pack_data)
  227. self.assertGreater(len(bundle.pack_data), 0)
  228. # Use git to verify the bundle can be used for cloning
  229. clone_path = os.path.join(self.test_dir, "cloned_repo")
  230. run_git_or_fail(["clone", bundle_path, clone_path])
  231. # Verify the cloned repository exists and has content
  232. self.assertTrue(os.path.exists(clone_path))
  233. self.assertTrue(os.path.exists(os.path.join(clone_path, "test.txt")))