Add command line support for "git mergetool"

see: https://git-scm.com/docs/git-mergetool
see: https://git-scm.com/docs/git-config

* add command line support for "git mergetool"
  * add option handling for "--tool-help", "--tool=<mytool>",
"--[no-]prompt",  "--[no-]gui"
  * handle prompt
  * add MergeTools
  * add pre-defined mergetools
  * print merge actions --> no execute, will be done later

Bug: 356832
Change-Id: I6e505ffc3d03f75ecf4bba452a25d25dfcf5793f
Signed-off-by: Andre Bossert <andre.bossert@siemens.com>
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 e2ff189..017a5d9 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
+ * Copyright (C) 2021-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
@@ -14,68 +14,30 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool;
-import org.eclipse.jgit.lib.CLIRepositoryTestCase;
 import org.eclipse.jgit.lib.StoredConfig;
-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.junit.Test;
-import org.kohsuke.args4j.Argument;
 
 /**
  * Testing the {@code difftool} command.
  */
-public class DiffToolTest extends CLIRepositoryTestCase {
-	public static class GitCliJGitWrapperParser {
-		@Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
-		TextBuiltin subcommand;
+public class DiffToolTest extends ExternalToolTestCase {
 
-		@Argument(index = 1, metaVar = "metaVar_arg")
-		List<String> arguments = new ArrayList<>();
-	}
-
-	private 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]);
-	}
-
-	private static final String TOOL_NAME = "some_tool";
-	private Git git;
+	private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION;
 
 	@Override
 	@Before
 	public void setUp() throws Exception {
 		super.setUp();
-		git = new Git(db);
-		git.commit().setMessage("initial commit").call();
 		configureEchoTool(TOOL_NAME);
 	}
 
@@ -83,7 +45,7 @@ public void setUp() throws Exception {
 	public void testNotDefinedTool() throws Exception {
 		createUnstagedChanges();
 
-		runAndCaptureUsingInitRaw("difftool", "--tool", "undefined");
+		runAndCaptureUsingInitRaw(DIFF_TOOL, "--tool", "undefined");
 		fail("Expected exception when trying to run undefined tool");
 	}
 
@@ -91,7 +53,7 @@ public void testNotDefinedTool() throws Exception {
 	public void testTool() throws Exception {
 		RevCommit commit = createUnstagedChanges();
 		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedDiffToolOutput(changes);
+		String[] expectedOutput = getExpectedToolOutput(changes);
 
 		String[] options = {
 				"--tool",
@@ -101,7 +63,7 @@ public void testTool() throws Exception {
 		for (String option : options) {
 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
 					expectedOutput,
-					runAndCaptureUsingInitRaw("difftool", option,
+					runAndCaptureUsingInitRaw(DIFF_TOOL, option,
 							TOOL_NAME));
 		}
 	}
@@ -110,13 +72,13 @@ public void testTool() throws Exception {
 	public void testToolTrustExitCode() throws Exception {
 		RevCommit commit = createUnstagedChanges();
 		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedDiffToolOutput(changes);
+		String[] expectedOutput = getExpectedToolOutput(changes);
 
 		String[] options = { "--tool", "-t", };
 
 		for (String option : options) {
 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
-					expectedOutput, runAndCaptureUsingInitRaw("difftool",
+					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
 							"--trust-exit-code", option, TOOL_NAME));
 		}
 	}
@@ -125,13 +87,13 @@ expectedOutput, runAndCaptureUsingInitRaw("difftool",
 	public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
 		RevCommit commit = createUnstagedChanges();
 		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedDiffToolOutput(changes);
+		String[] expectedOutput = getExpectedToolOutput(changes);
 
 		String[] options = { "--tool", "-t", };
 
 		for (String option : options) {
 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
-					expectedOutput, runAndCaptureUsingInitRaw("difftool",
+					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
 							"--no-gui", "--no-prompt", "--no-trust-exit-code",
 							option, TOOL_NAME));
 		}
@@ -141,13 +103,13 @@ expectedOutput, runAndCaptureUsingInitRaw("difftool",
 	public void testToolCached() throws Exception {
 		RevCommit commit = createStagedChanges();
 		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedDiffToolOutput(changes);
+		String[] expectedOutput = getExpectedToolOutput(changes);
 
 		String[] options = { "--cached", "--staged", };
 
 		for (String option : options) {
 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
-					expectedOutput, runAndCaptureUsingInitRaw("difftool",
+					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
 							option, "--tool", TOOL_NAME));
 		}
 	}
@@ -174,7 +136,8 @@ public void testToolHelp() throws Exception {
 
 		String option = "--tool-help";
 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
-				expectedOutput.toArray(new String[0]), runAndCaptureUsingInitRaw("difftool", option));
+				expectedOutput.toArray(new String[0]),
+				runAndCaptureUsingInitRaw(DIFF_TOOL, option));
 	}
 
 	private void configureEchoTool(String toolName) {
@@ -196,33 +159,7 @@ private void configureEchoTool(String toolName) {
 				String.valueOf(false));
 	}
 
-	private 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;
-	}
-
-	private RevCommit createStagedChanges() throws Exception {
-		RevCommit commit = createUnstagedChanges();
-		git.add().addFilepattern(".").call();
-		return commit;
-	}
-
-	private 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;
-	}
-
-	private String[] getExpectedDiffToolOutput(List<DiffEntry> changes) {
+	private String[] getExpectedToolOutput(List<DiffEntry> changes) {
 		String[] expectedToolOutput = new String[changes.size()];
 		for (int i = 0; i < changes.size(); ++i) {
 			DiffEntry change = changes.get(i);
@@ -232,17 +169,4 @@ private List<DiffEntry> getRepositoryChanges(RevCommit commit)
 		}
 		return expectedToolOutput;
 	}
-
-	private static void assertArrayOfLinesEquals(String failMessage,
-			String[] expected, String[] actual) {
-		assertEquals(failMessage, toString(expected), toString(actual));
-	}
-
-	private 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/ExternalToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java
new file mode 100644
index 0000000..e10b13e
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java
@@ -0,0 +1,136 @@
+/*
+ * 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
new file mode 100644
index 0000000..32cd604
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jgit.internal.diffmergetool.CommandLineMergeTool;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Testing the {@code mergetool} command.
+ */
+public class MergeToolTest extends ExternalToolTestCase {
+
+	private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		configureEchoTool(TOOL_NAME);
+	}
+
+	@Test
+	public void testTool() throws Exception {
+		createMergeConflict();
+		String[] expectedOutput = getExpectedToolOutput();
+
+		String[] options = {
+				"--tool",
+				"-t",
+		};
+
+		for (String option : options) {
+			assertArrayOfLinesEquals("Incorrect output for option: " + option,
+					expectedOutput,
+					runAndCaptureUsingInitRaw(MERGE_TOOL, option,
+							TOOL_NAME));
+		}
+	}
+
+	@Test
+	public void testToolNoGuiNoPrompt() throws Exception {
+		createMergeConflict();
+		String[] expectedOutput = getExpectedToolOutput();
+
+		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));
+		}
+	}
+
+	@Test
+	public void testToolHelp() throws Exception {
+		CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
+		List<String> expectedOutput = new ArrayList<>();
+		expectedOutput.add(
+				"'git mergetool --tool=<tool>' may be set to one of the following:");
+		for (CommandLineMergeTool defaultTool : defaultTools) {
+			String toolName = defaultTool.name();
+			expectedOutput.add(toolName);
+		}
+		String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
+				+ getEchoCommand();
+		expectedOutput.add("user-defined:");
+		expectedOutput.add(customToolHelpLine);
+		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.",
+		};
+		expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));
+
+		String option = "--tool-help";
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput.toArray(new String[0]),
+				runAndCaptureUsingInitRaw(MERGE_TOOL, option));
+	}
+
+	private void configureEchoTool(String toolName) {
+		StoredConfig config = db.getConfig();
+		// the default merge tool is configured without a subsection
+		String subsection = null;
+		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
+				toolName);
+
+		String command = getEchoCommand();
+
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+		/*
+		 * prevent prompts as we are running in tests and there is no user to
+		 * interact with on the command line
+		 */
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PROMPT,
+				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);
+		}
+		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 + "'...");
+		}
+		return expectedOutput.toArray(new String[0]);
+	}
+}
diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
index 8c44764..ea1d1e3 100644
--- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
+++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
@@ -25,6 +25,7 @@
 org.eclipse.jgit.pgm.LsTree
 org.eclipse.jgit.pgm.Merge
 org.eclipse.jgit.pgm.MergeBase
