test_cli_merge.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. # test_cli_merge.py -- Tests for dulwich merge CLI command
  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 dulwich merge CLI command."""
  22. import io
  23. import os
  24. import tempfile
  25. import unittest
  26. from unittest.mock import patch
  27. from dulwich import porcelain
  28. from dulwich.cli import main
  29. from dulwich.tests import TestCase
  30. class CLIMergeTests(TestCase):
  31. """Tests for the dulwich merge CLI command."""
  32. def test_merge_fast_forward(self):
  33. """Test CLI merge with fast-forward."""
  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_branch(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. porcelain.commit(tmpdir, message=b"Add feature")
  50. # Go back to master
  51. porcelain.checkout_branch(tmpdir, "master")
  52. # Test merge via CLI
  53. old_cwd = os.getcwd()
  54. try:
  55. os.chdir(tmpdir)
  56. with patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
  57. ret = main(["merge", "feature"])
  58. output = mock_stdout.getvalue()
  59. self.assertEqual(ret, None) # Success
  60. self.assertIn("Merge successful", output)
  61. # Check that file2.txt exists
  62. self.assertTrue(os.path.exists(os.path.join(tmpdir, "file2.txt")))
  63. finally:
  64. os.chdir(old_cwd)
  65. def test_merge_with_conflicts(self):
  66. """Test CLI merge with conflicts."""
  67. with tempfile.TemporaryDirectory() as tmpdir:
  68. # Initialize repo
  69. porcelain.init(tmpdir)
  70. # Create initial commit
  71. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  72. f.write("Initial content\n")
  73. porcelain.add(tmpdir, paths=["file1.txt"])
  74. porcelain.commit(tmpdir, message=b"Initial commit")
  75. # Create a branch and modify file1
  76. porcelain.branch_create(tmpdir, "feature")
  77. porcelain.checkout_branch(tmpdir, "feature")
  78. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  79. f.write("Feature content\n")
  80. porcelain.add(tmpdir, paths=["file1.txt"])
  81. porcelain.commit(
  82. tmpdir, message=b"Modify file1 in feature"
  83. )
  84. # Go back to master and modify file1 differently
  85. porcelain.checkout_branch(tmpdir, "master")
  86. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  87. f.write("Master content\n")
  88. porcelain.add(tmpdir, paths=["file1.txt"])
  89. porcelain.commit(tmpdir, message=b"Modify file1 in master")
  90. # Test merge via CLI - should exit with error
  91. old_cwd = os.getcwd()
  92. try:
  93. os.chdir(tmpdir)
  94. with patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
  95. with patch("sys.exit") as mock_exit:
  96. main(["merge", "feature"])
  97. mock_exit.assert_called_with(1)
  98. output = mock_stdout.getvalue()
  99. self.assertIn("Merge conflicts", output)
  100. self.assertIn("file1.txt", output)
  101. finally:
  102. os.chdir(old_cwd)
  103. def test_merge_already_up_to_date(self):
  104. """Test CLI merge when already up to date."""
  105. with tempfile.TemporaryDirectory() as tmpdir:
  106. # Initialize repo
  107. porcelain.init(tmpdir)
  108. # Create initial commit
  109. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  110. f.write("Initial content\n")
  111. porcelain.add(tmpdir, paths=["file1.txt"])
  112. porcelain.commit(tmpdir, message=b"Initial commit")
  113. # Test merge via CLI
  114. old_cwd = os.getcwd()
  115. try:
  116. os.chdir(tmpdir)
  117. with patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
  118. ret = main(["merge", "HEAD"])
  119. output = mock_stdout.getvalue()
  120. self.assertEqual(ret, None) # Success
  121. self.assertIn("Already up to date", output)
  122. finally:
  123. os.chdir(old_cwd)
  124. def test_merge_no_commit(self):
  125. """Test CLI merge with --no-commit."""
  126. with tempfile.TemporaryDirectory() as tmpdir:
  127. # Initialize repo
  128. porcelain.init(tmpdir)
  129. # Create initial commit
  130. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  131. f.write("Initial content\n")
  132. porcelain.add(tmpdir, paths=["file1.txt"])
  133. porcelain.commit(tmpdir, message=b"Initial commit")
  134. # Create a branch
  135. porcelain.branch_create(tmpdir, "feature")
  136. porcelain.checkout_branch(tmpdir, "feature")
  137. # Add a file on feature branch
  138. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  139. f.write("Feature content\n")
  140. porcelain.add(tmpdir, paths=["file2.txt"])
  141. porcelain.commit(tmpdir, message=b"Add feature")
  142. # Go back to master and add another file
  143. porcelain.checkout_branch(tmpdir, "master")
  144. with open(os.path.join(tmpdir, "file3.txt"), "w") as f:
  145. f.write("Master content\n")
  146. porcelain.add(tmpdir, paths=["file3.txt"])
  147. porcelain.commit(tmpdir, message=b"Add file3")
  148. # Test merge via CLI with --no-commit
  149. old_cwd = os.getcwd()
  150. try:
  151. os.chdir(tmpdir)
  152. with patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
  153. ret = main(["merge", "--no-commit", "feature"])
  154. output = mock_stdout.getvalue()
  155. self.assertEqual(ret, None) # Success
  156. self.assertIn("not committing", output)
  157. # Check that files are merged
  158. self.assertTrue(os.path.exists(os.path.join(tmpdir, "file2.txt")))
  159. self.assertTrue(os.path.exists(os.path.join(tmpdir, "file3.txt")))
  160. finally:
  161. os.chdir(old_cwd)
  162. def test_merge_no_ff(self):
  163. """Test CLI merge with --no-ff."""
  164. with tempfile.TemporaryDirectory() as tmpdir:
  165. # Initialize repo
  166. porcelain.init(tmpdir)
  167. # Create initial commit
  168. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  169. f.write("Initial content\n")
  170. porcelain.add(tmpdir, paths=["file1.txt"])
  171. porcelain.commit(tmpdir, message=b"Initial commit")
  172. # Create a branch
  173. porcelain.branch_create(tmpdir, "feature")
  174. porcelain.checkout_branch(tmpdir, "feature")
  175. # Add a file on feature branch
  176. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  177. f.write("Feature content\n")
  178. porcelain.add(tmpdir, paths=["file2.txt"])
  179. porcelain.commit(tmpdir, message=b"Add feature")
  180. # Go back to master
  181. porcelain.checkout_branch(tmpdir, "master")
  182. # Test merge via CLI with --no-ff
  183. old_cwd = os.getcwd()
  184. try:
  185. os.chdir(tmpdir)
  186. with patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
  187. ret = main(["merge", "--no-ff", "feature"])
  188. output = mock_stdout.getvalue()
  189. self.assertEqual(ret, None) # Success
  190. self.assertIn("Merge successful", output)
  191. self.assertIn("Created merge commit", output)
  192. finally:
  193. os.chdir(old_cwd)
  194. def test_merge_with_message(self):
  195. """Test CLI merge with custom message."""
  196. with tempfile.TemporaryDirectory() as tmpdir:
  197. # Initialize repo
  198. porcelain.init(tmpdir)
  199. # Create initial commit
  200. with open(os.path.join(tmpdir, "file1.txt"), "w") as f:
  201. f.write("Initial content\n")
  202. porcelain.add(tmpdir, paths=["file1.txt"])
  203. porcelain.commit(tmpdir, message=b"Initial commit")
  204. # Create a branch
  205. porcelain.branch_create(tmpdir, "feature")
  206. porcelain.checkout_branch(tmpdir, "feature")
  207. # Add a file on feature branch
  208. with open(os.path.join(tmpdir, "file2.txt"), "w") as f:
  209. f.write("Feature content\n")
  210. porcelain.add(tmpdir, paths=["file2.txt"])
  211. porcelain.commit(tmpdir, message=b"Add feature")
  212. # Go back to master and add another file
  213. porcelain.checkout_branch(tmpdir, "master")
  214. with open(os.path.join(tmpdir, "file3.txt"), "w") as f:
  215. f.write("Master content\n")
  216. porcelain.add(tmpdir, paths=["file3.txt"])
  217. porcelain.commit(tmpdir, message=b"Add file3")
  218. # Test merge via CLI with custom message
  219. old_cwd = os.getcwd()
  220. try:
  221. os.chdir(tmpdir)
  222. with patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
  223. ret = main(["merge", "-m", "Custom merge message", "feature"])
  224. output = mock_stdout.getvalue()
  225. self.assertEqual(ret, None) # Success
  226. self.assertIn("Merge successful", output)
  227. finally:
  228. os.chdir(old_cwd)
  229. if __name__ == "__main__":
  230. unittest.main()