test_cli_merge.py 11 KB

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