+org.eclipse.jgit.pgm.MergeTool
 org.eclipse.jgit.pgm.Push
 org.eclipse.jgit.pgm.ReceivePack
 org.eclipse.jgit.pgm.Reflog
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 3653b9d..8e2eef7 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
@@ -255,6 +255,7 @@
 usage_Gc=Cleanup unnecessary files and optimize the local repository
 usage_Glog=View commit history as a graph
 usage_DiffGuiTool=When git-difftool is invoked with the -g or --gui option the default diff tool will be read from the configured diff.guitool variable instead of diff.tool.
+usage_MergeGuiTool=When git-mergetool is invoked with the -g or --gui option the default merge tool will be read from the configured merge.guitool variable instead of merge.tool.
 usage_noGui=The --no-gui option can be used to override -g or --gui setting.
 usage_IndexPack=Build pack index file for an existing packed archive
 usage_LFSDirectory=Directory to store large objects
@@ -303,6 +304,7 @@
 usage_StopTrackingAFile=Stop tracking a file
 usage_TextHashFunctions=Scan repository to compute maximum number of collisions for hash functions
 usage_ToolForDiff=Use the diff tool specified by <tool>. Run git difftool --tool-help for the list of valid <tool> settings.\nIf a diff tool is not specified, git difftool will use the configuration variable diff.tool.
+usage_ToolForMerge=Use the merge resolution program specified by <tool>. Run git mergetool --tool-help for the list of valid <tool> settings.\nIf a merge resolution program is not specified, git mergetool will use the configuration variable merge.tool.
 usage_UpdateRemoteRepositoryFromLocalRefs=Update remote repository from local refs
 usage_UseAll=Use all refs found in refs/
 usage_UseTags=Use any tag including lightweight tags
@@ -350,6 +352,7 @@
 usage_detectRenames=detect renamed files
 usage_diffAlgorithm=the diff algorithm to use. Currently supported are: 'myers', 'histogram'
 usage_DiffTool=git difftool is a Git command that allows you to compare and edit files between revisions using common diff tools.\ngit difftool is a frontend to git diff and accepts the same options and arguments.
