Implement ours/theirs content conflict resolution

Git has different conflict resolution strategies:

* There is a tree merge strategy "ours" which just ignores any changes
  from theirs ("-s ours"). JGit also has the mirror strategy "theirs"
  ignoring any changes from "ours". (This doesn't exist in C git.)
  Adapt StashApplyCommand and CherrypickCommand to be able to use those
  tree merge strategies.
* For the resolve/recursive tree merge strategies, there are content
  conflict resolution strategies "ours" and "theirs", which resolve
  any conflict hunks by taking the "ours" or "theirs" hunk. In C git
  those correspond to "-Xours" or -Xtheirs". Implement that in
  MergeAlgorithm, and add API to set and pass through such a strategy
  for resolving content conflicts.
* The "ours/theirs" content conflict resolution strategies also apply
  for binary files. Handle these cases in ResolveMerger.

Note that the content conflict resolution strategies ("-X ours/theirs")
do _not_ apply to modify/delete or delete/modify conflicts. Such
conflicts are always reported as conflicts by C git. They do apply,
however, if one side completely clears a file's content.

Bug: 501111
Change-Id: I2c9c170c61c440a2ab9c387991e7a0c3ab960e07
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index 83846ee..38deab9 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -115,6 +115,7 @@
 metaVar_connProp=conn.prop
 metaVar_diffAlg=ALGORITHM
 metaVar_directory=DIRECTORY
+metaVar_extraArgument=ours|theirs
 metaVar_file=FILE
 metaVar_filepattern=filepattern
 metaVar_gitDir=GIT_DIR
@@ -217,6 +218,7 @@
 treeIsRequired=argument tree is required
 tooManyRefsGiven=Too many refs given
 unknownIoErrorStdout=An unknown I/O error occurred on standard output
+unknownExtraArgument=unknown extra argument -X {0} specified
 unknownMergeStrategy=unknown merge strategy {0} specified
 unknownSubcommand=Unknown subcommand: {0}
 unmergedPaths=Unmerged paths:
@@ -226,6 +228,7 @@
 usage_Aggressive=This option will cause gc to more aggressively optimize the repository at the expense of taking much more time
 usage_AlwaysFallback=Show uniquely abbreviated commit object as fallback
 usage_bareClone=Make a bare Git repository. That is, instead of creating [DIRECTORY] and placing the administrative files in [DIRECTORY]/.git, make the [DIRECTORY] itself the $GIT_DIR.
+usage_extraArgument=Pass an extra argument to a merge driver. Currently supported are "-X ours" and "-X theirs".
 usage_mirrorClone=Set up a mirror of the source repository. This implies --bare. Compared to --bare, --mirror not only maps \
 local branches of the source to local branches of the target, it maps all refs (including remote-tracking branches, notes etc.) \
 and sets up a refspec configuration such that all these refs are overwritten by a git remote update in the target repository.
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
index fdc449e..ca4877f 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
@@ -24,6 +24,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
 import org.eclipse.jgit.pgm.internal.CLIText;
@@ -69,6 +70,20 @@
 	@Option(name = "-m", usage = "usage_message")
 	private String message;
 
+	private ContentMergeStrategy contentStrategy = null;
+
+	@Option(name = "--strategy-option", aliases = { "-X" },
+			metaVar = "metaVar_extraArgument", usage = "usage_extraArgument")
+	void extraArg(String name) {
+		if (ContentMergeStrategy.OURS.name().equalsIgnoreCase(name)) {
+			contentStrategy = ContentMergeStrategy.OURS;
+		} else if (ContentMergeStrategy.THEIRS.name().equalsIgnoreCase(name)) {
+			contentStrategy = ContentMergeStrategy.THEIRS;
+		} else {
+			throw die(MessageFormat.format(CLIText.get().unknownExtraArgument, name));
+		}
+	}
+
 	/** {@inheritDoc} */
 	@Override
 	protected void run() {
@@ -96,8 +111,11 @@
 			Ref oldHead = getOldHead();
 			MergeResult result;
 			try (Git git = new Git(db)) {
-				MergeCommand mergeCmd = git.merge().setStrategy(mergeStrategy)
-						.setSquash(squash).setFastForward(ff)
+				MergeCommand mergeCmd = git.merge()
+						.setStrategy(mergeStrategy)
+						.setContentMergeStrategy(contentStrategy)
+						.setSquash(squash)
+						.setFastForward(ff)
 						.setCommit(!noCommit);
 				if (srcRef != null) {
 					mergeCmd.include(srcRef);
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
index 991b3ba..8e49a76 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
@@ -284,6 +284,7 @@
 	/***/ public String tooManyRefsGiven;
 	/***/ public String treeIsRequired;
 	/***/ public char[] unknownIoErrorStdout;
+	/***/ public String unknownExtraArgument;
 	/***/ public String unknownMergeStrategy;
 	/***/ public String unknownSubcommand;
 	/***/ public String unmergedPaths;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
index 9dd129c..f4f0ecd 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
@@ -34,6 +34,8 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.lib.RepositoryState;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
+import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
@@ -193,7 +195,7 @@
 	}
 
 	@Test
-	public void testCherryPickConflictResolutionNoCOmmit() throws Exception {
+	public void testCherryPickConflictResolutionNoCommit() throws Exception {
 		Git git = new Git(db);
 		RevCommit sideCommit = prepareCherryPick(git);
 
@@ -280,6 +282,70 @@
 	}
 
 	@Test
+	public void testCherryPickOurs() throws Exception {
+		try (Git git = new Git(db)) {
+			RevCommit sideCommit = prepareCherryPick(git);
+
+			CherryPickResult result = git.cherryPick()
+					.include(sideCommit.getId())
+					.setStrategy(MergeStrategy.OURS)
+					.call();
+			assertEquals(CherryPickStatus.OK, result.getStatus());
+
+			String expected = "a(master)";
+			checkFile(new File(db.getWorkTree(), "a"), expected);
+		}
+	}
+
+	@Test
+	public void testCherryPickTheirs() throws Exception {
+		try (Git git = new Git(db)) {
+			RevCommit sideCommit = prepareCherryPick(git);
+
+			CherryPickResult result = git.cherryPick()
+					.include(sideCommit.getId())
+					.setStrategy(MergeStrategy.THEIRS)
+					.call();
+			assertEquals(CherryPickStatus.OK, result.getStatus());
+
+			String expected = "a(side)";
+			checkFile(new File(db.getWorkTree(), "a"), expected);
+		}
+	}
+
+	@Test
+	public void testCherryPickXours() throws Exception {
+		try (Git git = new Git(db)) {
+			RevCommit sideCommit = prepareCherryPickStrategyOption(git);
+
+			CherryPickResult result = git.cherryPick()
+					.include(sideCommit.getId())
+					.setContentMergeStrategy(ContentMergeStrategy.OURS)
+					.call();
+			assertEquals(CherryPickStatus.OK, result.getStatus());
+
+			String expected = "a\nmaster\nc\nd\n";
+			checkFile(new File(db.getWorkTree(), "a"), expected);
+		}
+	}
+
+	@Test
+	public void testCherryPickXtheirs() throws Exception {
+		try (Git git = new Git(db)) {
+			RevCommit sideCommit = prepareCherryPickStrategyOption(git);
+
+			CherryPickResult result = git.cherryPick()
+					.include(sideCommit.getId())
+					.setContentMergeStrategy(ContentMergeStrategy.THEIRS)
+					.call();
+			assertEquals(CherryPickStatus.OK, result.getStatus());
+
+			String expected = "a\nside\nc\nd\n";
+			checkFile(new File(db.getWorkTree(), "a"), expected);
+		}
+	}
+
+	@Test
 	public void testCherryPickConflictMarkers() throws Exception {
 		try (Git git = new Git(db)) {
 			RevCommit sideCommit = prepareCherryPick(git);
@@ -384,6 +450,31 @@
 		return sideCommit;
 	}
 
+	private RevCommit prepareCherryPickStrategyOption(Git git)
+			throws Exception {
+		// create, add and commit file a
+		writeTrashFile("a", "a\nb\nc\n");
+		git.add().addFilepattern("a").call();
+		RevCommit firstMasterCommit = git.commit().setMessage("first master")
+				.call();
+
+		// create and checkout side branch
+		createBranch(firstMasterCommit, "refs/heads/side");
+		checkoutBranch("refs/heads/side");
+		// modify, add and commit file a
+		writeTrashFile("a", "a\nside\nc\nd\n");
+		git.add().addFilepattern("a").call();
+		RevCommit sideCommit = git.commit().setMessage("side").call();
+
+		// checkout master branch
+		checkoutBranch("refs/heads/master");
+		// modify, add and commit file a
+		writeTrashFile("a", "a\nmaster\nc\n");
+		git.add().addFilepattern("a").call();
+		git.commit().setMessage("second master").call();
+		return sideCommit;
+	}
+
 	private void doCherryPickAndCheckResult(final Git git,
 			final RevCommit sideCommit, final MergeFailureReason reason)
 			throws Exception {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
index 8747c85..bc4e940 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
@@ -14,6 +14,7 @@
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 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 static org.junit.Assert.fail;
@@ -25,6 +26,7 @@
 
 import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
 import org.eclipse.jgit.api.MergeResult.MergeStatus;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
 import org.eclipse.jgit.api.errors.InvalidMergeHeadsException;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
@@ -34,6 +36,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.lib.Sets;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -306,6 +309,200 @@
 	}
 
 	@Test
+	public void testContentMergeXtheirs() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile("a", "1\na\n3\n");
+			writeTrashFile("b", "1\nb\n3\n");
+			writeTrashFile("c/c/c", "1\nc\n3\n");
+			git.add().addFilepattern("a").addFilepattern("b")
+					.addFilepattern("c/c/c").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+
+			writeTrashFile("a", "1\na(side)\n3\n4\n");
+			writeTrashFile("b", "1\nb(side)\n3\n4\n");
+			git.add().addFilepattern("a").addFilepattern("b").call();
+			RevCommit secondCommit = git.commit().setMessage("side").call();
+
+			assertEquals("1\nb(side)\n3\n4\n",
+					read(new File(db.getWorkTree(), "b")));
+			checkoutBranch("refs/heads/master");
+			assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
+
+			writeTrashFile("a", "1\na(main)\n3\n");
+			writeTrashFile("c/c/c", "1\nc(main)\n3\n");
+			git.add().addFilepattern("a").addFilepattern("c/c/c").call();
+			git.commit().setMessage("main").call();
+
+			MergeResult result = git.merge().include(secondCommit.getId())
+					.setStrategy(MergeStrategy.RESOLVE)
+					.setContentMergeStrategy(ContentMergeStrategy.THEIRS)
+					.call();
+			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+
+			assertEquals("1\na(side)\n3\n4\n",
+					read(new File(db.getWorkTree(), "a")));
+			assertEquals("1\nb(side)\n3\n4\n",
+					read(new File(db.getWorkTree(), "b")));
+			assertEquals("1\nc(main)\n3\n",
+					read(new File(db.getWorkTree(), "c/c/c")));
+
+			assertNull(result.getConflicts());
+
+			assertEquals(RepositoryState.SAFE, db.getRepositoryState());
+		}
+	}
+
+	@Test
+	public void testContentMergeXours() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile("a", "1\na\n3\n");
+			writeTrashFile("b", "1\nb\n3\n");
+			writeTrashFile("c/c/c", "1\nc\n3\n");
+			git.add().addFilepattern("a").addFilepattern("b")
+					.addFilepattern("c/c/c").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+
+			writeTrashFile("a", "1\na(side)\n3\n4\n");
+			writeTrashFile("b", "1\nb(side)\n3\n4\n");
+			git.add().addFilepattern("a").addFilepattern("b").call();
+			RevCommit secondCommit = git.commit().setMessage("side").call();
+
+			assertEquals("1\nb(side)\n3\n4\n",
+					read(new File(db.getWorkTree(), "b")));
+			checkoutBranch("refs/heads/master");
+			assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
+
+			writeTrashFile("a", "1\na(main)\n3\n");
+			writeTrashFile("c/c/c", "1\nc(main)\n3\n");
+			git.add().addFilepattern("a").addFilepattern("c/c/c").call();
+			git.commit().setMessage("main").call();
+
+			MergeResult result = git.merge().include(secondCommit.getId())
+					.setStrategy(MergeStrategy.RESOLVE)
+					.setContentMergeStrategy(ContentMergeStrategy.OURS).call();
+			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+
+			assertEquals("1\na(main)\n3\n4\n",
+					read(new File(db.getWorkTree(), "a")));
+			assertEquals("1\nb(side)\n3\n4\n",
+					read(new File(db.getWorkTree(), "b")));
+			assertEquals("1\nc(main)\n3\n",
+					read(new File(db.getWorkTree(), "c/c/c")));
+
+			assertNull(result.getConflicts());
+
+			assertEquals(RepositoryState.SAFE, db.getRepositoryState());
+		}
+	}
+
+	@Test
+	public void testBinaryContentMerge() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile(".gitattributes", "a binary");
+			writeTrashFile("a", "initial");
+			git.add().addFilepattern(".").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+
+			writeTrashFile("a", "side");
+			git.add().addFilepattern("a").call();
+			RevCommit secondCommit = git.commit().setMessage("side").call();
+
+			checkoutBranch("refs/heads/master");
+
+			writeTrashFile("a", "main");
+			git.add().addFilepattern("a").call();
+			git.commit().setMessage("main").call();
+
+			MergeResult result = git.merge().include(secondCommit.getId())
+					.setStrategy(MergeStrategy.RESOLVE).call();
+			assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+
+			assertEquals("main", read(new File(db.getWorkTree(), "a")));
+
+			// Hmmm... there doesn't seem to be a way to figure out which files
+			// had a binary conflict from a MergeResult...
+
+			assertEquals(RepositoryState.MERGING, db.getRepositoryState());
+		}
+	}
+
+	@Test
+	public void testBinaryContentMergeXtheirs() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile(".gitattributes", "a binary");
+			writeTrashFile("a", "initial");
+			git.add().addFilepattern(".").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+
+			writeTrashFile("a", "side");
+			git.add().addFilepattern("a").call();
+			RevCommit secondCommit = git.commit().setMessage("side").call();
+
+			checkoutBranch("refs/heads/master");
+
+			writeTrashFile("a", "main");
+			git.add().addFilepattern("a").call();
+			git.commit().setMessage("main").call();
+
+			MergeResult result = git.merge().include(secondCommit.getId())
+					.setStrategy(MergeStrategy.RESOLVE)
+					.setContentMergeStrategy(ContentMergeStrategy.THEIRS)
+					.call();
+			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+
+			assertEquals("side", read(new File(db.getWorkTree(), "a")));
+
+			assertNull(result.getConflicts());
+			assertEquals(RepositoryState.SAFE, db.getRepositoryState());
+		}
+	}
+
+	@Test
+	public void testBinaryContentMergeXours() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile(".gitattributes", "a binary");
+			writeTrashFile("a", "initial");
+			git.add().addFilepattern(".").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+
+			writeTrashFile("a", "side");
+			git.add().addFilepattern("a").call();
+			RevCommit secondCommit = git.commit().setMessage("side").call();
+
+			checkoutBranch("refs/heads/master");
+
+			writeTrashFile("a", "main");
+			git.add().addFilepattern("a").call();
+			git.commit().setMessage("main").call();
+
+			MergeResult result = git.merge().include(secondCommit.getId())
+					.setStrategy(MergeStrategy.RESOLVE)
+					.setContentMergeStrategy(ContentMergeStrategy.OURS).call();
+			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+
+			assertEquals("main", read(new File(db.getWorkTree(), "a")));
+
+			assertNull(result.getConflicts());
+			assertEquals(RepositoryState.SAFE, db.getRepositoryState());
+		}
+	}
+
+	@Test
 	public void testMergeTag() throws Exception {
 		try (Git git = new Git(db)) {
 			writeTrashFile("a", "a");
@@ -790,17 +987,96 @@
 			// delete a on master to generate conflict
 			checkoutBranch("refs/heads/master");
 			git.rm().addFilepattern("a").call();
+			RevCommit thirdCommit = git.commit().setMessage("main").call();
+
+			for (ContentMergeStrategy contentStrategy : ContentMergeStrategy
+					.values()) {
+				// merge side with master
+				MergeResult result = git.merge().include(secondCommit.getId())
+						.setStrategy(MergeStrategy.RESOLVE)
+						.setContentMergeStrategy(contentStrategy)
+						.call();
+				assertEquals("merge -X " + contentStrategy.name(),
+						MergeStatus.CONFLICTING, result.getMergeStatus());
+
+				// result should be 'a' conflicting with workspace content from
+				// side
+				assertTrue("merge -X " + contentStrategy.name(),
+						new File(db.getWorkTree(), "a").exists());
+				assertEquals("merge -X " + contentStrategy.name(),
+						"1\na(side)\n3\n",
+						read(new File(db.getWorkTree(), "a")));
+				assertEquals("merge -X " + contentStrategy.name(), "1\nb\n3\n",
+						read(new File(db.getWorkTree(), "b")));
+				git.reset().setMode(ResetType.HARD).setRef(thirdCommit.name())
+						.call();
+			}
+		}
+	}
+
+	@Test
+	public void testDeletionOnMasterTheirs() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile("a", "1\na\n3\n");
+			writeTrashFile("b", "1\nb\n3\n");
+			git.add().addFilepattern("a").addFilepattern("b").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			// create side branch and modify "a"
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+			writeTrashFile("a", "1\na(side)\n3\n");
+			git.add().addFilepattern("a").call();
+			RevCommit secondCommit = git.commit().setMessage("side").call();
+
+			// delete a on master to generate conflict
+			checkoutBranch("refs/heads/master");
+			git.rm().addFilepattern("a").call();
 			git.commit().setMessage("main").call();
 
 			// merge side with master
 			MergeResult result = git.merge().include(secondCommit.getId())
