test_reftable.py 30 KB

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