+usage_MergeTool=git-mergetool - Run merge conflict resolution tools to resolve merge conflicts.\nUse git mergetool to run one of several merge utilities to resolve merge conflicts. It is typically run after git merge.
 usage_directoriesToExport=directories to export
 usage_disableTheServiceInAllRepositories=disable the service in all repositories
 usage_displayAListOfAllRegisteredJgitCommands=Display a list of all registered jgit commands
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 2f74177..2e90d52 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -192,7 +192,7 @@ private void compare(List<DiffEntry> files, boolean showPrompt,
 						outw.flush();
 						errw.println(e.getMessage());
 						throw die(MessageFormat.format(
-								CLIText.get().diffToolDied, mergedFilePath, e));
+								CLIText.get().diffToolDied, mergedFilePath), e);
 					}
 				} else {
 					break;
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
new file mode 100644
index 0000000..37afa54
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * 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 java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+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.errors.NoWorkTreeException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.internal.diffmergetool.MergeTools;
+import org.eclipse.jgit.lib.IndexDiff.StageState;
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
+
+@Command(name = "mergetool", common = true, usage = "usage_MergeTool")
+class MergeTool extends TextBuiltin {
+	private MergeTools mergeTools;
+
+	@Option(name = "--tool", aliases = {
+			"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge")
+	private String toolName;
+
+	private Optional<Boolean> prompt = Optional.empty();
+
+	@Option(name = "--prompt", usage = "usage_prompt")
+	void setPrompt(@SuppressWarnings("unused") boolean on) {
+		prompt = Optional.of(Boolean.TRUE);
+	}
+
+	@Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt")
+	void noPrompt(@SuppressWarnings("unused") boolean on) {
+		prompt = Optional.of(Boolean.FALSE);
+	}
+
+	@Option(name = "--tool-help", usage = "usage_toolHelp")
+	private boolean toolHelp;
+
+	private BooleanTriState gui = BooleanTriState.UNSET;
+
+	@Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool")
+	void setGui(@SuppressWarnings("unused") boolean on) {
+		gui = BooleanTriState.TRUE;
+	}
+
+	@Option(name = "--no-gui", usage = "usage_noGui")
+	void noGui(@SuppressWarnings("unused") boolean on) {
+		gui = BooleanTriState.FALSE;
+	}
+
+	@Argument(required = false, index = 0, metaVar = "metaVar_paths")
+	@Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class)
+	protected List<String> filterPaths;
+
+	@Override
+	protected void init(Repository repository, String gitDir) {
+		super.init(repository, gitDir);
+		mergeTools = new MergeTools(repository);
+	}
+
+	@Override
+	protected void run() {
+		try {
+			if (toolHelp) {
+				showToolHelp();
+			} else {
+				// get prompt
+				boolean showPrompt = mergeTools.isInteractive();
+				if (prompt.isPresent()) {
+					showPrompt = prompt.get().booleanValue();
+				}
+				// get passed or default tool name
+				String toolNameSelected = toolName;
+				if ((toolNameSelected == null) || toolNameSelected.isEmpty()) {
+					toolNameSelected = mergeTools.getDefaultToolName(gui);
+				}
+				// get the changed files
+				Map<String, StageState> files = getFiles();
+				if (files.size() > 0) {
+					merge(files, showPrompt, toolNameSelected);
+				} else {
+					outw.println("No files need merging"); //$NON-NLS-1$
+				}
+			}
+			outw.flush();
+		} catch (Exception e) {
+			throw die(e.getMessage(), e);
+		}
+	}
+
+	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);
+		// show the files
+		outw.println("Merging:"); //$NON-NLS-1$
+		for (String fileName : fileNames) {
+			outw.println(fileName);
+		}
+		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);
+				}
+				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$
+				break;
+			}
+		}
+	}
+
+	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$
+		outw.flush();
+		BufferedReader br = new BufferedReader(new InputStreamReader(ins));
+		String line = null;
+		if ((line = br.readLine()) != null) {
+			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$
+		}
+		outw.println(""); //$NON-NLS-1$
+		outw.println("\tuser-defined:"); //$NON-NLS-1$
+		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());
+		}
+		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;
+	}
+
+	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();
+			if (filterPaths != null && filterPaths.size() > 0) {
+				for (String path : filterPaths) {
+					statusCommand.addPath(path);
+				}
+			}
+			Status status = statusCommand.call();
+			files = status.getConflictingStageState();
+		}
+		return files;
+	}
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java
index 96fd102..1dea44e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java
@@ -9,13 +9,27 @@
  */
 package org.eclipse.jgit.internal.diffmergetool;
 
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Map;
 import java.util.Set;
 
 import org.eclipse.jgit.lib.internal.BooleanTriState;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS.ExecutionResult;
 import org.junit.Test;
 
 /**
@@ -23,12 +37,60 @@
  */
 public class ExternalMergeToolTest extends ExternalToolTestCase {
 
+	@Test(expected = ToolException.class)
+	public void testUserToolWithError() throws Exception {
+		String toolName = "customTool";
+
+		int errorReturnCode = 1;
+		String command = "exit " + errorReturnCode;
+
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName,
+				CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(Boolean.TRUE));
+
+		MergeTools manager = new MergeTools(db);
+
+		BooleanTriState prompt = BooleanTriState.UNSET;
+		BooleanTriState gui = BooleanTriState.UNSET;
+
+		manager.merge(db, local, remote, base, merged.getPath(), toolName,
+				prompt, gui);
+
+		fail("Expected exception to be thrown due to external tool exiting with error code: "
+				+ errorReturnCode);
+	}
+
+	@Test(expected = ToolException.class)
+	public void testUserToolWithCommandNotFoundError() throws Exception {
+		String toolName = "customTool";
+
+		int errorReturnCode = 127; // command not found
+		String command = "exit " + errorReturnCode;
+
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+
+		MergeTools manager = new MergeTools(db);
+
+		BooleanTriState prompt = BooleanTriState.UNSET;
+		BooleanTriState gui = BooleanTriState.UNSET;
+
+		manager.merge(db, local, remote, base, merged.getPath(), toolName,
+				prompt, gui);
+
+		fail("Expected exception to be thrown due to external tool exiting with error code: "
+				+ errorReturnCode);
+	}
+
 	@Test
 	public void testToolNames() {
 		MergeTools manager = new MergeTools(db);
 		Set<String> actualToolNames = manager.getToolNames();
 		Set<String> expectedToolNames = Collections.emptySet();
-		assertEquals("Incorrect set of external diff tool names",
+		assertEquals("Incorrect set of external merge tool names",
 				expectedToolNames, actualToolNames);
 	}
 
@@ -36,18 +98,58 @@ public void testToolNames() {
 	public void testAllTools() {
 		MergeTools manager = new MergeTools(db);
 		Set<String> actualToolNames = manager.getAvailableTools().keySet();
-		Set<String> expectedToolNames = Collections.emptySet();
-		assertEquals("Incorrect set of available external diff tools",
-				expectedToolNames, actualToolNames);
+		Set<String> expectedToolNames = new LinkedHashSet<>();
+		CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
+		for (CommandLineMergeTool defaultTool : defaultTools) {
+			String toolName = defaultTool.name();
+			expectedToolNames.add(toolName);
+		}
+		assertEquals("Incorrect set of external merge tools", expectedToolNames,
+				actualToolNames);
+	}
+
+	@Test
+	public void testOverridePredefinedToolPath() {
+		String toolName = CommandLineMergeTool.guiffy.name();
+		String customToolPath = "/usr/bin/echo";
+
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				"echo");
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PATH,
+				customToolPath);
+
+		MergeTools manager = new MergeTools(db);
+		Map<String, ExternalMergeTool> tools = manager.getUserDefinedTools();
+		ExternalMergeTool mergeTool = tools.get(toolName);
+		assertNotNull("Expected tool \"" + toolName + "\" to be user defined",
+				mergeTool);
+
+		String toolPath = mergeTool.getPath();
+		assertEquals("Expected external merge tool to have an overriden path",
+				customToolPath, toolPath);
 	}
 
 	@Test
 	public void testUserDefinedTools() {
+		FileBasedConfig config = db.getConfig();
+		String customToolname = "customTool";
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
+				CONFIG_KEY_CMD, "echo");
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
+				CONFIG_KEY_PATH, "/usr/bin/echo");
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
+				CONFIG_KEY_PROMPT, String.valueOf(false));
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
+				CONFIG_KEY_GUITOOL, String.valueOf(false));
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
+				CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false));
 		MergeTools manager = new MergeTools(db);
 		Set<String> actualToolNames = manager.getUserDefinedTools().keySet();