-					.setStrategy(MergeStrategy.RESOLVE).call();
-			assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+					.setStrategy(MergeStrategy.THEIRS)
+					.call();
+			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
 
-			// result should be 'a' conflicting with workspace content from side
+			// result should be 'a'
 			assertTrue(new File(db.getWorkTree(), "a").exists());
-			assertEquals("1\na(side)\n3\n", read(new File(db.getWorkTree(), "a")));
+			assertEquals("1\na(side)\n3\n",
+					read(new File(db.getWorkTree(), "a")));
 			assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
+			assertTrue(git.status().call().isClean());
+		}
+	}
+
+	@Test
+	public void testDeletionOnMasterOurs() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile("a", "1\na\n3\n");
+			writeTrashFile("b", "1\nb\n3\n");
+			git.add().addFilepattern("a").addFilepattern("b").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			// create side branch and modify "a"
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+			writeTrashFile("a", "1\na(side)\n3\n");
+			git.add().addFilepattern("a").call();
+			RevCommit secondCommit = git.commit().setMessage("side").call();
+
+			// delete a on master to generate conflict
+			checkoutBranch("refs/heads/master");
+			git.rm().addFilepattern("a").call();
+			git.commit().setMessage("main").call();
+
+			// merge side with master
+			MergeResult result = git.merge().include(secondCommit.getId())
+					.setStrategy(MergeStrategy.OURS).call();
+			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+
+			assertFalse(new File(db.getWorkTree(), "a").exists());
+			assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
+			assertTrue(git.status().call().isClean());
 		}
 	}
 
