test_reftable.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. """Reftable compatibility tests with C git."""
  2. import os
  3. import struct
  4. import tempfile
  5. from dulwich.reftable import (
  6. REF_VALUE_DELETE,
  7. REF_VALUE_REF,
  8. REF_VALUE_SYMREF,
  9. REFTABLE_MAGIC,
  10. REFTABLE_VERSION,
  11. ReftableReader,
  12. ReftableRefsContainer,
  13. )
  14. from dulwich.repo import Repo
  15. from .utils import CompatTestCase, rmtree_ro, run_git_or_fail
  16. class ReftableCompatTestCase(CompatTestCase):
  17. """Test reftable compatibility with C git."""
  18. min_git_version = (2, 45, 0) # Version with stable reftable support
  19. def setUp(self):
  20. """Set up test environment."""
  21. super().setUp()
  22. self.test_dir = tempfile.mkdtemp()
  23. self.addCleanup(self._cleanup)
  24. def _cleanup(self):
  25. """Clean up test directory."""
  26. rmtree_ro(self.test_dir)
  27. def _run_git(self, args, **kwargs):
  28. """Run git command in test directory."""
  29. return run_git_or_fail(args, cwd=self.test_dir, **kwargs)
  30. def _create_git_repo_with_reftable(self):
  31. """Create a git repository with reftable format."""
  32. # Initialize repo with reftable format
  33. self._run_git(["init", "--bare", "--ref-format=reftable", "."])
  34. # Create some test objects and refs using proper commits
  35. blob1_sha = self._run_git(
  36. ["hash-object", "-w", "--stdin"], input=b"test content 1\n"
  37. ).strip()
  38. blob2_sha = self._run_git(
  39. ["hash-object", "-w", "--stdin"], input=b"test content 2\n"
  40. ).strip()
  41. # Create tree objects
  42. tree1_sha = self._run_git(
  43. ["mktree"], input=f"100644 blob {blob1_sha.decode()}\tfile1.txt\n".encode()
  44. ).strip()
  45. tree2_sha = self._run_git(
  46. ["mktree"], input=f"100644 blob {blob2_sha.decode()}\tfile2.txt\n".encode()
  47. ).strip()
  48. # Create commit objects
  49. sha1 = self._run_git(
  50. ["commit-tree", tree1_sha.decode(), "-m", "First commit"]
  51. ).strip()
  52. sha2 = self._run_git(
  53. ["commit-tree", tree2_sha.decode(), "-m", "Second commit"]
  54. ).strip()
  55. # Create refs
  56. self._run_git(["update-ref", "refs/heads/master", sha1.decode()])
  57. self._run_git(["update-ref", "refs/heads/develop", sha2.decode()])
  58. self._run_git(["symbolic-ref", "HEAD", "refs/heads/master"])
  59. return sha1, sha2
  60. def _create_reftable_repo(self):
  61. """Create a Dulwich repository with reftable extension configured."""
  62. from io import BytesIO
  63. from dulwich.config import ConfigFile
  64. # Initialize bare repo
  65. repo = Repo.init_bare(self.test_dir)
  66. repo.close()
  67. # Add reftable extension to config
  68. config_path = os.path.join(self.test_dir, "config")
  69. with open(config_path, "rb") as f:
  70. config_data = f.read()
  71. config = ConfigFile()
  72. config.from_file(BytesIO(config_data))
  73. config.set(b"core", b"repositoryformatversion", b"1")
  74. config.set(b"extensions", b"refStorage", b"reftable")
  75. # Use master as default branch for reftable compatibility
  76. config.set(b"init", b"defaultBranch", b"master")
  77. with open(config_path, "wb") as f:
  78. config.write_to_file(f)
  79. # Reopen repo with reftable extension
  80. return Repo(self.test_dir)
  81. def _get_reftable_files(self):
  82. """Get list of reftable files in the repository."""
  83. reftable_dir = os.path.join(self.test_dir, "reftable")
  84. if not os.path.exists(reftable_dir):
  85. return []
  86. files = []
  87. for filename in os.listdir(reftable_dir):
  88. if filename.endswith((".ref", ".log")):
  89. files.append(os.path.join(reftable_dir, filename))
  90. return sorted(files)
  91. def _validate_reftable_format(self, filepath: str):
  92. """Validate that a file follows the reftable format specification."""
  93. with open(filepath, "rb") as f:
  94. # Check magic header
  95. magic = f.read(4)
  96. self.assertEqual(magic, REFTABLE_MAGIC, "Invalid reftable magic")
  97. # Check version + block size (4 bytes total)
  98. # Format: uint8(version) + uint24(block_size)
  99. version_and_blocksize = struct.unpack(">I", f.read(4))[0]
  100. version = (version_and_blocksize >> 24) & 0xFF # First byte
  101. block_size = version_and_blocksize & 0xFFFFFF # Last 3 bytes
  102. self.assertEqual(
  103. version, REFTABLE_VERSION, f"Unsupported version: {version}"
  104. )
  105. self.assertGreater(block_size, 0, "Invalid block size")
  106. # Check that we can read the header without errors
  107. min_update_index = struct.unpack(">Q", f.read(8))[0]
  108. max_update_index = struct.unpack(">Q", f.read(8))[0]
  109. # Update indices should be valid
  110. self.assertGreaterEqual(max_update_index, min_update_index)
  111. def test_git_creates_valid_reftable_format(self):
  112. """Test that git creates reftable files with valid format."""
  113. sha1, sha2 = self._create_git_repo_with_reftable()
  114. # Check that reftable files were created
  115. reftable_files = self._get_reftable_files()
  116. self.assertGreater(len(reftable_files), 0, "No reftable files created")
  117. # Validate format of each reftable file
  118. for filepath in reftable_files:
  119. if filepath.endswith(".ref"):
  120. self._validate_reftable_format(filepath)
  121. def test_dulwich_can_read_git_reftables(self):
  122. """Test that Dulwich can read reftables created by git."""
  123. sha1, sha2 = self._create_git_repo_with_reftable()
  124. # Open with Dulwich
  125. repo = Repo(self.test_dir)
  126. # Verify it's using reftable
  127. self.assertIsInstance(repo.refs, ReftableRefsContainer)
  128. # Check that we can read the refs
  129. all_refs = repo.get_refs()
  130. self.assertIn(b"refs/heads/master", all_refs)
  131. self.assertIn(b"refs/heads/develop", all_refs)
  132. self.assertIn(b"HEAD", all_refs)
  133. # Verify ref values
  134. self.assertEqual(all_refs[b"refs/heads/master"], sha1)
  135. self.assertEqual(all_refs[b"refs/heads/develop"], sha2)
  136. self.assertEqual(all_refs[b"HEAD"], sha1) # HEAD -> refs/heads/master
  137. repo.close()
  138. def test_git_can_read_dulwich_reftables(self):
  139. """Test that git can read reftables created by Dulwich."""
  140. # Create a repo with reftable extension
  141. repo = self._create_reftable_repo()
  142. self.assertIsInstance(repo.refs, ReftableRefsContainer)
  143. # Create real objects that git can validate
  144. blob1_sha = self._run_git(
  145. ["hash-object", "-w", "--stdin"], input=b"test content 1\n"
  146. ).strip()
  147. blob2_sha = self._run_git(
  148. ["hash-object", "-w", "--stdin"], input=b"test content 2\n"
  149. ).strip()
  150. # Create tree objects
  151. tree1_sha = self._run_git(
  152. ["mktree"], input=f"100644 blob {blob1_sha.decode()}\tfile1.txt\n".encode()
  153. ).strip()
  154. tree2_sha = self._run_git(
  155. ["mktree"], input=f"100644 blob {blob2_sha.decode()}\tfile2.txt\n".encode()
  156. ).strip()
  157. # Create commit objects
  158. sha1 = self._run_git(
  159. ["commit-tree", tree1_sha.decode(), "-m", "First commit"]
  160. ).strip()
  161. sha2 = self._run_git(
  162. ["commit-tree", tree2_sha.decode(), "-m", "Second commit"]
  163. ).strip()
  164. # Create exactly what Git does: consolidated table with multiple refs
  165. # Use batched operations to create a single consolidated reftable like git
  166. with repo.refs.batch_update():
  167. repo.refs.set_if_equals(b"refs/heads/master", None, sha1)
  168. repo.refs.set_if_equals(b"refs/heads/develop", None, sha2)
  169. repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
  170. repo.refs._update_tables_list()
  171. repo.close()
  172. # Test that git can read our reftables
  173. # Test symbolic ref reading
  174. head_output = self._run_git(["symbolic-ref", "HEAD"])
  175. self.assertEqual(head_output.strip(), b"refs/heads/master")
  176. # Test ref parsing
  177. master_output = self._run_git(["rev-parse", "HEAD"])
  178. self.assertEqual(master_output.strip(), sha1)
  179. # Test show-ref
  180. show_output = self._run_git(["show-ref"])
  181. self.assertIn(b"refs/heads/master", show_output)
  182. self.assertIn(b"refs/heads/develop", show_output)
  183. def test_reftable_file_structure_compatibility(self):
  184. """Test that reftable file structure matches git's expectations."""
  185. self._create_git_repo_with_reftable()
  186. reftable_files = self._get_reftable_files()
  187. ref_files = [f for f in reftable_files if f.endswith(".ref")]
  188. for ref_file in ref_files:
  189. with open(ref_file, "rb") as f:
  190. # Read with our ReftableReader
  191. reader = ReftableReader(f)
  192. refs = reader.all_refs()
  193. # Should have some refs
  194. self.assertGreater(len(refs), 0)
  195. # All refs should have valid types and values
  196. for refname, (ref_type, value) in refs.items():
  197. self.assertIsInstance(refname, bytes)
  198. self.assertIn(
  199. ref_type,
  200. [REF_VALUE_REF, REF_VALUE_SYMREF, REF_VALUE_DELETE],
  201. )
  202. self.assertIsInstance(value, bytes)
  203. if ref_type == REF_VALUE_REF:
  204. self.assertEqual(
  205. len(value), 40, f"Invalid SHA length for {refname}"
  206. )
  207. def test_ref_operations_match_git_behavior(self):
  208. """Test that ref operations produce the same results as git."""
  209. self._create_git_repo_with_reftable()
  210. # Read refs with git
  211. git_output = self._run_git(["show-ref"])
  212. git_refs = {}
  213. for line in git_output.split(b"\n"):
  214. if line.strip():
  215. sha, refname = line.strip().split(b" ", 1)
  216. git_refs[refname] = sha
  217. # Read refs with Dulwich
  218. repo = Repo(self.test_dir)
  219. dulwich_refs = repo.get_refs()
  220. # Compare non-symbolic refs
  221. for refname, sha in git_refs.items():
  222. if refname != b"HEAD": # HEAD is symbolic, compare differently
  223. self.assertIn(refname, dulwich_refs)
  224. self.assertEqual(dulwich_refs[refname], sha)
  225. # Check HEAD symbolic ref
  226. head_target = self._run_git(["rev-parse", "HEAD"]).strip()
  227. self.assertEqual(dulwich_refs[b"HEAD"], head_target)
  228. repo.close()
  229. def test_multiple_table_files_compatibility(self):
  230. """Test compatibility when multiple reftable files exist."""
  231. sha1, sha2 = self._create_git_repo_with_reftable()
  232. # Add more refs to potentially create multiple table files
  233. for i in range(10):
  234. content = f"test content {i}\n".encode()
  235. sha = self._run_git(["hash-object", "-w", "--stdin"], input=content).strip()
  236. self._run_git(["update-ref", f"refs/tags/tag{i}", sha.decode()])
  237. # Read with both git and Dulwich
  238. repo = Repo(self.test_dir)
  239. dulwich_refs = repo.get_refs()
  240. git_output = self._run_git(["show-ref"])
  241. git_ref_count = len([line for line in git_output.split(b"\n") if line.strip()])
  242. # Should have roughly the same number of refs
  243. self.assertGreaterEqual(
  244. len(dulwich_refs), git_ref_count - 1
  245. ) # -1 for potential HEAD differences
  246. repo.close()
  247. def test_empty_reftable_compatibility(self):
  248. """Test compatibility with empty reftable repositories."""
  249. # Create repo with reftable extension
  250. repo = self._create_reftable_repo()
  251. self.assertIsInstance(repo.refs, ReftableRefsContainer)
  252. # Should have no refs initially (reftable doesn't create default HEAD)
  253. all_keys = list(repo.refs.allkeys())
  254. self.assertEqual(len(all_keys), 0)
  255. # Add a single ref
  256. test_sha = b"1234567890abcdef1234567890abcdef12345678"
  257. repo.refs.set_if_equals(b"refs/heads/master", None, test_sha)
  258. # Should now have our ref
  259. all_keys = list(repo.refs.allkeys())
  260. self.assertEqual(len(all_keys), 1)
  261. self.assertIn(b"refs/heads/master", all_keys)
  262. self.assertEqual(repo.refs.read_loose_ref(b"refs/heads/master"), test_sha)
  263. repo.close()
  264. def test_reftable_update_compatibility(self):
  265. """Test that ref updates work compatibly with git."""
  266. repo = self._create_reftable_repo()
  267. # Create initial ref
  268. sha1 = b"1111111111111111111111111111111111111111"
  269. sha2 = b"2222222222222222222222222222222222222222"
  270. repo.refs.set_if_equals(b"refs/heads/master", None, sha1)
  271. # Update ref
  272. success = repo.refs.set_if_equals(b"refs/heads/master", sha1, sha2)
  273. self.assertTrue(success)
  274. # Verify update
  275. self.assertEqual(repo.refs.read_loose_ref(b"refs/heads/master"), sha2)
  276. # Try invalid update (should fail)
  277. success = repo.refs.set_if_equals(b"refs/heads/master", sha1, b"3" * 40)
  278. self.assertFalse(success)
  279. # Ref should be unchanged
  280. self.assertEqual(repo.refs.read_loose_ref(b"refs/heads/master"), sha2)
  281. repo.close()
  282. def test_symbolic_ref_compatibility(self):
  283. """Test symbolic reference compatibility."""
  284. repo = self._create_reftable_repo()
  285. # Create target ref
  286. test_sha = b"abcdef1234567890abcdef1234567890abcdef12"
  287. repo.refs.set_if_equals(b"refs/heads/master", None, test_sha)
  288. # Create symbolic ref
  289. repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
  290. # Verify symbolic ref resolves correctly
  291. self.assertEqual(repo.refs[b"HEAD"], test_sha)
  292. # Verify read_loose_ref returns the symbolic ref format
  293. self.assertEqual(repo.refs.read_loose_ref(b"HEAD"), b"ref: refs/heads/master")
  294. # Update target and verify symbolic ref follows
  295. new_sha = b"fedcba0987654321fedcba0987654321fedcba09"
  296. repo.refs.set_if_equals(b"refs/heads/master", test_sha, new_sha)
  297. self.assertEqual(repo.refs[b"HEAD"], new_sha)
  298. repo.close()
  299. def test_complex_ref_scenarios_compatibility(self):
  300. """Test complex ref scenarios that git should handle correctly."""
  301. # Initialize repo
  302. self._run_git(["init", "--bare", "."])
  303. self._run_git(["config", "core.repositoryformatversion", "1"])
  304. self._run_git(["config", "extensions.refStorage", "reftable"])
  305. # Create test objects
  306. blob_sha = self._run_git(
  307. ["hash-object", "-w", "--stdin"], input=b"test content"
  308. ).strip()
  309. tree_sha = self._run_git(
  310. ["mktree"], input=f"100644 blob {blob_sha.decode()}\tfile.txt\n".encode()
  311. ).strip()
  312. commit_sha1 = self._run_git(
  313. ["commit-tree", tree_sha.decode(), "-m", "First commit"]
  314. ).strip()
  315. commit_sha2 = self._run_git(
  316. ["commit-tree", tree_sha.decode(), "-m", "Second commit"]
  317. ).strip()
  318. commit_sha3 = self._run_git(
  319. ["commit-tree", tree_sha.decode(), "-m", "Third commit"]
  320. ).strip()
  321. repo = Repo(self.test_dir)
  322. # Test complex batched operations
  323. with repo.refs.batch_update():
  324. # Multiple branches
  325. repo.refs.set_if_equals(b"refs/heads/master", None, commit_sha1)
  326. repo.refs.set_if_equals(b"refs/heads/feature/awesome", None, commit_sha2)
  327. repo.refs.set_if_equals(b"refs/heads/hotfix/critical", None, commit_sha3)
  328. # Multiple tags
  329. repo.refs.set_if_equals(b"refs/tags/v1.0.0", None, commit_sha1)
  330. repo.refs.set_if_equals(b"refs/tags/v1.1.0", None, commit_sha2)
  331. # Symbolic refs
  332. repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
  333. repo.refs.set_symbolic_ref(
  334. b"refs/remotes/origin/HEAD", b"refs/remotes/origin/main"
  335. )
  336. # Remote refs
  337. repo.refs.set_if_equals(b"refs/remotes/origin/main", None, commit_sha1)
  338. repo.refs.set_if_equals(
  339. b"refs/remotes/origin/feature/awesome", None, commit_sha2
  340. )
  341. repo.close()
  342. # Verify git can read all refs correctly
  343. git_refs = {}
  344. git_output = self._run_git(["show-ref", "--head"])
  345. for line in git_output.split(b"\n"):
  346. if line.strip():
  347. sha, refname = line.strip().split(b" ", 1)
  348. git_refs[refname] = sha
  349. # Verify HEAD symbolic ref
  350. head_target = self._run_git(["symbolic-ref", "HEAD"]).strip()
  351. self.assertEqual(head_target, b"refs/heads/master")
  352. # Verify all branches resolve correctly
  353. main_sha = self._run_git(["rev-parse", "refs/heads/master"]).strip()
  354. self.assertEqual(main_sha, commit_sha1)
  355. feature_sha = self._run_git(["rev-parse", "refs/heads/feature/awesome"]).strip()
  356. self.assertEqual(feature_sha, commit_sha2)
  357. # Verify tags
  358. tag_sha = self._run_git(["rev-parse", "refs/tags/v1.0.0"]).strip()
  359. self.assertEqual(tag_sha, commit_sha1)
  360. def test_ref_deletion_compatibility(self):
  361. """Test that ref deletion works correctly with git."""
  362. # Initialize repo with refs
  363. self._run_git(["init", "--bare", "."])
  364. self._run_git(["config", "core.repositoryformatversion", "1"])
  365. self._run_git(["config", "extensions.refStorage", "reftable"])
  366. # Create test objects
  367. blob_sha = self._run_git(
  368. ["hash-object", "-w", "--stdin"], input=b"test content"
  369. ).strip()
  370. tree_sha = self._run_git(
  371. ["mktree"], input=f"100644 blob {blob_sha.decode()}\tfile.txt\n".encode()
  372. ).strip()
  373. commit_sha1 = self._run_git(
  374. ["commit-tree", tree_sha.decode(), "-m", "Commit 1"]
  375. ).strip()
  376. commit_sha2 = self._run_git(
  377. ["commit-tree", tree_sha.decode(), "-m", "Commit 2"]
  378. ).strip()
  379. repo = Repo(self.test_dir)
  380. # Create multiple refs
  381. with repo.refs.batch_update():
  382. repo.refs.set_if_equals(b"refs/heads/master", None, commit_sha1)
  383. repo.refs.set_if_equals(b"refs/heads/feature", None, commit_sha2)
  384. repo.refs.set_if_equals(b"refs/tags/v1.0", None, commit_sha1)
  385. repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
  386. # Verify refs exist
  387. self.assertEqual(
  388. self._run_git(["rev-parse", "refs/heads/feature"]).strip(), commit_sha2
  389. )
  390. # Delete a ref using dulwich
  391. with repo.refs.batch_update():
  392. repo.refs.set_if_equals(b"refs/heads/feature", commit_sha2, None)
  393. repo.close()
  394. # Verify git sees the ref as deleted (should fail)
  395. with self.assertRaises(AssertionError):
  396. self._run_git(["rev-parse", "refs/heads/feature"])
  397. # Verify other refs still exist
  398. self.assertEqual(
  399. self._run_git(["rev-parse", "refs/heads/master"]).strip(), commit_sha1
  400. )
  401. self.assertEqual(
  402. self._run_git(["symbolic-ref", "HEAD"]).strip(), b"refs/heads/master"
  403. )
  404. def test_large_number_of_refs_compatibility(self):
  405. """Test compatibility with large numbers of refs."""
  406. # Initialize repo
  407. self._run_git(["init", "--bare", "."])
  408. self._run_git(["config", "core.repositoryformatversion", "1"])
  409. self._run_git(["config", "extensions.refStorage", "reftable"])
  410. # Create base objects
  411. blob_sha = self._run_git(
  412. ["hash-object", "-w", "--stdin"], input=b"test content"
  413. ).strip()
  414. tree_sha = self._run_git(
  415. ["mktree"], input=f"100644 blob {blob_sha.decode()}\tfile.txt\n".encode()
  416. ).strip()
  417. base_commit = self._run_git(
  418. ["commit-tree", tree_sha.decode(), "-m", "Base commit"]
  419. ).strip()
  420. # Create many refs efficiently
  421. repo = Repo(self.test_dir)
  422. with repo.refs.batch_update():
  423. # Create 50 branches
  424. for i in range(50):
  425. content = f"branch content {i}".encode()
  426. commit_sha = self._run_git(
  427. ["commit-tree", tree_sha.decode(), "-m", f"Branch {i} commit"],
  428. input=content,
  429. ).strip()
  430. repo.refs.set_if_equals(
  431. f"refs/heads/branch{i:02d}".encode(), None, commit_sha
  432. )
  433. # Create 30 tags
  434. for i in range(30):
  435. repo.refs.set_if_equals(
  436. f"refs/tags/tag{i:02d}".encode(), None, base_commit
  437. )
  438. # Set HEAD to first branch
  439. repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/branch00")
  440. repo.close()
  441. # Verify git can list all refs
  442. git_output = self._run_git(["show-ref"])
  443. ref_count = len([line for line in git_output.split(b"\n") if line.strip()])
  444. # Should have 50 branches + 30 tags = 80 refs
  445. self.assertGreaterEqual(ref_count, 80)
  446. # Verify HEAD points to correct branch
  447. head_target = self._run_git(["symbolic-ref", "HEAD"]).strip()
  448. self.assertEqual(head_target, b"refs/heads/branch00")
  449. # Verify some random refs resolve correctly
  450. branch10_sha = self._run_git(["rev-parse", "refs/heads/branch10"]).strip()
  451. self.assertEqual(len(branch10_sha), 40) # Valid SHA
  452. tag05_sha = self._run_git(["rev-parse", "refs/tags/tag05"]).strip()
  453. self.assertEqual(tag05_sha, base_commit)
  454. def test_nested_symbolic_refs_compatibility(self):
  455. """Test compatibility with nested symbolic references."""
  456. # Initialize repo
  457. self._run_git(["init", "--bare", "."])
  458. self._run_git(["config", "core.repositoryformatversion", "1"])
  459. self._run_git(["config", "extensions.refStorage", "reftable"])
  460. # Create test objects
  461. blob_sha = self._run_git(
  462. ["hash-object", "-w", "--stdin"], input=b"test content"
  463. ).strip()
  464. tree_sha = self._run_git(
  465. ["mktree"], input=f"100644 blob {blob_sha.decode()}\tfile.txt\n".encode()
  466. ).strip()
  467. commit_sha = self._run_git(
  468. ["commit-tree", tree_sha.decode(), "-m", "Test commit"]
  469. ).strip()
  470. repo = Repo(self.test_dir)
  471. # Create chain of symbolic refs
  472. with repo.refs.batch_update():
  473. # Real ref
  474. repo.refs.set_if_equals(b"refs/heads/master", None, commit_sha)
  475. # Chain: HEAD -> current -> master
  476. repo.refs.set_symbolic_ref(b"refs/heads/current", b"refs/heads/master")
  477. repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/current")
  478. repo.close()
  479. # Verify git can resolve the chain correctly
  480. head_sha = self._run_git(["rev-parse", "HEAD"]).strip()
  481. self.assertEqual(head_sha, commit_sha)
  482. # Verify intermediate symbolic ref
  483. current_target = self._run_git(["symbolic-ref", "refs/heads/current"]).strip()
  484. self.assertEqual(current_target, b"refs/heads/master")
  485. # Verify HEAD points to master (Git resolves symref chains for HEAD)
  486. head_target = self._run_git(["symbolic-ref", "HEAD"]).strip()
  487. self.assertEqual(head_target, b"refs/heads/master")
  488. def test_special_ref_names_compatibility(self):
  489. """Test compatibility with special and edge-case ref names."""
  490. # Initialize repo
  491. self._run_git(["init", "--bare", "."])
  492. self._run_git(["config", "core.repositoryformatversion", "1"])
  493. self._run_git(["config", "extensions.refStorage", "reftable"])
  494. # Create test objects
  495. blob_sha = self._run_git(
  496. ["hash-object", "-w", "--stdin"], input=b"test content"
  497. ).strip()
  498. tree_sha = self._run_git(
  499. ["mktree"], input=f"100644 blob {blob_sha.decode()}\tfile.txt\n".encode()
  500. ).strip()
  501. commit_sha = self._run_git(
  502. ["commit-tree", tree_sha.decode(), "-m", "Test commit"]
  503. ).strip()
  504. repo = Repo(self.test_dir)
  505. # Test refs with special characters and structures
  506. special_refs = [
  507. b"refs/heads/feature/sub-feature",
  508. b"refs/heads/feature_with_underscores",
  509. b"refs/heads/UPPERCASE-BRANCH",
  510. b"refs/tags/v1.0.0-alpha.1",
  511. b"refs/tags/release/2023.12.31",
  512. b"refs/remotes/origin/main",
  513. b"refs/remotes/upstream/develop",
  514. b"refs/notes/commits",
  515. ]
  516. with repo.refs.batch_update():
  517. for ref_name in special_refs:
  518. repo.refs.set_if_equals(ref_name, None, commit_sha)
  519. # Set HEAD to a normal branch
  520. repo.refs.set_if_equals(b"refs/heads/master", None, commit_sha)
  521. repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
  522. repo.close()
  523. # Verify git can read all special refs
  524. for ref_name in special_refs:
  525. ref_sha = self._run_git(["rev-parse", ref_name.decode()]).strip()
  526. self.assertEqual(ref_sha, commit_sha, f"Failed to resolve {ref_name}")
  527. # Verify show-ref includes all refs
  528. git_output = self._run_git(["show-ref"])
  529. for ref_name in special_refs:
  530. self.assertIn(ref_name, git_output, f"show-ref missing {ref_name}")
  531. def test_concurrent_ref_operations_compatibility(self):
  532. """Test compatibility with multiple ref operations."""
  533. # Initialize repo
  534. self._run_git(["init", "--bare", "."])
  535. self._run_git(["config", "core.repositoryformatversion", "1"])
  536. self._run_git(["config", "extensions.refStorage", "reftable"])
  537. # Create test objects
  538. blob_sha = self._run_git(
  539. ["hash-object", "-w", "--stdin"], input=b"test content"
  540. ).strip()
  541. tree_sha = self._run_git(
  542. ["mktree"], input=f"100644 blob {blob_sha.decode()}\tfile.txt\n".encode()
  543. ).strip()
  544. commits = []
  545. for i in range(5):
  546. commit_sha = self._run_git(
  547. ["commit-tree", tree_sha.decode(), "-m", f"Commit {i}"]
  548. ).strip()
  549. commits.append(commit_sha)
  550. repo = Repo(self.test_dir)
  551. # Simulate concurrent operations with multiple batch updates
  552. # First batch: Create initial refs
  553. with repo.refs.batch_update():
  554. repo.refs.set_if_equals(b"refs/heads/master", None, commits[0])
  555. repo.refs.set_if_equals(b"refs/heads/develop", None, commits[1])
  556. repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
  557. # Second batch: Update some refs and add new ones
  558. with repo.refs.batch_update():
  559. repo.refs.set_if_equals(
  560. b"refs/heads/master", commits[0], commits[2]
  561. ) # Update main
  562. repo.refs.set_if_equals(
  563. b"refs/heads/feature", None, commits[3]
  564. ) # Add feature
  565. repo.refs.set_if_equals(b"refs/tags/v1.0", None, commits[0]) # Add tag
  566. # Third batch: More complex operations
  567. with repo.refs.batch_update():
  568. repo.refs.set_if_equals(
  569. b"refs/heads/develop", commits[1], commits[4]
  570. ) # Update develop
  571. repo.refs.set_if_equals(
  572. b"refs/heads/feature", commits[3], None
  573. ) # Delete feature
  574. repo.refs.set_symbolic_ref(
  575. b"HEAD", b"refs/heads/develop"
  576. ) # Change HEAD target
  577. repo.close()
  578. # Verify final state with git
  579. main_sha = self._run_git(["rev-parse", "refs/heads/master"]).strip()
  580. self.assertEqual(main_sha, commits[2])
  581. develop_sha = self._run_git(["rev-parse", "refs/heads/develop"]).strip()
  582. self.assertEqual(develop_sha, commits[4])
  583. # Verify feature branch was deleted (should fail)
  584. with self.assertRaises(AssertionError):
  585. self._run_git(["rev-parse", "refs/heads/feature"])
  586. # Verify HEAD points to develop
  587. head_target = self._run_git(["symbolic-ref", "HEAD"]).strip()
  588. self.assertEqual(head_target, b"refs/heads/develop")
  589. # Verify tag exists
  590. tag_sha = self._run_git(["rev-parse", "refs/tags/v1.0"]).strip()
  591. self.assertEqual(tag_sha, commits[0])
  592. def test_reftable_gc_compatibility(self):
  593. """Test that reftables work correctly after git operations."""
  594. # Initialize repo
  595. self._run_git(["init", "--bare", "."])
  596. self._run_git(["config", "core.repositoryformatversion", "1"])
  597. self._run_git(["config", "extensions.refStorage", "reftable"])
  598. # Create initial state with dulwich
  599. blob_sha = self._run_git(
  600. ["hash-object", "-w", "--stdin"], input=b"test content"
  601. ).strip()
  602. tree_sha = self._run_git(
  603. ["mktree"], input=f"100644 blob {blob_sha.decode()}\tfile.txt\n".encode()
  604. ).strip()
  605. commit_sha = self._run_git(
  606. ["commit-tree", tree_sha.decode(), "-m", "Initial commit"]
  607. ).strip()
  608. repo = Repo(self.test_dir)
  609. with repo.refs.batch_update():
  610. repo.refs.set_if_equals(b"refs/heads/master", None, commit_sha)
  611. repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
  612. repo.close()
  613. # Perform git operations
  614. new_commit = self._run_git(
  615. ["commit-tree", tree_sha.decode(), "-m", "New commit"]
  616. ).strip()
  617. self._run_git(["update-ref", "refs/heads/master", new_commit.decode()])
  618. self._run_git(["update-ref", "refs/heads/branch2", new_commit.decode()])
  619. # Verify dulwich can still read after git modifications
  620. repo = Repo(self.test_dir)
  621. dulwich_refs = repo.get_refs()
  622. # Should be able to read git-modified refs
  623. self.assertEqual(dulwich_refs[b"refs/heads/master"], new_commit)
  624. self.assertEqual(dulwich_refs[b"refs/heads/branch2"], new_commit)
  625. # Should still resolve HEAD correctly
  626. head_sha = repo.refs[b"HEAD"]
  627. self.assertEqual(head_sha, new_commit)
  628. repo.close()
  629. if __name__ == "__main__":
  630. import unittest
  631. unittest.main()