-		Set<String> expectedToolNames = Collections.emptySet();
-		assertEquals("Incorrect set of user defined external diff tools",
-				expectedToolNames, actualToolNames);
+		Set<String> expectedToolNames = new LinkedHashSet<>();
+		expectedToolNames.add(customToolname);
+		assertEquals("Incorrect set of external merge tools", expectedToolNames,
+				actualToolNames);
 	}
 
 	@Test
@@ -55,55 +157,118 @@ public void testNotAvailableTools() {
 		MergeTools manager = new MergeTools(db);
 		Set<String> actualToolNames = manager.getNotAvailableTools().keySet();
 		Set<String> expectedToolNames = Collections.emptySet();
-		assertEquals("Incorrect set of not available external diff tools",
+		assertEquals("Incorrect set of not available external merge tools",
 				expectedToolNames, actualToolNames);
 	}
 
 	@Test
 	public void testCompare() throws ToolException {
-		MergeTools manager = new MergeTools(db);
+		String toolName = "customTool";
 
-		String newPath = "";
-		String oldPath = "";
-		String newId = "";
-		String oldId = "";
-		String toolName = "";
+		FileBasedConfig config = db.getConfig();
+		// the default merge tool is configured without a subsection
+		String subsection = null;
+		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
+				toolName);
+
+		String command = getEchoCommand();
+
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+
 		BooleanTriState prompt = BooleanTriState.UNSET;
 		BooleanTriState gui = BooleanTriState.UNSET;
-		BooleanTriState trustExitCode = BooleanTriState.UNSET;
+
+		MergeTools manager = new MergeTools(db);
 
 		int expectedCompareResult = 0;
-		int compareResult = manager.merge(newPath, oldPath, newId, oldId,
-				toolName, prompt, gui, trustExitCode);
-		assertEquals("Incorrect compare result for external diff tool",
-				expectedCompareResult, compareResult);
+		ExecutionResult compareResult = manager.merge(db, local, remote, base,
+				merged.getPath(), toolName, prompt, gui);
+		assertEquals("Incorrect compare result for external merge tool",
+				expectedCompareResult, compareResult.getRc());
 	}
 
 	@Test
 	public void testDefaultTool() throws Exception {
+		String toolName = "customTool";
+		String guiToolName = "customGuiTool";
+
 		FileBasedConfig config = db.getConfig();
-		// the default diff tool is configured without a subsection
+		// the default merge tool is configured without a subsection
 		String subsection = null;
-		config.setString("diff", subsection, "tool", "customTool");
+		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
+				toolName);
 
 		MergeTools manager = new MergeTools(db);
 		BooleanTriState gui = BooleanTriState.UNSET;
 		String defaultToolName = manager.getDefaultToolName(gui);
 		assertEquals(
-				"Expected configured difftool to be the default external diff tool",
-				"my_default_toolname", defaultToolName);
+				"Expected configured mergetool to be the default external merge tool",
+				toolName, defaultToolName);
 
 		gui = BooleanTriState.TRUE;
 		String defaultGuiToolName = manager.getDefaultToolName(gui);
 		assertEquals(
-				"Expected configured difftool to be the default external diff tool",
+				"Expected configured mergetool to be the default external merge tool",
 				"my_gui_tool", defaultGuiToolName);
 
-		config.setString("diff", subsection, "guitool", "customGuiTool");
+		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL,
+				guiToolName);
 		manager = new MergeTools(db);
 		defaultGuiToolName = manager.getDefaultToolName(gui);
 		assertEquals(
-				"Expected configured difftool to be the default external diff guitool",
+				"Expected configured mergetool to be the default external merge guitool",
 				"my_gui_tool", defaultGuiToolName);
 	}