@@ -822,19 +1098,99 @@
 			checkoutBranch("refs/heads/master");
 			writeTrashFile("a", "1\na(main)\n3\n");
 			git.add().addFilepattern("a").call();
+			RevCommit thirdCommit = git.commit().setMessage("main").call();
+
+			for (ContentMergeStrategy contentStrategy : ContentMergeStrategy
+					.values()) {
+				// merge side with master
+				MergeResult result = git.merge().include(secondCommit.getId())
+						.setStrategy(MergeStrategy.RESOLVE)
+						.setContentMergeStrategy(contentStrategy)
+						.call();
+				assertEquals("merge -X " + contentStrategy.name(),
+						MergeStatus.CONFLICTING, result.getMergeStatus());
+
+				assertTrue("merge -X " + contentStrategy.name(),
+						new File(db.getWorkTree(), "a").exists());
+				assertEquals("merge -X " + contentStrategy.name(),
+						"1\na(main)\n3\n",
+						read(new File(db.getWorkTree(), "a")));
+				assertEquals("merge -X " + contentStrategy.name(), "1\nb\n3\n",
+						read(new File(db.getWorkTree(), "b")));
+
+				assertNotNull("merge -X " + contentStrategy.name(),
+						result.getConflicts());
+				assertEquals("merge -X " + contentStrategy.name(), 1,
+						result.getConflicts().size());
+				assertEquals("merge -X " + contentStrategy.name(), 3,
+						result.getConflicts().get("a")[0].length);
+				git.reset().setMode(ResetType.HARD).setRef(thirdCommit.name())
+						.call();
+			}
+		}
+	}
+
+	@Test
+	public void testDeletionOnSideTheirs() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile("a", "1\na\n3\n");
+			writeTrashFile("b", "1\nb\n3\n");
+			git.add().addFilepattern("a").addFilepattern("b").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			// create side branch and delete "a"
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+			git.rm().addFilepattern("a").call();
+			RevCommit secondCommit = git.commit().setMessage("side").call();
+
+			// update a on master to generate conflict
+			checkoutBranch("refs/heads/master");
+			writeTrashFile("a", "1\na(main)\n3\n");
+			git.add().addFilepattern("a").call();
 			git.commit().setMessage("main").call();
 
 			// merge side with master
 			MergeResult result = git.merge().include(secondCommit.getId())
-					.setStrategy(MergeStrategy.RESOLVE).call();
-			assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+					.setStrategy(MergeStrategy.THEIRS).call();
+			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+
+			assertFalse(new File(db.getWorkTree(), "a").exists());
+			assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
+			assertTrue(git.status().call().isClean());
+		}
+	}
+
+	@Test
+	public void testDeletionOnSideOurs() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile("a", "1\na\n3\n");
+			writeTrashFile("b", "1\nb\n3\n");
+			git.add().addFilepattern("a").addFilepattern("b").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			// create side branch and delete "a"
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+			git.rm().addFilepattern("a").call();
+			RevCommit secondCommit = git.commit().setMessage("side").call();
+
+			// update a on master to generate conflict
+			checkoutBranch("refs/heads/master");
+			writeTrashFile("a", "1\na(main)\n3\n");
+			git.add().addFilepattern("a").call();
+			git.commit().setMessage("main").call();
+
+			// merge side with master
+			MergeResult result = git.merge().include(secondCommit.getId())
+					.setStrategy(MergeStrategy.OURS).call();
+			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
 
 			assertTrue(new File(db.getWorkTree(), "a").exists());
