diff options
Diffstat (limited to 'org.eclipse.jgit.test/tst/org/eclipse/jgit/merge')
6 files changed, 727 insertions, 40 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java index ae811f830f..8865ba9ebd 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java @@ -15,6 +15,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.time.Instant; +import java.time.ZoneOffset; + import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.junit.RepositoryTestCase; @@ -162,7 +165,8 @@ public class CherryPickTest extends RepositoryTestCase { final ObjectId[] parentIds) throws Exception { final CommitBuilder c = new CommitBuilder(); c.setTreeId(treeB.writeTree(odi)); - c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0)); + c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", + Instant.ofEpochSecond(1), ZoneOffset.UTC)); c.setCommitter(c.getAuthor()); c.setParentIds(parentIds); c.setMessage("Tree " + c.getTreeId().name()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java index f410960bec..b1998f30f8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java @@ -15,6 +15,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.dircache.DirCache; @@ -357,7 +359,8 @@ public class GitlinkMergeTest extends SampleDataRepositoryTestCase { ObjectId[] parentIds) throws Exception { CommitBuilder c = new CommitBuilder(); c.setTreeId(treeB.writeTree(odi)); - c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0)); + c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", + Instant.ofEpochSecond(1), ZoneOffset.UTC)); c.setCommitter(c.getAuthor()); c.setParentIds(parentIds); c.setMessage("Tree " + c.getTreeId().name()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java index 5f4331b04d..7a8a93e977 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java @@ -47,7 +47,10 @@ public class MergeAlgorithmTest { @Test public void testTwoConflictingModifications() throws IOException { assertEquals(t("a<b=Z>Zdefghij"), - merge("abcdefghij", "abZdefghij", "aZZdefghij")); + merge("abcdefghij", "abZdefghij", "aZZdefghij", false)); + + assertEquals(t("a<b|b=Z>Zdefghij"), + merge("abcdefghij", "abZdefghij", "aZZdefghij", true)); } /** @@ -60,7 +63,10 @@ public class MergeAlgorithmTest { @Test public void testOneAgainstTwoConflictingModifications() throws IOException { assertEquals(t("aZ<Z=c>Zefghij"), - merge("abcdefghij", "aZZZefghij", "aZcZefghij")); + merge("abcdefghij", "aZZZefghij", "aZcZefghij", false)); + + assertEquals(t("aZ<Z|c=c>Zefghij"), + merge("abcdefghij", "aZZZefghij", "aZcZefghij", true)); } /** @@ -72,7 +78,10 @@ public class MergeAlgorithmTest { @Test public void testNoAgainstOneModification() throws IOException { assertEquals(t("aZcZefghij"), - merge("abcdefghij", "abcdefghij", "aZcZefghij")); + merge("abcdefghij", "abcdefghij", "aZcZefghij", false)); + + assertEquals(t("aZcZefghij"), + merge("abcdefghij", "abcdefghij", "aZcZefghij", true)); } /** @@ -84,7 +93,10 @@ public class MergeAlgorithmTest { @Test public void testTwoNonConflictingModifications() throws IOException { assertEquals(t("YbZdefghij"), - merge("abcdefghij", "abZdefghij", "Ybcdefghij")); + merge("abcdefghij", "abZdefghij", "Ybcdefghij", false)); + + assertEquals(t("YbZdefghij"), + merge("abcdefghij", "abZdefghij", "Ybcdefghij", true)); } /** @@ -96,7 +108,10 @@ public class MergeAlgorithmTest { @Test public void testTwoComplicatedModifications() throws IOException { assertEquals(t("a<ZZZZfZhZj=bYdYYYYiY>"), - merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY")); + merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY", false)); + + assertEquals(t("a<ZZZZfZhZj|bcdefghij=bYdYYYYiY>"), + merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY", true)); } /** @@ -109,7 +124,9 @@ public class MergeAlgorithmTest { @Test public void testTwoModificationsWithSharedDelete() throws IOException { assertEquals(t("Cb}n}"), - merge("ab}n}n}", "ab}n}", "Cb}n}")); + merge("ab}n}n}", "ab}n}", "Cb}n}", false)); + + assertEquals(t("Cb}n}"), merge("ab}n}n}", "ab}n}", "Cb}n}", true)); } /** @@ -122,7 +139,11 @@ public class MergeAlgorithmTest { @Test public void testModificationsWithMiddleInsert() throws IOException { assertEquals(t("aBcd123123uvwxPq"), - merge("abcd123uvwxpq", "aBcd123123uvwxPq", "abcd123123uvwxpq")); + merge("abcd123uvwxpq", "aBcd123123uvwxPq", "abcd123123uvwxpq", + false)); + + assertEquals(t("aBcd123123uvwxPq"), merge("abcd123uvwxpq", + "aBcd123123uvwxPq", "abcd123123uvwxpq", true)); } /** @@ -135,7 +156,23 @@ public class MergeAlgorithmTest { @Test public void testModificationsWithMiddleDelete() throws IOException { assertEquals(t("Abz}z123Q"), - merge("abz}z}z123q", "Abz}z123Q", "abz}z123q")); + merge("abz}z}z123q", "Abz}z123Q", "abz}z123q", false)); + + assertEquals(t("Abz}z123Q"), + merge("abz}z}z123q", "Abz}z123Q", "abz}z123q", true)); + } + + @Test + public void testInsertionAfterDeletion() throws IOException { + assertEquals(t("a<=bc>d"), merge("abd", "ad", "abcd", false)); + assertEquals(t("a<|b=bc>d"), + merge("abd", "ad", "abcd", true)); + } + + @Test + public void testInsertionBeforeDeletion() throws IOException { + assertEquals(t("a<=cb>d"), merge("abd", "ad", "acbd", false)); + assertEquals(t("a<|b=cb>d"), merge("abd", "ad", "acbd", true)); } /** @@ -146,7 +183,10 @@ public class MergeAlgorithmTest { @Test public void testConflictAtStart() throws IOException { assertEquals(t("<Z=Y>bcdefghij"), - merge("abcdefghij", "Zbcdefghij", "Ybcdefghij")); + merge("abcdefghij", "Zbcdefghij", "Ybcdefghij", false)); + + assertEquals(t("<Z|a=Y>bcdefghij"), + merge("abcdefghij", "Zbcdefghij", "Ybcdefghij", true)); } /** @@ -157,7 +197,10 @@ public class MergeAlgorithmTest { @Test public void testConflictAtEnd() throws IOException { assertEquals(t("abcdefghi<Z=Y>"), - merge("abcdefghij", "abcdefghiZ", "abcdefghiY")); + merge("abcdefghij", "abcdefghiZ", "abcdefghiY", false)); + + assertEquals(t("abcdefghi<Z|j=Y>"), + merge("abcdefghij", "abcdefghiZ", "abcdefghiY", true)); } /** @@ -169,7 +212,10 @@ public class MergeAlgorithmTest { @Test public void testSameModification() throws IOException { assertEquals(t("abZdefghij"), - merge("abcdefghij", "abZdefghij", "abZdefghij")); + merge("abcdefghij", "abZdefghij", "abZdefghij", false)); + + assertEquals(t("abZdefghij"), + merge("abcdefghij", "abZdefghij", "abZdefghij", true)); } /** @@ -181,27 +227,36 @@ public class MergeAlgorithmTest { @Test public void testDeleteVsModify() throws IOException { assertEquals(t("ab<=Z>defghij"), - merge("abcdefghij", "abdefghij", "abZdefghij")); + merge("abcdefghij", "abdefghij", "abZdefghij", false)); + + assertEquals(t("ab<|c=Z>defghij"), + merge("abcdefghij", "abdefghij", "abZdefghij", true)); } @Test public void testInsertVsModify() throws IOException { - assertEquals(t("a<bZ=XY>"), merge("ab", "abZ", "aXY")); + assertEquals(t("a<bZ=XY>"), merge("ab", "abZ", "aXY", false)); + assertEquals(t("a<bZ|b=XY>"), merge("ab", "abZ", "aXY", true)); } @Test public void testAdjacentModifications() throws IOException { - assertEquals(t("a<Zc=bY>d"), merge("abcd", "aZcd", "abYd")); + assertEquals(t("a<Zc=bY>d"), merge("abcd", "aZcd", "abYd", false)); + assertEquals(t("a<Zc|bc=bY>d"), merge("abcd", "aZcd", "abYd", true)); } @Test public void testSeparateModifications() throws IOException { - assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe")); + assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe", false)); + assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe", true)); } @Test public void testBlankLines() throws IOException { - assertEquals(t("aZc\nYe"), merge("abc\nde", "aZc\nde", "abc\nYe")); + assertEquals(t("aZc\nYe"), + merge("abc\nde", "aZc\nde", "abc\nYe", false)); + assertEquals(t("aZc\nYe"), + merge("abc\nde", "aZc\nde", "abc\nYe", true)); } /** @@ -214,11 +269,22 @@ public class MergeAlgorithmTest { */ @Test public void testTwoSimilarModsAndOneInsert() throws IOException { - assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde")); - assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB")); - assertEquals(t("HIAAAJCAB"), merge("HiACAB", "HIACAB", "HIAAAJCAB")); + assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde", false)); + assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde", true)); + + assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB", false)); + assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB", true)); + + assertEquals(t("HIAAAJCAB"), + merge("HiACAB", "HIACAB", "HIAAAJCAB", false)); + assertEquals(t("HIAAAJCAB"), + merge("HiACAB", "HIACAB", "HIAAAJCAB", true)); + + assertEquals(t("AGADEFHIAAAJCAB"), + merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB", + false)); assertEquals(t("AGADEFHIAAAJCAB"), - merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB")); + merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB", true)); } /** @@ -232,18 +298,28 @@ public class MergeAlgorithmTest { @Test public void testTwoSimilarModsAndOneInsertAtEnd() throws IOException { Assume.assumeTrue(newlineAtEnd); - assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ")); - assertEquals(t("IAJ"), merge("iA", "IA", "IAJ")); - assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ")); + assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ", false)); + assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ", true)); + + assertEquals(t("IAJ"), merge("iA", "IA", "IAJ", false)); + assertEquals(t("IAJ"), merge("iA", "IA", "IAJ", true)); + + assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ", false)); + assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ", true)); } @Test public void testTwoSimilarModsAndOneInsertAtEndNoNewlineAtEnd() throws IOException { Assume.assumeFalse(newlineAtEnd); - assertEquals(t("I<A=AAJ>"), merge("iA", "IA", "IAAJ")); - assertEquals(t("I<A=AJ>"), merge("iA", "IA", "IAJ")); - assertEquals(t("I<A=AAAJ>"), merge("iA", "IA", "IAAAJ")); + assertEquals(t("I<A=AAJ>"), merge("iA", "IA", "IAAJ", false)); + assertEquals(t("I<A|A=AAJ>"), merge("iA", "IA", "IAAJ", true)); + + assertEquals(t("I<A=AJ>"), merge("iA", "IA", "IAJ", false)); + assertEquals(t("I<A|A=AJ>"), merge("iA", "IA", "IAJ", true)); + + assertEquals(t("I<A=AAAJ>"), merge("iA", "IA", "IAAAJ", false)); + assertEquals(t("I<A|A=AAAJ>"), merge("iA", "IA", "IAAAJ", true)); } /** @@ -254,22 +330,34 @@ public class MergeAlgorithmTest { @Test public void testEmptyTexts() throws IOException { // test modification against deletion - assertEquals(t("<AB=>"), merge("A", "AB", "")); - assertEquals(t("<=AB>"), merge("A", "", "AB")); + assertEquals(t("<AB=>"), merge("A", "AB", "", false)); + assertEquals(t("<AB|A=>"), merge("A", "AB", "", true)); + + assertEquals(t("<=AB>"), merge("A", "", "AB", false)); + assertEquals(t("<|A=AB>"), merge("A", "", "AB", true)); // test unmodified against deletion - assertEquals(t(""), merge("AB", "AB", "")); - assertEquals(t(""), merge("AB", "", "AB")); + assertEquals(t(""), merge("AB", "AB", "", false)); + assertEquals(t(""), merge("AB", "AB", "", true)); + + assertEquals(t(""), merge("AB", "", "AB", false)); + assertEquals(t(""), merge("AB", "", "AB", true)); // test deletion against deletion - assertEquals(t(""), merge("AB", "", "")); + assertEquals(t(""), merge("AB", "", "", false)); + assertEquals(t(""), merge("AB", "", "", true)); } - private String merge(String commonBase, String ours, String theirs) throws IOException { + private String merge(String commonBase, String ours, String theirs, + boolean diff3) throws IOException { MergeResult r = new MergeAlgorithm().merge(RawTextComparator.DEFAULT, T(commonBase), T(ours), T(theirs)); ByteArrayOutputStream bo=new ByteArrayOutputStream(50); - fmt.formatMerge(bo, r, "B", "O", "T", UTF_8); + if (diff3) { + fmt.formatMergeDiff3(bo, r, "B", "O", "T", UTF_8); + } else { + fmt.formatMerge(bo, r, "B", "O", "T", UTF_8); + } return new String(bo.toByteArray(), UTF_8); } @@ -284,6 +372,9 @@ public class MergeAlgorithmTest { case '=': r.append("=======\n"); break; + case '|': + r.append("||||||| B\n"); + break; case '>': r.append(">>>>>>> T\n"); break; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java new file mode 100644 index 0000000000..3a8af7a00e --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2024 Qualcomm Innovation Center, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.merge; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.lib.Constants; +import org.junit.Assume; +import org.junit.Test; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.runner.RunWith; + +@RunWith(Theories.class) +public class MergeAlgorithmUnionTest { + MergeFormatter fmt = new MergeFormatter(); + + private final boolean newlineAtEnd; + + @DataPoints + public static boolean[] newlineAtEndDataPoints = { false, true }; + + public MergeAlgorithmUnionTest(boolean newlineAtEnd) { + this.newlineAtEnd = newlineAtEnd; + } + + /** + * Check for a conflict where the second text was changed similar to the + * first one, but the second texts modification covers one more line. + * + * @throws java.io.IOException + */ + @Test + public void testTwoConflictingModifications() throws IOException { + assertEquals(t("abZZdefghij"), + merge("abcdefghij", "abZdefghij", "aZZdefghij")); + } + + /** + * Test a case where we have three consecutive chunks. The first text + * modifies all three chunks. The second text modifies the first and the + * last chunk. This should be reported as one conflicting region. + * + * @throws java.io.IOException + */ + @Test + public void testOneAgainstTwoConflictingModifications() throws IOException { + assertEquals(t("aZZcZefghij"), + merge("abcdefghij", "aZZZefghij", "aZcZefghij")); + } + + /** + * Test a merge where only the second text contains modifications. Expect as + * merge result the second text. + * + * @throws java.io.IOException + */ + @Test + public void testNoAgainstOneModification() throws IOException { + assertEquals(t("aZcZefghij"), + merge("abcdefghij", "abcdefghij", "aZcZefghij")); + } + + /** + * Both texts contain modifications but not on the same chunks. Expect a + * non-conflict merge result. + * + * @throws java.io.IOException + */ + @Test + public void testTwoNonConflictingModifications() throws IOException { + assertEquals(t("YbZdefghij"), + merge("abcdefghij", "abZdefghij", "Ybcdefghij")); + } + + /** + * Merge two complicated modifications. The merge algorithm has to extend + * and combine conflicting regions to get to the expected merge result. + * + * @throws java.io.IOException + */ + @Test + public void testTwoComplicatedModifications() throws IOException { + assertEquals(t("aZZZZfZhZjbYdYYYYiY"), + merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY")); + } + + /** + * Merge two modifications with a shared delete at the end. The underlying + * diff algorithm has to provide consistent edit results to get the expected + * merge result. + * + * @throws java.io.IOException + */ + @Test + public void testTwoModificationsWithSharedDelete() throws IOException { + assertEquals(t("Cb}n}"), merge("ab}n}n}", "ab}n}", "Cb}n}")); + } + + /** + * Merge modifications with a shared insert in the middle. The underlying + * diff algorithm has to provide consistent edit results to get the expected + * merge result. + * + * @throws java.io.IOException + */ + @Test + public void testModificationsWithMiddleInsert() throws IOException { + assertEquals(t("aBcd123123uvwxPq"), + merge("abcd123uvwxpq", "aBcd123123uvwxPq", "abcd123123uvwxpq")); + } + + /** + * Merge modifications with a shared delete in the middle. The underlying + * diff algorithm has to provide consistent edit results to get the expected + * merge result. + * + * @throws java.io.IOException + */ + @Test + public void testModificationsWithMiddleDelete() throws IOException { + assertEquals(t("Abz}z123Q"), + merge("abz}z}z123q", "Abz}z123Q", "abz}z123q")); + } + + @Test + public void testInsertionAfterDeletion() throws IOException { + assertEquals(t("abcd"), merge("abd", "ad", "abcd")); + } + + @Test + public void testInsertionBeforeDeletion() throws IOException { + assertEquals(t("acbd"), merge("abd", "ad", "acbd")); + } + + /** + * Test a conflicting region at the very start of the text. + * + * @throws java.io.IOException + */ + @Test + public void testConflictAtStart() throws IOException { + assertEquals(t("ZYbcdefghij"), + merge("abcdefghij", "Zbcdefghij", "Ybcdefghij")); + } + + /** + * Test a conflicting region at the very end of the text. + * + * @throws java.io.IOException + */ + @Test + public void testConflictAtEnd() throws IOException { + assertEquals(t("abcdefghiZY"), + merge("abcdefghij", "abcdefghiZ", "abcdefghiY")); + } + + /** + * Check for a conflict where the second text was changed similar to the + * first one, but the second texts modification covers one more line. + * + * @throws java.io.IOException + */ + @Test + public void testSameModification() throws IOException { + assertEquals(t("abZdefghij"), + merge("abcdefghij", "abZdefghij", "abZdefghij")); + } + + /** + * Check that a deleted vs. a modified line shows up as conflict (see Bug + * 328551) + * + * @throws java.io.IOException + */ + @Test + public void testDeleteVsModify() throws IOException { + assertEquals(t("abZdefghij"), + merge("abcdefghij", "abdefghij", "abZdefghij")); + } + + @Test + public void testInsertVsModify() throws IOException { + assertEquals(t("abZXY"), merge("ab", "abZ", "aXY")); + } + + @Test + public void testAdjacentModifications() throws IOException { + assertEquals(t("aZcbYd"), merge("abcd", "aZcd", "abYd")); + } + + @Test + public void testSeparateModifications() throws IOException { + assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe")); + } + + @Test + public void testBlankLines() throws IOException { + assertEquals(t("aZc\nYe"), merge("abc\nde", "aZc\nde", "abc\nYe")); + } + + /** + * Test merging two contents which do one similar modification and one + * insertion is only done by one side, in the middle. Between modification + * and insertion is a block which is common between the two contents and the + * common base + * + * @throws java.io.IOException + */ + @Test + public void testTwoSimilarModsAndOneInsert() throws IOException { + assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde")); + + assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB")); + + assertEquals(t("HIAAAJCAB"), merge("HiACAB", "HIACAB", "HIAAAJCAB")); + + assertEquals(t("AGADEFHIAAAJCAB"), + merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB")); + } + + /** + * Test merging two contents which do one similar modification and one + * insertion is only done by one side, at the end. Between modification and + * insertion is a block which is common between the two contents and the + * common base + * + * @throws java.io.IOException + */ + @Test + public void testTwoSimilarModsAndOneInsertAtEnd() throws IOException { + Assume.assumeTrue(newlineAtEnd); + assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ")); + + assertEquals(t("IAJ"), merge("iA", "IA", "IAJ")); + + assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ")); + } + + @Test + public void testTwoSimilarModsAndOneInsertAtEndNoNewlineAtEnd() + throws IOException { + Assume.assumeFalse(newlineAtEnd); + assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAJ")); + + assertEquals(t("IAAJ"), merge("iA", "IA", "IAJ")); + + assertEquals(t("IAAAAJ"), merge("iA", "IA", "IAAAJ")); + } + + // Test situations where (at least) one input value is the empty text + + @Test + public void testEmptyTextModifiedAgainstDeletion() throws IOException { + // NOTE: git.git merge-file appends a '\n' to the end of the file even + // when the input files do not have a newline at the end. That appears + // to be a bug in git.git. + assertEquals(t("AB"), merge("A", "AB", "")); + assertEquals(t("AB"), merge("A", "", "AB")); + } + + @Test + public void testEmptyTextUnmodifiedAgainstDeletion() throws IOException { + assertEquals(t(""), merge("AB", "AB", "")); + + assertEquals(t(""), merge("AB", "", "AB")); + } + + @Test + public void testEmptyTextDeletionAgainstDeletion() throws IOException { + assertEquals(t(""), merge("AB", "", "")); + } + + private String merge(String commonBase, String ours, String theirs) + throws IOException { + MergeAlgorithm ma = new MergeAlgorithm(); + ma.setContentMergeStrategy(ContentMergeStrategy.UNION); + MergeResult<RawText> r = ma.merge(RawTextComparator.DEFAULT, + T(commonBase), T(ours), T(theirs)); + ByteArrayOutputStream bo = new ByteArrayOutputStream(50); + fmt.formatMerge(bo, r, "B", "O", "T", UTF_8); + return bo.toString(UTF_8); + } + + public String t(String text) { + StringBuilder r = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + switch (c) { + case '<': + r.append("<<<<<<< O\n"); + break; + case '=': + r.append("=======\n"); + break; + case '|': + r.append("||||||| B\n"); + break; + case '>': + r.append(">>>>>>> T\n"); + break; + default: + r.append(c); + if (newlineAtEnd || i < text.length() - 1) + r.append('\n'); + } + } + return r.toString(); + } + + public RawText T(String text) { + return new RawText(Constants.encode(t(text))); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java index 022e8cd55e..c6a6321cf8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java @@ -22,9 +22,12 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.nio.file.Files; import java.time.Instant; import java.util.Arrays; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeResult; @@ -51,6 +54,7 @@ import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ObjectStream; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; import org.eclipse.jgit.revwalk.RevCommit; @@ -1442,6 +1446,8 @@ public class MergerTest extends RepositoryTestCase { git.checkout().setName("master").call(); mergeResult = git.merge().include(commitX).setStrategy(strategy) .call(); + assertEquals(MergeResult.MergeStatus.MERGED, + mergeResult.getMergeStatus()); // Now, merge commit A and B (i.e. "master" and "second-branch"). // None of them have the file "a", so there is no conflict, BUT while @@ -1735,25 +1741,25 @@ public class MergerTest extends RepositoryTestCase { git.add().addFilepattern("c").call(); RevCommit commitI = git.commit().setMessage("Initial commit").call(); - File a = writeTrashFile("a", "content in Ancestor"); + writeTrashFile("a", "content in Ancestor"); git.add().addFilepattern("a").call(); RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call(); - a = writeTrashFile("a", "content in Child 1 (commited on master)"); + writeTrashFile("a", "content in Child 1 (commited on master)"); git.add().addFilepattern("a").call(); // commit C1M git.commit().setMessage("Child 1 on master").call(); git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call(); // "a" becomes executable in A2 - a = writeTrashFile("a", "content in Ancestor"); + File a = writeTrashFile("a", "content in Ancestor"); a.setExecutable(true); git.add().addFilepattern("a").call(); RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call(); // second branch git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call(); - a = writeTrashFile("a", "content in Child 2 (commited on second-branch)"); + writeTrashFile("a", "content in Child 2 (commited on second-branch)"); git.add().addFilepattern("a").call(); // commit C2S git.commit().setMessage("Child 2 on second-branch").call(); @@ -1786,7 +1792,259 @@ public class MergerTest extends RepositoryTestCase { // children mergeResult = git.merge().include(commitC3S).call(); assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED); + } + + /** + * Merging two commits when binary files have equal content, but conflicting content in the + * virtual ancestor. + * + * <p> + * This test has the same set up as + * {@code checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren}, only + * with the content conflict in A1 and A2. + */ + @Theory + public void checkBinaryMergeConflictInVirtualAncestor(MergeStrategy strategy) throws Exception { + if (!strategy.equals(MergeStrategy.RECURSIVE)) { + return; + } + + Git git = Git.wrap(db); + + // master + writeTrashFile("c", "initial file"); + git.add().addFilepattern("c").call(); + RevCommit commitI = git.commit().setMessage("Initial commit").call(); + + writeTrashFile("a", "\0\1\1\1\1\0"); // content in Ancestor 1 + git.add().addFilepattern("a").call(); + RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call(); + + writeTrashFile("a", "\0\1\2\3\4\5\0"); // content in Child 1 (commited on master) + git.add().addFilepattern("a").call(); + // commit C1M + git.commit().setMessage("Child 1 on master").call(); + + git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call(); + writeTrashFile("a", "\0\2\2\2\2\0"); // content in Ancestor 1 + git.add().addFilepattern("a").call(); + RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call(); + + // second branch + git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call(); + writeTrashFile("a", "\0\5\4\3\2\1\0"); // content in Child 2 (commited on second-branch) + git.add().addFilepattern("a").call(); + // commit C2S + git.commit().setMessage("Child 2 on second-branch").call(); + + // Merge branch-to-merge into second-branch + MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call(); + assertEquals(mergeResult.getNewHead(), null); + assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING); + // Resolve the conflict manually + writeTrashFile("a", "\0\3\3\3\3\0"); // merge conflict resolution + git.add().addFilepattern("a").call(); + RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict").call(); + + // Merge branch-to-merge into master + git.checkout().setName("master").call(); + mergeResult = git.merge().include(commitA2).setStrategy(strategy).call(); + assertEquals(mergeResult.getNewHead(), null); + assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING); + + // Resolve the conflict manually - set the same value as in resolution above + writeTrashFile("a", "\0\3\3\3\3\0"); // merge conflict resolution + git.add().addFilepattern("a").call(); + // commit C4M + git.commit().setMessage("Child 4 on master - resolve merge conflict").call(); + + // Merge C4M (second-branch) into master (C3S) + // Conflict in virtual base should be here, but there are no conflicts in + // children + mergeResult = git.merge().include(commitC3S).call(); + assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED); + } + + /** + * File is binary in ours, theirs and base with different content in each of + * them. Content of the file should not change after the merge conflict as + * no conflict markers are added to the binary files + */ + @Theory + public void oursBinaryTheirsBinaryBaseBinary(MergeStrategy strategy) + throws Exception { + Git git = Git.wrap(db); + String binaryFile = "file"; + + writeTrashFile(binaryFile, "\u0000\u0001"); + git.add().addFilepattern(binaryFile).call(); + RevCommit parent = git.commit().setMessage("BASE COMMIT").call(); + String fileHashInBase = getFileHashInWorkTree(git, binaryFile); + + writeTrashFile(binaryFile, "\u0001\u0002"); + git.add().addFilepattern(binaryFile).call(); + RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call(); + String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile); + + git.checkout().setCreateBranch(true).setStartPoint(parent) + .setName("side").call(); + + writeTrashFile(binaryFile, "\u0002\u0000"); + git.add().addFilepattern(binaryFile).call(); + git.commit().setMessage("OURS COMMIT").call(); + String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile); + + MergeResult mergeResult = git.merge().setStrategy(strategy) + .include(child1).call(); + + // check if the merge caused a conflict + assertTrue(mergeResult.getConflicts() != null + && !mergeResult.getConflicts().isEmpty()); + String fileHashInChild2AfterMerge = getFileHashInWorkTree(git, + binaryFile); + + // check if the file content changed during a conflicting merge + assertEquals(fileHashInChild2AfterMerge, fileHashInChild2); + + Set<String> hashesInIndexFile = new HashSet<>(); + DirCache indexContent = git.getRepository().readDirCache(); + for (int i = 0; i < indexContent.getEntryCount(); ++i) { + DirCacheEntry indexEntry = indexContent.getEntry(i); + if (binaryFile.equals(indexEntry.getPathString())) { + hashesInIndexFile.add(indexEntry.getObjectId().name()); + } + } + + // check if all the three stages are added to index file + assertTrue(hashesInIndexFile.contains(fileHashInBase)); + assertTrue(hashesInIndexFile.contains(fileHashInChild1)); + assertTrue(hashesInIndexFile.contains(fileHashInChild2)); + } + + /** + * File is text in ours and theirs with different content but binary in + * base. Even in this case, file will be treated as a binary and no conflict + * markers are added to it + */ + @Theory + public void oursAndTheirsDifferentTextBaseBinary(MergeStrategy strategy) + throws Exception { + Git git = Git.wrap(db); + String binaryFile = "file"; + + writeTrashFile(binaryFile, "\u0000\u0001"); + git.add().addFilepattern(binaryFile).call(); + RevCommit parent = git.commit().setMessage("BASE COMMIT").call(); + String fileHashInBase = getFileHashInWorkTree(git, binaryFile); + + writeTrashFile(binaryFile, "TEXT1"); + git.add().addFilepattern(binaryFile).call(); + RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call(); + String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile); + + git.checkout().setCreateBranch(true).setStartPoint(parent) + .setName("side").call(); + + writeTrashFile(binaryFile, "TEXT2"); + git.add().addFilepattern(binaryFile).call(); + git.commit().setMessage("OURS COMMIT").call(); + String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile); + + MergeResult mergeResult = git.merge().setStrategy(strategy) + .include(child1).call(); + + assertTrue(mergeResult.getConflicts() != null + && !mergeResult.getConflicts().isEmpty()); + String fileHashInChild2AfterMerge = getFileHashInWorkTree(git, + binaryFile); + + assertEquals(fileHashInChild2AfterMerge, fileHashInChild2); + + Set<String> hashesInIndexFile = new HashSet<>(); + DirCache indexContent = git.getRepository().readDirCache(); + for (int i = 0; i < indexContent.getEntryCount(); ++i) { + DirCacheEntry indexEntry = indexContent.getEntry(i); + if (binaryFile.equals(indexEntry.getPathString())) { + hashesInIndexFile.add(indexEntry.getObjectId().name()); + } + } + + assertTrue(hashesInIndexFile.contains(fileHashInBase)); + assertTrue(hashesInIndexFile.contains(fileHashInChild1)); + assertTrue(hashesInIndexFile.contains(fileHashInChild2)); + } + + /** + * Tests the scenario where a file is expected to be treated as binary + * according to Git attributes + */ + @Theory + public void fileInBinaryInAttribute(MergeStrategy strategy) + throws Exception { + Git git = Git.wrap(db); + String binaryFile = "file.bin"; + + writeTrashFile(".gitattributes", binaryFile + " binary"); + git.add().addFilepattern(".gitattributes").call(); + git.commit().setMessage("ADDING GITATTRIBUTES").call(); + + writeTrashFile(binaryFile, "\u0000\u0001"); + git.add().addFilepattern(binaryFile).call(); + RevCommit parent = git.commit().setMessage("BASE COMMIT").call(); + String fileHashInBase = getFileHashInWorkTree(git, binaryFile); + + writeTrashFile(binaryFile, "\u0001\u0002"); + git.add().addFilepattern(binaryFile).call(); + RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call(); + String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile); + + git.checkout().setCreateBranch(true).setStartPoint(parent) + .setName("side").call(); + + writeTrashFile(binaryFile, "\u0002\u0000"); + git.add().addFilepattern(binaryFile).call(); + git.commit().setMessage("OURS COMMIT").call(); + String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile); + + MergeResult mergeResult = git.merge().setStrategy(strategy) + .include(child1).call(); + + // check if the merge caused a conflict + assertTrue(mergeResult.getConflicts() != null + && !mergeResult.getConflicts().isEmpty()); + String fileHashInChild2AfterMerge = getFileHashInWorkTree(git, + binaryFile); + + // check if the file content changed during a conflicting merge + assertEquals(fileHashInChild2AfterMerge, fileHashInChild2); + + Set<String> hashesInIndexFile = new HashSet<>(); + DirCache indexContent = git.getRepository().readDirCache(); + for (int i = 0; i < indexContent.getEntryCount(); ++i) { + DirCacheEntry indexEntry = indexContent.getEntry(i); + if (binaryFile.equals(indexEntry.getPathString())) { + hashesInIndexFile.add(indexEntry.getObjectId().name()); + } + } + + // check if all the three stages are added to index file + assertTrue(hashesInIndexFile.contains(fileHashInBase)); + assertTrue(hashesInIndexFile.contains(fileHashInChild1)); + assertTrue(hashesInIndexFile.contains(fileHashInChild2)); + } + + private String getFileHashInWorkTree(Git git, String filePath) + throws IOException { + Repository repository = git.getRepository(); + ObjectInserter objectInserter = repository.newObjectInserter(); + + File conflictingFile = new File(repository.getWorkTree(), filePath); + byte[] fileContent = Files.readAllBytes(conflictingFile.toPath()); + ObjectId blobId = objectInserter.insert(Constants.OBJ_BLOB, + fileContent); + objectInserter.flush(); + return blobId.name(); } private void writeSubmodule(String path, ObjectId commit) diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java index 798aebe3b0..0016adfb66 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java @@ -16,6 +16,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; @@ -375,7 +377,8 @@ public class SimpleMergeTest extends SampleDataRepositoryTestCase { ObjectId[] parentIds) throws Exception { CommitBuilder c = new CommitBuilder(); c.setTreeId(treeB.writeTree(odi)); - c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0)); + c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", + Instant.ofEpochMilli(1L), ZoneOffset.UTC)); c.setCommitter(c.getAuthor()); c.setParentIds(parentIds); c.setMessage("Tree " + c.getTreeId().name()); |