test_porcelain_merge.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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 public 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 os
  23. import tempfile
  24. import unittest
  25. from dulwich import porcelain
  26. from dulwich.repo import Repo
  27. from dulwich.tests import TestCase
  28. class PorcelainMergeTests(TestCase):
  29. """Tests for the porcelain merge functionality."""
  30. def test_merge_fast_forward(self):
  31. """Test fast-forward merge."""
  32. with tempfile.TemporaryDirectory() as tmpdir:
  33. # Initialize repo
  34. porcelain.init(tmpdir)
  35. # Create initial commit
  36. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  37. f.write("Initial content\n")
  38. porcelain.add(tmpdir, paths=["file1.txt"])
  39. porcelain.commit(tmpdir, message=b"Initial commit")
  40. # Create a branch
  41. porcelain.branch_create(tmpdir, "feature")
  42. porcelain.checkout_branch(tmpdir, "feature")
  43. # Add a file on feature branch
  44. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  45. f.write("Feature content\n")
  46. porcelain.add(tmpdir, paths=["file2.txt"])
  47. feature_commit = porcelain.commit(tmpdir, message=b"Add feature")
  48. # Go back to master
  49. porcelain.checkout_branch(tmpdir, "master")
  50. # Merge feature branch (should fast-forward)
  51. merge_commit, conflicts = porcelain.merge(tmpdir, "feature")
  52. self.assertEqual(merge_commit, feature_commit)
  53. self.assertEqual(conflicts, [])
  54. # Check that file2.txt exists
  55. self.assertTrue(os.path.exists(os.path.join(tmpdir, "file2.txt")))
  56. def test_merge_already_up_to_date(self):
  57. """Test merge when already up to date."""
  58. with tempfile.TemporaryDirectory() as tmpdir:
  59. # Initialize repo
  60. porcelain.init(tmpdir)
  61. # Create initial commit
  62. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  63. f.write("Initial content\n")
  64. porcelain.add(tmpdir, paths=["file1.txt"])
  65. porcelain.commit(tmpdir, message=b"Initial commit")
  66. # Try to merge the same commit
  67. merge_commit, conflicts = porcelain.merge(tmpdir, "HEAD")
  68. self.assertIsNone(merge_commit)
  69. self.assertEqual(conflicts, [])
  70. def test_merge_no_ff(self):
  71. """Test merge with --no-ff flag."""
  72. with tempfile.TemporaryDirectory() as tmpdir:
  73. # Initialize repo
  74. porcelain.init(tmpdir)
  75. # Create initial commit
  76. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  77. f.write("Initial content\n")
  78. porcelain.add(tmpdir, paths=["file1.txt"])
  79. porcelain.commit(tmpdir, message=b"Initial commit")
  80. # Create a branch
  81. porcelain.branch_create(tmpdir, "feature")
  82. porcelain.checkout_branch(tmpdir, "feature")
  83. # Add a file on feature branch
  84. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  85. f.write("Feature content\n")
  86. porcelain.add(tmpdir, paths=["file2.txt"])
  87. feature_commit = porcelain.commit(tmpdir, message=b"Add feature")
  88. # Go back to master
  89. porcelain.checkout_branch(tmpdir, "master")
  90. # Merge feature branch with no-ff
  91. merge_commit, conflicts = porcelain.merge(tmpdir, "feature", no_ff=True)
  92. # Should create a new merge commit
  93. self.assertIsNotNone(merge_commit)
  94. self.assertNotEqual(merge_commit, feature_commit)
  95. self.assertEqual(conflicts, [])
  96. # Check that it's a merge commit with two parents
  97. with Repo(tmpdir) as repo:
  98. commit = repo[merge_commit]
  99. self.assertEqual(len(commit.parents), 2)
  100. def test_merge_three_way(self):
  101. """Test three-way merge without conflicts."""
  102. with tempfile.TemporaryDirectory() as tmpdir:
  103. # Initialize repo
  104. porcelain.init(tmpdir)
  105. # Create initial commit
  106. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  107. f.write("Initial content\n")
  108. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  109. f.write("Initial file2\n")
  110. porcelain.add(tmpdir, paths=["file1.txt", "file2.txt"])
  111. porcelain.commit(tmpdir, message=b"Initial commit")
  112. # Create a branch and modify file1
  113. porcelain.branch_create(tmpdir, "feature")
  114. porcelain.checkout_branch(tmpdir, "feature")
  115. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  116. f.write("Feature content\n")
  117. porcelain.add(tmpdir, paths=["file1.txt"])
  118. porcelain.commit(tmpdir, message=b"Modify file1 in feature")
  119. # Go back to master and modify file2
  120. porcelain.checkout_branch(tmpdir, "master")
  121. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  122. f.write("Master file2\n")
  123. porcelain.add(tmpdir, paths=["file2.txt"])
  124. porcelain.commit(tmpdir, message=b"Modify file2 in master")
  125. # Merge feature branch
  126. merge_commit, conflicts = porcelain.merge(tmpdir, "feature")
  127. self.assertIsNotNone(merge_commit)
  128. self.assertEqual(conflicts, [])
  129. # Check both modifications are present
  130. with open(os.path.join(tmpdir, "file1.txt")) as f:
  131. self.assertEqual(f.read(), "Feature content\n")
  132. with open(os.path.join(tmpdir, "file2.txt")) as f:
  133. self.assertEqual(f.read(), "Master file2\n")
  134. def test_merge_with_conflicts(self):
  135. """Test merge with conflicts."""
  136. with tempfile.TemporaryDirectory() as tmpdir:
  137. # Initialize repo
  138. porcelain.init(tmpdir)
  139. # Create initial commit
  140. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  141. f.write("Initial content\n")
  142. porcelain.add(tmpdir, paths=["file1.txt"])
  143. porcelain.commit(tmpdir, message=b"Initial commit")
  144. # Create a branch and modify file1
  145. porcelain.branch_create(tmpdir, "feature")
  146. porcelain.checkout_branch(tmpdir, "feature")
  147. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  148. f.write("Feature content\n")
  149. porcelain.add(tmpdir, paths=["file1.txt"])
  150. porcelain.commit(tmpdir, message=b"Modify file1 in feature")
  151. # Go back to master and modify file1 differently
  152. porcelain.checkout_branch(tmpdir, "master")
  153. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  154. f.write("Master content\n")
  155. porcelain.add(tmpdir, paths=["file1.txt"])
  156. porcelain.commit(tmpdir, message=b"Modify file1 in master")
  157. # Merge feature branch - should have conflicts
  158. merge_commit, conflicts = porcelain.merge(tmpdir, "feature")
  159. self.assertIsNone(merge_commit)
  160. self.assertEqual(len(conflicts), 1)
  161. self.assertEqual(conflicts[0], b"file1.txt")
  162. # Check conflict markers in file
  163. with open(os.path.join(tmpdir, "file1.txt"), "rb") as f:
  164. content = f.read()
  165. self.assertIn(b"<<<<<<< ours", content)
  166. self.assertIn(b"=======", content)
  167. self.assertIn(b">>>>>>> theirs", content)
  168. def test_merge_no_commit(self):
  169. """Test merge with no_commit flag."""
  170. with tempfile.TemporaryDirectory() as tmpdir:
  171. # Initialize repo
  172. porcelain.init(tmpdir)
  173. # Create initial commit
  174. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  175. f.write("Initial content\n")
  176. porcelain.add(tmpdir, paths=["file1.txt"])
  177. porcelain.commit(tmpdir, message=b"Initial commit")
  178. # Create a branch
  179. porcelain.branch_create(tmpdir, "feature")
  180. porcelain.checkout_branch(tmpdir, "feature")
  181. # Add a file on feature branch
  182. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  183. f.write("Feature content\n")
  184. porcelain.add(tmpdir, paths=["file2.txt"])
  185. porcelain.commit(tmpdir, message=b"Add feature")
  186. # Go back to master and add another file
  187. porcelain.checkout_branch(tmpdir, "master")
  188. with open(os.path.join(tmpdir, "file3.txt"), "w") as f:
  189. f.write("Master content\n")
  190. porcelain.add(tmpdir, paths=["file3.txt"])
  191. master_commit = porcelain.commit(tmpdir, message=b"Add file3")
  192. # Merge feature branch with no_commit
  193. merge_commit, conflicts = porcelain.merge(tmpdir, "feature", no_commit=True)
  194. self.assertIsNone(merge_commit)
  195. self.assertEqual(conflicts, [])
  196. # Check that files are merged but no commit was created
  197. self.assertTrue(os.path.exists(os.path.join(tmpdir, "file2.txt")))
  198. self.assertTrue(os.path.exists(os.path.join(tmpdir, "file3.txt")))
  199. # HEAD should still point to master_commit
  200. with Repo(tmpdir) as repo:
  201. self.assertEqual(repo.refs[b"HEAD"], master_commit)
  202. def test_merge_no_head(self):
  203. """Test merge with no HEAD reference."""
  204. with tempfile.TemporaryDirectory() as tmpdir:
  205. # Initialize repo without any commits
  206. porcelain.init(tmpdir)
  207. # Try to merge - should fail with no HEAD
  208. self.assertRaises(porcelain.Error, porcelain.merge, tmpdir, "nonexistent")
  209. def test_merge_invalid_commit(self):
  210. """Test merge with invalid commit reference."""
  211. with tempfile.TemporaryDirectory() as tmpdir:
  212. # Initialize repo
  213. porcelain.init(tmpdir)
  214. # Create initial commit
  215. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  216. f.write("Initial content\n")
  217. porcelain.add(tmpdir, paths=["file1.txt"])
  218. porcelain.commit(tmpdir, message=b"Initial commit")
  219. # Try to merge nonexistent commit
  220. self.assertRaises(porcelain.Error, porcelain.merge, tmpdir, "nonexistent")
  221. if __name__ == "__main__":
  222. unittest.main()