-			assertEquals("1\na(main)\n3\n", read(new File(db.getWorkTree(), "a")));
+			assertEquals("1\na(main)\n3\n",
+					read(new File(db.getWorkTree(), "a")));
 			assertEquals("1\nb\n3\n", read(new File(db.getWorkTree(), "b")));
-
-			assertEquals(1, result.getConflicts().size());
-			assertEquals(3, result.getConflicts().get("a")[0].length);
+			assertTrue(git.status().call().isClean());
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
index e4af44e..9af77aa 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
@@ -34,6 +34,8 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
+import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -154,6 +156,75 @@
 	}
 
 	@Test
+	public void testPullConflictTheirs() throws Exception {
+		PullResult res = target.pull().call();
+		// nothing to update since we don't have different data yet
+		assertTrue(res.getFetchResult().getTrackingRefUpdates().isEmpty());
+		assertTrue(res.getMergeResult().getMergeStatus()
+				.equals(MergeStatus.ALREADY_UP_TO_DATE));
+
+		assertFileContentsEqual(targetFile, "Hello world");
+
+		// change the source file
+		writeToFile(sourceFile, "Source change");
+		source.add().addFilepattern("SomeFile.txt").call();
+		source.commit().setMessage("Source change in remote").call();
+
+		// change the target file
+		writeToFile(targetFile, "Target change");
+		target.add().addFilepattern("SomeFile.txt").call();
+		target.commit().setMessage("Target change in local").call();
+
+		res = target.pull().setStrategy(MergeStrategy.THEIRS).call();
+
+		assertTrue(res.isSuccessful());
+		assertFileContentsEqual(targetFile, "Source change");
+		assertEquals(RepositoryState.SAFE,
+				target.getRepository().getRepositoryState());
+		assertTrue(target.status().call().isClean());
+	}
+
+	@Test
+	public void testPullConflictXtheirs() throws Exception {
+		PullResult res = target.pull().call();
+		// nothing to update since we don't have different data yet
+		assertTrue(res.getFetchResult().getTrackingRefUpdates().isEmpty());
+		assertTrue(res.getMergeResult().getMergeStatus()
+				.equals(MergeStatus.ALREADY_UP_TO_DATE));
+
+		assertFileContentsEqual(targetFile, "Hello world");
+
+		// change the source file
+		writeToFile(sourceFile, "a\nHello\nb\n");
+		source.add().addFilepattern("SomeFile.txt").call();
+		source.commit().setMessage("Multi-line change in remote").call();
+
+		// Pull again
+		res = target.pull().call();
+		assertTrue(res.isSuccessful());
+		assertFileContentsEqual(targetFile, "a\nHello\nb\n");
+
+		// change the source file
+		writeToFile(sourceFile, "a\nSource change\nb\n");
+		source.add().addFilepattern("SomeFile.txt").call();
+		source.commit().setMessage("Source change in remote").call();
+
+		// change the target file
+		writeToFile(targetFile, "a\nTarget change\nb\nc\n");
+		target.add().addFilepattern("SomeFile.txt").call();
+		target.commit().setMessage("Target change in local").call();
+
+		res = target.pull().setContentMergeStrategy(ContentMergeStrategy.THEIRS)
+				.call();
+
+		assertTrue(res.isSuccessful());
+		assertFileContentsEqual(targetFile, "a\nSource change\nb\nc\n");
+		assertEquals(RepositoryState.SAFE,
+				target.getRepository().getRepositoryState());
+		assertTrue(target.status().call().isClean());
+	}
+
+	@Test
 	public void testPullWithUntrackedStash() throws Exception {
 		target.pull().call();
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
index f109cbf..49b31b1 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012, GitHub Inc. and others
+ * Copyright (C) 2012, 2021 GitHub Inc. 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
@@ -28,6 +28,8 @@
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
+import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.util.FileUtils;
 import org.junit.After;
@@ -427,6 +429,135 @@
 	}
 
 	@Test
