test_porcelain_merge.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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 . 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()