# test_porcelain_merge.py -- Tests for porcelain merge functionality # Copyright (C) 2024 Jelmer Vernooij # # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU # General Public License as public by the Free Software Foundation; version 2.0 # or (at your option) any later version. You can redistribute it and/or # modify it under the terms of either of these two licenses. # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # You should have received a copy of the licenses; if not, see # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Tests for porcelain merge functionality.""" import os import tempfile import unittest from dulwich import porcelain from dulwich.repo import Repo from dulwich.tests import TestCase class PorcelainMergeTests(TestCase): """Tests for the porcelain merge functionality.""" def test_merge_fast_forward(self): """Test fast-forward merge.""" with tempfile.TemporaryDirectory() as tmpdir: # Initialize repo porcelain.init(tmpdir) # Create initial commit with open(os.path.join(tmpdir, "file1.txt"), "w") as f: f.write("Initial content\n") porcelain.add(tmpdir, paths=["file1.txt"]) porcelain.commit(tmpdir, message=b"Initial commit") # Create a branch porcelain.branch_create(tmpdir, "feature") porcelain.checkout_branch(tmpdir, "feature") # Add a file on feature branch with open(os.path.join(tmpdir, "file2.txt"), "w") as f: f.write("Feature content\n") porcelain.add(tmpdir, paths=["file2.txt"]) feature_commit = porcelain.commit(tmpdir, message=b"Add feature") # Go back to master porcelain.checkout_branch(tmpdir, "master") # Merge feature branch (should fast-forward) merge_commit, conflicts = porcelain.merge(tmpdir, "feature") self.assertEqual(merge_commit, feature_commit) self.assertEqual(conflicts, []) # Check that file2.txt exists self.assertTrue(os.path.exists(os.path.join(tmpdir, "file2.txt"))) def test_merge_already_up_to_date(self): """Test merge when already up to date.""" with tempfile.TemporaryDirectory() as tmpdir: # Initialize repo porcelain.init(tmpdir) # Create initial commit with open(os.path.join(tmpdir, "file1.txt"), "w") as f: f.write("Initial content\n") porcelain.add(tmpdir, paths=["file1.txt"]) porcelain.commit(tmpdir, message=b"Initial commit") # Try to merge the same commit merge_commit, conflicts = porcelain.merge(tmpdir, "HEAD") self.assertIsNone(merge_commit) self.assertEqual(conflicts, []) def test_merge_no_ff(self): """Test merge with --no-ff flag.""" with tempfile.TemporaryDirectory() as tmpdir: # Initialize repo porcelain.init(tmpdir) # Create initial commit with open(os.path.join(tmpdir, "file1.txt"), "w") as f: f.write("Initial content\n") porcelain.add(tmpdir, paths=["file1.txt"]) porcelain.commit(tmpdir, message=b"Initial commit") # Create a branch porcelain.branch_create(tmpdir, "feature") porcelain.checkout_branch(tmpdir, "feature") # Add a file on feature branch with open(os.path.join(tmpdir, "file2.txt"), "w") as f: f.write("Feature content\n") porcelain.add(tmpdir, paths=["file2.txt"]) feature_commit = porcelain.commit(tmpdir, message=b"Add feature") # Go back to master porcelain.checkout_branch(tmpdir, "master") # Merge feature branch with no-ff merge_commit, conflicts = porcelain.merge(tmpdir, "feature", no_ff=True) # Should create a new merge commit self.assertIsNotNone(merge_commit) self.assertNotEqual(merge_commit, feature_commit) self.assertEqual(conflicts, []) # Check that it's a merge commit with two parents with Repo(tmpdir) as repo: commit = repo[merge_commit] self.assertEqual(len(commit.parents), 2) def test_merge_three_way(self): """Test three-way merge without conflicts.""" with tempfile.TemporaryDirectory() as tmpdir: # Initialize repo porcelain.init(tmpdir) # Create initial commit with open(os.path.join(tmpdir, "file1.txt"), "w") as f: f.write("Initial content\n") with open(os.path.join(tmpdir, "file2.txt"), "w") as f: f.write("Initial file2\n") porcelain.add(tmpdir, paths=["file1.txt", "file2.txt"]) porcelain.commit(tmpdir, message=b"Initial commit") # Create a branch and modify file1 porcelain.branch_create(tmpdir, "feature") porcelain.checkout_branch(tmpdir, "feature") with open(os.path.join(tmpdir, "file1.txt"), "w") as f: f.write("Feature content\n") porcelain.add(tmpdir, paths=["file1.txt"]) porcelain.commit(tmpdir, message=b"Modify file1 in feature") # Go back to master and modify file2 porcelain.checkout_branch(tmpdir, "master") with open(os.path.join(tmpdir, "file2.txt"), "w") as f: f.write("Master file2\n") porcelain.add(tmpdir, paths=["file2.txt"]) porcelain.commit(tmpdir, message=b"Modify file2 in master") # Merge feature branch merge_commit, conflicts = porcelain.merge(tmpdir, "feature") self.assertIsNotNone(merge_commit) self.assertEqual(conflicts, []) # Check both modifications are present with open(os.path.join(tmpdir, "file1.txt")) as f: self.assertEqual(f.read(), "Feature content\n") with open(os.path.join(tmpdir, "file2.txt")) as f: self.assertEqual(f.read(), "Master file2\n") def test_merge_with_conflicts(self): """Test merge with conflicts.""" with tempfile.TemporaryDirectory() as tmpdir: # Initialize repo porcelain.init(tmpdir) # Create initial commit with open(os.path.join(tmpdir, "file1.txt"), "w") as f: f.write("Initial content\n") porcelain.add(tmpdir, paths=["file1.txt"]) porcelain.commit(tmpdir, message=b"Initial commit") # Create a branch and modify file1 porcelain.branch_create(tmpdir, "feature") porcelain.checkout_branch(tmpdir, "feature") with open(os.path.join(tmpdir, "file1.txt"), "w") as f: f.write("Feature content\n") porcelain.add(tmpdir, paths=["file1.txt"]) porcelain.commit(tmpdir, message=b"Modify file1 in feature") # Go back to master and modify file1 differently porcelain.checkout_branch(tmpdir, "master") with open(os.path.join(tmpdir, "file1.txt"), "w") as f: f.write("Master content\n") porcelain.add(tmpdir, paths=["file1.txt"]) porcelain.commit(tmpdir, message=b"Modify file1 in master") # Merge feature branch - should have conflicts merge_commit, conflicts = porcelain.merge(tmpdir, "feature") self.assertIsNone(merge_commit) self.assertEqual(len(conflicts), 1) self.assertEqual(conflicts[0], b"file1.txt") # Check conflict markers in file with open(os.path.join(tmpdir, "file1.txt"), "rb") as f: content = f.read() self.assertIn(b"<<<<<<< ours", content) self.assertIn(b"=======", content) self.assertIn(b">>>>>>> theirs", content) def test_merge_no_commit(self): """Test merge with no_commit flag.""" with tempfile.TemporaryDirectory() as tmpdir: # Initialize repo porcelain.init(tmpdir) # Create initial commit with open(os.path.join(tmpdir, "file1.txt"), "w") as f: f.write("Initial content\n") porcelain.add(tmpdir, paths=["file1.txt"]) porcelain.commit(tmpdir, message=b"Initial commit") # Create a branch porcelain.branch_create(tmpdir, "feature") porcelain.checkout_branch(tmpdir, "feature") # Add a file on feature branch with open(os.path.join(tmpdir, "file2.txt"), "w") as f: f.write("Feature content\n") porcelain.add(tmpdir, paths=["file2.txt"]) porcelain.commit(tmpdir, message=b"Add feature") # Go back to master and add another file porcelain.checkout_branch(tmpdir, "master") with open(os.path.join(tmpdir, "file3.txt"), "w") as f: f.write("Master content\n") porcelain.add(tmpdir, paths=["file3.txt"]) master_commit = porcelain.commit(tmpdir, message=b"Add file3") # Merge feature branch with no_commit merge_commit, conflicts = porcelain.merge(tmpdir, "feature", no_commit=True) self.assertIsNone(merge_commit) self.assertEqual(conflicts, []) # Check that files are merged but no commit was created self.assertTrue(os.path.exists(os.path.join(tmpdir, "file2.txt"))) self.assertTrue(os.path.exists(os.path.join(tmpdir, "file3.txt"))) # HEAD should still point to master_commit with Repo(tmpdir) as repo: self.assertEqual(repo.refs[b"HEAD"], master_commit) def test_merge_no_head(self): """Test merge with no HEAD reference.""" with tempfile.TemporaryDirectory() as tmpdir: # Initialize repo without any commits porcelain.init(tmpdir) # Try to merge - should fail with no HEAD self.assertRaises(porcelain.Error, porcelain.merge, tmpdir, "nonexistent") def test_merge_invalid_commit(self): """Test merge with invalid commit reference.""" with tempfile.TemporaryDirectory() as tmpdir: # Initialize repo porcelain.init(tmpdir) # Create initial commit with open(os.path.join(tmpdir, "file1.txt"), "w") as f: f.write("Initial content\n") porcelain.add(tmpdir, paths=["file1.txt"]) porcelain.commit(tmpdir, message=b"Initial commit") # Try to merge nonexistent commit self.assertRaises(porcelain.Error, porcelain.merge, tmpdir, "nonexistent") if __name__ == "__main__": unittest.main()