ResolveMerger: Adding test cases for GITLINK merge

Add test cases which cover content-merge resolve logic.

Git clients try to agressively merge blobs by content, but GITLINK types
of entries can't be merged with each other or with blobs. This change
ensures all possible permutations which can trigger blob and GITLINK
content merge are covered.

Signed-off-by: Demetr Starshov <dstarshov@google.com>
Change-Id: I7e83a28a14d4d2f9e0ba2b1cffbf3224fb7f3fef
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
index de11e2c..cc84f19 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
@@ -34,6 +34,7 @@
 import org.eclipse.jgit.dircache.DirCacheCheckout;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
@@ -513,6 +514,21 @@
 	}
 
 	/**
+	 * Create <code>DirCacheEntry</code>
+	 *
+	 * @param path
+	 * @param objectId
+	 * @return the DirCacheEntry
+	 */
+	protected DirCacheEntry createGitLink(String path, AnyObjectId objectId) {
+		final DirCacheEntry entry = new DirCacheEntry(path,
+				DirCacheEntry.STAGE_0);
+		entry.setFileMode(FileMode.GITLINK);
+		entry.setObjectId(objectId);
+		return entry;
+	}
+
+	/**
 	 * Assert files are equal
 	 *
 	 * @param expected
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
new file mode 100644
index 0000000..4165812
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (c) 2020, Google LLC  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
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.merge;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class GitlinkMergeTest extends SampleDataRepositoryTestCase {
+	private static final String LINK_ID1 = "DEADBEEFDEADBEEFBABEDEADBEEFDEADBEEFBABE";
+	private static final String LINK_ID2 = "DEADDEADDEADDEADDEADDEADDEADDEADDEADDEAD";
+	private static final String LINK_ID3 = "BEEFBEEFBEEFBEEFBEEFBEEFBEEFBEEFBEEFBEEF";
+
+	private static final String SUBMODULE_PATH = "submodule.link";
+
+	@Test
+	public void testGitLinkMerging_UpdateUpdate() throws Exception {
+		testGitLink(LINK_ID1, LINK_ID2, LINK_ID3, newResolveMerger(), false);
+	}
+
+	@Test
+	public void testGitLinkMerging_bothAddedSameLink() throws Exception {
+		assertGitLinkValue(
+				testGitLink(null, LINK_ID2, LINK_ID2, newResolveMerger(), true),
+				LINK_ID2);
+	}
+
+	@Test
+	public void testGitLinkMerging_bothAddedDifferentLink() throws Exception {
+		testGitLink(null, LINK_ID2, LINK_ID3, newResolveMerger(), false);
+	}
+
+	@Test
+	public void testGitLinkMerging_AddNew_ignoreConflicts() throws Exception {
+		assertGitLinkValue(
+				testGitLink(null, null, LINK_ID3, newIgnoreConflictMerger(),
+						true),
+				LINK_ID3);
+	}
+
+	@Test
+	@Ignore("broken - doesn't ignore conflicts")
+	public void testGitLinkMerging_UpdateUpdate_ignoreConflicts()
+			throws Exception {
+		assertGitLinkValue(testGitLink(LINK_ID1, LINK_ID2, LINK_ID3,
+				newIgnoreConflictMerger(), true), LINK_ID2);
+	}
+
+	@Test
+	@Ignore("broken - doesn't ignore conflicts")
+	public void testGitLinkMerging_bothAddedSameLink_ignoreConflicts()
+			throws Exception {
+		assertGitLinkValue(testGitLink(null, LINK_ID2, LINK_ID2,
+				newIgnoreConflictMerger(), true), LINK_ID2);
+	}
+
+	@Test
+	@Ignore("broken - doesn't ignore conflicts")
+	public void testGitLinkMerging_bothAddedDifferentLink_ignoreConflicts()
+			throws Exception {
+		assertGitLinkValue(testGitLink(null, LINK_ID2, LINK_ID3,
+				newIgnoreConflictMerger(), true), LINK_ID2);
+	}
+
+	protected Merger testGitLink(@Nullable String baseLink,
+			@Nullable String oursLink, @Nullable String theirsLink,
+			Merger merger, boolean shouldMerge)
+			throws Exception {
+		DirCache treeB = db.readDirCache();
+		DirCache treeO = db.readDirCache();
+		DirCache treeT = db.readDirCache();
+
+		DirCacheBuilder bTreeBuilder = treeB.builder();
+		DirCacheBuilder oTreeBuilder = treeO.builder();
+		DirCacheBuilder tTreeBuilder = treeT.builder();
+
+		maybeAddLink(bTreeBuilder, baseLink);
+		maybeAddLink(oTreeBuilder, oursLink);
+		maybeAddLink(tTreeBuilder, theirsLink);
+
+		bTreeBuilder.finish();
+		oTreeBuilder.finish();
+		tTreeBuilder.finish();
+
+		ObjectInserter ow = db.newObjectInserter();
+		ObjectId b = commit(ow, treeB, new ObjectId[] {});
+		ObjectId o = commit(ow, treeO, new ObjectId[] { b });
+		ObjectId t = commit(ow, treeT, new ObjectId[] { b });
+
+		boolean merge = merger.merge(new ObjectId[] { o, t });
+		assertEquals(shouldMerge, merge);
+
+		return merger;
+	}
+
+	private Merger newResolveMerger() {
+		return MergeStrategy.RESOLVE.newMerger(db, true);
+	}
+
+	private Merger newIgnoreConflictMerger() {
+		return new ResolveMerger(db, true) {
+			@Override
+			protected boolean mergeImpl() throws IOException {
+				// emulate call with ignore conflicts.
+				return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1],
+						true);
+			}
+		};
+	}
+
+	@Test
+	public void testGitLinkMerging_blobWithLink() throws Exception {
+		DirCache treeB = db.readDirCache();
+		DirCache treeO = db.readDirCache();
+		DirCache treeT = db.readDirCache();
+
+		DirCacheBuilder bTreeBuilder = treeB.builder();
+		DirCacheBuilder oTreeBuilder = treeO.builder();
+		DirCacheBuilder tTreeBuilder = treeT.builder();
+
+		bTreeBuilder.add(
+				createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob"));
+		oTreeBuilder.add(
+				createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob 2"));
+
+		maybeAddLink(tTreeBuilder, LINK_ID3);
+
+		bTreeBuilder.finish();
+		oTreeBuilder.finish();
+		tTreeBuilder.finish();
+
+		ObjectInserter ow = db.newObjectInserter();
+		ObjectId b = commit(ow, treeB, new ObjectId[] {});
+		ObjectId o = commit(ow, treeO, new ObjectId[] { b });
+		ObjectId t = commit(ow, treeT, new ObjectId[] { b });
+
+		Merger resolveMerger = MergeStrategy.RESOLVE.newMerger(db);
+		boolean merge = resolveMerger.merge(new ObjectId[] { o, t });
+		assertFalse(merge);
+	}
+
+	@Test
+	public void testGitLinkMerging_linkWithBlob() throws Exception {
+		DirCache treeB = db.readDirCache();
+		DirCache treeO = db.readDirCache();
+		DirCache treeT = db.readDirCache();
+
+		DirCacheBuilder bTreeBuilder = treeB.builder();
+		DirCacheBuilder oTreeBuilder = treeO.builder();
+		DirCacheBuilder tTreeBuilder = treeT.builder();
+
+		maybeAddLink(bTreeBuilder, LINK_ID1);
+		maybeAddLink(oTreeBuilder, LINK_ID2);
+		tTreeBuilder.add(
+				createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob 3"));
+
+		bTreeBuilder.finish();
+		oTreeBuilder.finish();
+		tTreeBuilder.finish();
+
+		ObjectInserter ow = db.newObjectInserter();
+		ObjectId b = commit(ow, treeB, new ObjectId[] {});
+		ObjectId o = commit(ow, treeO, new ObjectId[] { b });
+		ObjectId t = commit(ow, treeT, new ObjectId[] { b });
+
+		Merger resolveMerger = MergeStrategy.RESOLVE.newMerger(db);
+		boolean merge = resolveMerger.merge(new ObjectId[] { o, t });
+		assertFalse(merge);
+	}
+
+	@Test
+	public void testGitLinkMerging_linkWithLink() throws Exception {
+		DirCache treeB = db.readDirCache();
+		DirCache treeO = db.readDirCache();
+		DirCache treeT = db.readDirCache();
+
+		DirCacheBuilder bTreeBuilder = treeB.builder();
+		DirCacheBuilder oTreeBuilder = treeO.builder();
+		DirCacheBuilder tTreeBuilder = treeT.builder();
+
+		bTreeBuilder.add(
+				createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob"));
+		maybeAddLink(oTreeBuilder, LINK_ID2);
+		maybeAddLink(tTreeBuilder, LINK_ID3);
+
+		bTreeBuilder.finish();
+		oTreeBuilder.finish();
+		tTreeBuilder.finish();
+
+		ObjectInserter ow = db.newObjectInserter();
+		ObjectId b = commit(ow, treeB, new ObjectId[] {});
+		ObjectId o = commit(ow, treeO, new ObjectId[] { b });
+		ObjectId t = commit(ow, treeT, new ObjectId[] { b });
+
+		Merger resolveMerger = MergeStrategy.RESOLVE.newMerger(db);
+		boolean merge = resolveMerger.merge(new ObjectId[] { o, t });
+		assertFalse(merge);
+	}
+
+	@Test
+	@Ignore("broken - try to do content-merge with GITLINK")
+	public void testGitLinkMerging_blobWithBlobFromLink() throws Exception {
+		DirCache treeB = db.readDirCache();
+		DirCache treeO = db.readDirCache();
+		DirCache treeT = db.readDirCache();
+
+		DirCacheBuilder bTreeBuilder = treeB.builder();
+		DirCacheBuilder oTreeBuilder = treeO.builder();
+		DirCacheBuilder tTreeBuilder = treeT.builder();
+
+		maybeAddLink(bTreeBuilder, LINK_ID1);
+		oTreeBuilder.add(
+				createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob 2"));
+		tTreeBuilder.add(
+				createEntry(SUBMODULE_PATH, FileMode.REGULAR_FILE, "blob 3"));
+
+		bTreeBuilder.finish();
+		oTreeBuilder.finish();
+		tTreeBuilder.finish();
+
+		ObjectInserter ow = db.newObjectInserter();
+		ObjectId b = commit(ow, treeB, new ObjectId[] {});
+		ObjectId o = commit(ow, treeO, new ObjectId[] { b });
+		ObjectId t = commit(ow, treeT, new ObjectId[] { b });
+
+		Merger resolveMerger = MergeStrategy.RESOLVE.newMerger(db);
+		boolean merge = resolveMerger.merge(new ObjectId[] { o, t });
+		assertFalse(merge);
+	}
+
+	private void maybeAddLink(DirCacheBuilder builder,
+			@Nullable String linkId) {
+		if (linkId == null) {
+			return;
+		}
+		DirCacheEntry newLink = createGitLink(SUBMODULE_PATH,
+				ObjectId.fromString(linkId));
+		builder.add(newLink);
+	}
+
+	private void assertGitLinkValue(Merger resolveMerger, String expectedValue)
+			throws Exception {
+		try (TreeWalk tw = new TreeWalk(db)) {
+			tw.setRecursive(true);
+			tw.reset(resolveMerger.getResultTreeId());
+
+			assertTrue(tw.next());
+			assertEquals(SUBMODULE_PATH, tw.getPathString());
+			assertEquals(ObjectId.fromString(expectedValue), tw.getObjectId(0));
+
+			assertFalse(tw.next());
+		}
+	}
+
+	private static ObjectId commit(ObjectInserter odi, DirCache treeB,
+			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.setCommitter(c.getAuthor());
+		c.setParentIds(parentIds);
+		c.setMessage("Tree " + c.getTreeId().name());
+		ObjectId id = odi.insert(c);
+		odi.flush();
+		return id;
+	}
+}