test_rerere.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. """Tests for rerere functionality."""
  2. import os
  3. import tempfile
  4. import unittest
  5. from dulwich.rerere import (
  6. RerereCache,
  7. _extract_conflict_regions,
  8. _has_conflict_markers,
  9. _normalize_conflict_markers,
  10. _remove_conflict_markers,
  11. is_rerere_autoupdate,
  12. is_rerere_enabled,
  13. )
  14. class NormalizeConflictMarkersTests(unittest.TestCase):
  15. """Tests for _normalize_conflict_markers function."""
  16. def test_normalize_basic_conflict(self) -> None:
  17. """Test normalizing a basic conflict."""
  18. content = b"""line 1
  19. <<<<<<< ours
  20. our change
  21. =======
  22. their change
  23. >>>>>>> theirs
  24. line 2
  25. """
  26. expected = b"""line 1
  27. <<<<<<<
  28. our change
  29. =======
  30. their change
  31. >>>>>>>
  32. line 2
  33. """
  34. result = _normalize_conflict_markers(content)
  35. self.assertEqual(expected, result)
  36. def test_normalize_with_branch_names(self) -> None:
  37. """Test normalizing conflict with branch names."""
  38. content = b"""<<<<<<< HEAD
  39. content from HEAD
  40. =======
  41. content from feature
  42. >>>>>>> feature
  43. """
  44. expected = b"""<<<<<<<
  45. content from HEAD
  46. =======
  47. content from feature
  48. >>>>>>>
  49. """
  50. result = _normalize_conflict_markers(content)
  51. self.assertEqual(expected, result)
  52. class ExtractConflictRegionsTests(unittest.TestCase):
  53. """Tests for _extract_conflict_regions function."""
  54. def test_extract_single_conflict(self) -> None:
  55. """Test extracting a single conflict region."""
  56. content = b"""line 1
  57. <<<<<<< ours
  58. our change
  59. =======
  60. their change
  61. >>>>>>> theirs
  62. line 2
  63. """
  64. regions = _extract_conflict_regions(content)
  65. self.assertEqual(1, len(regions))
  66. ours, sep, theirs = regions[0]
  67. self.assertEqual(b"our change", ours)
  68. self.assertEqual(b"=======", sep)
  69. self.assertEqual(b"their change", theirs)
  70. def test_extract_multiple_conflicts(self) -> None:
  71. """Test extracting multiple conflict regions."""
  72. content = b"""<<<<<<< ours
  73. change 1
  74. =======
  75. change 2
  76. >>>>>>> theirs
  77. middle line
  78. <<<<<<< ours
  79. change 3
  80. =======
  81. change 4
  82. >>>>>>> theirs
  83. """
  84. regions = _extract_conflict_regions(content)
  85. self.assertEqual(2, len(regions))
  86. class HasConflictMarkersTests(unittest.TestCase):
  87. """Tests for _has_conflict_markers function."""
  88. def test_has_conflict_markers(self) -> None:
  89. """Test detecting conflict markers."""
  90. content = b"""<<<<<<< ours
  91. our change
  92. =======
  93. their change
  94. >>>>>>> theirs
  95. """
  96. self.assertTrue(_has_conflict_markers(content))
  97. def test_no_conflict_markers(self) -> None:
  98. """Test content without conflict markers."""
  99. content = b"""line 1
  100. line 2
  101. line 3
  102. """
  103. self.assertFalse(_has_conflict_markers(content))
  104. def test_partial_conflict_markers(self) -> None:
  105. """Test content with only some conflict markers."""
  106. content = b"""<<<<<<< ours
  107. our change
  108. line 3
  109. """
  110. self.assertFalse(_has_conflict_markers(content))
  111. class RemoveConflictMarkersTests(unittest.TestCase):
  112. """Tests for _remove_conflict_markers function."""
  113. def test_remove_conflict_markers(self) -> None:
  114. """Test removing conflict markers from resolved content."""
  115. content = b"""line 1
  116. <<<<<<< ours
  117. our change
  118. =======
  119. their change
  120. >>>>>>> theirs
  121. line 2
  122. """
  123. # This is a simplified test - in reality the resolved content
  124. # would have the user's chosen resolution
  125. result = _remove_conflict_markers(content)
  126. # The function keeps only lines outside conflict blocks
  127. self.assertNotIn(b"<<<<<<<", result)
  128. self.assertNotIn(b"=======", result)
  129. self.assertNotIn(b">>>>>>>", result)
  130. class RerereCacheTests(unittest.TestCase):
  131. """Tests for RerereCache class."""
  132. def setUp(self) -> None:
  133. """Set up test fixtures."""
  134. self.tempdir = tempfile.mkdtemp()
  135. self.cache = RerereCache(self.tempdir)
  136. def tearDown(self) -> None:
  137. """Clean up test fixtures."""
  138. import shutil
  139. shutil.rmtree(self.tempdir, ignore_errors=True)
  140. def test_record_conflict(self) -> None:
  141. """Test recording a conflict."""
  142. content = b"""line 1
  143. <<<<<<< ours
  144. our change
  145. =======
  146. their change
  147. >>>>>>> theirs
  148. line 2
  149. """
  150. conflict_id = self.cache.record_conflict(b"test.txt", content)
  151. self.assertIsNotNone(conflict_id)
  152. self.assertEqual(40, len(conflict_id)) # SHA-1 hash length
  153. def test_record_conflict_no_markers(self) -> None:
  154. """Test recording content without conflict markers."""
  155. content = b"line 1\nline 2\n"
  156. conflict_id = self.cache.record_conflict(b"test.txt", content)
  157. self.assertIsNone(conflict_id)
  158. def test_status_empty(self) -> None:
  159. """Test status with no conflicts."""
  160. status = self.cache.status()
  161. self.assertEqual([], status)
  162. def test_status_with_conflict(self) -> None:
  163. """Test status with a recorded conflict."""
  164. content = b"""<<<<<<< ours
  165. our change
  166. =======
  167. their change
  168. >>>>>>> theirs
  169. """
  170. conflict_id = self.cache.record_conflict(b"test.txt", content)
  171. status = self.cache.status()
  172. self.assertEqual(1, len(status))
  173. cid, has_resolution = status[0]
  174. self.assertEqual(conflict_id, cid)
  175. self.assertFalse(has_resolution)
  176. def test_has_resolution(self) -> None:
  177. """Test checking for resolution."""
  178. content = b"""<<<<<<< ours
  179. our change
  180. =======
  181. their change
  182. >>>>>>> theirs
  183. """
  184. conflict_id = self.cache.record_conflict(b"test.txt", content)
  185. self.assertIsNotNone(conflict_id)
  186. self.assertFalse(self.cache.has_resolution(conflict_id))
  187. def test_diff(self) -> None:
  188. """Test getting diff for a conflict."""
  189. content = b"""<<<<<<< ours
  190. our change
  191. =======
  192. their change
  193. >>>>>>> theirs
  194. """
  195. conflict_id = self.cache.record_conflict(b"test.txt", content)
  196. self.assertIsNotNone(conflict_id)
  197. preimage, postimage = self.cache.diff(conflict_id)
  198. self.assertIsNotNone(preimage)
  199. self.assertIsNone(postimage) # No resolution recorded yet
  200. def test_clear(self) -> None:
  201. """Test clearing all conflicts."""
  202. content = b"""<<<<<<< ours
  203. our change
  204. =======
  205. their change
  206. >>>>>>> theirs
  207. """
  208. self.cache.record_conflict(b"test.txt", content)
  209. status = self.cache.status()
  210. self.assertEqual(1, len(status))
  211. self.cache.clear()
  212. status = self.cache.status()
  213. self.assertEqual([], status)
  214. def test_forget(self) -> None:
  215. """Test forgetting a specific conflict."""
  216. content = b"""<<<<<<< ours
  217. our change
  218. =======
  219. their change
  220. >>>>>>> theirs
  221. """
  222. conflict_id = self.cache.record_conflict(b"test.txt", content)
  223. self.assertIsNotNone(conflict_id)
  224. self.cache.forget(conflict_id)
  225. status = self.cache.status()
  226. self.assertEqual([], status)
  227. class ConfigTests(unittest.TestCase):
  228. """Tests for rerere configuration functions."""
  229. def test_is_rerere_enabled_false_by_default(self) -> None:
  230. """Test that rerere is disabled by default."""
  231. from dulwich.config import ConfigDict
  232. config = ConfigDict()
  233. self.assertFalse(is_rerere_enabled(config))
  234. def test_is_rerere_enabled_true(self) -> None:
  235. """Test rerere enabled config."""
  236. from dulwich.config import ConfigDict
  237. config = ConfigDict()
  238. config.set((b"rerere",), b"enabled", b"true")
  239. self.assertTrue(is_rerere_enabled(config))
  240. def test_is_rerere_autoupdate_false_by_default(self) -> None:
  241. """Test that rerere.autoupdate is disabled by default."""
  242. from dulwich.config import ConfigDict
  243. config = ConfigDict()
  244. self.assertFalse(is_rerere_autoupdate(config))
  245. def test_is_rerere_autoupdate_true(self) -> None:
  246. """Test rerere.autoupdate enabled config."""
  247. from dulwich.config import ConfigDict
  248. config = ConfigDict()
  249. config.set((b"rerere",), b"autoupdate", b"true")
  250. self.assertTrue(is_rerere_autoupdate(config))
  251. class RerereAutoTests(unittest.TestCase):
  252. """Tests for rerere_auto functionality."""
  253. def setUp(self) -> None:
  254. """Set up test fixtures."""
  255. from dulwich.repo import Repo
  256. self.tempdir = tempfile.mkdtemp()
  257. self.repo = Repo.init(self.tempdir)
  258. # Enable rerere
  259. config = self.repo.get_config()
  260. config.set((b"rerere",), b"enabled", b"true")
  261. config.write_to_path()
  262. def tearDown(self) -> None:
  263. """Clean up test fixtures."""
  264. import shutil
  265. shutil.rmtree(self.tempdir, ignore_errors=True)
  266. def test_rerere_auto_disabled(self) -> None:
  267. """Test that rerere_auto does nothing when disabled."""
  268. from dulwich.rerere import rerere_auto
  269. # Disable rerere
  270. config = self.repo.get_config()
  271. config.set((b"rerere",), b"enabled", b"false")
  272. config.write_to_path()
  273. # Create a fake conflicted file
  274. conflict_file = os.path.join(self.tempdir, "test.txt")
  275. with open(conflict_file, "wb") as f:
  276. f.write(
  277. b"""<<<<<<< ours
  278. our change
  279. =======
  280. their change
  281. >>>>>>> theirs
  282. """
  283. )
  284. recorded, resolved = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
  285. self.assertEqual([], recorded)
  286. self.assertEqual([], resolved)
  287. def test_rerere_auto_records_conflicts(self) -> None:
  288. """Test that rerere_auto records conflicts from working tree."""
  289. from dulwich.rerere import rerere_auto
  290. # Create a conflicted file in the working tree
  291. conflict_file = os.path.join(self.tempdir, "test.txt")
  292. with open(conflict_file, "wb") as f:
  293. f.write(
  294. b"""line 1
  295. <<<<<<< ours
  296. our change
  297. =======
  298. their change
  299. >>>>>>> theirs
  300. line 2
  301. """
  302. )
  303. recorded, resolved = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
  304. self.assertEqual(1, len(recorded))
  305. self.assertEqual(0, len(resolved))
  306. path, conflict_id = recorded[0]
  307. self.assertEqual(b"test.txt", path)
  308. self.assertEqual(40, len(conflict_id)) # SHA-1 hash length
  309. def test_rerere_auto_skips_non_conflicted_files(self) -> None:
  310. """Test that rerere_auto skips files without conflict markers."""
  311. from dulwich.rerere import rerere_auto
  312. # Create a non-conflicted file
  313. file_path = os.path.join(self.tempdir, "test.txt")
  314. with open(file_path, "wb") as f:
  315. f.write(b"line 1\nline 2\n")
  316. recorded, resolved = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
  317. self.assertEqual([], recorded)
  318. self.assertEqual([], resolved)
  319. def test_rerere_auto_handles_missing_files(self) -> None:
  320. """Test that rerere_auto handles deleted files gracefully."""
  321. from dulwich.rerere import rerere_auto
  322. # Don't create the file
  323. recorded, resolved = rerere_auto(self.repo, self.tempdir, [b"missing.txt"])
  324. self.assertEqual([], recorded)
  325. self.assertEqual([], resolved)
  326. def test_rerere_auto_applies_known_resolution(self) -> None:
  327. """Test that rerere_auto applies known resolutions when autoupdate is enabled."""
  328. from dulwich.rerere import RerereCache, rerere_auto
  329. # Enable autoupdate
  330. config = self.repo.get_config()
  331. config.set((b"rerere",), b"autoupdate", b"true")
  332. config.write_to_path()
  333. # Create a conflicted file
  334. conflict_file = os.path.join(self.tempdir, "test.txt")
  335. conflict_content = b"""line 1
  336. <<<<<<< ours
  337. our change
  338. =======
  339. their change
  340. >>>>>>> theirs
  341. line 2
  342. """
  343. with open(conflict_file, "wb") as f:
  344. f.write(conflict_content)
  345. # Record the conflict first time
  346. recorded, resolved = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
  347. self.assertEqual(1, len(recorded))
  348. self.assertEqual(0, len(resolved)) # No resolution yet
  349. conflict_id = recorded[0][1]
  350. # Manually record a resolution
  351. cache = RerereCache.from_repo(self.repo)
  352. resolution = b"line 1\nresolved change\nline 2\n"
  353. cache.record_resolution(conflict_id, resolution)
  354. # Create the same conflict again
  355. with open(conflict_file, "wb") as f:
  356. f.write(conflict_content)
  357. # rerere_auto should now apply the resolution
  358. recorded2, resolved2 = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
  359. self.assertEqual(1, len(recorded2))
  360. self.assertEqual(1, len(resolved2))
  361. self.assertEqual(b"test.txt", resolved2[0])
  362. # Verify the file was resolved
  363. with open(conflict_file, "rb") as f:
  364. actual = f.read()
  365. self.assertEqual(resolution, actual)
  366. def test_rerere_auto_no_apply_without_autoupdate(self) -> None:
  367. """Test that rerere_auto doesn't apply resolutions when autoupdate is disabled."""
  368. from dulwich.rerere import RerereCache, rerere_auto
  369. # autoupdate is disabled by default
  370. # Create a conflicted file
  371. conflict_file = os.path.join(self.tempdir, "test.txt")
  372. conflict_content = b"""line 1
  373. <<<<<<< ours
  374. our change
  375. =======
  376. their change
  377. >>>>>>> theirs
  378. line 2
  379. """
  380. with open(conflict_file, "wb") as f:
  381. f.write(conflict_content)
  382. # Record the conflict first time
  383. recorded, _resolved = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
  384. conflict_id = recorded[0][1]
  385. # Manually record a resolution
  386. cache = RerereCache.from_repo(self.repo)
  387. resolution = b"line 1\nresolved change\nline 2\n"
  388. cache.record_resolution(conflict_id, resolution)
  389. # Create the same conflict again
  390. with open(conflict_file, "wb") as f:
  391. f.write(conflict_content)
  392. # rerere_auto should NOT apply the resolution (autoupdate disabled)
  393. recorded2, resolved2 = rerere_auto(self.repo, self.tempdir, [b"test.txt"])
  394. self.assertEqual(1, len(recorded2))
  395. self.assertEqual(0, len(resolved2)) # Should not auto-apply
  396. # Verify the file still has conflicts
  397. with open(conflict_file, "rb") as f:
  398. actual = f.read()
  399. self.assertEqual(conflict_content, actual)
  400. class RerereEndToEndTests(unittest.TestCase):
  401. """End-to-end tests for rerere with real merge operations."""
  402. def setUp(self) -> None:
  403. """Set up test fixtures."""
  404. from dulwich.objects import Blob, Commit, Tree
  405. from dulwich.repo import Repo
  406. self.tempdir = tempfile.mkdtemp()
  407. self.repo = Repo.init(self.tempdir)
  408. # Enable rerere
  409. config = self.repo.get_config()
  410. config.set((b"rerere",), b"enabled", b"true")
  411. config.write_to_path()
  412. # Create initial commit on master
  413. blob1 = Blob.from_string(b"line 1\noriginal line\nline 3\n")
  414. self.repo.object_store.add_object(blob1)
  415. tree1 = Tree()
  416. tree1.add(b"file.txt", 0o100644, blob1.id)
  417. self.repo.object_store.add_object(tree1)
  418. commit1 = Commit()
  419. commit1.tree = tree1.id
  420. commit1.author = commit1.committer = b"Test User <test@example.com>"
  421. commit1.author_time = commit1.commit_time = 1234567890
  422. commit1.author_timezone = commit1.commit_timezone = 0
  423. commit1.encoding = b"UTF-8"
  424. commit1.message = b"Initial commit"
  425. self.repo.object_store.add_object(commit1)
  426. self.repo.refs[b"refs/heads/master"] = commit1.id
  427. self.repo.refs[b"HEAD"] = commit1.id
  428. # Write file to working tree
  429. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  430. f.write(b"line 1\noriginal line\nline 3\n")
  431. self.initial_commit = commit1.id
  432. def tearDown(self) -> None:
  433. """Clean up test fixtures."""
  434. import shutil
  435. shutil.rmtree(self.tempdir, ignore_errors=True)
  436. def test_rerere_full_workflow(self) -> None:
  437. """Test complete rerere workflow with real merge conflicts."""
  438. from dulwich.diff_tree import tree_changes
  439. from dulwich.graph import find_merge_base
  440. from dulwich.index import update_working_tree
  441. from dulwich.merge import recursive_merge
  442. from dulwich.objects import Blob, Commit, Tree
  443. from dulwich.rerere import rerere_auto
  444. # Create branch1: change "original line" to "branch1 change"
  445. blob_branch1 = Blob.from_string(b"line 1\nbranch1 change\nline 3\n")
  446. self.repo.object_store.add_object(blob_branch1)
  447. tree_branch1 = Tree()
  448. tree_branch1.add(b"file.txt", 0o100644, blob_branch1.id)
  449. self.repo.object_store.add_object(tree_branch1)
  450. commit_branch1 = Commit()
  451. commit_branch1.tree = tree_branch1.id
  452. commit_branch1.parents = [self.initial_commit]
  453. commit_branch1.author = commit_branch1.committer = (
  454. b"Test User <test@example.com>"
  455. )
  456. commit_branch1.author_time = commit_branch1.commit_time = 1234567891
  457. commit_branch1.author_timezone = commit_branch1.commit_timezone = 0
  458. commit_branch1.encoding = b"UTF-8"
  459. commit_branch1.message = b"Branch1 changes"
  460. self.repo.object_store.add_object(commit_branch1)
  461. self.repo.refs[b"refs/heads/branch1"] = commit_branch1.id
  462. # Create branch2: change "original line" to "branch2 change"
  463. blob_branch2 = Blob.from_string(b"line 1\nbranch2 change\nline 3\n")
  464. self.repo.object_store.add_object(blob_branch2)
  465. tree_branch2 = Tree()
  466. tree_branch2.add(b"file.txt", 0o100644, blob_branch2.id)
  467. self.repo.object_store.add_object(tree_branch2)
  468. commit_branch2 = Commit()
  469. commit_branch2.tree = tree_branch2.id
  470. commit_branch2.parents = [self.initial_commit]
  471. commit_branch2.author = commit_branch2.committer = (
  472. b"Test User <test@example.com>"
  473. )
  474. commit_branch2.author_time = commit_branch2.commit_time = 1234567892
  475. commit_branch2.author_timezone = commit_branch2.commit_timezone = 0
  476. commit_branch2.encoding = b"UTF-8"
  477. commit_branch2.message = b"Branch2 changes"
  478. self.repo.object_store.add_object(commit_branch2)
  479. self.repo.refs[b"refs/heads/branch2"] = commit_branch2.id
  480. # Checkout branch1
  481. self.repo.refs[b"HEAD"] = commit_branch1.id
  482. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  483. f.write(b"line 1\nbranch1 change\nline 3\n")
  484. # Merge branch2 into branch1 - should create conflict
  485. # Using lower-level merge APIs
  486. head_commit = commit_branch1
  487. merge_commit = commit_branch2
  488. merge_bases = find_merge_base(self.repo, [head_commit.id, merge_commit.id])
  489. gitattributes = self.repo.get_gitattributes()
  490. config = self.repo.get_config()
  491. merged_tree, conflicts = recursive_merge(
  492. self.repo.object_store,
  493. merge_bases,
  494. head_commit,
  495. merge_commit,
  496. gitattributes,
  497. config,
  498. )
  499. self.repo.object_store.add_object(merged_tree)
  500. changes = tree_changes(self.repo.object_store, head_commit.tree, merged_tree.id)
  501. update_working_tree(
  502. self.repo, head_commit.tree, merged_tree.id, change_iterator=changes
  503. )
  504. # Should have conflicts
  505. self.assertEqual([b"file.txt"], conflicts)
  506. # File should have conflict markers
  507. with open(os.path.join(self.tempdir, "file.txt"), "rb") as f:
  508. content = f.read()
  509. self.assertIn(b"<<<<<<<", content)
  510. self.assertIn(b"branch1 change", content)
  511. self.assertIn(b"branch2 change", content)
  512. # Record the conflict with rerere
  513. recorded, resolved = rerere_auto(self.repo, self.tempdir, conflicts)
  514. self.assertEqual(1, len(recorded))
  515. self.assertEqual(0, len(resolved)) # No resolution yet
  516. conflict_id = recorded[0][1]
  517. # User manually resolves the conflict
  518. resolved_content = b"line 1\nmerged change\nline 3\n"
  519. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  520. f.write(resolved_content)
  521. # Record the resolution
  522. from dulwich.rerere import RerereCache
  523. cache = RerereCache.from_repo(self.repo)
  524. cache.record_resolution(conflict_id, resolved_content)
  525. # Reset to initial state and try the merge again
  526. self.repo.refs[b"HEAD"] = commit_branch1.id
  527. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  528. f.write(b"line 1\nbranch1 change\nline 3\n")
  529. # Merge again - should create same conflict
  530. merge_bases2 = find_merge_base(
  531. self.repo, [commit_branch1.id, commit_branch2.id]
  532. )
  533. merged_tree2, conflicts2 = recursive_merge(
  534. self.repo.object_store,
  535. merge_bases2,
  536. commit_branch1,
  537. commit_branch2,
  538. gitattributes,
  539. config,
  540. )
  541. self.repo.object_store.add_object(merged_tree2)
  542. changes2 = tree_changes(
  543. self.repo.object_store, commit_branch1.tree, merged_tree2.id
  544. )
  545. update_working_tree(
  546. self.repo, commit_branch1.tree, merged_tree2.id, change_iterator=changes2
  547. )
  548. self.assertEqual([b"file.txt"], conflicts2)
  549. # Now rerere should recognize the conflict
  550. recorded2, resolved2 = rerere_auto(self.repo, self.tempdir, conflicts2)
  551. self.assertEqual(1, len(recorded2))
  552. # With autoupdate disabled, it shouldn't auto-apply
  553. self.assertEqual(0, len(resolved2))
  554. def test_rerere_with_autoupdate(self) -> None:
  555. """Test rerere with autoupdate enabled."""
  556. from dulwich.diff_tree import tree_changes
  557. from dulwich.graph import find_merge_base
  558. from dulwich.index import update_working_tree
  559. from dulwich.merge import recursive_merge
  560. from dulwich.objects import Blob, Commit, Tree
  561. from dulwich.rerere import RerereCache, rerere_auto
  562. # Enable autoupdate
  563. config = self.repo.get_config()
  564. config.set((b"rerere",), b"autoupdate", b"true")
  565. config.write_to_path()
  566. # Create branch1
  567. blob_branch1 = Blob.from_string(b"line 1\nbranch1 change\nline 3\n")
  568. self.repo.object_store.add_object(blob_branch1)
  569. tree_branch1 = Tree()
  570. tree_branch1.add(b"file.txt", 0o100644, blob_branch1.id)
  571. self.repo.object_store.add_object(tree_branch1)
  572. commit_branch1 = Commit()
  573. commit_branch1.tree = tree_branch1.id
  574. commit_branch1.parents = [self.initial_commit]
  575. commit_branch1.author = commit_branch1.committer = (
  576. b"Test User <test@example.com>"
  577. )
  578. commit_branch1.author_time = commit_branch1.commit_time = 1234567891
  579. commit_branch1.author_timezone = commit_branch1.commit_timezone = 0
  580. commit_branch1.encoding = b"UTF-8"
  581. commit_branch1.message = b"Branch1 changes"
  582. self.repo.object_store.add_object(commit_branch1)
  583. self.repo.refs[b"refs/heads/branch1"] = commit_branch1.id
  584. # Create branch2
  585. blob_branch2 = Blob.from_string(b"line 1\nbranch2 change\nline 3\n")
  586. self.repo.object_store.add_object(blob_branch2)
  587. tree_branch2 = Tree()
  588. tree_branch2.add(b"file.txt", 0o100644, blob_branch2.id)
  589. self.repo.object_store.add_object(tree_branch2)
  590. commit_branch2 = Commit()
  591. commit_branch2.tree = tree_branch2.id
  592. commit_branch2.parents = [self.initial_commit]
  593. commit_branch2.author = commit_branch2.committer = (
  594. b"Test User <test@example.com>"
  595. )
  596. commit_branch2.author_time = commit_branch2.commit_time = 1234567892
  597. commit_branch2.author_timezone = commit_branch2.commit_timezone = 0
  598. commit_branch2.encoding = b"UTF-8"
  599. commit_branch2.message = b"Branch2 changes"
  600. self.repo.object_store.add_object(commit_branch2)
  601. self.repo.refs[b"refs/heads/branch2"] = commit_branch2.id
  602. # Checkout branch1 and merge branch2
  603. self.repo.refs[b"HEAD"] = commit_branch1.id
  604. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  605. f.write(b"line 1\nbranch1 change\nline 3\n")
  606. # Perform merge using lower-level APIs
  607. merge_bases = find_merge_base(self.repo, [commit_branch1.id, commit_branch2.id])
  608. gitattributes = self.repo.get_gitattributes()
  609. config = self.repo.get_config()
  610. merged_tree, conflicts = recursive_merge(
  611. self.repo.object_store,
  612. merge_bases,
  613. commit_branch1,
  614. commit_branch2,
  615. gitattributes,
  616. config,
  617. )
  618. self.repo.object_store.add_object(merged_tree)
  619. changes = tree_changes(
  620. self.repo.object_store, commit_branch1.tree, merged_tree.id
  621. )
  622. update_working_tree(
  623. self.repo, commit_branch1.tree, merged_tree.id, change_iterator=changes
  624. )
  625. # Record conflict and resolution
  626. recorded, _ = rerere_auto(self.repo, self.tempdir, conflicts)
  627. conflict_id = recorded[0][1]
  628. resolved_content = b"line 1\nmerged change\nline 3\n"
  629. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  630. f.write(resolved_content)
  631. cache = RerereCache.from_repo(self.repo)
  632. cache.record_resolution(conflict_id, resolved_content)
  633. # Reset and merge again
  634. self.repo.refs[b"HEAD"] = commit_branch1.id
  635. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  636. f.write(b"line 1\nbranch1 change\nline 3\n")
  637. # Perform merge again using lower-level APIs
  638. merge_bases2 = find_merge_base(
  639. self.repo, [commit_branch1.id, commit_branch2.id]
  640. )
  641. merged_tree2, conflicts2 = recursive_merge(
  642. self.repo.object_store,
  643. merge_bases2,
  644. commit_branch1,
  645. commit_branch2,
  646. gitattributes,
  647. config,
  648. )
  649. self.repo.object_store.add_object(merged_tree2)
  650. changes2 = tree_changes(
  651. self.repo.object_store, commit_branch1.tree, merged_tree2.id
  652. )
  653. update_working_tree(
  654. self.repo, commit_branch1.tree, merged_tree2.id, change_iterator=changes2
  655. )
  656. # With autoupdate, rerere should auto-apply the resolution
  657. recorded2, resolved2 = rerere_auto(self.repo, self.tempdir, conflicts2)
  658. self.assertEqual(1, len(recorded2))
  659. self.assertEqual(1, len(resolved2))
  660. self.assertEqual(b"file.txt", resolved2[0])
  661. # Verify the file was auto-resolved
  662. with open(os.path.join(self.tempdir, "file.txt"), "rb") as f:
  663. actual = f.read()
  664. self.assertEqual(resolved_content, actual)