+
+	@Test
+	public void testOverridePreDefinedToolPath() {
+		String newToolPath = "/tmp/path/";
+
+		CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
+		assertTrue("Expected to find pre-defined external merge tools",
+				defaultTools.length > 0);
+
+		CommandLineMergeTool overridenTool = defaultTools[0];
+		String overridenToolName = overridenTool.name();
+		String overridenToolPath = newToolPath + overridenToolName;
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, overridenToolName,
+				CONFIG_KEY_PATH, overridenToolPath);
+
+		MergeTools manager = new MergeTools(db);
+		Map<String, ExternalMergeTool> availableTools = manager
+				.getAvailableTools();
+		ExternalMergeTool externalMergeTool = availableTools
+				.get(overridenToolName);
+		String actualMergeToolPath = externalMergeTool.getPath();
+		assertEquals(
+				"Expected pre-defined external merge tool to have overriden path",
+				overridenToolPath, actualMergeToolPath);
+		boolean withBase = true;
+		String expectedMergeToolCommand = overridenToolPath + " "
+				+ overridenTool.getParameters(withBase);
+		String actualMergeToolCommand = externalMergeTool.getCommand();
+		assertEquals(
+				"Expected pre-defined external merge tool to have overriden command",
+				expectedMergeToolCommand, actualMergeToolCommand);
+	}
+
+	@Test(expected = ToolException.class)
+	public void testUndefinedTool() throws Exception {
+		MergeTools manager = new MergeTools(db);
+
+		String toolName = "undefined";
+		BooleanTriState prompt = BooleanTriState.UNSET;
+		BooleanTriState gui = BooleanTriState.UNSET;
+
+		manager.merge(db, local, remote, base, merged.getPath(), toolName,
+				prompt, gui);
+		fail("Expected exception to be thrown due to not defined external merge tool");
+	}
+
+	private String getEchoCommand() {
+		return "(echo \"$LOCAL\" \"$REMOTE\") > "
+				+ commandResult.getAbsolutePath();
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java
new file mode 100644
index 0000000..3a22124
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * 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.internal.diffmergetool;
+
+/**
+ * Pre-defined merge tools.
+ *
+ * Adds same merge tools as also pre-defined in C-Git see "git-core\mergetools\"
+ * see links to command line parameter description for the tools
+ *
+ * <pre>
+ * araxis
+ * bc
+ * bc3
+ * codecompare
+ * deltawalker
+ * diffmerge
+ * diffuse
+ * ecmerge
+ * emerge
+ * examdiff
+ * guiffy
+ * gvimdiff
+ * gvimdiff2
+ * gvimdiff3
+ * kdiff3
+ * kompare
+ * meld
+ * opendiff
+ * p4merge
+ * tkdiff
+ * tortoisemerge
+ * vimdiff
+ * vimdiff2
+ * vimdiff3
+ * winmerge
+ * xxdiff
+ * </pre>
+ *
+ */
+@SuppressWarnings("nls")
+public enum CommandLineMergeTool {
+	/**
+	 * See: <a href=
+	 * "https://www.araxis.com/merge/documentation-windows/command-line.en">https://www.araxis.com/merge/documentation-windows/command-line.en</a>
+	 */
+	araxis("compare",
+			"-wait -merge -3 -a1 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+			"-wait -2 \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a>
+	 */
+	bc("bcomp", "\"$LOCAL\" \"$REMOTE\" \"$BASE\" --mergeoutput=\"$MERGED\"",
+			"\"$LOCAL\" \"$REMOTE\" --mergeoutput=\"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a>
+	 */
+	bc3("bcompare", bc),
+	/**
+	 * See: <a href=
+	 * "https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm">https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm</a>
+	 */
+	codecompare("CodeMerge",
+			"-MF=\"$LOCAL\" -TF=\"$REMOTE\" -BF=\"$BASE\" -RF=\"$MERGED\"",
+			"-MF=\"$LOCAL\" -TF=\"$REMOTE\" -RF=\"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.deltawalker.com/integrate/command-line">https://www.deltawalker.com/integrate/command-line</a>
+	 * <p>
+	 * Hint: $(pwd) command must be defined
+	 * </p>
+	 */
+	deltawalker("DeltaWalker",
+			"\"$LOCAL\" \"$REMOTE\" \"$BASE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"",
+			"\"$LOCAL\" \"$REMOTE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html">https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html</a>
+	 */
+	diffmerge("diffmerge", //$NON-NLS-1$
+			"--merge --result=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"",
+			"--merge --result=\"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "http://diffuse.sourceforge.net/manual.html#introduction-usage">http://diffuse.sourceforge.net/manual.html#introduction-usage</a>
+	 * <p>
+	 * Hint: check the ' | cat' for the call
+	 * </p>
+	 */
+	diffuse("diffuse", "\"$LOCAL\" \"$MERGED\" \"$REMOTE\" \"$BASE\"",
+			"\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", false),
+	/**
+	 * See: <a href=
+	 * "http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp">http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp</a>
+	 */
+	ecmerge("ecmerge",
+			"--default --mode=merge3 \"$BASE\" \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"",
+			"--default --mode=merge2 \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html">https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html</a>
+	 * <p>
+	 * Hint: $(basename) command must be defined
+	 * </p>
+	 */
+	emerge("emacs",
+			"-f emerge-files-with-ancestor-command \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$(basename \"$MERGED\")\"",
+			"-f emerge-files-command \"$LOCAL\" \"$REMOTE\" \"$(basename \"$MERGED\")\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options">https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options</a>
+	 */
+	examdiff("ExamDiff",
+			"-merge \"$LOCAL\" \"$BASE\" \"$REMOTE\" -o:\"$MERGED\" -nh",
+			"-merge \"$LOCAL\" \"$REMOTE\" -o:\"$MERGED\" -nh",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html">https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html</a>
+	 */
+	guiffy("guiffy", "-s \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"",
+			"-m \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true),
+	/**
+	 * See: <a href=
+	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+	 */
+	gvimdiff("gvim",
+			"-f -d -c '4wincmd w | wincmd J' \"$LOCAL\" \"$BASE\" \"$REMOTE\" \"$MERGED\"",
+			"-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+	 */
+	gvimdiff2("gvim", "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"",
+			"-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", true),
+	/**
+	 * See: <a href= "http://vimdoc.sourceforge.net/htmldoc/diff.html"></a>
+	 */
+	gvimdiff3("gvim",
+			"-f -d -c 'hid | hid | hid' \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"",
+			"-f -d -c 'hid | hid' \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true),
+	/**
+	 * See: <a href=
+	 * "http://kdiff3.sourceforge.net/doc/documentation.html">http://kdiff3.sourceforge.net/doc/documentation.html</a>
+	 */
+	kdiff3("kdiff3",
+			"--auto --L1 \"$MERGED (Base)\" --L2 \"$MERGED (Local)\" --L3 \"$MERGED (Remote)\" -o \"$MERGED\" \"$BASE\" \"$LOCAL\" \"$REMOTE\"",
+			"--auto --L1 \"$MERGED (Local)\" --L2 \"$MERGED (Remote)\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "http://meldmerge.org/help/file-mode.html">http://meldmerge.org/help/file-mode.html</a>
+	 * <p>
+	 * Hint: use meld with output option only (new versions)
+	 * </p>
+	 */
+	meld("meld", "--output=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"",
+			"\"$LOCAL\" \"$MERGED\" \"$REMOTE\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "http://www.manpagez.com/man/1/opendiff/">http://www.manpagez.com/man/1/opendiff/</a>
+	 * <p>
+	 * Hint: check the ' | cat' for the call
+	 * </p>
+	 */
+	opendiff("opendiff",
+			"\"$LOCAL\" \"$REMOTE\" -ancestor \"$BASE\" -merge \"$MERGED\"",
+			"\"$LOCAL\" \"$REMOTE\" -merge \"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html">https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html</a>
+	 * <p>
+	 * Hint: check how to fix "no base present" / create_virtual_base problem
+	 * </p>
+	 */
+	p4merge("p4merge", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"",
+			"\"$REMOTE\" \"$LOCAL\" \"$MERGED\"", false),
+	/**
+	 * See: <a href=
+	 * "http://linux.math.tifr.res.in/manuals/man/tkdiff.html">http://linux.math.tifr.res.in/manuals/man/tkdiff.html</a>
+	 */
+	tkdiff("tkdiff", "-a \"$BASE\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+			"-o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics">https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics</a>
+	 * <p>
+	 * Hint: merge without base is not supported
+	 * </p>
+	 * <p>
+	 * Hint: cannot diff
+	 * </p>
+	 */
+	tortoisegitmerge("tortoisegitmerge",
+			"-base \"$BASE\" -mine \"$LOCAL\" -theirs \"$REMOTE\" -merged \"$MERGED\"",
+			null, false),
+	/**
+	 * See: <a href=
+	 * "https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics">https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics</a>
+	 * <p>
+	 * Hint: merge without base is not supported
+	 * </p>
+	 * <p>
+	 * Hint: cannot diff
+	 * </p>
+	 */
+	tortoisemerge("tortoisemerge",
+			"-base:\"$BASE\" -mine:\"$LOCAL\" -theirs:\"$REMOTE\" -merged:\"$MERGED\"",
+			null, false),
+	/**
+	 * See: <a href=
+	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+	 */
+	vimdiff("vim", gvimdiff),
+	/**
+	 * See: <a href=
+	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+	 */
+	vimdiff2("vim", gvimdiff2),
+	/**
+	 * See: <a href=
+	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+	 */
+	vimdiff3("vim", gvimdiff3),
+	/**
+	 * See: <a href=
+	 * "http://manual.winmerge.org/Command_line.html">http://manual.winmerge.org/Command_line.html</a>
+	 * <p>
+	 * Hint: check how 'mergetool_find_win32_cmd "WinMergeU.exe" "WinMerge"'
+	 * works
+	 * </p>
+	 */
+	winmerge("WinMergeU",
+			"-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+			"-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "http://furius.ca/xxdiff/doc/xxdiff-doc.html">http://furius.ca/xxdiff/doc/xxdiff-doc.html</a>
+	 */
+	xxdiff("xxdiff",
+			"-X --show-merged-pane -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"",
+			"-X -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+			false);
+
+	CommandLineMergeTool(String path, String parametersWithBase,
+			String parametersWithoutBase,
+			boolean exitCodeTrustable) {
+		this.path = path;
+		this.parametersWithBase = parametersWithBase;
+		this.parametersWithoutBase = parametersWithoutBase;
+		this.exitCodeTrustable = exitCodeTrustable;
+    }
+
+	CommandLineMergeTool(CommandLineMergeTool from) {
+		this(from.getPath(), from.getParameters(true),
+				from.getParameters(false), from.isExitCodeTrustable());
+	}
+
+	CommandLineMergeTool(String path, CommandLineMergeTool from) {
+		this(path, from.getParameters(true), from.getParameters(false),
+				from.isExitCodeTrustable());
+	}
+
+	private final String path;
+
+	private final String parametersWithBase;
+
+	private final String parametersWithoutBase;
+
+	private final boolean exitCodeTrustable;
+
+	/**
+	 * @return path
+	 */
+	public String getPath() {
+		return path;
+	}
+
+	/**
+	 * @param withBase
+	 *            return parameters with base present?
+	 * @return parameters with or without base present
+	 */
+	public String getParameters(boolean withBase) {
+		if (withBase) {
+			return parametersWithBase;
+		}
+		return parametersWithoutBase;
+	}
+
+	/**
+	 * @return parameters
+	 */
+	public boolean isExitCodeTrustable() {
+		return exitCodeTrustable;
+	}
+
+	/**
+	 * @return true if command with base present is valid, false otherwise
+	 */
+	public boolean canMergeWithoutBasePresent() {
+		return parametersWithoutBase != null;
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
index b15cbdc..2f2b9de 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -56,8 +56,7 @@ public DiffTools(Repository repo) {
 	 * @param remoteFile
 	 *            the remote file element
 	 * @param mergedFilePath
-	 *            the path of 'merged' file, it equals local or remote path for
-	 *            difftool
+	 *            the path of 'merged' file, it equals local or remote path
 	 * @param toolName
 	 *            the selected tool name (can be null)
 	 * @param prompt
@@ -66,7 +65,7 @@ public DiffTools(Repository repo) {
 	 *            the GUI option
 	 * @param trustExitCode
 	 *            the "trust exit code" option
-	 * @return the return code from executed tool
+	 * @return the execution result from tool
 	 * @throws ToolException
 	 */
 	public ExecutionResult compare(Repository repo, FileElement localFile,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java
index bcc749a..0c3ddf9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java
@@ -10,6 +10,8 @@
 
 package org.eclipse.jgit.internal.diffmergetool;
 
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+
 /**
  * The merge tool interface.
  */
@@ -18,6 +20,14 @@ public interface ExternalMergeTool extends ExternalDiffTool {
 	/**
 	 * @return the tool "trust exit code" option
 	 */
-	boolean isTrustExitCode();
+	BooleanTriState getTrustExitCode();
+
+	/**
+	 * @param withBase
+	 *            get command with base present (true) or without base present
+	 *            (false)
+	 * @return the tool command
+	 */
+	String getCommand(boolean withBase);
 
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java
index e912822..9be20b7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java
@@ -10,13 +10,24 @@
 
 package org.eclipse.jgit.internal.diffmergetool;
 
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_BACKUP;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_TEMPORARIES;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WRITE_TO_TEMP;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
+
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Config.SectionParser;
-import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
 
 /**
@@ -42,31 +53,27 @@ public class MergeToolConfig {
 	private final Map<String, ExternalMergeTool> tools;
 
 	private MergeToolConfig(Config rc) {
-		toolName = rc.getString(ConfigConstants.CONFIG_MERGE_SECTION, null,
-				ConfigConstants.CONFIG_KEY_TOOL);
-		guiToolName = rc.getString(ConfigConstants.CONFIG_MERGE_SECTION, null,
-				ConfigConstants.CONFIG_KEY_GUITOOL);
-		prompt = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION,
-				ConfigConstants.CONFIG_KEY_PROMPT, true);
-		keepBackup = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION,
-				ConfigConstants.CONFIG_KEY_KEEP_BACKUP, true);
-		keepTemporaries = rc.getBoolean(
-				ConfigConstants.CONFIG_MERGETOOL_SECTION,
-				ConfigConstants.CONFIG_KEY_KEEP_TEMPORARIES, false);
-		writeToTemp = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION,
-				ConfigConstants.CONFIG_KEY_WRITE_TO_TEMP, false);
+		toolName = rc.getString(CONFIG_MERGE_SECTION, null, CONFIG_KEY_TOOL);
+		guiToolName = rc.getString(CONFIG_MERGE_SECTION, null,
+				CONFIG_KEY_GUITOOL);
+		prompt = rc.getBoolean(CONFIG_MERGETOOL_SECTION, toolName,
+				CONFIG_KEY_PROMPT, true);
+		keepBackup = rc.getBoolean(CONFIG_MERGETOOL_SECTION,
+				CONFIG_KEY_KEEP_BACKUP, true);
+		keepTemporaries = rc.getBoolean(CONFIG_MERGETOOL_SECTION,
+				CONFIG_KEY_KEEP_TEMPORARIES, false);
+		writeToTemp = rc.getBoolean(CONFIG_MERGETOOL_SECTION,
+				CONFIG_KEY_WRITE_TO_TEMP, false);
 		tools = new HashMap<>();
-		Set<String> subsections = rc
-				.getSubsections(ConfigConstants.CONFIG_MERGETOOL_SECTION);
+		Set<String> subsections = rc.getSubsections(CONFIG_MERGETOOL_SECTION);
 		for (String name : subsections) {
-			String cmd = rc.getString(ConfigConstants.CONFIG_MERGETOOL_SECTION,
-					name, ConfigConstants.CONFIG_KEY_CMD);
-			String path = rc.getString(ConfigConstants.CONFIG_MERGETOOL_SECTION,
-					name, ConfigConstants.CONFIG_KEY_PATH);
+			String cmd = rc.getString(CONFIG_MERGETOOL_SECTION, name,
+					CONFIG_KEY_CMD);
+			String path = rc.getString(CONFIG_MERGETOOL_SECTION, name,
+					CONFIG_KEY_PATH);
 			BooleanTriState trustExitCode = BooleanTriState.FALSE;
-			String trustStr = rc.getString(
-					ConfigConstants.CONFIG_MERGETOOL_SECTION, name,
-					ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE);
+			String trustStr = rc.getString(CONFIG_MERGETOOL_SECTION, name,
+					CONFIG_KEY_TRUST_EXIT_CODE);
 			if (trustStr != null) {
 				trustExitCode = Boolean.valueOf(trustStr).booleanValue()
 						? BooleanTriState.TRUE
@@ -75,9 +82,8 @@ private MergeToolConfig(Config rc) {
 				trustExitCode = BooleanTriState.UNSET;
 			}
 			if ((cmd != null) || (path != null)) {
-				tools.put(name,
-						new UserDefinedMergeTool(name, path, cmd,
-								trustExitCode));
+				tools.put(name, new UserDefinedMergeTool(name, path, cmd,
+						trustExitCode));
 			}
 		}
 	}
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 bb5d73e..cefefb8 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
@@ -9,17 +9,21 @@
  */
 package org.eclipse.jgit.internal.diffmergetool;
 
+import java.io.File;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.util.FS.ExecutionResult;
 
 /**
  * Manages merge tools.
  */
 public class MergeTools {
+
 	private final MergeToolConfig config;
 
 	private final Map<String, ExternalMergeTool> predefinedTools;
@@ -33,10 +37,12 @@ public class MergeTools {
 	public MergeTools(Repository repo) {
 		config = repo.getConfig().get(MergeToolConfig.KEY);
 		predefinedTools = setupPredefinedTools();
-		userDefinedTools = setupUserDefinedTools();
+		userDefinedTools = setupUserDefinedTools(config, predefinedTools);
 	}
 
 	/**
+	 * @param repo
+	 *            the repository
 	 * @param localFile
 	 *            the local file element
 	 * @param remoteFile
@@ -49,19 +55,43 @@ public MergeTools(Repository repo) {
 	 *            the selected tool name (can be null)
 	 * @param prompt
 	 *            the prompt option
-	 * @param trustExitCode
-	 *            the "trust exit code" option
 	 * @param gui
 	 *            the GUI option
 	 * @return the execution result from tool
 	 * @throws ToolException
 	 */
-	public int merge(String localFile,
-			String remoteFile, String baseFile, String mergedFilePath,
-			String toolName, BooleanTriState prompt, BooleanTriState gui,
-			BooleanTriState trustExitCode)
+	public ExecutionResult merge(Repository repo, FileElement localFile,
+			FileElement remoteFile, FileElement baseFile, String mergedFilePath,
+			String toolName, BooleanTriState prompt, BooleanTriState gui)
 			throws ToolException {
-		return 0;
+		ExternalMergeTool tool = guessTool(toolName, gui);
+		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$
+			boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
+			CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
+			return cmdExec.run(command, workingDir, env);
+		} catch (Exception e) {
+			throw new ToolException(e);
+		} finally {
+			localFile.cleanTemporaries();
+			remoteFile.cleanTemporaries();
+			baseFile.cleanTemporaries();
+		}
 	}
 
 	/**
@@ -99,7 +129,7 @@ public Map<String, ExternalMergeTool> getNotAvailableTools() {
 	 */
 	public String getDefaultToolName(BooleanTriState gui) {
 		return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$
-				: "my_default_toolname"; //$NON-NLS-1$
+				: config.getDefaultToolName();
 	}
 
 	/**
@@ -109,11 +139,58 @@ public boolean isInteractive() {
 		return config.isPrompt();
 	}
 
-	private Map<String, ExternalMergeTool> setupPredefinedTools() {
-		return new TreeMap<>();
+	private ExternalMergeTool guessTool(String toolName, BooleanTriState gui)
+			throws ToolException {
+		if ((toolName == null) || toolName.isEmpty()) {
+			toolName = getDefaultToolName(gui);
+		}
+		ExternalMergeTool tool = getTool(toolName);
+		if (tool == null) {
+			throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$
+		}
+		return tool;
 	}
 
-	private Map<String, ExternalMergeTool> setupUserDefinedTools() {
-		return new TreeMap<>();
+	private ExternalMergeTool getTool(final String name) {
+		ExternalMergeTool tool = userDefinedTools.get(name);
+		if (tool == null) {
+			tool = predefinedTools.get(name);
+		}
+		return tool;
 	}
-}
\ No newline at end of file
+
+	private Map<String, ExternalMergeTool> setupPredefinedTools() {
+		Map<String, ExternalMergeTool> tools = new TreeMap<>();
+		for (CommandLineMergeTool tool : CommandLineMergeTool.values()) {
+			tools.put(tool.name(), new PreDefinedMergeTool(tool));
+		}
+		return tools;
+	}
+
+	private Map<String, ExternalMergeTool> setupUserDefinedTools(
+			MergeToolConfig cfg, Map<String, ExternalMergeTool> predefTools) {
+		Map<String, ExternalMergeTool> tools = new TreeMap<>();
+		Map<String, ExternalMergeTool> userTools = cfg.getTools();
+		for (String name : userTools.keySet()) {
+			ExternalMergeTool userTool = userTools.get(name);
+			// if mergetool.<name>.cmd is defined we have user defined tool
+			if (userTool.getCommand() != null) {
+				tools.put(name, userTool);
+			} else if (userTool.getPath() != null) {
+				// if mergetool.<name>.path is defined we just overload the path
+				// of predefined tool
+				PreDefinedMergeTool predefTool = (PreDefinedMergeTool) predefTools
+						.get(name);
+				if (predefTool != null) {
+					predefTool.setPath(userTool.getPath());
+					if (userTool.getTrustExitCode() != BooleanTriState.UNSET) {
+						predefTool
+								.setTrustExitCode(userTool.getTrustExitCode());
+					}
+				}
+			}
+		}
+		return tools;
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java
new file mode 100644
index 0000000..2c64c16
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * 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.internal.diffmergetool;
+
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+
+/**
+ * The pre-defined merge tool.
+ */
+public class PreDefinedMergeTool extends UserDefinedMergeTool {
+
+	/**
+	 * the tool parameters without base
+	 */
+	private final String parametersWithoutBase;
+
+	/**
+	 * Creates the pre-defined merge tool
+	 *
+	 * @param name
+	 *            the name
+	 * @param path
+	 *            the path
+	 * @param parametersWithBase
+	 *            the tool parameters that are used together with path as
+	 *            command and "base is present" ($BASE)
+	 * @param parametersWithoutBase
+	 *            the tool parameters that are used together with path as
+	 *            command and "base is present" ($BASE)
+	 * @param trustExitCode
+	 *            the "trust exit code" option
+	 */
+	public PreDefinedMergeTool(String name, String path,
+			String parametersWithBase, String parametersWithoutBase,
+			BooleanTriState trustExitCode) {
+		super(name, path, parametersWithBase, trustExitCode);
+		this.parametersWithoutBase = parametersWithoutBase;
+	}
+
+	/**
+	 * Creates the pre-defined merge tool
+	 *
+	 * @param tool
+	 *            the command line merge tool
+	 *
+	 */
+	public PreDefinedMergeTool(CommandLineMergeTool tool) {
+		this(tool.name(), tool.getPath(), tool.getParameters(true),
+				tool.getParameters(false),
+				tool.isExitCodeTrustable() ? BooleanTriState.TRUE
+						: BooleanTriState.FALSE);
+	}
+
+	/**
+	 * @param trustExitCode
+	 *            the "trust exit code" option
+	 */
+	@Override
+	public void setTrustExitCode(BooleanTriState trustExitCode) {
+		super.setTrustExitCode(trustExitCode);
+	}
+
+	/**
+	 * @return the tool command (with base present)
+	 */
+	@Override
+	public String getCommand() {
+		return getCommand(true);
+	}
+
+	/**
+	 * @param withBase
+	 *            get command with base present (true) or without base present
+	 *            (false)
+	 * @return the tool command
+	 */
+	@Override
+	public String getCommand(boolean withBase) {
+		return getPath() + " " //$NON-NLS-1$
+				+ (withBase ? super.getCommand() : parametersWithoutBase);
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java
index df4d8cb..1dd2f0d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java
@@ -21,7 +21,7 @@ public class UserDefinedMergeTool extends UserDefinedDiffTool
 	/**
 	 * the merge tool "trust exit code" option
 	 */
-	private final BooleanTriState trustExitCode;
+	private BooleanTriState trustExitCode;
 
 	/**
 	 * Creates the merge tool
@@ -40,20 +40,30 @@ public UserDefinedMergeTool(String name, String path, String cmd,
 		super(name, path, cmd);
 		this.trustExitCode = trustExitCode;
 	}
-
 	/**
 	 * @return the "trust exit code" flag
 	 */
 	@Override
-	public boolean isTrustExitCode() {
-		return trustExitCode == BooleanTriState.TRUE;
-	}
-
-	/**
-	 * @return the "trust exit code" option
-	 */
 	public BooleanTriState getTrustExitCode() {
 		return trustExitCode;
 	}
 
+	/**
+	 * @param trustExitCode
+	 *            the new "trust exit code" flag
+	 */
+	protected void setTrustExitCode(BooleanTriState trustExitCode) {
+		this.trustExitCode = trustExitCode;
+	}
+
+	/**
+	 * @param withBase
+	 *            not used, because user-defined merge tool can only define one
+	 *            cmd -> it must handle with and without base present (empty)
+	 * @return the tool command
+	 */
+	@Override
+	public String getCommand(boolean withBase) {
+		return getCommand();
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index e982a33..29c66f5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -10,6 +10,7 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
+
 package org.eclipse.jgit.lib;
 
 /**
@@ -66,7 +67,7 @@ public final class ConfigConstants {
 	public static final String CONFIG_KEY_TRUST_EXIT_CODE = "trustExitCode";
 
 	/**
-	 * The "cmd" key within "difftool.*." section
+	 * The "cmd" key within "difftool.*." or "mergetool.*." section
 	 *
 	 * @since 6.1
 	 */