aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java')
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java2140
1 files changed, 2140 insertions, 0 deletions
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
new file mode 100644
index 0000000000..c6a6321cf8
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
@@ -0,0 +1,2140 @@
+/*
+ * Copyright (C) 2012, 2020 Robin Stocker <robin@nibor.org> and others
+ *
+ * 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 java.time.Instant.EPOCH;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+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;
+import org.eclipse.jgit.api.MergeResult.MergeStatus;
+import org.eclipse.jgit.api.RebaseResult;
+import org.eclipse.jgit.api.errors.CheckoutConflictException;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.NoMergeBaseException;
+import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+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;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+import org.junit.Assert;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public class MergerTest extends RepositoryTestCase {
+
+ @DataPoints
+ public static MergeStrategy[] strategiesUnderTest = new MergeStrategy[] {
+ MergeStrategy.RECURSIVE, MergeStrategy.RESOLVE };
+
+ @Theory
+ public void failingDeleteOfDirectoryWithUntrackedContent(
+ MergeStrategy strategy) throws Exception {
+ File folder1 = new File(db.getWorkTree(), "folder1");
+ FileUtils.mkdir(folder1);
+ File file = new File(folder1, "file1.txt");
+ write(file, "folder1--file1.txt");
+ file = new File(folder1, "file2.txt");
+ write(file, "folder1--file2.txt");
+
+ try (Git git = new Git(db)) {
+ git.add().addFilepattern(folder1.getName()).call();
+ RevCommit base = git.commit().setMessage("adding folder").call();
+
+ recursiveDelete(folder1);
+ git.rm().addFilepattern("folder1/file1.txt")
+ .addFilepattern("folder1/file2.txt").call();
+ RevCommit other = git.commit()
+ .setMessage("removing folders on 'other'").call();
+
+ git.checkout().setName(base.name()).call();
+
+ file = new File(db.getWorkTree(), "unrelated.txt");
+ write(file, "unrelated");
+
+ git.add().addFilepattern("unrelated.txt").call();
+ RevCommit head = git.commit().setMessage("Adding another file").call();
+
+ // Untracked file to cause failing path for delete() of folder1
+ // but that's ok.
+ file = new File(folder1, "file3.txt");
+ write(file, "folder1--file3.txt");
+
+ ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, false);
+ merger.setCommitNames(new String[] { "BASE", "HEAD", "other" });
+ merger.setWorkingTreeIterator(new FileTreeIterator(db));
+ boolean ok = merger.merge(head.getId(), other.getId());
+ assertTrue(ok);
+ assertTrue(file.exists());
+ }
+ }
+
+ /**
+ * Merging two conflicting subtrees when the index does not contain any file
+ * in that subtree should lead to a conflicting state.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeConflictingTreesWithoutIndex(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("d/1", "orig");
+ git.add().addFilepattern("d/1").call();
+ RevCommit first = git.commit().setMessage("added d/1").call();
+
+ writeTrashFile("d/1", "master");
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified d/1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("d/1", "side");
+ git.commit().setAll(true).setMessage("modified d/1 on side").call();
+
+ git.rm().addFilepattern("d/1").call();
+ git.rm().addFilepattern("d").call();
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
+ assertEquals(
+ "[d/1, mode:100644, stage:1, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * Merging two different but mergeable subtrees when the index does not
+ * contain any file in that subtree should lead to a merged state.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeMergeableTreesWithoutIndex(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("d/1", "1\n2\n3");
+ git.add().addFilepattern("d/1").call();
+ RevCommit first = git.commit().setMessage("added d/1").call();
+
+ writeTrashFile("d/1", "1master\n2\n3");
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified d/1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("d/1", "1\n2\n3side");
+ git.commit().setAll(true).setMessage("modified d/1 on side").call();
+
+ git.rm().addFilepattern("d/1").call();
+ git.rm().addFilepattern("d").call();
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
+ assertEquals("[d/1, mode:100644, content:1master\n2\n3side]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * An existing directory without tracked content should not prevent merging
+ * a tree where that directory exists.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkUntrackedFolderIsNotAConflict(
+ MergeStrategy strategy) throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("d/1", "1");
+ git.add().addFilepattern("d/1").call();
+ RevCommit first = git.commit().setMessage("added d/1").call();
+
+ writeTrashFile("e/1", "4");
+ git.add().addFilepattern("e/1").call();
+ RevCommit masterCommit = git.commit().setMessage("added e/1").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("f/1", "5");
+ git.add().addFilepattern("f/1").call();
+ git.commit().setAll(true).setMessage("added f/1")
+ .call();
+
+ // Untracked directory e shall not conflict with merged e/1
+ writeTrashFile("e/2", "d two");
+
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
+ assertEquals(
+ "[d/1, mode:100644, content:1][e/1, mode:100644, content:4][f/1, mode:100644, content:5]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * A tracked file is replaced by a folder in THEIRS.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkFileReplacedByFolderInTheirs(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("sub", "file");
+ git.add().addFilepattern("sub").call();
+ RevCommit first = git.commit().setMessage("initial").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+
+ git.rm().addFilepattern("sub").call();
+ writeTrashFile("sub/file", "subfile");
+ git.add().addFilepattern("sub/file").call();
+ RevCommit masterCommit = git.commit().setMessage("file -> folder")
+ .call();
+
+ git.checkout().setName("master").call();
+ writeTrashFile("noop", "other");
+ git.add().addFilepattern("noop").call();
+ git.commit().setAll(true).setMessage("noop").call();
+
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
+ assertEquals(
+ "[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * A tracked file is replaced by a folder in OURS.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkFileReplacedByFolderInOurs(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("sub", "file");
+ git.add().addFilepattern("sub").call();
+ RevCommit first = git.commit().setMessage("initial").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("noop", "other");
+ git.add().addFilepattern("noop").call();
+ RevCommit sideCommit = git.commit().setAll(true).setMessage("noop")
+ .call();
+
+ git.checkout().setName("master").call();
+ git.rm().addFilepattern("sub").call();
+ writeTrashFile("sub/file", "subfile");
+ git.add().addFilepattern("sub/file").call();
+ git.commit().setMessage("file -> folder")
+ .call();
+
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(sideCommit).call();
+ assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
+ assertEquals(
+ "[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * An existing directory without tracked content should not prevent merging
+ * a file with that name.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkUntrackedEmpytFolderIsNotAConflictWithFile(
+ MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("d/1", "1");
+ git.add().addFilepattern("d/1").call();
+ RevCommit first = git.commit().setMessage("added d/1").call();
+
+ writeTrashFile("e", "4");
+ git.add().addFilepattern("e").call();
+ RevCommit masterCommit = git.commit().setMessage("added e").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("f/1", "5");
+ git.add().addFilepattern("f/1").call();
+ git.commit().setAll(true).setMessage("added f/1").call();
+
+ // Untracked empty directory hierarcy e/1 shall not conflict with merged
+ // e/1
+ FileUtils.mkdirs(new File(trash, "e/1"), true);
+
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
+ assertEquals(
+ "[d/1, mode:100644, content:1][e, mode:100644, content:4][f/1, mode:100644, content:5]",
+ indexState(CONTENT));
+ }
+
+ @Theory
+ public void mergeWithCrlfInWT(MergeStrategy strategy) throws IOException,
+ GitAPIException {
+ Git git = Git.wrap(db);
+ db.getConfig().setString("core", null, "autocrlf", "false");
+ db.getConfig().save();
+ writeTrashFile("crlf.txt", "some\r\ndata\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ git.commit().setMessage("base").call();
+
+ git.branchCreate().setName("brancha").call();
+
+ writeTrashFile("crlf.txt", "some\r\nmore\r\ndata\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ git.commit().setMessage("on master").call();
+
+ git.checkout().setName("brancha").call();
+ writeTrashFile("crlf.txt", "some\r\ndata\r\ntoo\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ git.commit().setMessage("on brancha").call();
+
+ db.getConfig().setString("core", null, "autocrlf", "input");
+ db.getConfig().save();
+
+ MergeResult mergeResult = git.merge().setStrategy(strategy)
+ .include(db.resolve("master"))
+ .call();
+ assertEquals(MergeResult.MergeStatus.MERGED,
+ mergeResult.getMergeStatus());
+ }
+
+ @Theory
+ public void mergeConflictWithCrLfTextAuto(MergeStrategy strategy)
+ throws IOException, GitAPIException {
+ Git git = Git.wrap(db);
+ writeTrashFile("crlf.txt", "a crlf file\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ git.commit().setMessage("base").call();
+ assertEquals("[crlf.txt, mode:100644, content:a crlf file\r\n]",
+ indexState(CONTENT));
+ writeTrashFile(".gitattributes", "crlf.txt text=auto");
+ git.add().addFilepattern(".gitattributes").call();
+ git.commit().setMessage("attributes").call();
+
+ git.branchCreate().setName("brancha").call();
+
+ writeTrashFile("crlf.txt", "a crlf file\r\na second line\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ git.commit().setMessage("on master").call();
+ assertEquals(
+ "[.gitattributes, mode:100644, content:crlf.txt text=auto]"
+ + "[crlf.txt, mode:100644, content:a crlf file\r\na second line\r\n]",
+ indexState(CONTENT));
+
+ git.checkout().setName("brancha").call();
+ File testFile = writeTrashFile("crlf.txt",
+ "a crlf file\r\nanother line\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ git.commit().setMessage("on brancha").call();
+
+ MergeResult mergeResult = git.merge().setStrategy(strategy)
+ .include(db.resolve("master")).call();
+ assertEquals(MergeResult.MergeStatus.CONFLICTING,
+ mergeResult.getMergeStatus());
+ checkFile(testFile,
+ "a crlf file\r\n" //
+ + "<<<<<<< HEAD\n" //
+ + "another line\r\n" //
+ + "=======\n" //
+ + "a second line\r\n" //
+ + ">>>>>>> 8e9e704742f1bc8a41eac88aac4aeefd338b7384\n");
+ }
+
+ @Theory
+ public void mergeWithCrlfAutoCrlfTrue(MergeStrategy strategy)
+ throws IOException, GitAPIException {
+ Git git = Git.wrap(db);
+ db.getConfig().setString("core", null, "autocrlf", "true");
+ db.getConfig().save();
+ writeTrashFile("crlf.txt", "a crlf file\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ git.commit().setMessage("base").call();
+
+ git.branchCreate().setName("brancha").call();
+
+ writeTrashFile("crlf.txt", "a crlf file\r\na second line\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ git.commit().setMessage("on master").call();
+
+ git.checkout().setName("brancha").call();
+ File testFile = writeTrashFile("crlf.txt",
+ "a first line\r\na crlf file\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ git.commit().setMessage("on brancha").call();
+
+ MergeResult mergeResult = git.merge().setStrategy(strategy)
+ .include(db.resolve("master")).call();
+ assertEquals(MergeResult.MergeStatus.MERGED,
+ mergeResult.getMergeStatus());
+ checkFile(testFile, "a first line\r\na crlf file\r\na second line\r\n");
+ assertEquals(
+ "[crlf.txt, mode:100644, content:a first line\na crlf file\na second line\n]",
+ indexState(CONTENT));
+ }
+
+ @Theory
+ public void rebaseWithCrlfAutoCrlfTrue(MergeStrategy strategy)
+ throws IOException, GitAPIException {
+ Git git = Git.wrap(db);
+ db.getConfig().setString("core", null, "autocrlf", "true");
+ db.getConfig().save();
+ writeTrashFile("crlf.txt", "line 1\r\nline 2\r\nline 3\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ RevCommit first = git.commit().setMessage("base").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("brancha").call();
+
+ File testFile = writeTrashFile("crlf.txt",
+ "line 1\r\nmodified line\r\nline 3\r\n");
+ git.add().addFilepattern("crlf.txt").call();
+ git.commit().setMessage("on brancha").call();
+
+ git.checkout().setName("master").call();
+ File otherFile = writeTrashFile("otherfile.txt", "a line\r\n");
+ git.add().addFilepattern("otherfile.txt").call();
+ git.commit().setMessage("on master").call();
+
+ git.checkout().setName("brancha").call();
+ checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n");
+ assertFalse(otherFile.exists());
+
+ RebaseResult rebaseResult = git.rebase().setStrategy(strategy)
+ .setUpstream(db.resolve("master")).call();
+ assertEquals(RebaseResult.Status.OK, rebaseResult.getStatus());
+ checkFile(testFile, "line 1\r\nmodified line\r\nline 3\r\n");
+ checkFile(otherFile, "a line\r\n");
+ assertEquals(
+ "[crlf.txt, mode:100644, content:line 1\nmodified line\nline 3\n]"
+ + "[otherfile.txt, mode:100644, content:a line\n]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * Merging two equal subtrees when the index does not contain any file in
+ * that subtree should lead to a merged state.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeEqualTreesWithoutIndex(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("d/1", "orig");
+ git.add().addFilepattern("d/1").call();
+ RevCommit first = git.commit().setMessage("added d/1").call();
+
+ writeTrashFile("d/1", "modified");
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified d/1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("d/1", "modified");
+ git.commit().setAll(true).setMessage("modified d/1 on side").call();
+
+ git.rm().addFilepattern("d/1").call();
+ git.rm().addFilepattern("d").call();
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
+ assertEquals("[d/1, mode:100644, content:modified]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * Merging two equal subtrees with an incore merger should lead to a merged
+ * state.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeEqualTreesInCore(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("d/1", "orig");
+ git.add().addFilepattern("d/1").call();
+ RevCommit first = git.commit().setMessage("added d/1").call();
+
+ writeTrashFile("d/1", "modified");
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified d/1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("d/1", "modified");
+ RevCommit sideCommit = git.commit().setAll(true)
+ .setMessage("modified d/1 on side").call();
+
+ git.rm().addFilepattern("d/1").call();
+ git.rm().addFilepattern("d").call();
+
+ ThreeWayMerger resolveMerger = (ThreeWayMerger) strategy.newMerger(db,
+ true);
+ boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
+ assertTrue(noProblems);
+ }
+
+ /**
+ * Merging two equal subtrees with an incore merger should lead to a merged
+ * state, without using a Repository (the 'Gerrit' use case).
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeEqualTreesInCore_noRepo(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("d/1", "orig");
+ git.add().addFilepattern("d/1").call();
+ RevCommit first = git.commit().setMessage("added d/1").call();
+
+ writeTrashFile("d/1", "modified");
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified d/1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("d/1", "modified");
+ RevCommit sideCommit = git.commit().setAll(true)
+ .setMessage("modified d/1 on side").call();
+
+ git.rm().addFilepattern("d/1").call();
+ git.rm().addFilepattern("d").call();
+
+ try (ObjectInserter ins = db.newObjectInserter()) {
+ ThreeWayMerger resolveMerger =
+ (ThreeWayMerger) strategy.newMerger(ins, db.getConfig());
+ boolean noProblems = resolveMerger.merge(masterCommit, sideCommit);
+ assertTrue(noProblems);
+ }
+ }
+
+ /**
+ * Merging two equal subtrees when the index and HEAD does not contain any
+ * file in that subtree should lead to a merged state.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeEqualNewTrees(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("2", "orig");
+ git.add().addFilepattern("2").call();
+ RevCommit first = git.commit().setMessage("added 2").call();
+
+ writeTrashFile("d/1", "orig");
+ git.add().addFilepattern("d/1").call();
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("added d/1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("d/1", "orig");
+ git.add().addFilepattern("d/1").call();
+ git.commit().setAll(true).setMessage("added d/1 on side").call();
+
+ git.rm().addFilepattern("d/1").call();
+ git.rm().addFilepattern("d").call();
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
+ assertEquals(
+ "[2, mode:100644, content:orig][d/1, mode:100644, content:orig]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * Merging two conflicting subtrees when the index and HEAD does not contain
+ * any file in that subtree should lead to a conflicting state.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeConflictingNewTrees(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("2", "orig");
+ git.add().addFilepattern("2").call();
+ RevCommit first = git.commit().setMessage("added 2").call();
+
+ writeTrashFile("d/1", "master");
+ git.add().addFilepattern("d/1").call();
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("added d/1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("d/1", "side");
+ git.add().addFilepattern("d/1").call();
+ git.commit().setAll(true).setMessage("added d/1 on side").call();
+
+ git.rm().addFilepattern("d/1").call();
+ git.rm().addFilepattern("d").call();
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ assertEquals(MergeStatus.CONFLICTING, mergeRes.getMergeStatus());
+ assertEquals(
+ "[2, mode:100644, content:orig][d/1, mode:100644, stage:2, content:side][d/1, mode:100644, stage:3, content:master]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * Merging two conflicting files when the index contains a tree for that
+ * path should lead to a failed state.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeConflictingFilesWithTreeInIndex(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("0", "orig");
+ git.add().addFilepattern("0").call();
+ RevCommit first = git.commit().setMessage("added 0").call();
+
+ writeTrashFile("0", "master");
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified 0 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("0", "side");
+ git.commit().setAll(true).setMessage("modified 0 on side").call();
+
+ git.rm().addFilepattern("0").call();
+ writeTrashFile("0/0", "side");
+ git.add().addFilepattern("0/0").call();
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ assertEquals(MergeStatus.FAILED, mergeRes.getMergeStatus());
+ }
+
+ /**
+ * Merging two equal files when the index contains a tree for that path
+ * should lead to a failed state.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeMergeableFilesWithTreeInIndex(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("0", "orig");
+ writeTrashFile("1", "1\n2\n3");
+ git.add().addFilepattern("0").addFilepattern("1").call();
+ RevCommit first = git.commit().setMessage("added 0, 1").call();
+
+ writeTrashFile("1", "1master\n2\n3");
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified 1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("1", "1\n2\n3side");
+ git.commit().setAll(true).setMessage("modified 1 on side").call();
+
+ git.rm().addFilepattern("0").call();
+ writeTrashFile("0/0", "modified");
+ git.add().addFilepattern("0/0").call();
+ try {
+ git.merge().setStrategy(strategy).include(masterCommit).call();
+ Assert.fail("Didn't get the expected exception");
+ } catch (CheckoutConflictException e) {
+ assertEquals(1, e.getConflictingPaths().size());
+ assertEquals("0/0", e.getConflictingPaths().get(0));
+ }
+ }
+
+ @Theory
+ public void checkContentMergeNoConflict(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("file", "1\n2\n3");
+ git.add().addFilepattern("file").call();
+ RevCommit first = git.commit().setMessage("added file").call();
+
+ writeTrashFile("file", "1master\n2\n3");
+ git.commit().setAll(true).setMessage("modified file on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("file", "1\n2\n3side");
+ RevCommit sideCommit = git.commit().setAll(true)
+ .setMessage("modified file on side").call();
+
+ git.checkout().setName("master").call();
+ MergeResult result =
+ git.merge().setStrategy(strategy).include(sideCommit).call();
+ assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+ String expected = "1master\n2\n3side";
+ assertEquals(expected, read("file"));
+ }
+
+ @Theory
+ public void checkContentMergeNoConflict_noRepo(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("file", "1\n2\n3");
+ git.add().addFilepattern("file").call();
+ RevCommit first = git.commit().setMessage("added file").call();
+
+ writeTrashFile("file", "1master\n2\n3");
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified file on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("file", "1\n2\n3side");
+ RevCommit sideCommit = git.commit().setAll(true)
+ .setMessage("modified file on side").call();
+
+ try (ObjectInserter ins = db.newObjectInserter()) {
+ ResolveMerger merger =
+ (ResolveMerger) strategy.newMerger(ins, db.getConfig());
+ boolean noProblems = merger.merge(masterCommit, sideCommit);
+ assertTrue(noProblems);
+ assertEquals("1master\n2\n3side",
+ readBlob(merger.getResultTreeId(), "file"));
+ }
+ }
+
+
+ /**
+ * Merging a change involving large binary files should short-circuit reads.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkContentMergeLargeBinaries(MergeStrategy strategy) throws Exception {
+ Git git = Git.wrap(db);
+ final int LINELEN = 72;
+
+ // setup a merge that would work correctly if we disconsider the stray '\0'
+ // that the file contains near the start.
+ byte[] binary = new byte[LINELEN * 2000];
+ for (int i = 0; i < binary.length; i++) {
+ binary[i] = (byte)((i % LINELEN) == 0 ? '\n' : 'x');
+ }
+ binary[50] = '\0';
+
+ writeTrashFile("file", new String(binary, UTF_8));
+ git.add().addFilepattern("file").call();
+ RevCommit first = git.commit().setMessage("added file").call();
+
+ // Generate an edit in a single line.
+ int idx = LINELEN * 1200 + 1;
+ byte save = binary[idx];
+ binary[idx] = '@';
+ writeTrashFile("file", new String(binary, UTF_8));
+
+ binary[idx] = save;
+ git.add().addFilepattern("file").call();
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified file l 1200").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first).setName("side").call();
+ binary[LINELEN * 1500 + 1] = '!';
+ writeTrashFile("file", new String(binary, UTF_8));
+ git.add().addFilepattern("file").call();
+ RevCommit sideCommit = git.commit().setAll(true)
+ .setMessage("modified file l 1500").call();
+
+ int originalBufferSize = RawText.getBufferSize();
+ int smallBufferSize = RawText.setBufferSize(8000);
+ try (ObjectInserter ins = db.newObjectInserter()) {
+ // Check that we don't read the large blobs.
+ ObjectInserter forbidInserter = new ObjectInserter.Filter() {
+ @Override
+ protected ObjectInserter delegate() {
+ return ins;
+ }
+
+ @Override
+ public ObjectReader newReader() {
+ return new BigReadForbiddenReader(super.newReader(),
+ smallBufferSize);
+ }
+ };
+
+ ResolveMerger merger =
+ (ResolveMerger) strategy.newMerger(forbidInserter, db.getConfig());
+ boolean noProblems = merger.merge(masterCommit, sideCommit);
+ assertFalse(noProblems);
+ } finally {
+ RawText.setBufferSize(originalBufferSize);
+ }
+ }
+
+ /**
+ * Throws an exception if reading beyond limit.
+ */
+ static class BigReadForbiddenStream extends ObjectStream.Filter {
+ long limit;
+
+ BigReadForbiddenStream(ObjectStream orig, long limit) {
+ super(orig.getType(), orig.getSize(), orig);
+ this.limit = limit;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ limit -= n;
+ if (limit < 0) {
+ throw new IllegalStateException();
+ }
+
+ return super.skip(n);
+ }
+
+ @Override
+ public int read() throws IOException {
+ int r = super.read();
+ limit--;
+ if (limit < 0) {
+ throw new IllegalStateException();
+ }
+ return r;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int n = super.read(b, off, len);
+ limit -= n;
+ if (limit < 0) {
+ throw new IllegalStateException();
+ }
+ return n;
+ }
+ }
+
+ static class BigReadForbiddenReader extends ObjectReader.Filter {
+ ObjectReader delegate;
+ int limit;
+
+ @Override
+ protected ObjectReader delegate() {
+ return delegate;
+ }
+
+ BigReadForbiddenReader(ObjectReader delegate, int limit) {
+ this.delegate = delegate;
+ this.limit = limit;
+ }
+
+ @Override
+ public ObjectLoader open(AnyObjectId objectId, int typeHint) throws IOException {
+ ObjectLoader orig = super.open(objectId, typeHint);
+ return new ObjectLoader.Filter() {
+ @Override
+ protected ObjectLoader delegate() {
+ return orig;
+ }
+
+ @Override
+ public ObjectStream openStream() throws IOException {
+ ObjectStream os = orig.openStream();
+ return new BigReadForbiddenStream(os, limit);
+ }
+ };
+ }
+ }
+
+ @Theory
+ public void checkContentMergeConflict(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("file", "1\n2\n3");
+ git.add().addFilepattern("file").call();
+ RevCommit first = git.commit().setMessage("added file").call();
+
+ writeTrashFile("file", "1master\n2\n3");
+ git.commit().setAll(true).setMessage("modified file on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("file", "1side\n2\n3");
+ RevCommit sideCommit = git.commit().setAll(true)
+ .setMessage("modified file on side").call();
+
+ git.checkout().setName("master").call();
+ MergeResult result =
+ git.merge().setStrategy(strategy).include(sideCommit).call();
+ assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+ String expected = "<<<<<<< HEAD\n"
+ + "1master\n"
+ + "=======\n"
+ + "1side\n"
+ + ">>>>>>> " + sideCommit.name() + "\n"
+ + "2\n"
+ + "3";
+ assertEquals(expected, read("file"));
+ }
+
+ @Theory
+ public void checkContentMergeConflict_noTree(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("file", "1\n2\n3");
+ git.add().addFilepattern("file").call();
+ RevCommit first = git.commit().setMessage("added file").call();
+
+ writeTrashFile("file", "1master\n2\n3");
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified file on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("file", "1side\n2\n3");
+ RevCommit sideCommit = git.commit().setAll(true)
+ .setMessage("modified file on side").call();
+
+ try (ObjectInserter ins = db.newObjectInserter()) {
+ ResolveMerger merger =
+ (ResolveMerger) strategy.newMerger(ins, db.getConfig());
+ boolean noProblems = merger.merge(masterCommit, sideCommit);
+ assertFalse(noProblems);
+ assertEquals(Arrays.asList("file"), merger.getUnmergedPaths());
+
+ MergeFormatter fmt = new MergeFormatter();
+ merger.getMergeResults().get("file");
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ fmt.formatMerge(out, merger.getMergeResults().get("file"),
+ "BASE", "OURS", "THEIRS", UTF_8);
+ String expected = "<<<<<<< OURS\n"
+ + "1master\n"
+ + "=======\n"
+ + "1side\n"
+ + ">>>>>>> THEIRS\n"
+ + "2\n"
+ + "3";
+ assertEquals(expected, new String(out.toByteArray(), UTF_8));
+ }
+ }
+ }
+
+ @Theory
+ public void fileBecomesDir_noTree(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("file", "1\n2\n3");
+ writeTrashFile("side", "1\n2\n3");
+ git.add().addFilepattern("file").addFilepattern("side").call();
+ RevCommit first = git.commit().setMessage("base").call();
+
+ writeTrashFile("side", "our changed");
+ RevCommit ours = git.commit().setAll(true)
+ .setMessage("ours").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("theirs").call();
+ deleteTrashFile("file");
+ writeTrashFile("file/file", "in subdir");
+ git.add().addFilepattern("file/file").call();
+
+ RevCommit theirs = git.commit().setAll(true)
+ .setMessage("theirs").call();
+
+ // Exercise inCore flavor of the merge.
+ try (ObjectInserter ins = db.newObjectInserter()) {
+ ResolveMerger merger =
+ (ResolveMerger) strategy.newMerger(ins, db.getConfig());
+ boolean success = merger.merge(ours, theirs);
+ assertTrue(success);
+ assertTrue(merger.getModifiedFiles().isEmpty());
+ }
+ }
+
+ /**
+ * This is a high-level test for https://bugs.eclipse.org/bugs/show_bug.cgi?id=535919
+ *
+ * The actual fix was made in {@link org.eclipse.jgit.treewalk.NameConflictTreeWalk}
+ * and tested in {@link org.eclipse.jgit.treewalk.NameConflictTreeWalkTest#tesdDF_LastItemsInTreeHasDFConflictAndSpecialNames}.
+ */
+ @Theory
+ public void checkMergeDoesntCrashWithSpecialFileNames(
+ MergeStrategy strategy) throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("subtree", "");
+ writeTrashFile("subtree-0", "");
+ git.add().addFilepattern("subtree").call();
+ git.add().addFilepattern("subtree-0").call();
+ RevCommit toMerge = git.commit().setMessage("commit-1").call();
+
+ git.rm().addFilepattern("subtree").call();
+ writeTrashFile("subtree/file", "");
+ git.add().addFilepattern("subtree").call();
+ RevCommit mergeTip = git.commit().setMessage("commit2").call();
+
+ ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, false);
+ assertTrue(merger.merge(mergeTip, toMerge));
+ }
+
+ /**
+ * Merging after criss-cross merges. In this case we merge together two
+ * commits which have two equally good common ancestors
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeCrissCross(MergeStrategy strategy) throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("1", "1\n2\n3");
+ git.add().addFilepattern("1").call();
+ RevCommit first = git.commit().setMessage("added 1").call();
+
+ writeTrashFile("1", "1master\n2\n3");
+ RevCommit masterCommit = git.commit().setAll(true)
+ .setMessage("modified 1 on master").call();
+
+ writeTrashFile("1", "1master2\n2\n3");
+ git.commit().setAll(true)
+ .setMessage("modified 1 on master again").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("1", "1\n2\na\nb\nc\n3side");
+ RevCommit sideCommit = git.commit().setAll(true)
+ .setMessage("modified 1 on side").call();
+
+ writeTrashFile("1", "1\n2\n3side2");
+ git.commit().setAll(true)
+ .setMessage("modified 1 on side again").call();
+
+ MergeResult result = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+ result.getNewHead();
+ git.checkout().setName("master").call();
+ result = git.merge().setStrategy(strategy).include(sideCommit).call();
+ assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+
+ // we have two branches which are criss-cross merged. Try to merge the
+ // tips. This should succeed with RecursiveMerge and fail with
+ // ResolveMerge
+ try {
+ MergeResult mergeResult = git.merge().setStrategy(strategy)
+ .include(git.getRepository().exactRef("refs/heads/side"))
+ .call();
+ assertEquals(MergeStrategy.RECURSIVE, strategy);
+ assertEquals(MergeResult.MergeStatus.MERGED,
+ mergeResult.getMergeStatus());
+ assertEquals("1master2\n2\n3side2", read("1"));
+ } catch (JGitInternalException e) {
+ assertEquals(MergeStrategy.RESOLVE, strategy);
+ assertTrue(e.getCause() instanceof NoMergeBaseException);
+ assertEquals(((NoMergeBaseException) e.getCause()).getReason(),
+ MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED);
+ }
+ }
+
+ @Theory
+ public void checkLockedFilesToBeDeleted(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+
+ writeTrashFile("a.txt", "orig");
+ writeTrashFile("b.txt", "orig");
+ git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
+ RevCommit first = git.commit().setMessage("added a.txt, b.txt").call();
+
+ // modify and delete files on the master branch
+ writeTrashFile("a.txt", "master");
+ git.rm().addFilepattern("b.txt").call();
+ RevCommit masterCommit = git.commit()
+ .setMessage("modified a.txt, deleted b.txt").setAll(true)
+ .call();
+
+ // switch back to a side branch
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("side").call();
+ writeTrashFile("c.txt", "side");
+ git.add().addFilepattern("c.txt").call();
+ git.commit().setMessage("added c.txt").call();
+
+ // Get a handle to the file so on windows it can't be deleted.
+ try (FileInputStream fis = new FileInputStream(
+ new File(db.getWorkTree(), "b.txt"))) {
+ MergeResult mergeRes = git.merge().setStrategy(strategy)
+ .include(masterCommit).call();
+ if (mergeRes.getMergeStatus().equals(MergeStatus.FAILED)) {
+ // probably windows
+ assertEquals(1, mergeRes.getFailingPaths().size());
+ assertEquals(MergeFailureReason.COULD_NOT_DELETE,
+ mergeRes.getFailingPaths().get("b.txt"));
+ }
+ assertEquals(
+ "[a.txt, mode:100644, content:master]"
+ + "[c.txt, mode:100644, content:side]",
+ indexState(CONTENT));
+ }
+ }
+
+ @Theory
+ public void checkForCorrectIndex(MergeStrategy strategy) throws Exception {
+ File f;
+ Instant lastTs4, lastTsIndex;
+ Git git = Git.wrap(db);
+ File indexFile = db.getIndexFile();
+
+ // Create initial content and remember when the last file was written.
+ f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
+ lastTs4 = FS.DETECTED.lastModifiedInstant(f);
+
+ // add all files, commit and check this doesn't update any working tree
+ // files and that the index is in a new file system timer tick. Make
+ // sure to wait long enough before adding so the index doesn't contain
+ // racily clean entries
+ fsTick(f);
+ git.add().addFilepattern(".").call();
+ RevCommit firstCommit = git.commit().setMessage("initial commit")
+ .call();
+ checkConsistentLastModified("0", "1", "2", "3", "4");
+ checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index");
+ assertEquals("Commit should not touch working tree file 4", lastTs4,
+ FS.DETECTED
+ .lastModifiedInstant(new File(db.getWorkTree(), "4")));
+ lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
+
+ // Do modifications on the master branch. Then add and commit. This
+ // should touch only "0", "2 and "3"
+ fsTick(indexFile);
+ f = writeTrashFiles(false, "master", null, "1master\n2\n3", "master",
+ null);
+ fsTick(f);
+ git.add().addFilepattern(".").call();
+ RevCommit masterCommit = git.commit().setMessage("master commit")
+ .call();
+ checkConsistentLastModified("0", "1", "2", "3", "4");
+ checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
+ + lastTsIndex, "<0", "2", "3", "<.git/index");
+ lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
+
+ // Checkout a side branch. This should touch only "0", "2 and "3"
+ fsTick(indexFile);
+ git.checkout().setCreateBranch(true).setStartPoint(firstCommit)
+ .setName("side").call();
+ checkConsistentLastModified("0", "1", "2", "3", "4");
+ checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*"
+ + lastTsIndex, "<0", "2", "3", ".git/index");
+ lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
+
+ // This checkout may have populated worktree and index so fast that we
+ // may have smudged entries now. Check that we have the right content
+ // and then rewrite the index to get rid of smudged state
+ assertEquals("[0, mode:100644, content:orig]" //
+ + "[1, mode:100644, content:orig]" //
+ + "[2, mode:100644, content:1\n2\n3]" //
+ + "[3, mode:100644, content:orig]" //
+ + "[4, mode:100644, content:orig]", //
+ indexState(CONTENT));
+ fsTick(indexFile);
+ f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig");
+ lastTs4 = FS.DETECTED.lastModifiedInstant(f);
+ fsTick(f);
+ git.add().addFilepattern(".").call();
+ checkConsistentLastModified("0", "1", "2", "3", "4");
+ checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3",
+ "4", "<.git/index");
+ lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
+
+ // Do modifications on the side branch. Touch only "1", "2 and "3"
+ fsTick(indexFile);
+ f = writeTrashFiles(false, null, "side", "1\n2\n3side", "side", null);
+ fsTick(f);
+ git.add().addFilepattern(".").call();
+ git.commit().setMessage("side commit").call();
+ checkConsistentLastModified("0", "1", "2", "3", "4");
+ checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*"
+ + lastTsIndex, "<1", "2", "3", "<.git/index");
+ lastTsIndex = FS.DETECTED.lastModifiedInstant(indexFile);
+
+ // merge master and side. Should only touch "0," "2" and "3"
+ fsTick(indexFile);
+ git.merge().setStrategy(strategy).include(masterCommit).call();
+ checkConsistentLastModified("0", "1", "2", "4");
+ checkModificationTimeStampOrder("4", "*" + lastTs4, "<1", "<*"
+ + lastTsIndex, "<0", "2", "3", ".git/index");
+ assertEquals(
+ "[0, mode:100644, content:master]" //
+ + "[1, mode:100644, content:side]" //
+ + "[2, mode:100644, content:1master\n2\n3side]" //
+ + "[3, mode:100644, stage:1, content:orig][3, mode:100644, stage:2, content:side][3, mode:100644, stage:3, content:master]" //
+ + "[4, mode:100644, content:orig]", //
+ indexState(CONTENT));
+ }
+
+ /**
+ * Merging two conflicting submodules when the index does not contain any
+ * entry for that submodule.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeConflictingSubmodulesWithoutIndex(
+ MergeStrategy strategy) throws Exception {
+ Git git = Git.wrap(db);
+ writeTrashFile("initial", "initial");
+ git.add().addFilepattern("initial").call();
+ RevCommit initial = git.commit().setMessage("initial").call();
+
+ writeSubmodule("one", ObjectId
+ .fromString("1000000000000000000000000000000000000000"));
+ git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
+ RevCommit right = git.commit().setMessage("added one").call();
+
+ // a second commit in the submodule
+
+ git.checkout().setStartPoint(initial).setName("left")
+ .setCreateBranch(true).call();
+ writeSubmodule("one", ObjectId
+ .fromString("2000000000000000000000000000000000000000"));
+
+ git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
+ git.commit().setMessage("a different one").call();
+
+ MergeResult result = git.merge().setStrategy(strategy).include(right)
+ .call();
+
+ assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+ Map<String, int[][]> conflicts = result.getConflicts();
+ assertEquals(1, conflicts.size());
+ assertNotNull(conflicts.get("one"));
+ }
+
+ /**
+ * Merging two non-conflicting submodules when the index does not contain
+ * any entry for either submodule.
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeNonConflictingSubmodulesWithoutIndex(
+ MergeStrategy strategy) throws Exception {
+ Git git = Git.wrap(db);
+ writeTrashFile("initial", "initial");
+ git.add().addFilepattern("initial").call();
+
+ writeSubmodule("one", ObjectId
+ .fromString("1000000000000000000000000000000000000000"));
+
+ // Our initial commit should include a .gitmodules with a bunch of
+ // comment lines, so that
+ // we don't have a content merge issue when we add a new submodule at
+ // the top and a different
+ // one at the bottom. This is sort of a hack, but it should allow
+ // add/add submodule merges
+ String existing = read(Constants.DOT_GIT_MODULES);
+ String context = "\n# context\n# more context\n# yet more context\n";
+ write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
+ existing + context + context + context);
+
+ git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
+ RevCommit initial = git.commit().setMessage("initial").call();
+
+ writeSubmodule("two", ObjectId
+ .fromString("1000000000000000000000000000000000000000"));
+ git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
+
+ RevCommit right = git.commit().setMessage("added two").call();
+
+ git.checkout().setStartPoint(initial).setName("left")
+ .setCreateBranch(true).call();
+
+ // we need to manually create the submodule for three for the
+ // .gitmodules hackery
+ addSubmoduleToIndex("three", ObjectId
+ .fromString("1000000000000000000000000000000000000000"));
+ new File(db.getWorkTree(), "three").mkdir();
+
+ existing = read(Constants.DOT_GIT_MODULES);
+ String three = "[submodule \"three\"]\n\tpath = three\n\turl = "
+ + db.getDirectory().toURI() + "\n";
+ write(new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
+ three + existing);
+
+ git.add().addFilepattern(Constants.DOT_GIT_MODULES).call();
+ git.commit().setMessage("a different one").call();
+
+ MergeResult result = git.merge().setStrategy(strategy).include(right)
+ .call();
+
+ assertNull(result.getCheckoutConflicts());
+ assertNull(result.getFailingPaths());
+ for (String dir : Arrays.asList("one", "two", "three")) {
+ assertTrue(new File(db.getWorkTree(), dir).isDirectory());
+ }
+ }
+
+ /**
+ * Merging two commits with a conflict in the virtual ancestor.
+ *
+ * Content conflicts while merging the virtual ancestor must be ignored.
+ *
+ * In the following tree, while merging A and B, the recursive algorithm
+ * finds as base commits X and Y and tries to merge them: X deletes file "a"
+ * and Y modifies it.
+ *
+ * Note: we delete "a" in (master) and (second-branch) to make avoid manual
+ * merges. The situation is the same without those deletions and fixing
+ * manually the merge of (merge-both-sides) on both branches.
+ *
+ * <pre>
+ * A (second-branch) Merge branch 'merge-both-sides' into second-branch
+ * |\
+ * o | Delete modified a
+ * | |
+ * | | B (master) Merge branch 'merge-both-sides' (into master)
+ * | |/|
+ * | X | (merge-both-sides) Delete original a
+ * | | |
+ * | | o Delete modified a
+ * | |/
+ * |/|
+ * Y | Modify a
+ * |/
+ * o Initial commit
+ * </pre>
+ *
+ * @param strategy
+ * @throws Exception
+ */
+ @Theory
+ public void checkMergeConflictInVirtualAncestor(
+ MergeStrategy strategy) throws Exception {
+ if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+ return;
+ }
+
+ Git git = Git.wrap(db);
+
+ // master
+ writeTrashFile("a", "aaaaaaaa");
+ writeTrashFile("b", "bbbbbbbb");
+ git.add().addFilepattern("a").addFilepattern("b").call();
+ RevCommit first = git.commit().setMessage("Initial commit").call();
+
+ writeTrashFile("a", "aaaaaaaaaaaaaaa");
+ git.add().addFilepattern("a").call();
+ RevCommit commitY = git.commit().setMessage("Modify a").call();
+
+ git.rm().addFilepattern("a").call();
+ // Do more in this commits, so it is not identical to the deletion in
+ // second-branch
+ writeTrashFile("c", "cccccccc");
+ git.add().addFilepattern("c").call();
+ git.commit().setMessage("Delete modified a").call();
+
+ // merge-both-sides: starts before "a" is modified and deletes it
+ git.checkout().setCreateBranch(true).setStartPoint(first)
+ .setName("merge-both-sides").call();
+ git.rm().addFilepattern("a").call();
+ RevCommit commitX = git.commit().setMessage("Delete original a").call();
+
+ // second branch
+ git.checkout().setCreateBranch(true).setStartPoint(commitY)
+ .setName("second-branch").call();
+ git.rm().addFilepattern("a").call();
+ git.commit().setMessage("Delete modified a").call();
+
+ // Merge merge-both-sides into second-branch
+ MergeResult mergeResult = git.merge().include(commitX)
+ .setStrategy(strategy)
+ .call();
+ ObjectId commitB = mergeResult.getNewHead();
+
+ // Merge merge-both-sides into master
+ 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
+ // building the virtual ancestor it will find a conflict between Y and X
+ git.merge().include(commitB).call();
+ }
+
+ /**
+ * Merging two commits with a file/dir conflict in the virtual ancestor.
+ *
+ * <p>
+ * Those conflicts should be ignored, otherwise the found base can not be used by the
+ * RecursiveMerger.
+ * <pre>
+ * --------------
+ * | \
+ * | C1 - C4 --- ? master
+ * | / /
+ * | I - A1 - C2 - C3 second-branch
+ * | \ /
+ * \ \ /
+ * ----A2-------- branch-to-merge
+ * </pre>
+ * <p>
+ * <p>
+ * Path "a" is initially a file in I and A1. It is changed to a directory in A2
+ * ("branch-to-merge").
+ * <p>
+ * A2 is merged into "master" and "second-branch". The dir/file merge conflict is resolved
+ * manually, results in C4 and C3.
+ * <p>
+ * While merging C3 and C4, A1 and A2 are the base commits found by the recursive merge that
+ * have the dir/file conflict.
+ */
+ @Theory
+ public void checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren(
+ MergeStrategy strategy)
+ throws Exception {
+ if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+ return;
+ }
+
+ Git git = Git.wrap(db);
+
+ // master
+ writeTrashFile("a", "initial content");
+ git.add().addFilepattern("a").call();
+ RevCommit commitI = git.commit().setMessage("Initial commit").call();
+
+ writeTrashFile("a", "content in Ancestor 1");
+ git.add().addFilepattern("a").call();
+ RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+ 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 a directory in A2
+ git.rm().addFilepattern("a").call();
+ writeTrashFile("a/content", "content in Ancestor 2 (commited on branch-to-merge)");
+ git.add().addFilepattern("a/content").call();
+ RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
+
+ // second branch
+ git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
+ 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();
+
+ // 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, merge "a" as a file
+ git.rm().addFilepattern("a").call();
+ git.rm().addFilepattern("a/content").call();
+ writeTrashFile("a", "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 - merge "a" as a file
+ git.rm().addFilepattern("a").call();
+ git.rm().addFilepattern("a/content").call();
+ writeTrashFile("a", "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);
+
+ }
+
+ @Theory
+ public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_FileDir(MergeStrategy strategy)
+ throws Exception {
+ if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+ return;
+ }
+
+ Git git = Git.wrap(db);
+
+ // master
+ writeTrashFile("a", "initial content");
+ git.add().addFilepattern("a").call();
+ RevCommit commitI = git.commit().setMessage("Initial commit").call();
+
+ writeTrashFile("a", "content in Ancestor 1");
+ git.add().addFilepattern("a").call();
+ RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+ 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 a directory in A2
+ git.rm().addFilepattern("a").call();
+ writeTrashFile("a/content", "content in Ancestor 2 (commited on branch-to-merge)");
+ git.add().addFilepattern("a/content").call();
+ RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
+
+ // second branch
+ git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
+ 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();
+
+ // 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 - write a file
+ git.rm().addFilepattern("a").call();
+ git.rm().addFilepattern("a/content").call();
+ writeTrashFile("a",
+ "content in Child 3 (commited on second-branch) - 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 - write a file
+ git.rm().addFilepattern("a").call();
+ git.rm().addFilepattern("a/content").call();
+ writeTrashFile("a", "content in Child 4 (commited on master) - 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
+ mergeResult = git.merge().include(commitC3S).call();
+ assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+ String expected =
+ "<<<<<<< HEAD\n" + "content in Child 4 (commited on master) - merge conflict resolution\n"
+ + "=======\n"
+ + "content in Child 3 (commited on second-branch) - merge conflict resolution\n"
+ + ">>>>>>> " + commitC3S.name() + "\n";
+ assertEquals(expected, read("a"));
+ // Nothing was populated from the ancestors.
+ assertEquals(
+ "[a, mode:100644, stage:2, content:content in Child 4 (commited on master) - merge conflict resolution][a, mode:100644, stage:3, content:content in Child 3 (commited on second-branch) - merge conflict resolution]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * Same test as above, but "a" is a dir in A1 and a file in A2
+ */
+ @Theory
+ public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_DirFile(MergeStrategy strategy)
+ throws Exception {
+ if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+ return;
+ }
+
+ Git git = Git.wrap(db);
+
+ // master
+ writeTrashFile("a/content", "initial content");
+ git.add().addFilepattern("a/content").call();
+ RevCommit commitI = git.commit().setMessage("Initial commit").call();
+
+ writeTrashFile("a/content", "content in Ancestor 1");
+ git.add().addFilepattern("a/content").call();
+ RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+ writeTrashFile("a/content", "content in Child 1 (commited on master)");
+ git.add().addFilepattern("a/content").call();
+ // commit C1M
+ git.commit().setMessage("Child 1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
+
+ // "a" becomes a file in A2
+ git.rm().addFilepattern("a/content").call();
+ writeTrashFile("a", "content in Ancestor 2 (commited on branch-to-merge)");
+ 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/content", "content in Child 2 (commited on second-branch)");
+ git.add().addFilepattern("a/content").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 - write a file
+ git.rm().addFilepattern("a").call();
+ git.rm().addFilepattern("a/content").call();
+ deleteTrashFile("a/content");
+ deleteTrashFile("a");
+ writeTrashFile("a", "content in Child 3 (commited on second-branch) - 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 - write a file
+ git.rm().addFilepattern("a").call();
+ git.rm().addFilepattern("a/content").call();
+ deleteTrashFile("a/content");
+ deleteTrashFile("a");
+ writeTrashFile("a", "content in Child 4 (commited on master) - 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
+ mergeResult = git.merge().include(commitC3S).call();
+ assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+ String expected = "<<<<<<< HEAD\n" + "content in Child 4 (commited on master) - merge conflict resolution\n"
+ + "=======\n" + "content in Child 3 (commited on second-branch) - merge conflict resolution\n"
+ + ">>>>>>> " + commitC3S.name() + "\n";
+ assertEquals(expected, read("a"));
+ // Nothing was populated from the ancestors.
+ assertEquals(
+ "[a, mode:100644, stage:2, content:content in Child 4 (commited on master) - merge conflict resolution][a, mode:100644, stage:3, content:content in Child 3 (commited on second-branch) - merge conflict resolution]",
+ indexState(CONTENT));
+ }
+
+ /**
+ * Merging two commits when files have equal content, but conflicting file mode
+ * in the virtual ancestor.
+ *
+ * <p>
+ * This test has the same set up as
+ * {@code checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren}, only
+ * with the mode conflict in A1 and A2.
+ */
+ @Theory
+ public void checkModeMergeConflictInVirtualAncestor(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", "content in Ancestor");
+ git.add().addFilepattern("a").call();
+ RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+ 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
+ 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();
+ 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();
+
+ // 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, merge "a" as non-executable
+ a = writeTrashFile("a", "merge conflict resolution");
+ a.setExecutable(false);
+ 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 - merge "a" as non-executable
+ a = writeTrashFile("a", "merge conflict resolution");
+ a.setExecutable(false);
+ 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);
+ }
+
+ /**
+ * 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)
+ throws IOException, ConfigInvalidException {
+ addSubmoduleToIndex(path, commit);
+ new File(db.getWorkTree(), path).mkdir();
+
+ StoredConfig config = db.getConfig();
+ config.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
+ ConfigConstants.CONFIG_KEY_URL,
+ db.getDirectory().toURI().toString());
+ config.save();
+
+ FileBasedConfig modulesConfig = new FileBasedConfig(
+ new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
+ db.getFS());
+ modulesConfig.load();
+ modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
+ ConfigConstants.CONFIG_KEY_PATH, path);
+ modulesConfig.save();
+
+ }
+
+ private void addSubmoduleToIndex(String path, ObjectId commit)
+ throws IOException {
+ DirCache cache = db.lockDirCache();
+ DirCacheEditor editor = cache.editor();
+ editor.add(new DirCacheEditor.PathEdit(path) {
+
+ @Override
+ public void apply(DirCacheEntry ent) {
+ ent.setFileMode(FileMode.GITLINK);
+ ent.setObjectId(commit);
+ }
+ });
+ editor.commit();
+ }
+
+ // Assert that every specified index entry has the same last modification
+ // timestamp as the associated file
+ private void checkConsistentLastModified(String... pathes)
+ throws IOException {
+ DirCache dc = db.readDirCache();
+ File workTree = db.getWorkTree();
+ for (String path : pathes)
+ assertEquals(
+ "IndexEntry with path "
+ + path
+ + " has lastmodified which is different from the worktree file",
+ FS.DETECTED.lastModifiedInstant(new File(workTree, path)),
+ dc.getEntry(path)
+ .getLastModifiedInstant());
+ }
+
+ // Assert that modification timestamps of working tree files are as
+ // expected. You may specify n files. It is asserted that every file
+ // i+1 is not older than file i. If a path of file i+1 is prefixed with "<"
+ // then this file must be younger then file i. A path "*<modtime>"
+ // represents a file with a modification time of <modtime>
+ // E.g. ("a", "b", "<c", "f/a.txt") means: a<=b<c<=f/a.txt
+ private void checkModificationTimeStampOrder(String... pathes) {
+ Instant lastMod = EPOCH;
+ for (String p : pathes) {
+ boolean strong = p.startsWith("<");
+ boolean fixed = p.charAt(strong ? 1 : 0) == '*';
+ p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0));
+ Instant curMod = fixed ? Instant.parse(p)
+ : FS.DETECTED
+ .lastModifiedInstant(new File(db.getWorkTree(), p));
+ if (strong) {
+ assertTrue("path " + p + " is not younger than predecesssor",
+ curMod.compareTo(lastMod) > 0);
+ } else {
+ assertTrue("path " + p + " is older than predecesssor",
+ curMod.compareTo(lastMod) >= 0);
+ }
+ }
+ }
+
+ private String readBlob(ObjectId treeish, String path) throws Exception {
+ try (TestRepository<?> tr = new TestRepository<>(db);
+ RevWalk rw = tr.getRevWalk()) {
+ db.incrementOpen();
+ RevTree tree = rw.parseTree(treeish);
+ RevObject obj = tr.get(tree, path);
+ if (obj == null) {
+ return null;
+ }
+ return new String(
+ rw.getObjectReader().open(obj, OBJ_BLOB).getBytes(), UTF_8);
+ }
+ }
+}