Merge "Add mergetool merge feature (execute external tool)"
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
index 017a5d9..dc34c0d 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
@@ -16,6 +16,7 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
 import static org.junit.Assert.fail;
 
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -30,7 +31,7 @@
 /**
  * Testing the {@code difftool} command.
  */
-public class DiffToolTest extends ExternalToolTestCase {
+public class DiffToolTest extends ToolTestCase {
 
 	private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION;
 
@@ -41,6 +42,46 @@ public void setUp() throws Exception {
 		configureEchoTool(TOOL_NAME);
 	}
 
+	@Test
+	public void testToolWithPrompt() throws Exception {
+		String[] inputLines = {
+				"y", // accept launching diff tool
+				"y", // accept launching diff tool
+		};
+
+		RevCommit commit = createUnstagedChanges();
+		List<DiffEntry> changes = getRepositoryChanges(commit);
+		String[] expectedOutput = getExpectedCompareOutput(changes);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+						DIFF_TOOL, "--prompt", option, TOOL_NAME));
+	}
+
+	@Test
+	public void testToolAbortLaunch() throws Exception {
+		String[] inputLines = {
+				"y", // accept launching diff tool
+				"n", // don't launch diff tool
+		};
+
+		RevCommit commit = createUnstagedChanges();
+		List<DiffEntry> changes = getRepositoryChanges(commit);
+		int abortIndex = 1;
+		String[] expectedOutput = getExpectedAbortOutput(changes, abortIndex);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput,
+				runAndCaptureUsingInitRaw(inputStream, DIFF_TOOL, "--prompt", option,
+						TOOL_NAME));
+	}
+
 	@Test(expected = Die.class)
 	public void testNotDefinedTool() throws Exception {
 		createUnstagedChanges();
@@ -53,7 +94,7 @@ public void testNotDefinedTool() throws Exception {
 	public void testTool() throws Exception {
 		RevCommit commit = createUnstagedChanges();
 		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedToolOutput(changes);
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
 
 		String[] options = {
 				"--tool",
@@ -72,7 +113,7 @@ public void testTool() throws Exception {
 	public void testToolTrustExitCode() throws Exception {
 		RevCommit commit = createUnstagedChanges();
 		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedToolOutput(changes);
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
 
 		String[] options = { "--tool", "-t", };
 
@@ -87,7 +128,7 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
 	public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
 		RevCommit commit = createUnstagedChanges();
 		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedToolOutput(changes);
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
 
 		String[] options = { "--tool", "-t", };
 
@@ -103,7 +144,7 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
 	public void testToolCached() throws Exception {
 		RevCommit commit = createStagedChanges();
 		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedToolOutput(changes);
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
 
 		String[] options = { "--cached", "--staged", };
 
@@ -118,7 +159,8 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
 	public void testToolHelp() throws Exception {
 		CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
 		List<String> expectedOutput = new ArrayList<>();
-		expectedOutput.add("git difftool --tool=<tool> may be set to one of the following:");
+		expectedOutput.add(
+				"'git difftool --tool=<tool>' may be set to one of the following:");
 		for (CommandLineDiffTool defaultTool : defaultTools) {
 			String toolName = defaultTool.name();
 			expectedOutput.add(toolName);
@@ -159,7 +201,7 @@ private void configureEchoTool(String toolName) {
 				String.valueOf(false));
 	}
 
-	private String[] getExpectedToolOutput(List<DiffEntry> changes) {
+	private static String[] getExpectedToolOutputNoPrompt(List<DiffEntry> changes) {
 		String[] expectedToolOutput = new String[changes.size()];
 		for (int i = 0; i < changes.size(); ++i) {
 			DiffEntry change = changes.get(i);
@@ -169,4 +211,36 @@ private void configureEchoTool(String toolName) {
 		}
 		return expectedToolOutput;
 	}
+
+	private static String[] getExpectedCompareOutput(List<DiffEntry> changes) {
+		List<String> expected = new ArrayList<>();
+		int n = changes.size();
+		for (int i = 0; i < n; ++i) {
+			DiffEntry change = changes.get(i);
+			String newPath = change.getNewPath();
+			expected.add(
+					"Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'");
+			expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
+			expected.add(newPath);
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private static String[] getExpectedAbortOutput(List<DiffEntry> changes,
+			int abortIndex) {
+		List<String> expected = new ArrayList<>();
+		int n = changes.size();
+		for (int i = 0; i < n; ++i) {
+			DiffEntry change = changes.get(i);
+			String newPath = change.getNewPath();
+			expected.add(
+					"Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'");
+			expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
+			if (i == abortIndex) {
+				break;
+			}
+			expected.add(newPath);
+		}
+		return expected.toArray(new String[0]);
+	}
 }
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java
deleted file mode 100644
index e10b13e..0000000
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.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
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-package org.eclipse.jgit.pgm;
-
-import static org.junit.Assert.assertEquals;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jgit.api.CherryPickResult;
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.lib.CLIRepositoryTestCase;
-import org.eclipse.jgit.pgm.opt.CmdLineParser;
-import org.eclipse.jgit.pgm.opt.SubcommandHandler;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.treewalk.FileTreeIterator;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.junit.Before;
-import org.kohsuke.args4j.Argument;
-
-/**
- * Base test case for the {@code difftool} and {@code mergetool} commands.
- */
-public abstract class ExternalToolTestCase extends CLIRepositoryTestCase {
-
-	public static class GitCliJGitWrapperParser {
-		@Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
-		TextBuiltin subcommand;
-
-		@Argument(index = 1, metaVar = "metaVar_arg")
-		List<String> arguments = new ArrayList<>();
-	}
-
-	protected static final String TOOL_NAME = "some_tool";
-
-	private static final String TEST_BRANCH_NAME = "test_branch";
-
-	private Git git;
-
-	@Override
-	@Before
-	public void setUp() throws Exception {
-		super.setUp();
-		git = new Git(db);
-		git.commit().setMessage("initial commit").call();
-		git.branchCreate().setName(TEST_BRANCH_NAME).call();
-	}
-
-	protected String[] runAndCaptureUsingInitRaw(String... args)
-			throws Exception {
-		CLIGitCommand.Result result = new CLIGitCommand.Result();
-
-		GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
-		CmdLineParser clp = new CmdLineParser(bean);
-		clp.parseArgument(args);
-
-		TextBuiltin cmd = bean.subcommand;
-		cmd.initRaw(db, null, null, result.out, result.err);
-		cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()]));
-		if (cmd.getOutputWriter() != null) {
-			cmd.getOutputWriter().flush();
-		}
-		if (cmd.getErrorWriter() != null) {
-			cmd.getErrorWriter().flush();
-		}
-		return result.outLines().toArray(new String[0]);
-	}
-
-	protected CherryPickResult createMergeConflict() throws Exception {
-		writeTrashFile("a", "Hello world a");
-		writeTrashFile("b", "Hello world b");
-		git.add().addFilepattern(".").call();
-		git.commit().setMessage("files a & b added").call();
-		writeTrashFile("a", "Hello world a 1");
-		writeTrashFile("b", "Hello world b 1");
-		git.add().addFilepattern(".").call();
-		RevCommit commit1 = git.commit().setMessage("files a & b commit 1")
-				.call();
-		git.branchCreate().setName("branch_1").call();
-		git.checkout().setName(TEST_BRANCH_NAME).call();
-		writeTrashFile("a", "Hello world a 2");
-		writeTrashFile("b", "Hello world b 2");
-		git.add().addFilepattern(".").call();
-		git.commit().setMessage("files a & b commit 2").call();
-		git.branchCreate().setName("branch_2").call();
-		CherryPickResult result = git.cherryPick().include(commit1).call();
-		return result;
-	}
-
-	protected RevCommit createUnstagedChanges() throws Exception {
-		writeTrashFile("a", "Hello world a");
-		writeTrashFile("b", "Hello world b");
-		git.add().addFilepattern(".").call();
-		RevCommit commit = git.commit().setMessage("files a & b").call();
-		writeTrashFile("a", "New Hello world a");
-		writeTrashFile("b", "New Hello world b");
-		return commit;
-	}
-
-	protected RevCommit createStagedChanges() throws Exception {
-		RevCommit commit = createUnstagedChanges();
-		git.add().addFilepattern(".").call();
-		return commit;
-	}
-
-	protected List<DiffEntry> getRepositoryChanges(RevCommit commit)
-			throws Exception {
-		TreeWalk tw = new TreeWalk(db);
-		tw.addTree(commit.getTree());
-		FileTreeIterator modifiedTree = new FileTreeIterator(db);
-		tw.addTree(modifiedTree);
-		List<DiffEntry> changes = DiffEntry.scan(tw);
-		return changes;
-	}
-
-	protected static void assertArrayOfLinesEquals(String failMessage,
-			String[] expected, String[] actual) {
-		assertEquals(failMessage, toString(expected), toString(actual));
-	}
-
-	protected static String getEchoCommand() {
-		/*
-		 * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be
-		 * replaced with full paths to a temporary file during some of the tests
-		 */
-		return "(echo \"$MERGED\")";
-	}
-}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
index 32cd604..2e50f09 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
@@ -15,6 +15,7 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
 
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -27,7 +28,7 @@
 /**
  * Testing the {@code mergetool} command.
  */
-public class MergeToolTest extends ExternalToolTestCase {
+public class MergeToolTest extends ToolTestCase {
 
 	private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION;
 
@@ -39,38 +40,122 @@ public void setUp() throws Exception {
 	}
 
 	@Test
-	public void testTool() throws Exception {
-		createMergeConflict();
-		String[] expectedOutput = getExpectedToolOutput();
-
-		String[] options = {
-				"--tool",
-				"-t",
+	public void testAbortMerge() throws Exception {
+		String[] inputLines = {
+				"y", // start tool for merge resolution
+				"n", // don't accept merge tool result
+				"n", // don't continue resolution
 		};
+		String[] conflictingFilenames = createMergeConflict();
+		int abortIndex = 1;
+		String[] expectedOutput = getExpectedAbortMergeOutput(
+				conflictingFilenames,
+				abortIndex);
 
-		for (String option : options) {
-			assertArrayOfLinesEquals("Incorrect output for option: " + option,
-					expectedOutput,
-					runAndCaptureUsingInitRaw(MERGE_TOOL, option,
-							TOOL_NAME));
-		}
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+						MERGE_TOOL, "--prompt", option, TOOL_NAME));
 	}
 
 	@Test
-	public void testToolNoGuiNoPrompt() throws Exception {
-		createMergeConflict();
-		String[] expectedOutput = getExpectedToolOutput();
+	public void testAbortLaunch() throws Exception {
+		String[] inputLines = {
+				"n", // abort merge tool launch
+		};
+		String[] conflictingFilenames = createMergeConflict();
+		String[] expectedOutput = getExpectedAbortLaunchOutput(
+				conflictingFilenames);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+						MERGE_TOOL, "--prompt", option, TOOL_NAME));
+	}
+
+	@Test
+	public void testMergeConflict() throws Exception {
+		String[] inputLines = {
+				"y", // start tool for merge resolution
+				"y", // accept merge result as successful
+				"y", // start tool for merge resolution
+				"y", // accept merge result as successful
+		};
+		String[] conflictingFilenames = createMergeConflict();
+		String[] expectedOutput = getExpectedMergeConflictOutput(
+				conflictingFilenames);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+						MERGE_TOOL, "--prompt", option, TOOL_NAME));
+	}
+
+	@Test
+	public void testDeletedConflict() throws Exception {
+		String[] inputLines = {
+				"d", // choose delete option to resolve conflict
+				"m", // choose merge option to resolve conflict
+		};
+		String[] conflictingFilenames = createDeletedConflict();
+		String[] expectedOutput = getExpectedDeletedConflictOutput(
+				conflictingFilenames);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+						MERGE_TOOL, "--prompt", option, TOOL_NAME));
+	}
+
+	@Test
+	public void testNoConflict() throws Exception {
+		createStagedChanges();
+		String[] expectedOutput = { "No files need merging" };
 
 		String[] options = { "--tool", "-t", };
 
 		for (String option : options) {
 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
-					expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL,
-							"--no-gui", "--no-prompt", option, TOOL_NAME));
+					expectedOutput,
+					runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME));
 		}
 	}
 
 	@Test
+	public void testMergeConflictNoPrompt() throws Exception {
+		String[] conflictingFilenames = createMergeConflict();
+		String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt(
+				conflictingFilenames);
+
+		String option = "--tool";
+
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput,
+				runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME));
+	}
+
+	@Test
+	public void testMergeConflictNoGuiNoPrompt() throws Exception {
+		String[] conflictingFilenames = createMergeConflict();
+		String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt(
+				conflictingFilenames);
+
+		String option = "--tool";
+
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL,
+						"--no-gui", "--no-prompt", option, TOOL_NAME));
+	}
+
+	@Test
 	public void testToolHelp() throws Exception {
 		CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
 		List<String> expectedOutput = new ArrayList<>();
@@ -87,8 +172,7 @@ public void testToolHelp() throws Exception {
 		String[] userDefinedToolsHelp = {
 				"The following tools are valid, but not currently available:",
 				"Some of the tools listed above only work in a windowed",
-				"environment. If run in a terminal-only session, they will fail.",
-		};
+				"environment. If run in a terminal-only session, they will fail.", };
 		expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));
 
 		String option = "--tool-help";