+	public void stashedContentMergeXtheirs() throws Exception {
+		writeTrashFile(PATH, "content\nmore content\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("more content").call();
+
+		writeTrashFile(PATH, "content\nhead change\nmore content\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("even content").call();
+
+		writeTrashFile(PATH, "content\nstashed change\nmore content\n");
+
+		RevCommit stashed = git.stashCreate().call();
+		assertNotNull(stashed);
+		assertEquals("content\nhead change\nmore content\n",
+				read(committedFile));
+		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
+
+		writeTrashFile(PATH, "content\nmore content\ncommitted change\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("committed change").call();
+		recorder.assertNoEvent();
+
+		git.stashApply().setContentMergeStrategy(ContentMergeStrategy.THEIRS)
+				.call();
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
+		Status status = new StatusCommand(db).call();
+		assertEquals('[' + PATH + ']', status.getModified().toString());
+		assertEquals(
+				"content\nstashed change\nmore content\ncommitted change\n",
+				read(PATH));
+	}
+
+	@Test
+	public void stashedContentMergeXours() throws Exception {
+		writeTrashFile(PATH, "content\nmore content\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("more content").call();
+
+		writeTrashFile(PATH, "content\nhead change\nmore content\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("even content").call();
+
+		writeTrashFile(PATH, "content\nstashed change\nmore content\n");
+
+		RevCommit stashed = git.stashCreate().call();
+		assertNotNull(stashed);
+		assertEquals("content\nhead change\nmore content\n",
+				read(committedFile));
+		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
+
+		writeTrashFile(PATH,
+				"content\nnew head\nmore content\ncommitted change\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("committed change").call();
+		recorder.assertNoEvent();
+
+		git.stashApply().setContentMergeStrategy(ContentMergeStrategy.OURS)
+				.call();
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
+		assertTrue(git.status().call().isClean());
+		assertEquals("content\nnew head\nmore content\ncommitted change\n",
+				read(PATH));
+	}
+
+	@Test
+	public void stashedContentMergeTheirs() throws Exception {
+		writeTrashFile(PATH, "content\nmore content\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("more content").call();
+
+		writeTrashFile(PATH, "content\nhead change\nmore content\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("even content").call();
+
+		writeTrashFile(PATH, "content\nstashed change\nmore content\n");
+
+		RevCommit stashed = git.stashCreate().call();
+		assertNotNull(stashed);
+		assertEquals("content\nhead change\nmore content\n",
+				read(committedFile));
+		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
+
+		writeTrashFile(PATH, "content\nmore content\ncommitted change\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("committed change").call();
+		recorder.assertNoEvent();
+
+		git.stashApply().setStrategy(MergeStrategy.THEIRS).call();
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
+		Status status = new StatusCommand(db).call();
+		assertEquals('[' + PATH + ']', status.getModified().toString());
+		assertEquals("content\nstashed change\nmore content\n", read(PATH));
+	}
+
+	@Test
+	public void stashedContentMergeOurs() throws Exception {
+		writeTrashFile(PATH, "content\nmore content\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("more content").call();
+
+		writeTrashFile(PATH, "content\nhead change\nmore content\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("even content").call();
+
+		writeTrashFile(PATH, "content\nstashed change\nmore content\n");
+
+		RevCommit stashed = git.stashCreate().call();
+		assertNotNull(stashed);
+		assertEquals("content\nhead change\nmore content\n",
+				read(committedFile));
+		assertTrue(git.status().call().isClean());
+		recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY);
+
+		writeTrashFile(PATH, "content\nmore content\ncommitted change\n");
+		git.add().addFilepattern(PATH).call();
+		git.commit().setMessage("committed change").call();
+		recorder.assertNoEvent();
+
+		// Doesn't make any sense... should be a no-op
+		git.stashApply().setStrategy(MergeStrategy.OURS).call();
+		recorder.assertNoEvent();
+		assertTrue(git.status().call().isClean());
+		assertEquals("content\nmore content\ncommitted change\n", read(PATH));
+	}
+
+	@Test
 	public void stashedApplyOnOtherBranch() throws Exception {
 		writeTrashFile(PATH, "content\nmore content\n");
 		git.add().addFilepattern(PATH).call();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
index 5d0154c..7922f9e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2010, 2021 Christian Halstrick <christian.halstrick@sap.com> 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
@@ -13,6 +13,7 @@
 import java.text.MessageFormat;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -35,9 +36,12 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Ref.Storage;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
 import org.eclipse.jgit.merge.MergeMessageFormatter;
 import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.Merger;
 import org.eclipse.jgit.merge.ResolveMerger;
+import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.FileTreeIterator;
@@ -61,6 +65,8 @@
 
 	private MergeStrategy strategy = MergeStrategy.RECURSIVE;
 
+	private ContentMergeStrategy contentStrategy;
+
 	private Integer mainlineParentNumber;
 
 	private boolean noCommit = false;
@@ -121,16 +127,30 @@
 				String cherryPickName = srcCommit.getId().abbreviate(7).name()
 						+ " " + srcCommit.getShortMessage(); //$NON-NLS-1$
 
-				ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo);
-				merger.setWorkingTreeIterator(new FileTreeIterator(repo));
-				merger.setBase(srcParent.getTree());
-				merger.setCommitNames(new String[] { "BASE", ourName, //$NON-NLS-1$
-						cherryPickName });
-				if (merger.merge(newHead, srcCommit)) {
-					if (!merger.getModifiedFiles().isEmpty()) {
+				Merger merger = strategy.newMerger(repo);
+				merger.setProgressMonitor(monitor);
+				boolean noProblems;
+				Map<String, MergeFailureReason> failingPaths = null;
+				List<String> unmergedPaths = null;
+				if (merger instanceof ResolveMerger) {
+					ResolveMerger resolveMerger = (ResolveMerger) merger;
+					resolveMerger.setContentMergeStrategy(contentStrategy);
+					resolveMerger.setCommitNames(
+							new String[] { "BASE", ourName, cherryPickName }); //$NON-NLS-1$
+					resolveMerger
+							.setWorkingTreeIterator(new FileTreeIterator(repo));
+					resolveMerger.setBase(srcParent.getTree());
+					noProblems = merger.merge(newHead, srcCommit);
+					failingPaths = resolveMerger.getFailingPaths();
+					unmergedPaths = resolveMerger.getUnmergedPaths();
+					if (!resolveMerger.getModifiedFiles().isEmpty()) {
 						repo.fireEvent(new WorkingTreeModifiedEvent(
-								merger.getModifiedFiles(), null));
+								resolveMerger.getModifiedFiles(), null));
 					}
+				} else {
+					noProblems = merger.merge(newHead, srcCommit);
+				}
+				if (noProblems) {
 					if (AnyObjectId.isEqual(newHead.getTree().getId(),
 							merger.getResultTreeId())) {
 						continue;
@@ -153,24 +173,26 @@
 					}
 					cherryPickedRefs.add(src);
 				} else {
-					if (merger.failed()) {
-						return new CherryPickResult(merger.getFailingPaths());
+					if (failingPaths != null && !failingPaths.isEmpty()) {
+						return new CherryPickResult(failingPaths);
 					}
 
 					// there are merge conflicts
 
-					String message = new MergeMessageFormatter()
+					String message;
+					if (unmergedPaths != null) {
+						message = new MergeMessageFormatter()
 							.formatWithConflicts(srcCommit.getFullMessage(),
-									merger.getUnmergedPaths());
+										unmergedPaths);
+					} else {
+						message = srcCommit.getFullMessage();
+					}
 
 					if (!noCommit) {
 						repo.writeCherryPickHead(srcCommit.getId());
 					}
 					repo.writeMergeCommitMsg(message);
 
-					repo.fireEvent(new WorkingTreeModifiedEvent(
-							merger.getModifiedFiles(), null));
-
 					return CherryPickResult.CONFLICT;
 				}
 			}
@@ -291,6 +313,22 @@
 	}
 
 	/**
+	 * Sets the content merge strategy to use if the
+	 * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+	 * "recursive".
+	 *
+	 * @param strategy
+	 *            the {@link ContentMergeStrategy} to be used
+	 * @return {@code this}
+	 * @since 5.12
+	 */
+	public CherryPickCommand setContentMergeStrategy(
+			ContentMergeStrategy strategy) {
+		this.contentStrategy = strategy;
+		return this;
+	}
+
+	/**
 	 * Set the (1-based) parent number to diff against
 	 *
 	 * @param mainlineParentNumber
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
index d88f4ec..c611f91 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
- * Copyright (C) 2010-2014, Stefan Lay <stefan.lay@sap.com>
- * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> and others
+ * Copyright (C) 2010, 2014, Stefan Lay <stefan.lay@sap.com>
+ * Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> 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
@@ -45,6 +45,7 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
 import org.eclipse.jgit.merge.MergeConfig;
 import org.eclipse.jgit.merge.MergeMessageFormatter;
 import org.eclipse.jgit.merge.MergeStrategy;
@@ -71,6 +72,8 @@
 
 	private MergeStrategy mergeStrategy = MergeStrategy.RECURSIVE;
 
+	private ContentMergeStrategy contentStrategy;
+
 	private List<Ref> commits = new LinkedList<>();
 
 	private Boolean squash;
@@ -320,6 +323,7 @@
 				List<String> unmergedPaths = null;
 				if (merger instanceof ResolveMerger) {
 					ResolveMerger resolveMerger = (ResolveMerger) merger;
+					resolveMerger.setContentMergeStrategy(contentStrategy);
 					resolveMerger.setCommitNames(new String[] {
 							"BASE", "HEAD", ref.getName() }); //$NON-NLS-1$ //$NON-NLS-2$
 					resolveMerger.setWorkingTreeIterator(new FileTreeIterator(repo));
@@ -473,6 +477,22 @@
 	}
 
 	/**
+	 * Sets the content merge strategy to use if the
+	 * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+	 * "recursive".
+	 *
+	 * @param strategy
+	 *            the {@link ContentMergeStrategy} to be used
+	 * @return {@code this}
+	 * @since 5.12
+	 */
+	public MergeCommand setContentMergeStrategy(ContentMergeStrategy strategy) {
+		checkCallable();
+		this.contentStrategy = strategy;
+		return this;
+	}
+
+	/**
 	 * Reference to a commit to be merged with the current head
 	 *
 	 * @param aCommit
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
index 4492508..281ecfd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
  * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
- * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> and others
+ * Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> 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
@@ -43,6 +43,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -69,6 +70,8 @@
 
 	private MergeStrategy strategy = MergeStrategy.RECURSIVE;
 
+	private ContentMergeStrategy contentStrategy;
+
 	private TagOpt tagOption;
 
 	private FastForwardMode fastForwardMode;
@@ -275,8 +278,7 @@
 					JGitText.get().pullTaskName));
 
 		// we check the updates to see which of the updated branches
-		// corresponds
-		// to the remote branch name
+		// corresponds to the remote branch name
 		AnyObjectId commitToMerge;
 		if (isRemote) {
 			Ref r = null;
@@ -354,8 +356,11 @@
 			}
 			RebaseCommand rebase = new RebaseCommand(repo);
 			RebaseResult rebaseRes = rebase.setUpstream(commitToMerge)
-					.setUpstreamName(upstreamName).setProgressMonitor(monitor)
-					.setOperation(Operation.BEGIN).setStrategy(strategy)
+					.setProgressMonitor(monitor)
+					.setUpstreamName(upstreamName)
+					.setOperation(Operation.BEGIN)
+					.setStrategy(strategy)
+					.setContentMergeStrategy(contentStrategy)
 					.setPreserveMerges(
 							pullRebaseMode == BranchRebaseMode.PRESERVE)
 					.call();
@@ -363,7 +368,9 @@
 		} else {
 			MergeCommand merge = new MergeCommand(repo);
 			MergeResult mergeRes = merge.include(upstreamName, commitToMerge)
-					.setStrategy(strategy).setProgressMonitor(monitor)
+					.setProgressMonitor(monitor)
+					.setStrategy(strategy)
+					.setContentMergeStrategy(contentStrategy)
 					.setFastForward(getFastForwardMode()).call();
 			monitor.update(1);
 			result = new PullResult(fetchRes, remote, mergeRes);
@@ -442,6 +449,21 @@
 	}
 
 	/**
+	 * Sets the content merge strategy to use if the
+	 * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+	 * "recursive".
+	 *
+	 * @param strategy
+	 *            the {@link ContentMergeStrategy} to be used
+	 * @return {@code this}
+	 * @since 5.12
+	 */
+	public PullCommand setContentMergeStrategy(ContentMergeStrategy strategy) {
+		this.contentStrategy = strategy;
+		return this;
+	}
+
+	/**
 	 * Set the specification of annotated tag behavior during fetch
 	 *
 	 * @param tagOpt
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
index 836175d..a26ffc2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com>
- * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> and others
+ * Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> 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
@@ -65,6 +65,7 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
@@ -212,6 +213,8 @@
 
 	private MergeStrategy strategy = MergeStrategy.RECURSIVE;
 
+	private ContentMergeStrategy contentStrategy;
+
 	private boolean preserveMerges = false;
 
 	/**
@@ -501,8 +504,11 @@
 			String ourCommitName = getOurCommitName();
 			try (Git git = new Git(repo)) {
 				CherryPickResult cherryPickResult = git.cherryPick()
-					.include(commitToPick).setOurCommitName(ourCommitName)
-					.setReflogPrefix(REFLOG_PREFIX).setStrategy(strategy)
+					.include(commitToPick)
+					.setOurCommitName(ourCommitName)
+					.setReflogPrefix(REFLOG_PREFIX)
+					.setStrategy(strategy)
+					.setContentMergeStrategy(contentStrategy)
 					.call();
 				switch (cherryPickResult.getStatus()) {
 				case FAILED:
@@ -556,7 +562,8 @@
 							.include(commitToPick)
 							.setOurCommitName(ourCommitName)
 							.setReflogPrefix(REFLOG_PREFIX)
-							.setStrategy(strategy);
+							.setStrategy(strategy)
+							.setContentMergeStrategy(contentStrategy);
 					if (isMerge) {
 						pickCommand.setMainlineParentNumber(1);
 						// We write a MERGE_HEAD and later commit explicitly
@@ -592,6 +599,8 @@
 					MergeCommand merge = git.merge()
 							.setFastForward(MergeCommand.FastForwardMode.NO_FF)
 							.setProgressMonitor(monitor)
+							.setStrategy(strategy)
+							.setContentMergeStrategy(contentStrategy)
 							.setCommit(false);
 					for (int i = 1; i < commitToPick.getParentCount(); i++)
 						merge.include(newParents.get(i));
@@ -1137,7 +1146,7 @@
 	}
 
 	private List<RevCommit> calculatePickList(RevCommit headCommit)
-			throws GitAPIException, NoHeadException, IOException {
+			throws IOException {
 		List<RevCommit> cherryPickList = new ArrayList<>();
 		try (RevWalk r = new RevWalk(repo)) {
 			r.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true);
@@ -1587,6 +1596,21 @@
 	}
 
 	/**
+	 * Sets the content merge strategy to use if the
+	 * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+	 * "recursive".
+	 *
+	 * @param strategy
+	 *            the {@link ContentMergeStrategy} to be used
+	 * @return {@code this}
+	 * @since 5.12
+	 */
+	public RebaseCommand setContentMergeStrategy(ContentMergeStrategy strategy) {
+		this.contentStrategy = strategy;
+		return this;
+	}
+
+	/**
 	 * Whether to preserve merges during rebase
 	 *
 	 * @param preserve
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
index 56b3992..1004d3e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012, 2017 GitHub Inc. and others
+ * Copyright (C) 2012, 2021 GitHub Inc. 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
@@ -38,7 +38,9 @@
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryState;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
 import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.Merger;
 import org.eclipse.jgit.merge.ResolveMerger;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
@@ -71,6 +73,8 @@
 
 	private MergeStrategy strategy = MergeStrategy.RECURSIVE;
 
+	private ContentMergeStrategy contentStrategy;
+
 	/**
 	 * Create command to apply the changes of a stashed commit
 	 *
@@ -166,16 +170,25 @@
 			if (restoreUntracked && stashCommit.getParentCount() == 3)
 				untrackedCommit = revWalk.parseCommit(stashCommit.getParent(2));
 
-			ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo);
-			merger.setCommitNames(new String[] { "stashed HEAD", "HEAD", //$NON-NLS-1$ //$NON-NLS-2$
-					"stash" }); //$NON-NLS-1$
-			merger.setBase(stashHeadCommit);
-			merger.setWorkingTreeIterator(new FileTreeIterator(repo));
-			boolean mergeSucceeded = merger.merge(headCommit, stashCommit);
-			List<String> modifiedByMerge = merger.getModifiedFiles();
-			if (!modifiedByMerge.isEmpty()) {
-				repo.fireEvent(
-						new WorkingTreeModifiedEvent(modifiedByMerge, null));
+			Merger merger = strategy.newMerger(repo);
+			boolean mergeSucceeded;
+			if (merger instanceof ResolveMerger) {
+				ResolveMerger resolveMerger = (ResolveMerger) merger;
+				resolveMerger
+						.setCommitNames(new String[] { "stashed HEAD", "HEAD", //$NON-NLS-1$ //$NON-NLS-2$
+								"stash" }); //$NON-NLS-1$
+				resolveMerger.setBase(stashHeadCommit);
+				resolveMerger
+						.setWorkingTreeIterator(new FileTreeIterator(repo));
+				resolveMerger.setContentMergeStrategy(contentStrategy);
+				mergeSucceeded = resolveMerger.merge(headCommit, stashCommit);
+				List<String> modifiedByMerge = resolveMerger.getModifiedFiles();
+				if (!modifiedByMerge.isEmpty()) {
+					repo.fireEvent(new WorkingTreeModifiedEvent(modifiedByMerge,
+							null));
+				}
+			} else {
+				mergeSucceeded = merger.merge(headCommit, stashCommit);
 			}
 			if (mergeSucceeded) {
 				DirCache dc = repo.lockDirCache();
@@ -184,11 +197,14 @@
 				dco.setFailOnConflict(true);
 				dco.checkout(); // Ignoring failed deletes....
 				if (restoreIndex) {
-					ResolveMerger ixMerger = (ResolveMerger) strategy
-							.newMerger(repo, true);
-					ixMerger.setCommitNames(new String[] { "stashed HEAD", //$NON-NLS-1$
-							"HEAD", "stashed index" }); //$NON-NLS-1$//$NON-NLS-2$
-					ixMerger.setBase(stashHeadCommit);
+					Merger ixMerger = strategy.newMerger(repo, true);
+					if (ixMerger instanceof ResolveMerger) {
+						ResolveMerger resolveMerger = (ResolveMerger) ixMerger;
+						resolveMerger.setCommitNames(new String[] { "stashed HEAD", //$NON-NLS-1$
+								"HEAD", "stashed index" }); //$NON-NLS-1$//$NON-NLS-2$
+						resolveMerger.setBase(stashHeadCommit);
+						resolveMerger.setContentMergeStrategy(contentStrategy);
+					}
 					boolean ok = ixMerger.merge(headCommit, stashIndexCommit);
 					if (ok) {
 						resetIndex(revWalk
@@ -200,16 +216,20 @@
 				}
 
 				if (untrackedCommit != null) {
-					ResolveMerger untrackedMerger = (ResolveMerger) strategy
-							.newMerger(repo, true);
-					untrackedMerger.setCommitNames(new String[] {
-							"null", "HEAD", "untracked files" }); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
-					// There is no common base for HEAD & untracked files
-					// because the commit for untracked files has no parent. If
-					// we use stashHeadCommit as common base (as in the other
-					// merges) we potentially report conflicts for files
-					// which are not even member of untracked files commit
-					untrackedMerger.setBase(null);
+					Merger untrackedMerger = strategy.newMerger(repo, true);
+					if (untrackedMerger instanceof ResolveMerger) {
+						ResolveMerger resolveMerger = (ResolveMerger) untrackedMerger;
+						resolveMerger.setCommitNames(new String[] { "null", "HEAD", //$NON-NLS-1$//$NON-NLS-2$
+								"untracked files" }); //$NON-NLS-1$
+						// There is no common base for HEAD & untracked files
+						// because the commit for untracked files has no parent.
+						// If we use stashHeadCommit as common base (as in the
+						// other merges) we potentially report conflicts for
+						// files which are not even member of untracked files
+						// commit.
+						resolveMerger.setBase(null);
+						resolveMerger.setContentMergeStrategy(contentStrategy);
+					}
 					boolean ok = untrackedMerger.merge(headCommit,
 							untrackedCommit);
 					if (ok) {
@@ -279,6 +299,23 @@
 	}
 
 	/**
+	 * Sets the content merge strategy to use if the
+	 * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+	 * "recursive".
+	 *
+	 * @param strategy
+	 *            the {@link ContentMergeStrategy} to be used
+	 * @return {@code this}
+	 * @since 5.12
+	 */
+	public StashApplyCommand setContentMergeStrategy(
+			ContentMergeStrategy strategy) {
+		checkCallable();
+		this.contentStrategy = strategy;
+		return this;
+	}
+
+	/**
 	 * Whether the command should restore untracked files
 	 *
 	 * @param applyUntracked
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java
new file mode 100644
index 0000000..6d56864
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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;
+
+/**
+ * How to handle content conflicts.
+ *
+ * @since 5.12
+ */
+public enum ContentMergeStrategy {
+
+	/** Produce a conflict. */
+	CONFLICT,
+
+	/** Resolve the conflict hunk using the ours version. */
+	OURS,
+
+	/** Resolve the conflict hunk using the theirs version. */
+	THEIRS
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
index 27141c1..8060735 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
@@ -14,6 +14,7 @@
 import java.util.Iterator;
 import java.util.List;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.diff.DiffAlgorithm;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.EditList;
@@ -28,8 +29,12 @@
  * diff algorithm.
  */
 public final class MergeAlgorithm {
+
 	private final DiffAlgorithm diffAlg;
 
+	@NonNull
+	private ContentMergeStrategy strategy = ContentMergeStrategy.CONFLICT;
+
 	/**
 	 * Creates a new MergeAlgorithm which uses
 	 * {@link org.eclipse.jgit.diff.HistogramDiff} as diff algorithm
@@ -48,6 +53,30 @@
 		this.diffAlg = diff;
 	}
 
+	/**
+	 * Retrieves the {@link ContentMergeStrategy}.
+	 *
+	 * @return the {@link ContentMergeStrategy} in effect
+	 * @since 5.12
+	 */
+	@NonNull
+	public ContentMergeStrategy getContentMergeStrategy() {
+		return strategy;
+	}
+
+	/**
+	 * Sets the {@link ContentMergeStrategy}.
+	 *
+	 * @param strategy
+	 *            {@link ContentMergeStrategy} to set; if {@code null}, set
+	 *            {@link ContentMergeStrategy#CONFLICT}
+	 * @since 5.12
+	 */
+	public void setContentMergeStrategy(ContentMergeStrategy strategy) {
+		this.strategy = strategy == null ? ContentMergeStrategy.CONFLICT
+				: strategy;
+	}
+
 	// An special edit which acts as a sentinel value by marking the end the
 	// list of edits
 	private static final Edit END_EDIT = new Edit(Integer.MAX_VALUE,
@@ -79,29 +108,54 @@
 			if (theirs.size() != 0) {
 				EditList theirsEdits = diffAlg.diff(cmp, base, theirs);
 				if (!theirsEdits.isEmpty()) {
-					// we deleted, they modified -> Let their complete content
-					// conflict with empty text
-					result.add(1, 0, 0, ConflictState.FIRST_CONFLICTING_RANGE);
-					result.add(2, 0, theirs.size(),
-							ConflictState.NEXT_CONFLICTING_RANGE);
-				} else
+					// we deleted, they modified
+					switch (strategy) {
+					case OURS:
+						result.add(1, 0, 0, ConflictState.NO_CONFLICT);
+						break;
+					case THEIRS:
+						result.add(2, 0, theirs.size(),
+								ConflictState.NO_CONFLICT);
+						break;
+					default:
+						// Let their complete content conflict with empty text
+						result.add(1, 0, 0,
+								ConflictState.FIRST_CONFLICTING_RANGE);
+						result.add(2, 0, theirs.size(),
+								ConflictState.NEXT_CONFLICTING_RANGE);
+						break;
+					}
+				} else {
 					// we deleted, they didn't modify -> Let our deletion win
 					result.add(1, 0, 0, ConflictState.NO_CONFLICT);
-			} else
+				}
+			} else {
 				// we and they deleted -> return a single chunk of nothing
 				result.add(1, 0, 0, ConflictState.NO_CONFLICT);
+			}
 			return result;
 		} else if (theirs.size() == 0) {
 			EditList oursEdits = diffAlg.diff(cmp, base, ours);
 			if (!oursEdits.isEmpty()) {
-				// we modified, they deleted -> Let our complete content
-				// conflict with empty text
-				result.add(1, 0, ours.size(),
-						ConflictState.FIRST_CONFLICTING_RANGE);
-				result.add(2, 0, 0, ConflictState.NEXT_CONFLICTING_RANGE);
-			} else
+				// we modified, they deleted
+				switch (strategy) {
+				case OURS:
+					result.add(1, 0, ours.size(), ConflictState.NO_CONFLICT);
+					break;
+				case THEIRS:
+					result.add(2, 0, 0, ConflictState.NO_CONFLICT);
+					break;
+				default:
+					// Let our complete content conflict with empty text
+					result.add(1, 0, ours.size(),
+							ConflictState.FIRST_CONFLICTING_RANGE);
+					result.add(2, 0, 0, ConflictState.NEXT_CONFLICTING_RANGE);
+					break;
+				}
+			} else {
 				// they deleted, we didn't modify -> Let their deletion win
 				result.add(2, 0, 0, ConflictState.NO_CONFLICT);
+			}
 			return result;
 		}
 
@@ -249,12 +303,26 @@
 
 				// Add the conflict (Only if there is a conflict left to report)
 				if (minBSize > 0 || BSizeDelta != 0) {
-					result.add(1, oursBeginB + commonPrefix, oursEndB
-							- commonSuffix,
-							ConflictState.FIRST_CONFLICTING_RANGE);
-					result.add(2, theirsBeginB + commonPrefix, theirsEndB
-							- commonSuffix,
-							ConflictState.NEXT_CONFLICTING_RANGE);
+					switch (strategy) {
+					case OURS:
+						result.add(1, oursBeginB + commonPrefix,
+								oursEndB - commonSuffix,
+								ConflictState.NO_CONFLICT);
+						break;
+					case THEIRS:
+						result.add(2, theirsBeginB + commonPrefix,
+								theirsEndB - commonSuffix,
+								ConflictState.NO_CONFLICT);
+						break;
+					default:
+						result.add(1, oursBeginB + commonPrefix,
+								oursEndB - commonSuffix,
+								ConflictState.FIRST_CONFLICTING_RANGE);
+						result.add(2, theirsBeginB + commonPrefix,
+								theirsEndB - commonSuffix,
+								ConflictState.NEXT_CONFLICTING_RANGE);
+						break;
+					}
 				}
 
 				// Add the common lines at end of conflict
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 b011258..7767662 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -37,6 +37,7 @@
 import java.util.List;
 import java.util.Map;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.attributes.Attributes;
 import org.eclipse.jgit.diff.DiffAlgorithm;
 import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
@@ -268,6 +269,13 @@
 	private int inCoreLimit;
 
 	/**
+	 * The {@link ContentMergeStrategy} to use for "resolve" and "recursive"
+	 * merges.
+	 */
+	@NonNull
+	private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT;
+
+	/**
 	 * Keeps {@link CheckoutMetadata} for {@link #checkout()} and
 	 * {@link #cleanUp()}.
 	 */
@@ -344,6 +352,29 @@
 		dircache = DirCache.newInCore();
 	}
 
+	/**
+	 * Retrieves the content merge strategy for content conflicts.
+	 *
+	 * @return the {@link ContentMergeStrategy} in effect
+	 * @since 5.12
+	 */
+	@NonNull
+	public ContentMergeStrategy getContentMergeStrategy() {
+		return contentStrategy;
+	}
+
+	/**
+	 * Sets the content merge strategy for content conflicts.
+	 *
+	 * @param strategy
+	 *            {@link ContentMergeStrategy} to use
+	 * @since 5.12
+	 */
+	public void setContentMergeStrategy(ContentMergeStrategy strategy) {
+		contentStrategy = strategy == null ? ContentMergeStrategy.CONFLICT
+				: strategy;
+	}
+
 	/** {@inheritDoc} */
 	@Override
 	protected boolean mergeImpl() throws IOException {
@@ -654,7 +685,8 @@
 				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()));
+				mergeResults.put(tw.getPathString(),
+						new MergeResult<>(Collections.emptyList()));
 			}
 			return true;
 		}
@@ -760,6 +792,19 @@
 				unmergedPaths.add(tw.getPathString());
 				return true;
 			} else if (!attributes.canBeContentMerged()) {
+				// File marked as binary
+				switch (getContentMergeStrategy()) {
+				case OURS:
+					keep(ourDce);
+					return true;
+				case THEIRS:
+					DirCacheEntry theirEntry = add(tw.getRawPath(), theirs,
+							DirCacheEntry.STAGE_0, EPOCH, 0);
+					addToCheckout(tw.getPathString(), theirEntry, attributes);
+					return true;
+				default:
+					break;
+				}
 				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);
@@ -774,8 +819,26 @@
 				return false;
 			}
 
-			MergeResult<RawText> result = contentMerge(base, ours, theirs,
-					attributes);
+			MergeResult<RawText> result = null;
+			try {
+				result = contentMerge(base, ours, theirs, attributes,
+						getContentMergeStrategy());
+			} catch (BinaryBlobException e) {
+				switch (getContentMergeStrategy()) {
+				case OURS:
+					keep(ourDce);
+					return true;
+				case THEIRS:
+					DirCacheEntry theirEntry = add(tw.getRawPath(), theirs,
+							DirCacheEntry.STAGE_0, EPOCH, 0);
+					addToCheckout(tw.getPathString(), theirEntry, attributes);
+					return true;
+				default:
+					result = new MergeResult<>(Collections.emptyList());
+					result.setContainsConflicts(true);
+					break;
+				}
+			}
 			if (ignoreConflicts) {
 				result.setContainsConflicts(false);
 			}
@@ -802,9 +865,16 @@
 					mergeResults.put(tw.getPathString(), result);
 					unmergedPaths.add(tw.getPathString());
 				} else {
-					MergeResult<RawText> result = contentMerge(base, ours,
-							theirs, attributes);
-
+					// Content merge strategy does not apply to delete-modify
+					// conflicts!
+					MergeResult<RawText> result;
+					try {
+						result = contentMerge(base, ours, theirs, attributes,
+								ContentMergeStrategy.CONFLICT);
+					} catch (BinaryBlobException e) {
+						result = new MergeResult<>(Collections.emptyList());
+						result.setContainsConflicts(true);
+					}
 					if (ignoreConflicts) {
 						// In case a conflict is detected the working tree file
 						// is again filled with new content (containing conflict
@@ -866,32 +936,26 @@
 	 * @param ours
 	 * @param theirs
 	 * @param attributes
+	 * @param strategy
 	 *
 	 * @return the result of the content merge
+	 * @throws BinaryBlobException
+	 *             if any of the blobs looks like a binary blob
 	 * @throws IOException
 	 */
 	private MergeResult<RawText> contentMerge(CanonicalTreeParser base,
 			CanonicalTreeParser ours, CanonicalTreeParser theirs,
-			Attributes attributes)
-			throws IOException {
-		RawText baseText;
-		RawText ourText;
-		RawText theirsText;
-
-		try {
-			baseText = base == null ? RawText.EMPTY_TEXT : getRawText(
-							base.getEntryObjectId(), attributes);
-			ourText = ours == null ? RawText.EMPTY_TEXT : getRawText(
-							ours.getEntryObjectId(), attributes);
-			theirsText = theirs == null ? RawText.EMPTY_TEXT : getRawText(
-							theirs.getEntryObjectId(), attributes);
-		} catch (BinaryBlobException e) {
-			MergeResult<RawText> r = new MergeResult<>(Collections.<RawText>emptyList());
-			r.setContainsConflicts(true);
-			return r;
-		}
-		return (mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText,
-				ourText, theirsText));
+			Attributes attributes, ContentMergeStrategy strategy)
+			throws BinaryBlobException, IOException {
+		RawText baseText = base == null ? RawText.EMPTY_TEXT
+				: getRawText(base.getEntryObjectId(), attributes);
+		RawText ourText = ours == null ? RawText.EMPTY_TEXT
+				: getRawText(ours.getEntryObjectId(), attributes);
+		RawText theirsText = theirs == null ? RawText.EMPTY_TEXT
+				: getRawText(theirs.getEntryObjectId(), attributes);
+		mergeAlgorithm.setContentMergeStrategy(strategy);
+		return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText,
+				ourText, theirsText);
 	}
 
 	private boolean isIndexDirty() {