test_merge.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. # test_porcelain_merge.py -- Tests for porcelain merge functionality
  2. # Copyright (C) 2024 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as published by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Tests for porcelain merge functionality."""
  22. import importlib.util
  23. import os
  24. import tempfile
  25. import unittest
  26. from dulwich import porcelain
  27. from dulwich.objects import ZERO_SHA
  28. from dulwich.repo import Repo
  29. from .. import DependencyMissing, TestCase
  30. class PorcelainMergeTests(TestCase):
  31. """Tests for the porcelain merge functionality."""
  32. def test_merge_fast_forward(self):
  33. """Test fast-forward merge."""
  34. with tempfile.TemporaryDirectory() as tmpdir:
  35. # Initialize repo
  36. porcelain.init(tmpdir)
  37. # Create initial commit
  38. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  39. f.write("Initial content\n")
  40. porcelain.add(tmpdir, paths=["file1.txt"])
  41. porcelain.commit(tmpdir, message=b"Initial commit")
  42. # Create a branch
  43. porcelain.branch_create(tmpdir, "feature")
  44. porcelain.checkout(tmpdir, "feature")
  45. # Add a file on feature branch
  46. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  47. f.write("Feature content\n")
  48. porcelain.add(tmpdir, paths=["file2.txt"])
  49. feature_commit = porcelain.commit(tmpdir, message=b"Add feature")
  50. # Go back to master
  51. porcelain.checkout(tmpdir, "master")
  52. # Merge feature branch (should fast-forward)
  53. merge_commit, conflicts = porcelain.merge(tmpdir, "feature")
  54. self.assertEqual(merge_commit, feature_commit)
  55. self.assertEqual(conflicts, [])
  56. # Check that file2.txt exists
  57. self.assertTrue(os.path.exists(os.path.join(tmpdir, "file2.txt")))
  58. def test_merge_already_up_to_date(self):
  59. """Test merge when already up to date."""
  60. with tempfile.TemporaryDirectory() as tmpdir:
  61. # Initialize repo
  62. porcelain.init(tmpdir)
  63. # Create initial commit
  64. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  65. f.write("Initial content\n")
  66. porcelain.add(tmpdir, paths=["file1.txt"])
  67. porcelain.commit(tmpdir, message=b"Initial commit")
  68. # Try to merge the same commit
  69. merge_commit, conflicts = porcelain.merge(tmpdir, "HEAD")
  70. self.assertIsNone(merge_commit)
  71. self.assertEqual(conflicts, [])
  72. def test_merge_no_ff(self):
  73. """Test merge with --no-ff flag."""
  74. with tempfile.TemporaryDirectory() as tmpdir:
  75. # Initialize repo
  76. porcelain.init(tmpdir)
  77. # Create initial commit
  78. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  79. f.write("Initial content\n")
  80. porcelain.add(tmpdir, paths=["file1.txt"])
  81. porcelain.commit(tmpdir, message=b"Initial commit")
  82. # Create a branch
  83. porcelain.branch_create(tmpdir, "feature")
  84. porcelain.checkout(tmpdir, "feature")
  85. # Add a file on feature branch
  86. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  87. f.write("Feature content\n")
  88. porcelain.add(tmpdir, paths=["file2.txt"])
  89. feature_commit = porcelain.commit(tmpdir, message=b"Add feature")
  90. # Go back to master
  91. porcelain.checkout(tmpdir, "master")
  92. # Merge feature branch with no-ff
  93. merge_commit, conflicts = porcelain.merge(tmpdir, "feature", no_ff=True)
  94. # Should create a new merge commit
  95. self.assertIsNotNone(merge_commit)
  96. self.assertNotEqual(merge_commit, feature_commit)
  97. self.assertEqual(conflicts, [])
  98. # Check that it's a merge commit with two parents
  99. with Repo(tmpdir) as repo:
  100. commit = repo[merge_commit]
  101. self.assertEqual(len(commit.parents), 2)
  102. def test_merge_three_way(self):
  103. """Test three-way merge without conflicts."""
  104. with tempfile.TemporaryDirectory() as tmpdir:
  105. # Initialize repo
  106. porcelain.init(tmpdir)
  107. # Create initial commit
  108. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  109. f.write("Initial content\n")
  110. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  111. f.write("Initial file2\n")
  112. porcelain.add(tmpdir, paths=["file1.txt", "file2.txt"])
  113. porcelain.commit(tmpdir, message=b"Initial commit")
  114. # Create a branch and modify file1
  115. porcelain.branch_create(tmpdir, "feature")
  116. porcelain.checkout(tmpdir, "feature")
  117. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  118. f.write("Feature content\n")
  119. porcelain.add(tmpdir, paths=["file1.txt"])
  120. porcelain.commit(tmpdir, message=b"Modify file1 in feature")
  121. # Go back to master and modify file2
  122. porcelain.checkout(tmpdir, "master")
  123. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  124. f.write("Master file2\n")
  125. porcelain.add(tmpdir, paths=["file2.txt"])
  126. porcelain.commit(tmpdir, message=b"Modify file2 in master")
  127. # Merge feature branch
  128. merge_commit, conflicts = porcelain.merge(tmpdir, "feature")
  129. self.assertIsNotNone(merge_commit)
  130. self.assertEqual(conflicts, [])
  131. # Check both modifications are present
  132. with open(os.path.join(tmpdir, "file1.txt")) as f:
  133. self.assertEqual(f.read(), "Feature content\n")
  134. with open(os.path.join(tmpdir, "file2.txt")) as f:
  135. self.assertEqual(f.read(), "Master file2\n")
  136. def test_merge_with_conflicts(self):
  137. # Check if merge3 module is available
  138. if importlib.util.find_spec("merge3") is None:
  139. raise DependencyMissing("merge3")
  140. """Test merge with conflicts."""
  141. with tempfile.TemporaryDirectory() as tmpdir:
  142. # Initialize repo
  143. porcelain.init(tmpdir)
  144. # Create initial commit
  145. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  146. f.write("Initial content\n")
  147. porcelain.add(tmpdir, paths=["file1.txt"])
  148. porcelain.commit(tmpdir, message=b"Initial commit")
  149. # Create a branch and modify file1
  150. porcelain.branch_create(tmpdir, "feature")
  151. porcelain.checkout(tmpdir, "feature")
  152. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  153. f.write("Feature content\n")
  154. porcelain.add(tmpdir, paths=["file1.txt"])
  155. porcelain.commit(tmpdir, message=b"Modify file1 in feature")
  156. # Go back to master and modify file1 differently
  157. porcelain.checkout(tmpdir, "master")
  158. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  159. f.write("Master content\n")
  160. porcelain.add(tmpdir, paths=["file1.txt"])
  161. porcelain.commit(tmpdir, message=b"Modify file1 in master")
  162. # Merge feature branch - should have conflicts
  163. merge_commit, conflicts = porcelain.merge(tmpdir, "feature")
  164. self.assertIsNone(merge_commit)
  165. self.assertEqual(len(conflicts), 1)
  166. self.assertEqual(conflicts[0], b"file1.txt")
  167. # Check conflict markers in file
  168. with open(os.path.join(tmpdir, "file1.txt"), "rb") as f:
  169. content = f.read()
  170. self.assertIn(b"<<<<<<< ours", content)
  171. self.assertIn(b"=======", content)
  172. self.assertIn(b">>>>>>> theirs", content)
  173. def test_merge_no_commit(self):
  174. """Test merge with no_commit flag."""
  175. with tempfile.TemporaryDirectory() as tmpdir:
  176. # Initialize repo
  177. porcelain.init(tmpdir)
  178. # Create initial commit
  179. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  180. f.write("Initial content\n")
  181. porcelain.add(tmpdir, paths=["file1.txt"])
  182. porcelain.commit(tmpdir, message=b"Initial commit")
  183. # Create a branch
  184. porcelain.branch_create(tmpdir, "feature")
  185. porcelain.checkout(tmpdir, "feature")
  186. # Add a file on feature branch
  187. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  188. f.write("Feature content\n")
  189. porcelain.add(tmpdir, paths=["file2.txt"])
  190. porcelain.commit(tmpdir, message=b"Add feature")
  191. # Go back to master and add another file
  192. porcelain.checkout(tmpdir, "master")
  193. with open(os.path.join(tmpdir, "file3.txt"), "w") as f:
  194. f.write("Master content\n")
  195. porcelain.add(tmpdir, paths=["file3.txt"])
  196. master_commit = porcelain.commit(tmpdir, message=b"Add file3")
  197. # Merge feature branch with no_commit
  198. merge_commit, conflicts = porcelain.merge(tmpdir, "feature", no_commit=True)
  199. self.assertIsNone(merge_commit)
  200. self.assertEqual(conflicts, [])
  201. # Check that files are merged but no commit was created
  202. self.assertTrue(os.path.exists(os.path.join(tmpdir, "file2.txt")))
  203. self.assertTrue(os.path.exists(os.path.join(tmpdir, "file3.txt")))
  204. # HEAD should still point to master_commit
  205. with Repo(tmpdir) as repo:
  206. self.assertEqual(repo.refs[b"HEAD"], master_commit)
  207. def test_merge_no_head(self):
  208. """Test merge with no HEAD reference."""
  209. with tempfile.TemporaryDirectory() as tmpdir:
  210. # Initialize repo without any commits
  211. porcelain.init(tmpdir)
  212. # Try to merge - should fail with no HEAD
  213. self.assertRaises(porcelain.Error, porcelain.merge, tmpdir, "nonexistent")
  214. def test_merge_invalid_commit(self):
  215. """Test merge with invalid commit reference."""
  216. with tempfile.TemporaryDirectory() as tmpdir:
  217. # Initialize repo
  218. porcelain.init(tmpdir)
  219. # Create initial commit
  220. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  221. f.write("Initial content\n")
  222. porcelain.add(tmpdir, paths=["file1.txt"])
  223. porcelain.commit(tmpdir, message=b"Initial commit")
  224. # Try to merge nonexistent commit
  225. self.assertRaises(porcelain.Error, porcelain.merge, tmpdir, "nonexistent")
  226. def test_octopus_merge_three_branches(self):
  227. """Test octopus merge with three branches."""
  228. with tempfile.TemporaryDirectory() as tmpdir:
  229. # Initialize repo
  230. porcelain.init(tmpdir)
  231. # Create initial commit with three files
  232. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  233. f.write("File 1 content\n")
  234. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  235. f.write("File 2 content\n")
  236. with open(os.path.join(tmpdir, "file3.txt"), "w") as f:
  237. f.write("File 3 content\n")
  238. porcelain.add(tmpdir, paths=["file1.txt", "file2.txt", "file3.txt"])
  239. porcelain.commit(tmpdir, message=b"Initial commit")
  240. # Create branch1 and modify file1
  241. porcelain.branch_create(tmpdir, "branch1")
  242. porcelain.checkout(tmpdir, "branch1")
  243. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  244. f.write("Branch1 modified file1\n")
  245. porcelain.add(tmpdir, paths=["file1.txt"])
  246. porcelain.commit(tmpdir, message=b"Branch1 modifies file1")
  247. # Create branch2 and modify file2
  248. porcelain.checkout(tmpdir, "master")
  249. porcelain.branch_create(tmpdir, "branch2")
  250. porcelain.checkout(tmpdir, "branch2")
  251. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  252. f.write("Branch2 modified file2\n")
  253. porcelain.add(tmpdir, paths=["file2.txt"])
  254. porcelain.commit(tmpdir, message=b"Branch2 modifies file2")
  255. # Create branch3 and modify file3
  256. porcelain.checkout(tmpdir, "master")
  257. porcelain.branch_create(tmpdir, "branch3")
  258. porcelain.checkout(tmpdir, "branch3")
  259. with open(os.path.join(tmpdir, "file3.txt"), "w") as f:
  260. f.write("Branch3 modified file3\n")
  261. porcelain.add(tmpdir, paths=["file3.txt"])
  262. porcelain.commit(tmpdir, message=b"Branch3 modifies file3")
  263. # Go back to master and octopus merge all three branches
  264. porcelain.checkout(tmpdir, "master")
  265. merge_commit, conflicts = porcelain.merge(
  266. tmpdir, ["branch1", "branch2", "branch3"]
  267. )
  268. # Should succeed with no conflicts
  269. self.assertIsNotNone(merge_commit)
  270. self.assertEqual(conflicts, [])
  271. # Check that the merge commit has 4 parents (master + 3 branches)
  272. with Repo(tmpdir) as repo:
  273. commit = repo[merge_commit]
  274. self.assertEqual(len(commit.parents), 4)
  275. # Check that all modifications are present
  276. with open(os.path.join(tmpdir, "file1.txt")) as f:
  277. self.assertEqual(f.read(), "Branch1 modified file1\n")
  278. with open(os.path.join(tmpdir, "file2.txt")) as f:
  279. self.assertEqual(f.read(), "Branch2 modified file2\n")
  280. with open(os.path.join(tmpdir, "file3.txt")) as f:
  281. self.assertEqual(f.read(), "Branch3 modified file3\n")
  282. def test_octopus_merge_with_conflicts(self):
  283. # Check if merge3 module is available
  284. if importlib.util.find_spec("merge3") is None:
  285. raise DependencyMissing("merge3")
  286. """Test that octopus merge refuses to proceed with conflicts."""
  287. with tempfile.TemporaryDirectory() as tmpdir:
  288. # Initialize repo
  289. porcelain.init(tmpdir)
  290. # Create initial commit
  291. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  292. f.write("Initial content\n")
  293. porcelain.add(tmpdir, paths=["file1.txt"])
  294. porcelain.commit(tmpdir, message=b"Initial commit")
  295. # Create branch1 and modify file1
  296. porcelain.branch_create(tmpdir, "branch1")
  297. porcelain.checkout(tmpdir, "branch1")
  298. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  299. f.write("Branch1 content\n")
  300. porcelain.add(tmpdir, paths=["file1.txt"])
  301. porcelain.commit(tmpdir, message=b"Branch1 modifies file1")
  302. # Create branch2 and modify file1 differently
  303. porcelain.checkout(tmpdir, "master")
  304. porcelain.branch_create(tmpdir, "branch2")
  305. porcelain.checkout(tmpdir, "branch2")
  306. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  307. f.write("Branch2 content\n")
  308. porcelain.add(tmpdir, paths=["file1.txt"])
  309. porcelain.commit(tmpdir, message=b"Branch2 modifies file1")
  310. # Go back to master and try octopus merge - should fail
  311. porcelain.checkout(tmpdir, "master")
  312. merge_commit, conflicts = porcelain.merge(tmpdir, ["branch1", "branch2"])
  313. # Should have conflicts and no merge commit
  314. self.assertIsNone(merge_commit)
  315. self.assertEqual(len(conflicts), 1)
  316. self.assertEqual(conflicts[0], b"file1.txt")
  317. def test_octopus_merge_no_commit_flag(self):
  318. """Test octopus merge with no_commit flag."""
  319. with tempfile.TemporaryDirectory() as tmpdir:
  320. # Initialize repo
  321. porcelain.init(tmpdir)
  322. # Create initial commit
  323. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  324. f.write("File 1 content\n")
  325. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  326. f.write("File 2 content\n")
  327. porcelain.add(tmpdir, paths=["file1.txt", "file2.txt"])
  328. master_commit = porcelain.commit(tmpdir, message=b"Initial commit")
  329. # Create branch1 and modify file1
  330. porcelain.branch_create(tmpdir, "branch1")
  331. porcelain.checkout(tmpdir, "branch1")
  332. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  333. f.write("Branch1 modified file1\n")
  334. porcelain.add(tmpdir, paths=["file1.txt"])
  335. porcelain.commit(tmpdir, message=b"Branch1 modifies file1")
  336. # Create branch2 and modify file2
  337. porcelain.checkout(tmpdir, "master")
  338. porcelain.branch_create(tmpdir, "branch2")
  339. porcelain.checkout(tmpdir, "branch2")
  340. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  341. f.write("Branch2 modified file2\n")
  342. porcelain.add(tmpdir, paths=["file2.txt"])
  343. porcelain.commit(tmpdir, message=b"Branch2 modifies file2")
  344. # Go back to master and octopus merge with no_commit
  345. porcelain.checkout(tmpdir, "master")
  346. merge_commit, conflicts = porcelain.merge(
  347. tmpdir, ["branch1", "branch2"], no_commit=True
  348. )
  349. # Should not create commit
  350. self.assertIsNone(merge_commit)
  351. self.assertEqual(conflicts, [])
  352. # Check that files are merged but no commit was created
  353. with open(os.path.join(tmpdir, "file1.txt")) as f:
  354. self.assertEqual(f.read(), "Branch1 modified file1\n")
  355. with open(os.path.join(tmpdir, "file2.txt")) as f:
  356. self.assertEqual(f.read(), "Branch2 modified file2\n")
  357. # HEAD should still point to master_commit
  358. with Repo(tmpdir) as repo:
  359. self.assertEqual(repo.refs[b"HEAD"], master_commit)
  360. def test_octopus_merge_single_branch(self):
  361. """Test that octopus merge with single branch falls back to regular merge."""
  362. with tempfile.TemporaryDirectory() as tmpdir:
  363. # Initialize repo
  364. porcelain.init(tmpdir)
  365. # Create initial commit
  366. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  367. f.write("Initial content\n")
  368. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  369. f.write("Initial file2\n")
  370. porcelain.add(tmpdir, paths=["file1.txt", "file2.txt"])
  371. porcelain.commit(tmpdir, message=b"Initial commit")
  372. # Create branch and modify file1
  373. porcelain.branch_create(tmpdir, "branch1")
  374. porcelain.checkout(tmpdir, "branch1")
  375. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  376. f.write("Branch1 content\n")
  377. porcelain.add(tmpdir, paths=["file1.txt"])
  378. porcelain.commit(tmpdir, message=b"Branch1 changes")
  379. # Go back to master and modify file2 to prevent fast-forward
  380. porcelain.checkout(tmpdir, "master")
  381. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  382. f.write("Master file2\n")
  383. porcelain.add(tmpdir, paths=["file2.txt"])
  384. porcelain.commit(tmpdir, message=b"Master changes")
  385. # Merge with list containing one branch
  386. merge_commit, conflicts = porcelain.merge(tmpdir, ["branch1"])
  387. # Should create a regular merge commit
  388. self.assertIsNotNone(merge_commit)
  389. self.assertEqual(conflicts, [])
  390. # Check the merge commit (should have 2 parents for regular merge)
  391. with Repo(tmpdir) as repo:
  392. commit = repo[merge_commit]
  393. self.assertEqual(len(commit.parents), 2)
  394. class PorcelainMergeTreeTests(TestCase):
  395. """Tests for the porcelain merge_tree functionality."""
  396. def test_merge_tree_no_conflicts(self):
  397. """Test merge_tree with no conflicts."""
  398. with tempfile.TemporaryDirectory() as tmpdir:
  399. # Initialize repo
  400. porcelain.init(tmpdir)
  401. repo = Repo(tmpdir)
  402. self.addCleanup(repo.close)
  403. # Create base tree
  404. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  405. f.write("Base content\n")
  406. porcelain.add(tmpdir, paths=["file1.txt"])
  407. base_commit = porcelain.commit(tmpdir, message=b"Base commit")
  408. # Create our branch
  409. porcelain.branch_create(tmpdir, "ours")
  410. porcelain.checkout(tmpdir, "ours")
  411. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  412. f.write("Our content\n")
  413. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  414. f.write("Our new file\n")
  415. porcelain.add(tmpdir, paths=["file1.txt", "file2.txt"])
  416. our_commit = porcelain.commit(tmpdir, message=b"Our commit")
  417. # Create their branch
  418. porcelain.checkout(tmpdir, b"master")
  419. porcelain.branch_create(tmpdir, "theirs")
  420. porcelain.checkout(tmpdir, "theirs")
  421. with open(os.path.join(tmpdir, "file3.txt"), "w") as f:
  422. f.write("Their new file\n")
  423. porcelain.add(tmpdir, paths=["file3.txt"])
  424. their_commit = porcelain.commit(tmpdir, message=b"Their commit")
  425. # Perform merge_tree
  426. merged_tree_id, conflicts = porcelain.merge_tree(
  427. tmpdir, base_commit, our_commit, their_commit
  428. )
  429. # Should have no conflicts
  430. self.assertEqual(conflicts, [])
  431. # Check merged tree contains all files
  432. merged_tree = repo[merged_tree_id]
  433. self.assertIn(b"file1.txt", merged_tree)
  434. self.assertIn(b"file2.txt", merged_tree)
  435. self.assertIn(b"file3.txt", merged_tree)
  436. def test_merge_tree_with_conflicts(self):
  437. # Check if merge3 module is available
  438. if importlib.util.find_spec("merge3") is None:
  439. raise DependencyMissing("merge3")
  440. """Test merge_tree with conflicts."""
  441. with tempfile.TemporaryDirectory() as tmpdir:
  442. # Initialize repo
  443. porcelain.init(tmpdir)
  444. repo = Repo(tmpdir)
  445. self.addCleanup(repo.close)
  446. # Create base tree
  447. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  448. f.write("Base content\n")
  449. porcelain.add(tmpdir, paths=["file1.txt"])
  450. base_commit = porcelain.commit(tmpdir, message=b"Base commit")
  451. # Create our branch with changes
  452. porcelain.branch_create(tmpdir, "ours")
  453. porcelain.checkout(tmpdir, "ours")
  454. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  455. f.write("Our content\n")
  456. porcelain.add(tmpdir, paths=["file1.txt"])
  457. our_commit = porcelain.commit(tmpdir, message=b"Our commit")
  458. # Create their branch with conflicting changes
  459. porcelain.checkout(tmpdir, b"master")
  460. porcelain.branch_create(tmpdir, "theirs")
  461. porcelain.checkout(tmpdir, "theirs")
  462. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  463. f.write("Their content\n")
  464. porcelain.add(tmpdir, paths=["file1.txt"])
  465. their_commit = porcelain.commit(tmpdir, message=b"Their commit")
  466. # Perform merge_tree
  467. merged_tree_id, conflicts = porcelain.merge_tree(
  468. tmpdir, base_commit, our_commit, their_commit
  469. )
  470. # Should have conflicts
  471. self.assertEqual(conflicts, [b"file1.txt"])
  472. # Check merged tree exists and contains conflict markers
  473. merged_tree = repo[merged_tree_id]
  474. self.assertIn(b"file1.txt", merged_tree)
  475. # Get the merged blob content
  476. _file_mode, file_sha = merged_tree[b"file1.txt"]
  477. merged_blob = repo[file_sha]
  478. content = merged_blob.data
  479. # Should contain conflict markers
  480. self.assertIn(b"<<<<<<< ours", content)
  481. self.assertIn(b"=======", content)
  482. self.assertIn(b">>>>>>> theirs", content)
  483. def test_merge_tree_no_base(self):
  484. """Test merge_tree without a base commit."""
  485. with tempfile.TemporaryDirectory() as tmpdir:
  486. # Initialize repo
  487. porcelain.init(tmpdir)
  488. repo = Repo(tmpdir)
  489. self.addCleanup(repo.close)
  490. # Create our tree
  491. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  492. f.write("Our content\n")
  493. porcelain.add(tmpdir, paths=["file1.txt"])
  494. our_commit = porcelain.commit(tmpdir, message=b"Our commit")
  495. # Create their tree (independent)
  496. os.remove(os.path.join(tmpdir, "file1.txt"))
  497. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  498. f.write("Their content\n")
  499. porcelain.add(tmpdir, paths=["file2.txt"])
  500. their_commit = porcelain.commit(tmpdir, message=b"Their commit")
  501. # Perform merge_tree without base
  502. merged_tree_id, conflicts = porcelain.merge_tree(
  503. tmpdir, None, our_commit, their_commit
  504. )
  505. # Should have no conflicts (different files)
  506. self.assertEqual(conflicts, [])
  507. # Check merged tree contains both files
  508. merged_tree = repo[merged_tree_id]
  509. self.assertIn(b"file1.txt", merged_tree)
  510. self.assertIn(b"file2.txt", merged_tree)
  511. def test_merge_tree_with_tree_objects(self):
  512. # Check if merge3 module is available
  513. if importlib.util.find_spec("merge3") is None:
  514. raise DependencyMissing("merge3")
  515. """Test merge_tree with tree objects instead of commits."""
  516. with tempfile.TemporaryDirectory() as tmpdir:
  517. # Initialize repo
  518. porcelain.init(tmpdir)
  519. repo = Repo(tmpdir)
  520. self.addCleanup(repo.close)
  521. # Create base tree
  522. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  523. f.write("Base content\n")
  524. porcelain.add(tmpdir, paths=["file1.txt"])
  525. base_commit_id = porcelain.commit(tmpdir, message=b"Base commit")
  526. base_tree_id = repo[base_commit_id].tree
  527. # Create our tree
  528. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  529. f.write("Our content\n")
  530. porcelain.add(tmpdir, paths=["file1.txt"])
  531. our_commit_id = porcelain.commit(tmpdir, message=b"Our commit")
  532. our_tree_id = repo[our_commit_id].tree
  533. # Create their tree
  534. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  535. f.write("Their content\n")
  536. porcelain.add(tmpdir, paths=["file1.txt"])
  537. their_commit_id = porcelain.commit(tmpdir, message=b"Their commit")
  538. their_tree_id = repo[their_commit_id].tree
  539. # Perform merge_tree with tree SHAs
  540. _merged_tree_id, conflicts = porcelain.merge_tree(
  541. tmpdir,
  542. base_tree_id if base_tree_id else None,
  543. our_tree_id,
  544. their_tree_id,
  545. )
  546. # Should have conflicts
  547. self.assertEqual(conflicts, [b"file1.txt"])
  548. def test_merge_tree_invalid_object(self):
  549. """Test merge_tree with invalid object reference."""
  550. with tempfile.TemporaryDirectory() as tmpdir:
  551. # Initialize repo
  552. porcelain.init(tmpdir)
  553. # Create a commit
  554. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  555. f.write("Content\n")
  556. porcelain.add(tmpdir, paths=["file1.txt"])
  557. commit_id = porcelain.commit(tmpdir, message=b"Commit")
  558. # Try to merge with nonexistent object
  559. self.assertRaises(
  560. KeyError,
  561. porcelain.merge_tree,
  562. tmpdir,
  563. None,
  564. commit_id,
  565. ZERO_SHA,
  566. )
  567. if __name__ == "__main__":
  568. unittest.main()