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