test_bitmap.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. # test_bitmap.py -- Compatibility tests for git pack bitmaps.
  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. """Compatibility tests for git pack bitmaps."""
  22. import os
  23. import shutil
  24. import tempfile
  25. from dulwich.pack import Pack
  26. from dulwich.repo import Repo
  27. from .. import SkipTest, TestCase
  28. from .utils import require_git_version, run_git_or_fail
  29. class BitmapCompatTests(TestCase):
  30. """Compatibility tests for reading git-generated bitmaps."""
  31. def setUp(self):
  32. super().setUp()
  33. # Git bitmap support was added in 2.0.0
  34. require_git_version((2, 0, 0))
  35. self._tempdir = tempfile.mkdtemp()
  36. self.addCleanup(shutil.rmtree, self._tempdir)
  37. def _init_repo_with_bitmap(self):
  38. """Create a repo and generate a bitmap using git."""
  39. repo_path = os.path.join(self._tempdir, "test-repo")
  40. os.mkdir(repo_path)
  41. # Initialize repo
  42. run_git_or_fail(["init"], cwd=repo_path)
  43. # Create some commits
  44. test_file = os.path.join(repo_path, "test.txt")
  45. for i in range(5):
  46. with open(test_file, "w") as f:
  47. f.write(f"Content {i}\n")
  48. run_git_or_fail(["add", "test.txt"], cwd=repo_path)
  49. run_git_or_fail(
  50. ["commit", "-m", f"Commit {i}"],
  51. cwd=repo_path,
  52. env={"GIT_AUTHOR_NAME": "Test", "GIT_AUTHOR_EMAIL": "test@example.com"},
  53. )
  54. # Enable bitmap writing and repack
  55. run_git_or_fail(
  56. ["config", "pack.writeBitmaps", "true"],
  57. cwd=repo_path,
  58. )
  59. run_git_or_fail(["repack", "-a", "-d", "-b"], cwd=repo_path)
  60. return repo_path
  61. def test_read_git_generated_bitmap(self):
  62. """Test that Dulwich can read a bitmap generated by git."""
  63. repo_path = self._init_repo_with_bitmap()
  64. # Find the pack file with bitmap
  65. pack_dir = os.path.join(repo_path, ".git", "objects", "pack")
  66. bitmap_files = [f for f in os.listdir(pack_dir) if f.endswith(".bitmap")]
  67. if not bitmap_files:
  68. raise SkipTest("Git did not generate a bitmap file")
  69. # Get the pack file (basename without extension)
  70. bitmap_name = bitmap_files[0]
  71. pack_basename = bitmap_name.replace(".bitmap", "")
  72. pack_path = os.path.join(pack_dir, pack_basename)
  73. # Verify bitmap file exists at expected location
  74. bitmap_path = pack_path + ".bitmap"
  75. self.assertTrue(
  76. os.path.exists(bitmap_path), f"Bitmap file not found at {bitmap_path}"
  77. )
  78. # Try to load the bitmap using Dulwich
  79. pack = Pack(pack_path)
  80. bitmap = pack.bitmap
  81. # Basic checks
  82. self.assertIsNotNone(bitmap, f"Failed to load bitmap from {pack_path}")
  83. self.assertIsNotNone(bitmap.pack_checksum, "Bitmap missing pack checksum")
  84. # Check that we have some type bitmaps
  85. # At minimum, we should have some commits
  86. self.assertGreater(
  87. len(bitmap.commit_bitmap.bits),
  88. 0,
  89. "Commit bitmap should not be empty",
  90. )
  91. def test_git_can_use_dulwich_repo_with_bitmap(self):
  92. """Test that git can work with a repo that has Dulwich-created objects."""
  93. repo_path = os.path.join(self._tempdir, "dulwich-repo")
  94. # Create a repo with Dulwich and add commits to ensure git creates bitmaps
  95. repo = Repo.init(repo_path, mkdir=True)
  96. self.addCleanup(repo.close)
  97. # Create actual commits, not just loose objects - git needs commits for bitmaps
  98. test_file = os.path.join(repo_path, "test.txt")
  99. for i in range(5):
  100. with open(test_file, "w") as f:
  101. f.write(f"Content {i}\n")
  102. run_git_or_fail(["add", "test.txt"], cwd=repo_path)
  103. run_git_or_fail(
  104. ["commit", "-m", f"Commit {i}"],
  105. cwd=repo_path,
  106. env={"GIT_AUTHOR_NAME": "Test", "GIT_AUTHOR_EMAIL": "test@example.com"},
  107. )
  108. # Configure git to write bitmaps
  109. run_git_or_fail(
  110. ["config", "pack.writeBitmaps", "true"],
  111. cwd=repo_path,
  112. )
  113. # Git should be able to repack with bitmaps
  114. run_git_or_fail(["repack", "-a", "-d", "-b"], cwd=repo_path)
  115. # Verify git created a bitmap
  116. pack_dir = os.path.join(repo_path, ".git", "objects", "pack")
  117. self.assertTrue(os.path.exists(pack_dir), "Pack directory should exist")
  118. bitmap_files = [f for f in os.listdir(pack_dir) if f.endswith(".bitmap")]
  119. self.assertGreater(
  120. len(bitmap_files), 0, "Git should have created a bitmap file after repack"
  121. )
  122. def test_git_can_read_dulwich_bitmap(self):
  123. """Test that git can read a bitmap file written by Dulwich."""
  124. repo_path = os.path.join(self._tempdir, "dulwich-bitmap-repo")
  125. # Create a repo with git and add commits
  126. run_git_or_fail(["init"], cwd=None, env={"GIT_DIR": repo_path})
  127. test_file = os.path.join(repo_path, "..", "test.txt")
  128. os.makedirs(os.path.dirname(test_file), exist_ok=True)
  129. for i in range(5):
  130. with open(test_file, "w") as f:
  131. f.write(f"Content {i}\n")
  132. run_git_or_fail(
  133. ["add", test_file],
  134. cwd=os.path.dirname(repo_path),
  135. env={
  136. "GIT_DIR": repo_path,
  137. "GIT_WORK_TREE": os.path.dirname(repo_path),
  138. },
  139. )
  140. run_git_or_fail(
  141. ["commit", "-m", f"Commit {i}"],
  142. cwd=os.path.dirname(repo_path),
  143. env={
  144. "GIT_DIR": repo_path,
  145. "GIT_WORK_TREE": os.path.dirname(repo_path),
  146. "GIT_AUTHOR_NAME": "Test",
  147. "GIT_AUTHOR_EMAIL": "test@example.com",
  148. },
  149. )
  150. # Create a pack with git first
  151. run_git_or_fail(["repack", "-a", "-d"], cwd=None, env={"GIT_DIR": repo_path})
  152. # Now use Dulwich to write a bitmap for the pack
  153. from dulwich.bitmap import (
  154. BITMAP_OPT_FULL_DAG,
  155. BITMAP_OPT_HASH_CACHE,
  156. BITMAP_OPT_LOOKUP_TABLE,
  157. BitmapEntry,
  158. EWAHBitmap,
  159. PackBitmap,
  160. write_bitmap,
  161. )
  162. pack_dir = os.path.join(repo_path, "objects", "pack")
  163. pack_files = [f for f in os.listdir(pack_dir) if f.endswith(".pack")]
  164. self.assertGreater(len(pack_files), 0, "Should have at least one pack file")
  165. pack_basename = pack_files[0].replace(".pack", "")
  166. pack_path = os.path.join(pack_dir, pack_basename)
  167. # Load the pack
  168. pack = Pack(pack_path)
  169. self.addCleanup(pack.close)
  170. # Create a simple bitmap for testing
  171. # Git requires BITMAP_OPT_FULL_DAG flag
  172. bitmap = PackBitmap(
  173. flags=BITMAP_OPT_FULL_DAG | BITMAP_OPT_HASH_CACHE | BITMAP_OPT_LOOKUP_TABLE
  174. )
  175. bitmap.pack_checksum = pack.get_stored_checksum()
  176. # Add bitmap entries for the first few commits in the pack
  177. for i, (sha, offset, crc) in enumerate(pack.index.iterentries()):
  178. if i >= 3: # Just add 3 entries
  179. break
  180. ewah = EWAHBitmap()
  181. # Mark this object and a couple others as reachable
  182. for j in range(i + 1):
  183. ewah.add(j)
  184. entry = BitmapEntry(object_pos=i, xor_offset=0, flags=0, bitmap=ewah)
  185. bitmap.entries[sha] = entry
  186. bitmap.entries_list.append((sha, entry))
  187. # Add name hash cache
  188. bitmap.name_hash_cache = [0x12345678, 0xABCDEF00, 0xFEDCBA98]
  189. # Write the bitmap
  190. bitmap_path = pack_path + ".bitmap"
  191. write_bitmap(bitmap_path, bitmap)
  192. # Verify git can use the repository with our bitmap
  193. # This should succeed if git can read our bitmap
  194. run_git_or_fail(
  195. ["rev-list", "--count", "--use-bitmap-index", "HEAD"],
  196. cwd=None,
  197. env={"GIT_DIR": repo_path},
  198. )
  199. # Verify git count-objects works with our bitmap
  200. run_git_or_fail(["count-objects", "-v"], cwd=None, env={"GIT_DIR": repo_path})
  201. def test_bitmap_file_format_structure(self):
  202. """Test that git-generated bitmap has expected structure."""
  203. repo_path = self._init_repo_with_bitmap()
  204. # Find bitmap
  205. pack_dir = os.path.join(repo_path, ".git", "objects", "pack")
  206. bitmap_files = [f for f in os.listdir(pack_dir) if f.endswith(".bitmap")]
  207. if not bitmap_files:
  208. raise SkipTest("Git did not generate a bitmap file")
  209. bitmap_path = os.path.join(pack_dir, bitmap_files[0])
  210. # Read the raw file to verify header
  211. with open(bitmap_path, "rb") as f:
  212. signature = f.read(4)
  213. self.assertEqual(b"BITM", signature, "Invalid bitmap signature")
  214. version = int.from_bytes(f.read(2), byteorder="big")
  215. self.assertGreaterEqual(version, 1, "Bitmap version should be >= 1")
  216. # Load with Dulwich and verify structure
  217. bitmap_name = bitmap_files[0]
  218. pack_basename = bitmap_name.replace(".bitmap", "")
  219. pack_path = os.path.join(pack_dir, pack_basename)
  220. pack = Pack(pack_path)
  221. bitmap = pack.bitmap
  222. self.assertIsNotNone(bitmap)
  223. self.assertEqual(bitmap.version, version)