test_rerere.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  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.objects import Blob, Commit, Tree
  439. from dulwich.porcelain import merge, rerere
  440. # Create branch1: change "original line" to "branch1 change"
  441. blob_branch1 = Blob.from_string(b"line 1\nbranch1 change\nline 3\n")
  442. self.repo.object_store.add_object(blob_branch1)
  443. tree_branch1 = Tree()
  444. tree_branch1.add(b"file.txt", 0o100644, blob_branch1.id)
  445. self.repo.object_store.add_object(tree_branch1)
  446. commit_branch1 = Commit()
  447. commit_branch1.tree = tree_branch1.id
  448. commit_branch1.parents = [self.initial_commit]
  449. commit_branch1.author = commit_branch1.committer = (
  450. b"Test User <test@example.com>"
  451. )
  452. commit_branch1.author_time = commit_branch1.commit_time = 1234567891
  453. commit_branch1.author_timezone = commit_branch1.commit_timezone = 0
  454. commit_branch1.encoding = b"UTF-8"
  455. commit_branch1.message = b"Branch1 changes"
  456. self.repo.object_store.add_object(commit_branch1)
  457. self.repo.refs[b"refs/heads/branch1"] = commit_branch1.id
  458. # Create branch2: change "original line" to "branch2 change"
  459. blob_branch2 = Blob.from_string(b"line 1\nbranch2 change\nline 3\n")
  460. self.repo.object_store.add_object(blob_branch2)
  461. tree_branch2 = Tree()
  462. tree_branch2.add(b"file.txt", 0o100644, blob_branch2.id)
  463. self.repo.object_store.add_object(tree_branch2)
  464. commit_branch2 = Commit()
  465. commit_branch2.tree = tree_branch2.id
  466. commit_branch2.parents = [self.initial_commit]
  467. commit_branch2.author = commit_branch2.committer = (
  468. b"Test User <test@example.com>"
  469. )
  470. commit_branch2.author_time = commit_branch2.commit_time = 1234567892
  471. commit_branch2.author_timezone = commit_branch2.commit_timezone = 0
  472. commit_branch2.encoding = b"UTF-8"
  473. commit_branch2.message = b"Branch2 changes"
  474. self.repo.object_store.add_object(commit_branch2)
  475. self.repo.refs[b"refs/heads/branch2"] = commit_branch2.id
  476. # Checkout branch1
  477. self.repo.refs[b"HEAD"] = commit_branch1.id
  478. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  479. f.write(b"line 1\nbranch1 change\nline 3\n")
  480. # Merge branch2 into branch1 - should create conflict
  481. merge_result, conflicts = merge(self.repo, b"branch2", no_commit=True)
  482. # Should have conflicts
  483. self.assertIsNone(merge_result) # No commit created due to conflicts
  484. self.assertEqual([b"file.txt"], conflicts)
  485. # File should have conflict markers
  486. with open(os.path.join(self.tempdir, "file.txt"), "rb") as f:
  487. content = f.read()
  488. self.assertIn(b"<<<<<<<", content)
  489. self.assertIn(b"branch1 change", content)
  490. self.assertIn(b"branch2 change", content)
  491. # Record the conflict with rerere
  492. recorded, resolved = rerere(self.repo)
  493. self.assertEqual(1, len(recorded))
  494. self.assertEqual(0, len(resolved)) # No resolution yet
  495. conflict_id = recorded[0][1]
  496. # User manually resolves the conflict
  497. resolved_content = b"line 1\nmerged change\nline 3\n"
  498. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  499. f.write(resolved_content)
  500. # Record the resolution
  501. from dulwich.rerere import RerereCache
  502. cache = RerereCache.from_repo(self.repo)
  503. cache.record_resolution(conflict_id, resolved_content)
  504. # Reset to initial state and try the merge again
  505. self.repo.refs[b"HEAD"] = commit_branch1.id
  506. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  507. f.write(b"line 1\nbranch1 change\nline 3\n")
  508. # Merge again - should create same conflict
  509. _merge_result2, conflicts2 = merge(self.repo, b"branch2", no_commit=True)
  510. self.assertEqual([b"file.txt"], conflicts2)
  511. # Now rerere should recognize the conflict
  512. recorded2, resolved2 = rerere(self.repo)
  513. self.assertEqual(1, len(recorded2))
  514. # With autoupdate disabled, it shouldn't auto-apply
  515. self.assertEqual(0, len(resolved2))
  516. def test_rerere_with_autoupdate(self) -> None:
  517. """Test rerere with autoupdate enabled."""
  518. from dulwich.objects import Blob, Commit, Tree
  519. from dulwich.porcelain import merge, rerere
  520. from dulwich.rerere import RerereCache
  521. # Enable autoupdate
  522. config = self.repo.get_config()
  523. config.set((b"rerere",), b"autoupdate", b"true")
  524. config.write_to_path()
  525. # Create branch1
  526. blob_branch1 = Blob.from_string(b"line 1\nbranch1 change\nline 3\n")
  527. self.repo.object_store.add_object(blob_branch1)
  528. tree_branch1 = Tree()
  529. tree_branch1.add(b"file.txt", 0o100644, blob_branch1.id)
  530. self.repo.object_store.add_object(tree_branch1)
  531. commit_branch1 = Commit()
  532. commit_branch1.tree = tree_branch1.id
  533. commit_branch1.parents = [self.initial_commit]
  534. commit_branch1.author = commit_branch1.committer = (
  535. b"Test User <test@example.com>"
  536. )
  537. commit_branch1.author_time = commit_branch1.commit_time = 1234567891
  538. commit_branch1.author_timezone = commit_branch1.commit_timezone = 0
  539. commit_branch1.encoding = b"UTF-8"
  540. commit_branch1.message = b"Branch1 changes"
  541. self.repo.object_store.add_object(commit_branch1)
  542. self.repo.refs[b"refs/heads/branch1"] = commit_branch1.id
  543. # Create branch2
  544. blob_branch2 = Blob.from_string(b"line 1\nbranch2 change\nline 3\n")
  545. self.repo.object_store.add_object(blob_branch2)
  546. tree_branch2 = Tree()
  547. tree_branch2.add(b"file.txt", 0o100644, blob_branch2.id)
  548. self.repo.object_store.add_object(tree_branch2)
  549. commit_branch2 = Commit()
  550. commit_branch2.tree = tree_branch2.id
  551. commit_branch2.parents = [self.initial_commit]
  552. commit_branch2.author = commit_branch2.committer = (
  553. b"Test User <test@example.com>"
  554. )
  555. commit_branch2.author_time = commit_branch2.commit_time = 1234567892
  556. commit_branch2.author_timezone = commit_branch2.commit_timezone = 0
  557. commit_branch2.encoding = b"UTF-8"
  558. commit_branch2.message = b"Branch2 changes"
  559. self.repo.object_store.add_object(commit_branch2)
  560. self.repo.refs[b"refs/heads/branch2"] = commit_branch2.id
  561. # Checkout branch1 and merge branch2
  562. self.repo.refs[b"HEAD"] = commit_branch1.id
  563. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  564. f.write(b"line 1\nbranch1 change\nline 3\n")
  565. merge(self.repo, b"branch2", no_commit=True)
  566. # Record conflict and resolution
  567. recorded, _ = rerere(self.repo)
  568. conflict_id = recorded[0][1]
  569. resolved_content = b"line 1\nmerged change\nline 3\n"
  570. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  571. f.write(resolved_content)
  572. cache = RerereCache.from_repo(self.repo)
  573. cache.record_resolution(conflict_id, resolved_content)
  574. # Reset and merge again
  575. self.repo.refs[b"HEAD"] = commit_branch1.id
  576. with open(os.path.join(self.tempdir, "file.txt"), "wb") as f:
  577. f.write(b"line 1\nbranch1 change\nline 3\n")
  578. merge(self.repo, b"branch2", no_commit=True)
  579. # With autoupdate, rerere should auto-apply the resolution
  580. recorded2, resolved2 = rerere(self.repo)
  581. self.assertEqual(1, len(recorded2))
  582. self.assertEqual(1, len(resolved2))
  583. self.assertEqual(b"file.txt", resolved2[0])
  584. # Verify the file was auto-resolved
  585. with open(os.path.join(self.tempdir, "file.txt"), "rb") as f:
  586. actual = f.read()
  587. self.assertEqual(resolved_content, actual)