Allow file mode conflicts in virtual base commit on recursive merge.

Similar to https://git.eclipse.org/r/c/jgit/jgit/+/175166, ignore
path that have conflicts on attributes, so that the virtual base could
be used by RecursiveMerger.

Change-Id: I99c95445a305558d55bbb9c9e97446caaf61c154
Signed-off-by: Marija Savtchouk <mariasavtchouk@google.com>
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 eecf25b..6cbb4a8 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
@@ -1648,6 +1648,82 @@ public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_DirFil
 				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();
+
+		File a = 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)");
+		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");
+		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)");
+		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);
+
+	}
+
 	private void writeSubmodule(String path, ObjectId commit)
 			throws IOException, ConfigInvalidException {
 		addSubmoduleToIndex(path, commit);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index 4bfb38d..b011258 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -644,15 +644,18 @@ protected boolean processEntry(CanonicalTreeParser base,
 				}
 				return true;
 			}
-			// FileModes are not mergeable. We found a conflict on modes.
-			// For conflicting entries we don't know lastModified and
-			// length.
-			add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
-			add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
-			add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
-			unmergedPaths.add(tw.getPathString());
-			mergeResults.put(tw.getPathString(),
-					new MergeResult<>(Collections.<RawText> emptyList()));
+			if (!ignoreConflicts) {
+				// FileModes are not mergeable. We found a conflict on modes.
+				// For conflicting entries we don't know lastModified and
+				// length.
+				// This path can be skipped on ignoreConflicts, so the caller
+				// could use virtual commit.
+				add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
+				add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
+				unmergedPaths.add(tw.getPathString());
+				mergeResults.put(tw.getPathString(), new MergeResult<>(Collections.<RawText>emptyList()));
+			}
 			return true;
 		}