test_rerere.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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)