@@ -116,21 +200,111 @@ private void configureEchoTool(String toolName) {
 				String.valueOf(false));
 	}
 
-	private String[] getExpectedToolOutput() {
-		String[] mergeConflictFilenames = { "a", "b", };
-		List<String> expectedOutput = new ArrayList<>();
-		expectedOutput.add("Merging:");
-		for (String mergeConflictFilename : mergeConflictFilenames) {
-			expectedOutput.add(mergeConflictFilename);
+	private static String[] getExpectedMergeConflictOutputNoPrompt(
+			String[] conflictFilenames) {
+		List<String> expected = new ArrayList<>();
+		expected.add("Merging:");
+		for (String conflictFilename : conflictFilenames) {
+			expected.add(conflictFilename);
 		}
-		for (String mergeConflictFilename : mergeConflictFilenames) {
-			expectedOutput.add("Normal merge conflict for '"
-					+ mergeConflictFilename + "':");
-			expectedOutput.add("{local}: modified file");
-			expectedOutput.add("{remote}: modified file");
-			expectedOutput.add("TODO: Launch mergetool '" + TOOL_NAME
-					+ "' for path '" + mergeConflictFilename + "'...");
+		for (String conflictFilename : conflictFilenames) {
+			expected.add("Normal merge conflict for '" + conflictFilename
+					+ "':");
+			expected.add("{local}: modified file");
+			expected.add("{remote}: modified file");
+			expected.add(conflictFilename);
+			expected.add(conflictFilename + " seems unchanged.");
 		}
-		return expectedOutput.toArray(new String[0]);
+		return expected.toArray(new String[0]);
+	}
+
+	private static String[] getExpectedAbortLaunchOutput(
+			String[] conflictFilenames) {
+		List<String> expected = new ArrayList<>();
+		expected.add("Merging:");
+		for (String conflictFilename : conflictFilenames) {
+			expected.add(conflictFilename);
+		}
+		if (conflictFilenames.length > 1) {
+			String conflictFilename = conflictFilenames[0];
+			expected.add(
+					"Normal merge conflict for '" + conflictFilename + "':");
+			expected.add("{local}: modified file");
+			expected.add("{remote}: modified file");
+			expected.add("Hit return to start merge resolution tool ("
+					+ TOOL_NAME + "):");
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private static String[] getExpectedAbortMergeOutput(
+			String[] conflictFilenames, int abortIndex) {
+		List<String> expected = new ArrayList<>();
+		expected.add("Merging:");
+		for (String conflictFilename : conflictFilenames) {
+			expected.add(conflictFilename);
+		}
+		for (int i = 0; i < conflictFilenames.length; ++i) {
+			if (i == abortIndex) {
+				break;
+			}
+
+			String conflictFilename = conflictFilenames[i];
+			expected.add(
+					"Normal merge conflict for '" + conflictFilename + "':");
+			expected.add("{local}: modified file");
+			expected.add("{remote}: modified file");
+			expected.add("Hit return to start merge resolution tool ("
+					+ TOOL_NAME + "): " + conflictFilename);
+			expected.add(conflictFilename + " seems unchanged.");
+			expected.add("Was the merge successful [y/n]?");
+			if (i < conflictFilenames.length - 1) {
+				expected.add(
+						"\tContinue merging other unresolved paths [y/n]?");
+			}
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private static String[] getExpectedMergeConflictOutput(
+			String[] conflictFilenames) {
+		List<String> expected = new ArrayList<>();
+		expected.add("Merging:");
+		for (String conflictFilename : conflictFilenames) {
+			expected.add(conflictFilename);
+		}
+		for (int i = 0; i < conflictFilenames.length; ++i) {
+			String conflictFilename = conflictFilenames[i];
+			expected.add("Normal merge conflict for '" + conflictFilename
+					+ "':");
+			expected.add("{local}: modified file");
+			expected.add("{remote}: modified file");
+			expected.add("Hit return to start merge resolution tool ("
+					+ TOOL_NAME + "): " + conflictFilename);
+			expected.add(conflictFilename + " seems unchanged.");
+			expected.add("Was the merge successful [y/n]?");
+			if (i < conflictFilenames.length - 1) {
+				// expected.add(
+				// "\tContinue merging other unresolved paths [y/n]?");
+			}
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private static String[] getExpectedDeletedConflictOutput(
+			String[] conflictFilenames) {
+		List<String> expected = new ArrayList<>();
+		expected.add("Merging:");
+		for (String mergeConflictFilename : conflictFilenames) {
+			expected.add(mergeConflictFilename);
+		}
+		for (int i = 0; i < conflictFilenames.length; ++i) {
+			String conflictFilename = conflictFilenames[i];
+			expected.add(conflictFilename + " seems unchanged.");
+			expected.add("{local}: deleted");
+			expected.add("{remote}: modified file");
+			expected.add("Use (m)odified or (d)eleted file, or (a)bort?");
+		}
+		return expected.toArray(new String[0]);
 	}
 }
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java
new file mode 100644
index 0000000..d13eeb7
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.pgm;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.eclipse.jgit.pgm.opt.CmdLineParser;
+import org.eclipse.jgit.pgm.opt.SubcommandHandler;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Before;
+import org.kohsuke.args4j.Argument;
+
+/**
+ * Base test case for the {@code difftool} and {@code mergetool} commands.
+ */
+public abstract class ToolTestCase extends CLIRepositoryTestCase {
+
+	public static class GitCliJGitWrapperParser {
+		@Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
+		TextBuiltin subcommand;
+
+		@Argument(index = 1, metaVar = "metaVar_arg")
+		List<String> arguments = new ArrayList<>();
+	}
+
+	protected static final String TOOL_NAME = "some_tool";
+
+	private static final String TEST_BRANCH_NAME = "test_branch";
+
+	private Git git;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		git = new Git(db);
+		git.commit().setMessage("initial commit").call();
+		git.branchCreate().setName(TEST_BRANCH_NAME).call();
+	}
+
+	protected String[] runAndCaptureUsingInitRaw(String... args)
+			throws Exception {
+		InputStream inputStream = null; // no input stream
+		return runAndCaptureUsingInitRaw(inputStream, args);
+	}
+
+	protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
+			String... args) throws Exception {
+		CLIGitCommand.Result result = new CLIGitCommand.Result();
+
+		GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
+		CmdLineParser clp = new CmdLineParser(bean);
+		clp.parseArgument(args);
+
+		TextBuiltin cmd = bean.subcommand;
+		cmd.initRaw(db, null, inputStream, result.out, result.err);
+		cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()]));
+		if (cmd.getOutputWriter() != null) {
+			cmd.getOutputWriter().flush();
+		}
+		if (cmd.getErrorWriter() != null) {
+			cmd.getErrorWriter().flush();
+		}
+
+		List<String> errLines = result.errLines().stream()
+				.filter(l -> !l.isBlank()) // we care only about error messages
+				.collect(Collectors.toList());
+		assertEquals("Expected no standard error output from tool",
+				Collections.EMPTY_LIST.toString(), errLines.toString());
+
+		return result.outLines().toArray(new String[0]);
+	}
+
+	protected String[] createMergeConflict() throws Exception {
+		// create files on initial branch
+		git.checkout().setName(TEST_BRANCH_NAME).call();
+		writeTrashFile("a", "Hello world a");
+		writeTrashFile("b", "Hello world b");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("files a & b added").call();
+		// create another branch and change files
+		git.branchCreate().setName("branch_1").call();
+		git.checkout().setName("branch_1").call();
+		writeTrashFile("a", "Hello world a 1");
+		writeTrashFile("b", "Hello world b 1");
+		git.add().addFilepattern(".").call();
+		RevCommit commit1 = git.commit()
+				.setMessage("files a & b modified commit 1").call();
+		// checkout initial branch
+		git.checkout().setName(TEST_BRANCH_NAME).call();
+		// create another branch and change files
+		git.branchCreate().setName("branch_2").call();
+		git.checkout().setName("branch_2").call();
+		writeTrashFile("a", "Hello world a 2");
+		writeTrashFile("b", "Hello world b 2");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("files a & b modified commit 2").call();
+		// cherry-pick conflicting changes
+		git.cherryPick().include(commit1).call();
+		String[] conflictingFilenames = { "a", "b" };
+		return conflictingFilenames;
+	}
+
+	protected String[] createDeletedConflict() throws Exception {
+		// create files on initial branch
+		git.checkout().setName(TEST_BRANCH_NAME).call();
+		writeTrashFile("a", "Hello world a");
+		writeTrashFile("b", "Hello world b");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("files a & b added").call();
+		// create another branch and change files
+		git.branchCreate().setName("branch_1").call();
+		git.checkout().setName("branch_1").call();
+		writeTrashFile("a", "Hello world a 1");
+		writeTrashFile("b", "Hello world b 1");
+		git.add().addFilepattern(".").call();
+		RevCommit commit1 = git.commit()
+				.setMessage("files a & b modified commit 1").call();
+		// checkout initial branch
+		git.checkout().setName(TEST_BRANCH_NAME).call();
+		// create another branch and change files
+		git.branchCreate().setName("branch_2").call();
+		git.checkout().setName("branch_2").call();
+		git.rm().addFilepattern("a").call();
+		git.rm().addFilepattern("b").call();
+		git.commit().setMessage("files a & b deleted commit 2").call();
+		// cherry-pick conflicting changes
+		git.cherryPick().include(commit1).call();
+		String[] conflictingFilenames = { "a", "b" };
+		return conflictingFilenames;
+	}
+
+	protected RevCommit createUnstagedChanges() throws Exception {
+		writeTrashFile("a", "Hello world a");
+		writeTrashFile("b", "Hello world b");
+		git.add().addFilepattern(".").call();
+		RevCommit commit = git.commit().setMessage("files a & b").call();
+		writeTrashFile("a", "New Hello world a");
+		writeTrashFile("b", "New Hello world b");
+		return commit;
+	}
+
+	protected RevCommit createStagedChanges() throws Exception {
+		RevCommit commit = createUnstagedChanges();
+		git.add().addFilepattern(".").call();
+		return commit;
+	}
+
+	protected List<DiffEntry> getRepositoryChanges(RevCommit commit)
+			throws Exception {
+		TreeWalk tw = new TreeWalk(db);
+		tw.addTree(commit.getTree());
+		FileTreeIterator modifiedTree = new FileTreeIterator(db);
+		tw.addTree(modifiedTree);
+		List<DiffEntry> changes = DiffEntry.scan(tw);
+		return changes;
+	}
+
+	protected static InputStream createInputStream(String[] inputLines) {
+		return createInputStream(Arrays.asList(inputLines));
+	}
+
+	protected static InputStream createInputStream(List<String> inputLines) {
+		String input = String.join(System.lineSeparator(), inputLines);
+		InputStream inputStream = new ByteArrayInputStream(input.getBytes());
+		return inputStream;
+	}
+
+	protected static void assertArrayOfLinesEquals(String failMessage,
+			String[] expected, String[] actual) {
+		assertEquals(failMessage, toString(expected), toString(actual));
+	}
+
+	protected static String getEchoCommand() {
+		/*
+		 * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be
+		 * replaced with full paths to a temporary file during some of the tests
+		 */
+		return "(echo \"$MERGED\")";
+	}
+}
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 8e2eef7..674185d 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
@@ -58,8 +58,8 @@
 dateInfo=Date:   {0}
 deletedBranch=Deleted branch {0}
 deletedRemoteBranch=Deleted remote branch {0}
-diffToolHelpSetToFollowing='git difftool --tool=<tool>' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
-diffToolLaunch=Viewing ({0}/{1}): '{2}'\nLaunch '{3}' [Y/n]?
+diffToolHelpSetToFollowing=''git difftool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
+diffToolLaunch=Viewing ({0}/{1}): ''{2}''\nLaunch ''{3}'' [Y/n]?
 diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1}
 doesNotExist={0} does not exist
 dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge:
@@ -91,6 +91,22 @@
 logNoSignatureVerifier="No signature verifier available"
 mergeConflict=CONFLICT(content): Merge conflict in {0}
 mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge:
+mergeToolHelpSetToFollowing=''git mergetool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
+mergeToolLaunch=Hit return to start merge resolution tool ({0}):
+mergeToolDied=local or remote cannot be found in cache, stopping at {0}
+mergeToolNoFiles=No files need merging
+mergeToolMerging=Merging:\n{0}
+mergeToolUnknownConflict=\nUnknown merge conflict for ''{0}'':
+mergeToolNormalConflict=\nNormal merge conflict for ''{0}'':\n  '{'local'}': modified file\n  '{'remote'}': modified file
+mergeToolMergeFailed=merge of {0} failed
+mergeToolExecutionError=excution error
+mergeToolFileUnchanged=\n{0} seems unchanged.
+mergeToolDeletedConflict=\nDeleted merge conflict for ''{0}'':
+mergeToolDeletedConflictByUs=  {local}: deleted\n  {remote}: modified file
+mergeToolDeletedConflictByThem=  {local}: modified file\n  {remote}: deleted
+mergeToolContinueUnresolvedPaths=\nContinue merging other unresolved paths [y/n]?
+mergeToolWasMergeSuccessfull=Was the merge successful [y/n]?
+mergeToolDeletedMergeDecision=Use (m)odified or (d)eleted file, or (a)bort?
 mergeFailed=Automatic merge failed; fix conflicts and then commit the result
 mergeCheckoutFailed=Please, commit your changes or stash them before you can merge.
 mergeMadeBy=Merge made by the ''{0}'' strategy.
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
index 2e90d52..ffba36f 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
@@ -113,11 +113,14 @@ void noTrustExitCode(@SuppressWarnings("unused") boolean on) {
 	@Option(name = "--", metaVar = "metaVar_paths", handler = PathTreeFilterHandler.class)
 	private TreeFilter pathFilter = TreeFilter.ALL;
 
+	private BufferedReader inputReader;
+
 	@Override
 	protected void init(Repository repository, String gitDir) {
 		super.init(repository, gitDir);
 		diffFmt = new DiffFormatter(new BufferedOutputStream(outs));
 		diffTools = new DiffTools(repository);
+		inputReader = new BufferedReader(new InputStreamReader(ins, StandardCharsets.UTF_8));
 	}
 
 	@Override
@@ -208,10 +211,9 @@ private boolean isLaunchCompare(int fileIndex, int fileCount,
 			String fileName, String toolNamePrompt) throws IOException {
 		boolean launchCompare = true;
 		outw.println(MessageFormat.format(CLIText.get().diffToolLaunch,
-				fileIndex, fileCount, fileName, toolNamePrompt));
+				fileIndex, fileCount, fileName, toolNamePrompt) + " "); //$NON-NLS-1$
 		outw.flush();
-		BufferedReader br = new BufferedReader(
-				new InputStreamReader(ins, StandardCharsets.UTF_8));
+		BufferedReader br = inputReader;
 		String line = null;
 		if ((line = br.readLine()) != null) {
 			if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$
@@ -224,17 +226,18 @@ private boolean isLaunchCompare(int fileIndex, int fileCount,
 	private void showToolHelp() throws IOException {
 		StringBuilder availableToolNames = new StringBuilder();
 		for (String name : diffTools.getAvailableTools().keySet()) {
-			availableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$
+			availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
 		}
 		StringBuilder notAvailableToolNames = new StringBuilder();
 		for (String name : diffTools.getNotAvailableTools().keySet()) {
-			notAvailableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$
+			notAvailableToolNames
+					.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
 		}
 		StringBuilder userToolNames = new StringBuilder();
 		Map<String, ExternalDiffTool> userTools = diffTools
 				.getUserDefinedTools();
 		for (String name : userTools.keySet()) {
-			userToolNames.append(String.format("\t\t%s.cmd %s\n", //$NON-NLS-1$
+			userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$
 					name, userTools.get(name).getCommand()));
 		}
 		outw.println(MessageFormat.format(
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
index 37afa54..dce5a79 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
@@ -11,26 +11,35 @@
 package org.eclipse.jgit.pgm;
 
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.TreeMap;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.Status;
 import org.eclipse.jgit.api.StatusCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
+import org.eclipse.jgit.diff.ContentSource;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.errors.NoWorkTreeException;
 import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
+import org.eclipse.jgit.internal.diffmergetool.FileElement;
 import org.eclipse.jgit.internal.diffmergetool.MergeTools;
+import org.eclipse.jgit.internal.diffmergetool.ToolException;
 import org.eclipse.jgit.lib.IndexDiff.StageState;
-import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.pgm.internal.CLIText;
+import org.eclipse.jgit.util.FS.ExecutionResult;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
@@ -43,16 +52,16 @@ class MergeTool extends TextBuiltin {
 			"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge")
 	private String toolName;
 
-	private Optional<Boolean> prompt = Optional.empty();
+	private BooleanTriState prompt = BooleanTriState.UNSET;
 
 	@Option(name = "--prompt", usage = "usage_prompt")
 	void setPrompt(@SuppressWarnings("unused") boolean on) {
-		prompt = Optional.of(Boolean.TRUE);
+		prompt = BooleanTriState.TRUE;
 	}
 
 	@Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt")
 	void noPrompt(@SuppressWarnings("unused") boolean on) {
-		prompt = Optional.of(Boolean.FALSE);
+		prompt = BooleanTriState.FALSE;
 	}
 
 	@Option(name = "--tool-help", usage = "usage_toolHelp")
@@ -74,10 +83,17 @@ void noGui(@SuppressWarnings("unused") boolean on) {
 	@Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class)
 	protected List<String> filterPaths;
 
+	private BufferedReader inputReader;
+
 	@Override
 	protected void init(Repository repository, String gitDir) {
 		super.init(repository, gitDir);
 		mergeTools = new MergeTools(repository);
+		inputReader = new BufferedReader(new InputStreamReader(ins));
+	}
+
+	enum MergeResult {
+		SUCCESSFUL, FAILED, ABORTED
 	}
 
 	@Override
@@ -88,8 +104,8 @@ protected void run() {
 			} else {
 				// get prompt
 				boolean showPrompt = mergeTools.isInteractive();
-				if (prompt.isPresent()) {
-					showPrompt = prompt.get().booleanValue();
+				if (prompt != BooleanTriState.UNSET) {
+					showPrompt = prompt == BooleanTriState.TRUE;
 				}
 				// get passed or default tool name
 				String toolNameSelected = toolName;
@@ -101,7 +117,7 @@ protected void run() {
 				if (files.size() > 0) {
 					merge(files, showPrompt, toolNameSelected);
 				} else {
-					outw.println("No files need merging"); //$NON-NLS-1$
+					outw.println(CLIText.get().mergeToolNoFiles);
 				}
 			}
 			outw.flush();
@@ -113,88 +129,273 @@ protected void run() {
 	private void merge(Map<String, StageState> files, boolean showPrompt,
 			String toolNamePrompt) throws Exception {
 		// sort file names
-		List<String> fileNames = new ArrayList<>(files.keySet());
-		Collections.sort(fileNames);
+		List<String> mergedFilePaths = new ArrayList<>(files.keySet());
+		Collections.sort(mergedFilePaths);
 		// show the files
-		outw.println("Merging:"); //$NON-NLS-1$
-		for (String fileName : fileNames) {
-			outw.println(fileName);
+		StringBuilder mergedFiles = new StringBuilder();
+		for (String mergedFilePath : mergedFilePaths) {
+			mergedFiles.append(MessageFormat.format("{0}\n", mergedFilePath)); //$NON-NLS-1$
 		}
+		outw.println(MessageFormat.format(CLIText.get().mergeToolMerging,
+				mergedFiles));
 		outw.flush();
-		for (String fileName : fileNames) {
-			StageState fileState = files.get(fileName);
-			// only both-modified is valid for mergetool
-			if (fileState == StageState.BOTH_MODIFIED) {
-				outw.println("\nNormal merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$
-				outw.println("  {local}: modified file"); //$NON-NLS-1$
-				outw.println("  {remote}: modified file"); //$NON-NLS-1$
-				// check if user wants to launch merge resolution tool
-				boolean launch = true;
-				if (showPrompt) {
-					launch = isLaunch(toolNamePrompt);
+		// merge the files
+		MergeResult mergeResult = MergeResult.SUCCESSFUL;
+		for (String mergedFilePath : mergedFilePaths) {
+			// if last merge failed...
+			if (mergeResult == MergeResult.FAILED) {
+				// check if user wants to continue
+				if (showPrompt && !isContinueUnresolvedPaths()) {
+					mergeResult = MergeResult.ABORTED;
 				}
-				if (launch) {
-					outw.println("TODO: Launch mergetool '" + toolNamePrompt //$NON-NLS-1$
-							+ "' for path '" + fileName + "'..."); //$NON-NLS-1$ //$NON-NLS-2$
-				} else {
-					break;
-				}
-			} else if ((fileState == StageState.DELETED_BY_US) || (fileState == StageState.DELETED_BY_THEM)) {
-				outw.println("\nDeleted merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$
-			} else {
-				outw.println(
-						"\nUnknown merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$
+			}
+			// aborted ?
+			if (mergeResult == MergeResult.ABORTED) {
 				break;
 			}
+			// get file stage state and merge
+			StageState fileState = files.get(mergedFilePath);
+			if (fileState == StageState.BOTH_MODIFIED) {
+				mergeResult = mergeModified(mergedFilePath, showPrompt,
+						toolNamePrompt);
+			} else if ((fileState == StageState.DELETED_BY_US)
+					|| (fileState == StageState.DELETED_BY_THEM)) {
+				mergeResult = mergeDeleted(mergedFilePath,
+						fileState == StageState.DELETED_BY_US);
+			} else {
+				outw.println(MessageFormat.format(
+						CLIText.get().mergeToolUnknownConflict,
+						mergedFilePath));
+				mergeResult = MergeResult.ABORTED;
+			}
 		}
 	}
 
-	private boolean isLaunch(String toolNamePrompt)
-			throws IOException {
-		boolean launch = true;
-		outw.println("Hit return to start merge resolution tool (" //$NON-NLS-1$
-				+ toolNamePrompt + "): "); //$NON-NLS-1$
+	private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
+			String toolNamePrompt) throws Exception {
+		outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict,
+				mergedFilePath));
 		outw.flush();
-		BufferedReader br = new BufferedReader(new InputStreamReader(ins));
+		// check if user wants to launch merge resolution tool
+		boolean launch = true;
+		if (showPrompt) {
+			launch = isLaunch(toolNamePrompt);
+		}
+		if (!launch) {
+			return MergeResult.ABORTED; // abort
+		}
+		boolean isMergeSuccessful = true;
+		ContentSource baseSource = ContentSource.create(db.newObjectReader());
+		ContentSource localSource = ContentSource.create(db.newObjectReader());
+		ContentSource remoteSource = ContentSource.create(db.newObjectReader());
+		try {
+			FileElement base = null;
+			FileElement local = null;
+			FileElement remote = null;
+			DirCache cache = db.readDirCache();
+			int firstIndex = cache.findEntry(mergedFilePath);
+			if (firstIndex >= 0) {
+				int nextIndex = cache.nextEntry(firstIndex);
+				for (; firstIndex < nextIndex; firstIndex++) {
+					DirCacheEntry entry = cache.getEntry(firstIndex);
+					ObjectId id = entry.getObjectId();
+					switch (entry.getStage()) {
+					case DirCacheEntry.STAGE_1:
+						base = new FileElement(mergedFilePath, id.name(),
+								baseSource.open(mergedFilePath, id)
+										.openStream());
+						break;
+					case DirCacheEntry.STAGE_2:
+						local = new FileElement(mergedFilePath, id.name(),
+								localSource.open(mergedFilePath, id)
+										.openStream());
+						break;
+					case DirCacheEntry.STAGE_3:
+						remote = new FileElement(mergedFilePath, id.name(),
+								remoteSource.open(mergedFilePath, id)
+										.openStream());
+						break;
+					}
+				}
+			}
+			if ((local == null) || (remote == null)) {
+				throw die(MessageFormat.format(CLIText.get().mergeToolDied,
+						mergedFilePath));
+			}
+			File merged = new File(mergedFilePath);
+			long modifiedBefore = merged.lastModified();
+			try {
+				// TODO: check how to return the exit-code of the
+				// tool to jgit / java runtime ?
+				// int rc =...
+				ExecutionResult executionResult = mergeTools.merge(db, local,
+						remote, base, mergedFilePath, toolName, prompt, gui);
+				outw.println(
+						new String(executionResult.getStdout().toByteArray()));
+				outw.flush();
+				errw.println(
+						new String(executionResult.getStderr().toByteArray()));
+				errw.flush();
+			} catch (ToolException e) {
+				isMergeSuccessful = false;
+				outw.println(e.getResultStdout());
+				outw.flush();
+				errw.println(MessageFormat.format(
+						CLIText.get().mergeToolMergeFailed, mergedFilePath));
+				errw.flush();
+				if (e.isCommandExecutionError()) {
+					errw.println(e.getMessage());
+					throw die(CLIText.get().mergeToolExecutionError, e);
+				}
+			}
+			// if merge was successful check file modified
+			if (isMergeSuccessful) {
+				long modifiedAfter = merged.lastModified();
+				if (modifiedBefore == modifiedAfter) {
+					outw.println(MessageFormat.format(
+							CLIText.get().mergeToolFileUnchanged,
+							mergedFilePath));
+					isMergeSuccessful = !showPrompt || isMergeSuccessful();
+				}
+			}
+			// if automatically or manually successful
+			// -> add the file to the index
+			if (isMergeSuccessful) {
+				addFile(mergedFilePath);
+			}
+		} finally {
+			baseSource.close();
+			localSource.close();
+			remoteSource.close();
+		}
+		return isMergeSuccessful ? MergeResult.SUCCESSFUL : MergeResult.FAILED;
+	}
+
+	private MergeResult mergeDeleted(String mergedFilePath, boolean deletedByUs)
+			throws Exception {
+		outw.println(MessageFormat.format(CLIText.get().mergeToolFileUnchanged,
+				mergedFilePath));
+		if (deletedByUs) {
+			outw.println(CLIText.get().mergeToolDeletedConflictByUs);
+		} else {
+			outw.println(CLIText.get().mergeToolDeletedConflictByThem);
+		}
+		int mergeDecision = getDeletedMergeDecision();
+		if (mergeDecision == 1) {
+			// add modified file
+			addFile(mergedFilePath);
+		} else if (mergeDecision == -1) {
+			// remove deleted file
+			rmFile(mergedFilePath);
+		} else {
+			return MergeResult.ABORTED;
+		}
+		return MergeResult.SUCCESSFUL;
+	}
+
+	private void addFile(String fileName) throws Exception {
+		try (Git git = new Git(db)) {
+			git.add().addFilepattern(fileName).call();
+		}
+	}
+
+	private void rmFile(String fileName) throws Exception {
+		try (Git git = new Git(db)) {
+			git.rm().addFilepattern(fileName).call();
+		}
+	}
+
+	private boolean hasUserAccepted(String message) throws IOException {
+		boolean yes = true;
+		outw.print(message + " "); //$NON-NLS-1$
+		outw.flush();
+		BufferedReader br = inputReader;
+		String line = null;
+		while ((line = br.readLine()) != null) {
+			if (line.equalsIgnoreCase("y")) { //$NON-NLS-1$
+				yes = true;
+				break;
+			} else if (line.equalsIgnoreCase("n")) { //$NON-NLS-1$
+				yes = false;
+				break;
+			}
+			outw.print(message);
+			outw.flush();
+		}
+		return yes;
+	}
+
+	private boolean isContinueUnresolvedPaths() throws IOException {
+		return hasUserAccepted(CLIText.get().mergeToolContinueUnresolvedPaths);
+	}
+
+	private boolean isMergeSuccessful() throws IOException {
+		return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull);
+	}
+
+	private boolean isLaunch(String toolNamePrompt) throws IOException {
+		boolean launch = true;
+		outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch,
+				toolNamePrompt) + " "); //$NON-NLS-1$
+		outw.flush();
+		BufferedReader br = inputReader;
 		String line = null;
 		if ((line = br.readLine()) != null) {
-			if (!line.equalsIgnoreCase("Y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$
+			if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$
 				launch = false;
 			}
 		}
 		return launch;
 	}
 
-	private void showToolHelp() throws IOException {
-		outw.println(
-				"'git mergetool --tool=<tool>' may be set to one of the following:"); //$NON-NLS-1$
-		for (String name : mergeTools.getAvailableTools().keySet()) {
-			outw.println("\t\t" + name); //$NON-NLS-1$
+	private int getDeletedMergeDecision() throws IOException {
+		int ret = 0; // abort
+		final String message = CLIText.get().mergeToolDeletedMergeDecision
+				+ " "; //$NON-NLS-1$
+		outw.print(message);
+		outw.flush();
+		BufferedReader br = inputReader;
+		String line = null;
+		while ((line = br.readLine()) != null) {
+			if (line.equalsIgnoreCase("m")) { //$NON-NLS-1$
+				ret = 1; // modified
+				break;
+			} else if (line.equalsIgnoreCase("d")) { //$NON-NLS-1$
+				ret = -1; // deleted
+				break;
+			} else if (line.equalsIgnoreCase("a")) { //$NON-NLS-1$
+				break;
+			}
+			outw.print(message);
+			outw.flush();
 		}
-		outw.println(""); //$NON-NLS-1$
-		outw.println("\tuser-defined:"); //$NON-NLS-1$
+		return ret;
+	}
+
+	private void showToolHelp() throws IOException {
+		StringBuilder availableToolNames = new StringBuilder();
+		for (String name : mergeTools.getAvailableTools().keySet()) {
+			availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
+		}
+		StringBuilder notAvailableToolNames = new StringBuilder();
+		for (String name : mergeTools.getNotAvailableTools().keySet()) {
+			notAvailableToolNames
+					.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
+		}
+		StringBuilder userToolNames = new StringBuilder();
 		Map<String, ExternalMergeTool> userTools = mergeTools
 				.getUserDefinedTools();
 		for (String name : userTools.keySet()) {
-			outw.println("\t\t" + name + ".cmd " //$NON-NLS-1$ //$NON-NLS-2$
-					+ userTools.get(name).getCommand());
+			userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$
+					name, userTools.get(name).getCommand()));
 		}
-		outw.println(""); //$NON-NLS-1$
-		outw.println(
-				"The following tools are valid, but not currently available:"); //$NON-NLS-1$
-		for (String name : mergeTools.getNotAvailableTools().keySet()) {
-			outw.println("\t\t" + name); //$NON-NLS-1$
-		}
-		outw.println(""); //$NON-NLS-1$
-		outw.println("Some of the tools listed above only work in a windowed"); //$NON-NLS-1$
-		outw.println(
-				"environment. If run in a terminal-only session, they will fail."); //$NON-NLS-1$
-		return;
+		outw.println(MessageFormat.format(
+				CLIText.get().mergeToolHelpSetToFollowing, availableToolNames,
+				userToolNames, notAvailableToolNames));
 	}
 
-	private Map<String, StageState> getFiles()
-			throws RevisionSyntaxException, NoWorkTreeException,
-			GitAPIException {
+	private Map<String, StageState> getFiles() throws RevisionSyntaxException,
+			NoWorkTreeException, GitAPIException {
 		Map<String, StageState> files = new TreeMap<>();
 		try (Git git = new Git(db)) {
 			StatusCommand statusCommand = git.status();
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 7fe5b0f..989e649 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
@@ -169,6 +169,22 @@ public static String fatalError(String message) {
 	/***/ public String logNoSignatureVerifier;
 	/***/ public String mergeCheckoutConflict;
 	/***/ public String mergeConflict;
+	/***/ public String mergeToolHelpSetToFollowing;
+	/***/ public String mergeToolLaunch;
+	/***/ public String mergeToolDied;
+	/***/ public String mergeToolNoFiles;
+	/***/ public String mergeToolMerging;
+	/***/ public String mergeToolUnknownConflict;
+	/***/ public String mergeToolNormalConflict;
+	/***/ public String mergeToolMergeFailed;
+	/***/ public String mergeToolExecutionError;
+	/***/ public String mergeToolFileUnchanged;
+	/***/ public String mergeToolDeletedConflict;
+	/***/ public String mergeToolDeletedConflictByUs;
+	/***/ public String mergeToolDeletedConflictByThem;
+	/***/ public String mergeToolContinueUnresolvedPaths;
+	/***/ public String mergeToolWasMergeSuccessfull;
+	/***/ public String mergeToolDeletedMergeDecision;
 	/***/ public String mergeFailed;
 	/***/ public String mergeCheckoutFailed;
 	/***/ public String mergeMadeBy;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
index 0dde9b5..ad79fe8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
@@ -72,10 +72,18 @@ public ExecutionResult run(String command, File workingDir,
 			}
 			ExecutionResult result = fs.execute(pb, null);
 			int rc = result.getRc();
-			if ((rc != 0) && (checkExitCode
-					|| isCommandExecutionError(rc))) {
-				throw new ToolException(
-						new String(result.getStderr().toByteArray()), result);
+			if (rc != 0) {
+				boolean execError = isCommandExecutionError(rc);
+				if (checkExitCode || execError) {
+					throw new ToolException(
+							"JGit: tool execution return code: " + rc + "\n" //$NON-NLS-1$ //$NON-NLS-2$
+									+ "checkExitCode: " + checkExitCode + "\n" //$NON-NLS-1$ //$NON-NLS-2$
+									+ "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$
+									+ "stderr: \n" //$NON-NLS-1$
+									+ new String(
+											result.getStderr().toByteArray()),
+							result, execError);
+				}
 			}
 			return result;
 		} finally {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
index cdc8f01..1ae87aa 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
@@ -11,6 +11,7 @@
 package org.eclipse.jgit.internal.diffmergetool;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -80,35 +81,27 @@ public void setStream(ObjectStream stream) {
 	}
 
 	/**
-	 * @param workingDir the working directory used if file cannot be found (e.g. /dev/null)
+	 * Returns a temporary file with in passed working directory and fills it
+	 * with stream if valid.
+	 *
+	 * @param directory
+	 *            the working directory where the temporary file is created
+	 * @param midName
+	 *            name added in the middle of generated temporary file name
 	 * @return the object stream
 	 * @throws IOException
 	 */
-	public File getFile(File workingDir) throws IOException {
+	public File getFile(File directory, String midName) throws IOException {
 		if (tempFile != null) {
 			return tempFile;
 		}
-		File file = new File(path);
-		String name = file.getName();
-		if (path.equals(DiffEntry.DEV_NULL)) {
-			file = new File(workingDir, "nul"); //$NON-NLS-1$
-		}
-		else if (stream != null) {
-			tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$
-			try (OutputStream outStream = new FileOutputStream(tempFile)) {
-				int read = 0;
-				byte[] bytes = new byte[8 * 1024];
-				while ((read = stream.read(bytes)) != -1) {
-					outStream.write(bytes, 0, read);
-				}
-			} finally {
-				// stream can only be consumed once --> close it
-				stream.close();
-				stream = null;
-			}
-			return tempFile;
-		}
-		return file;
+		String[] fileNameAndExtension = splitBaseFileNameAndExtension(
+				new File(path));
+		tempFile = File.createTempFile(
+				fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
+				fileNameAndExtension[1], directory);
+		copyFromStream();
+		return tempFile;
 	}
 
 	/**
@@ -130,19 +123,7 @@ public File getFile() throws IOException {
 			// TODO: avoid long random file name (number generated by
 			// createTempFile)
 			tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$
-			if (stream != null) {
-				try (OutputStream outStream = new FileOutputStream(tempFile)) {
-					int read = 0;
-					byte[] bytes = new byte[8 * 1024];
-					while ((read = stream.read(bytes)) != -1) {
-						outStream.write(bytes, 0, read);
-					}
-				} finally {
-					// stream can only be consumed once --> close it
-					stream.close();
-					stream = null;
-				}
-			}
+			copyFromStream();
 			return tempFile;
 		}
 		return file;
@@ -157,4 +138,34 @@ public void cleanTemporaries() {
 		tempFile = null;
 	}
 
+	private void copyFromStream() throws IOException, FileNotFoundException {
+		if (stream != null) {
+			try (OutputStream outStream = new FileOutputStream(tempFile)) {
+				int read = 0;
+				byte[] bytes = new byte[8 * 1024];
+				while ((read = stream.read(bytes)) != -1) {
+					outStream.write(bytes, 0, read);
+				}
+			} finally {
+				// stream can only be consumed once --> close it
+				stream.close();
+				stream = null;
+			}
+		}
+	}
+
+	private static String[] splitBaseFileNameAndExtension(File file) {
+		String[] result = new String[2];
+		result[0] = file.getName();
+		result[1] = ""; //$NON-NLS-1$
+		if (!result[0].startsWith(".")) { //$NON-NLS-1$
+			int idx = result[0].lastIndexOf("."); //$NON-NLS-1$
+			if (idx != -1) {
+				result[1] = result[0].substring(idx, result[0].length());
+				result[0] = result[0].substring(0, idx);
+			}
+		}
+		return result;
+	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
index cefefb8..c4c2cec 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
@@ -10,6 +10,11 @@
 package org.eclipse.jgit.internal.diffmergetool;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
@@ -48,7 +53,7 @@ public MergeTools(Repository repo) {
 	 * @param remoteFile
 	 *            the remote file element
 	 * @param baseFile
-	 *            the base file element
+	 *            the base file element (can be null)
 	 * @param mergedFilePath
 	 *            the path of 'merged' file
 	 * @param toolName
@@ -65,35 +70,79 @@ public ExecutionResult merge(Repository repo, FileElement localFile,
 			String toolName, BooleanTriState prompt, BooleanTriState gui)
 			throws ToolException {
 		ExternalMergeTool tool = guessTool(toolName, gui);
+		FileElement backup = null;
+		File tempDir = null;
+		ExecutionResult result = null;
 		try {
 			File workingDir = repo.getWorkTree();
-			String localFilePath = localFile.getFile().getPath();
-			String remoteFilePath = remoteFile.getFile().getPath();
-			String baseFilePath = baseFile.getFile().getPath();
-			String command = tool.getCommand();
-			command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$
-			command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$
-			command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$
-			command = command.replace("$BASE", baseFilePath); //$NON-NLS-1$
-			Map<String, String> env = new TreeMap<>();
-			env.put(Constants.GIT_DIR_KEY,
-					repo.getDirectory().getAbsolutePath());
-			env.put("LOCAL", localFilePath); //$NON-NLS-1$
-			env.put("REMOTE", remoteFilePath); //$NON-NLS-1$
-			env.put("MERGED", mergedFilePath); //$NON-NLS-1$
-			env.put("BASE", baseFilePath); //$NON-NLS-1$
+			// crate temp-directory or use working directory
+			tempDir = config.isWriteToTemp()
+					? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$
+					: workingDir;
+			// create additional backup file (copy worktree file)
+			backup = createBackupFile(mergedFilePath, tempDir);
+			// get local, remote and base file paths
+			String localFilePath = localFile.getFile(tempDir, "LOCAL") //$NON-NLS-1$
+					.getPath();
+			String remoteFilePath = remoteFile.getFile(tempDir, "REMOTE") //$NON-NLS-1$
+					.getPath();
+			String baseFilePath = ""; //$NON-NLS-1$
+			if (baseFile != null) {
+				baseFilePath = baseFile.getFile(tempDir, "BASE").getPath(); //$NON-NLS-1$
+			}
+			// prepare the command (replace the file paths)
 			boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
+			String command = prepareCommand(mergedFilePath, localFilePath,
+					remoteFilePath, baseFilePath,
+					tool.getCommand(baseFile != null));
+			// prepare the environment
+			Map<String, String> env = prepareEnvironment(repo, mergedFilePath,
+					localFilePath, remoteFilePath, baseFilePath);
 			CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
-			return cmdExec.run(command, workingDir, env);
+			result = cmdExec.run(command, workingDir, env);
+			// keep backup as .orig file
+			if (backup != null) {
+				keepBackupFile(mergedFilePath, backup);
+			}
+			return result;
 		} catch (Exception e) {
 			throw new ToolException(e);
 		} finally {
-			localFile.cleanTemporaries();
-			remoteFile.cleanTemporaries();
-			baseFile.cleanTemporaries();
+			// always delete backup file (ignore that it was may be already
+			// moved to keep-backup file)
+			if (backup != null) {
+				backup.cleanTemporaries();
+			}
+			// if the tool returns an error and keepTemporaries is set to true,
+			// then these temporary files will be preserved
+			if (!((result == null) && config.isKeepTemporaries())) {
+				// delete the files
+				localFile.cleanTemporaries();
+				remoteFile.cleanTemporaries();
+				if (baseFile != null) {
+					baseFile.cleanTemporaries();
+				}
+				// delete temporary directory if needed
+				if (config.isWriteToTemp() && (tempDir != null)
+						&& tempDir.exists()) {
+					tempDir.delete();
+				}
+			}
 		}
 	}
 
+	private static FileElement createBackupFile(String mergedFilePath,
+			File tempDir) throws IOException {
+		FileElement backup = null;
+		Path path = Paths.get(tempDir.getPath(), mergedFilePath);
+		if (Files.exists(path)) {
+			backup = new FileElement(mergedFilePath, "NOID", null); //$NON-NLS-1$
+			Files.copy(path, backup.getFile(tempDir, "BACKUP").toPath(), //$NON-NLS-1$
+					StandardCopyOption.REPLACE_EXISTING);
+		}
+		return backup;
+	}
+
 	/**
 	 * @return the tool names
 	 */
@@ -159,6 +208,38 @@ private ExternalMergeTool getTool(final String name) {
 		return tool;
 	}
 
+	private String prepareCommand(String mergedFilePath, String localFilePath,
+			String remoteFilePath, String baseFilePath, String command) {
+		command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$
+		command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$
+		command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$
+		command = command.replace("$BASE", baseFilePath); //$NON-NLS-1$
+		return command;
+	}
+
+	private Map<String, String> prepareEnvironment(Repository repo,
+			String mergedFilePath, String localFilePath, String remoteFilePath,
+			String baseFilePath) {
+		Map<String, String> env = new TreeMap<>();
+		env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath());
+		env.put("LOCAL", localFilePath); //$NON-NLS-1$
+		env.put("REMOTE", remoteFilePath); //$NON-NLS-1$
+		env.put("MERGED", mergedFilePath); //$NON-NLS-1$
+		env.put("BASE", baseFilePath); //$NON-NLS-1$
+		return env;
+	}
+
+	private void keepBackupFile(String mergedFilePath, FileElement backup)
+			throws IOException {
+		if (config.isKeepBackup()) {
+			Path backupPath = backup.getFile().toPath();
+			Files.move(backupPath,
+					backupPath.resolveSibling(
+							Paths.get(mergedFilePath).getFileName() + ".orig"), //$NON-NLS-1$
+					StandardCopyOption.REPLACE_EXISTING);
+		}
+	}
+
 	private Map<String, ExternalMergeTool> setupPredefinedTools() {
 		Map<String, ExternalMergeTool> tools = new TreeMap<>();
 		for (CommandLineMergeTool tool : CommandLineMergeTool.values()) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
index 7862cf5..1ae0780 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
@@ -26,6 +26,8 @@ public class ToolException extends Exception {
 
 	private final ExecutionResult result;
 
+	private final boolean commandExecutionError;
+
 	/**
 	 * the serial version UID
 	 */
@@ -35,8 +37,7 @@ public class ToolException extends Exception {
 	 *
 	 */
 	public ToolException() {
-		super();
-		result = null;
+		this(null, null, false);
 	}
 
 	/**
@@ -44,8 +45,7 @@ public ToolException() {
 	 *            the exception message
 	 */
 	public ToolException(String message) {
-		super(message);
-		result = null;
+		this(message, null, false);
 	}
 
 	/**
@@ -53,10 +53,14 @@ public ToolException(String message) {
 	 *            the exception message
 	 * @param result
 	 *            the execution result
+	 * @param commandExecutionError
+	 *            is command execution error happened ?
 	 */
-	public ToolException(String message, ExecutionResult result) {
+	public ToolException(String message, ExecutionResult result,
+			boolean commandExecutionError) {
 		super(message);
 		this.result = result;
+		this.commandExecutionError = commandExecutionError;
 	}
 
 	/**
@@ -68,6 +72,7 @@ public ToolException(String message, ExecutionResult result) {
 	public ToolException(String message, Throwable cause) {
 		super(message, cause);
 		result = null;
+		commandExecutionError = false;
 	}
 
 	/**
@@ -77,6 +82,7 @@ public ToolException(String message, Throwable cause) {
 	public ToolException(Throwable cause) {
 		super(cause);
 		result = null;
+		commandExecutionError = false;
 	}
 
 	/**
@@ -94,6 +100,13 @@ public ExecutionResult getResult() {
 	}
 
 	/**
+	 * @return true if command execution error appears, false otherwise
+	 */
+	public boolean isCommandExecutionError() {
+		return commandExecutionError;
+	}
+
+	/**
 	 * @return the result Stderr
 	 */
 	public String getResultStderr() {