summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs2
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java230
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java310
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java202
-rw-r--r--org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin1
-rw-r--r--org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties25
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java161
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java460
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java16
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java147
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java271
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java17
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java77
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java190
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java327
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java107
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java33
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java81
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java261
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java147
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java259
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java11
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java91
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java133
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java69
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java39
30 files changed, 3474 insertions, 204 deletions
diff --git a/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..99f26c0203
--- /dev/null
+++ b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..99f26c0203
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..99f26c0203
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..99f26c0203
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
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 e7bf48417d..8daaa6ad9e 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
@@ -9,72 +9,87 @@
*/
package org.eclipse.jgit.pgm;
-import static org.junit.Assert.assertEquals;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION;
+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.fail;
+import java.io.InputStream;
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.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.eclipse.jgit.lib.StoredConfig;
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 ToolTestCase {
- @Argument(index = 1, metaVar = "metaVar_arg")
- List<String> arguments = new ArrayList<>();
+ private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ configureEchoTool(TOOL_NAME);
}
- private String[] runAndCaptureUsingInitRaw(String... args)
- throws Exception {
- CLIGitCommand.Result result = new CLIGitCommand.Result();
+ @Test
+ public void testToolWithPrompt() throws Exception {
+ String[] inputLines = {
+ "y", // accept launching diff tool
+ "y", // accept launching diff tool
+ };
- GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
- CmdLineParser clp = new CmdLineParser(bean);
- clp.parseArgument(args);
+ String[] conflictingFilenames = createUnstagedChanges();
+ String[] expectedOutput = getExpectedCompareOutput(conflictingFilenames);
- 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]);
+ String option = "--tool";
+
+ InputStream inputStream = createInputStream(inputLines);
+ assertArrayOfLinesEquals("Incorrect output for option: " + option,
+ expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+ DIFF_TOOL, "--prompt", option, TOOL_NAME));
}
- private Git git;
+ @Test
+ public void testToolAbortLaunch() throws Exception {
+ String[] inputLines = {
+ "y", // accept launching diff tool
+ "n", // don't launch diff tool
+ };
- @Override
- @Before
- public void setUp() throws Exception {
- super.setUp();
- git = new Git(db);
- git.commit().setMessage("initial commit").call();
+ String[] conflictingFilenames = createUnstagedChanges();
+ int abortIndex = 1;
+ String[] expectedOutput = getExpectedAbortOutput(conflictingFilenames, 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();
+
+ runAndCaptureUsingInitRaw(DIFF_TOOL, "--tool", "undefined");
+ fail("Expected exception when trying to run undefined tool");
}
@Test
public void testTool() throws Exception {
- RevCommit commit = createUnstagedChanges();
- List<DiffEntry> changes = getRepositoryChanges(commit);
- String[] expectedOutput = getExpectedDiffToolOutput(changes);
+ String[] conflictFilenames = createUnstagedChanges();
+ String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictFilenames);
String[] options = {
"--tool",
@@ -84,54 +99,51 @@ public class DiffToolTest extends CLIRepositoryTestCase {
for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput,
- runAndCaptureUsingInitRaw("difftool", option,
- "some_tool"));
+ runAndCaptureUsingInitRaw(DIFF_TOOL, option,
+ TOOL_NAME));
}
}
@Test
public void testToolTrustExitCode() throws Exception {
- RevCommit commit = createUnstagedChanges();
- List<DiffEntry> changes = getRepositoryChanges(commit);
- String[] expectedOutput = getExpectedDiffToolOutput(changes);
+ String[] conflictingFilenames = createUnstagedChanges();
+ String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
String[] options = { "--tool", "-t", };
for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option,
- expectedOutput, runAndCaptureUsingInitRaw("difftool",
- "--trust-exit-code", option, "some_tool"));
+ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
+ "--trust-exit-code", option, TOOL_NAME));
}
}
@Test
public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
- RevCommit commit = createUnstagedChanges();
- List<DiffEntry> changes = getRepositoryChanges(commit);
- String[] expectedOutput = getExpectedDiffToolOutput(changes);
+ String[] conflictingFilenames = createUnstagedChanges();
+ String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
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, "some_tool"));
+ option, TOOL_NAME));
}
}
@Test
public void testToolCached() throws Exception {
- RevCommit commit = createStagedChanges();
- List<DiffEntry> changes = getRepositoryChanges(commit);
- String[] expectedOutput = getExpectedDiffToolOutput(changes);
+ String[] conflictingFilenames = createStagedChanges();
+ String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
String[] options = { "--cached", "--staged", };
for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option,
- expectedOutput, runAndCaptureUsingInitRaw("difftool",
- option, "--tool", "some_tool"));
+ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
+ option, "--tool", TOOL_NAME));
}
}
@@ -139,13 +151,17 @@ public class DiffToolTest extends CLIRepositoryTestCase {
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);
}
+ String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
+ + getEchoCommand();
+ expectedOutput.add("user-defined:");
+ expectedOutput.add(customToolHelpLine);
String[] userDefinedToolsHelp = {
- "user-defined:",
"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.",
@@ -154,52 +170,66 @@ public class DiffToolTest extends CLIRepositoryTestCase {
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 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 void configureEchoTool(String toolName) {
+ StoredConfig config = db.getConfig();
+ // the default diff tool is configured without a subsection
+ String subsection = null;
+ config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
+ toolName);
+
+ String command = getEchoCommand();
+
+ config.setString(CONFIG_DIFFTOOL_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_DIFFTOOL_SECTION, toolName, CONFIG_KEY_PROMPT,
+ String.valueOf(false));
}
- 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) {
- String[] expectedToolOutput = new String[changes.size()];
- for (int i = 0; i < changes.size(); ++i) {
- DiffEntry change = changes.get(i);
- String newPath = change.getNewPath();
- String oldPath = change.getOldPath();
- String newIdName = change.getNewId().name();
- String oldIdName = change.getOldId().name();
- String expectedLine = "M\t" + newPath + " (" + newIdName + ")"
- + "\t" + oldPath + " (" + oldIdName + ")";
+ private static String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) {
+ String[] expectedToolOutput = new String[conflictingFilenames.length];
+ for (int i = 0; i < conflictingFilenames.length; ++i) {
+ String newPath = conflictingFilenames[i];
+ String expectedLine = newPath;
expectedToolOutput[i] = expectedLine;
}
return expectedToolOutput;
}
- private static void assertArrayOfLinesEquals(String failMessage,
- String[] expected, String[] actual) {
- assertEquals(failMessage, toString(expected), toString(actual));
+ private static String[] getExpectedCompareOutput(String[] conflictingFilenames) {
+ List<String> expected = new ArrayList<>();
+ int n = conflictingFilenames.length;
+ for (int i = 0; i < n; ++i) {
+ String newPath = conflictingFilenames[i];
+ 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(String[] conflictingFilenames,
+ int abortIndex) {
+ List<String> expected = new ArrayList<>();
+ int n = conflictingFilenames.length;
+ for (int i = 0; i < n; ++i) {
+ String newPath = conflictingFilenames[i];
+ 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/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
new file mode 100644
index 0000000000..2e50f09081
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
@@ -0,0 +1,310 @@
+/*
+ * 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.io.InputStream;
+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 ToolTestCase {
+
+ private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ configureEchoTool(TOOL_NAME);
+ }
+
+ @Test
+ 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);
+
+ 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 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, 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<>();
+ 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 static String[] getExpectedMergeConflictOutputNoPrompt(
+ String[] conflictFilenames) {
+ List<String> expected = new ArrayList<>();
+ expected.add("Merging:");
+ for (String conflictFilename : conflictFilenames) {
+ expected.add(conflictFilename);
+ }
+ 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 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 0000000000..933f19bcc4
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java
@@ -0,0 +1,202 @@
+/*
+ * 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("dir1/a", "Hello world a");
+ writeTrashFile("dir2/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("dir1/a", "Hello world a 1");
+ writeTrashFile("dir2/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("dir1/a", "Hello world a 2");
+ writeTrashFile("dir2/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 = { "dir1/a", "dir2/b" };
+ return conflictingFilenames;
+ }
+
+ protected String[] createDeletedConflict() throws Exception {
+ // create files on initial branch
+ git.checkout().setName(TEST_BRANCH_NAME).call();
+ writeTrashFile("dir1/a", "Hello world a");
+ writeTrashFile("dir2/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("dir1/a", "Hello world a 1");
+ writeTrashFile("dir2/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("dir1/a").call();
+ git.rm().addFilepattern("dir2/b").call();
+ git.commit().setMessage("files a & b deleted commit 2").call();
+ // cherry-pick conflicting changes
+ git.cherryPick().include(commit1).call();
+ String[] conflictingFilenames = { "dir1/a", "dir2/b" };
+ return conflictingFilenames;
+ }
+
+ protected String[] createUnstagedChanges() throws Exception {
+ writeTrashFile("dir1/a", "Hello world a");
+ writeTrashFile("dir2/b", "Hello world b");
+ git.add().addFilepattern(".").call();
+ git.commit().setMessage("files a & b").call();
+ writeTrashFile("dir1/a", "New Hello world a");
+ writeTrashFile("dir2/b", "New Hello world b");
+ String[] conflictingFilenames = { "dir1/a", "dir2/b" };
+ return conflictingFilenames;
+ }
+
+ protected String[] createStagedChanges() throws Exception {
+ String[] conflictingFilenames = createUnstagedChanges();
+ git.add().addFilepattern(".").call();
+ return conflictingFilenames;
+ }
+
+ 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/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
index 8c44764c63..ea1d1e3faa 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.LsRemote
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 fda0bf6ff4..674185df2b 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,9 +58,9 @@ couldNotCreateBranch=Could not create branch {0}: {1}
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]?
-diffToolDied=external diff died, stopping at {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]?
+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:
everythingUpToDate=Everything up-to-date
@@ -91,6 +91,22 @@ listeningOn=Listening on {0}
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.
@@ -255,6 +271,7 @@ usage_DisplayTheVersionOfJgit=Display the version of jgit
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 +320,7 @@ usage_Status=Show the working tree status
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 +368,7 @@ usage_date=date format, one of default, rfc, local, iso, short, raw (as defined
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 128881779b..74d91cd3d7 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
@@ -11,9 +11,11 @@
package org.eclipse.jgit.pgm;
import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
@@ -21,27 +23,43 @@ import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
-
+import org.eclipse.jgit.diff.ContentSource;
+import org.eclipse.jgit.diff.ContentSource.Pair;
import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffEntry.Side;
+import org.eclipse.jgit.internal.diffmergetool.ToolException;
+import org.eclipse.jgit.internal.diffmergetool.DiffTools;
+import org.eclipse.jgit.internal.diffmergetool.FileElement;
+import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.errors.AmbiguousObjectException;
+import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
-import org.eclipse.jgit.internal.diffmergetool.DiffTools;
-import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
+import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.treewalk.WorkingTreeOptions;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.FS.ExecutionResult;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -106,11 +124,14 @@ class DiffTool extends TextBuiltin {
@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
@@ -145,40 +166,53 @@ class DiffTool extends TextBuiltin {
private void compare(List<DiffEntry> files, boolean showPrompt,
String toolNamePrompt) throws IOException {
- for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
- DiffEntry ent = files.get(fileIndex);
- String mergedFilePath = ent.getNewPath();
- if (mergedFilePath.equals(DiffEntry.DEV_NULL)) {
- mergedFilePath = ent.getOldPath();
- }
- // check if user wants to launch compare
- boolean launchCompare = true;
- if (showPrompt) {
- launchCompare = isLaunchCompare(fileIndex + 1, files.size(),
- mergedFilePath, toolNamePrompt);
- }
- if (launchCompare) {
- switch (ent.getChangeType()) {
- case MODIFY:
- outw.println("M\t" + ent.getNewPath() //$NON-NLS-1$
- + " (" + ent.getNewId().name() + ")" //$NON-NLS-1$ //$NON-NLS-2$
- + "\t" + ent.getOldPath() //$NON-NLS-1$
- + " (" + ent.getOldId().name() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
- int ret = diffTools.compare(ent.getNewPath(),
- ent.getOldPath(), ent.getNewId().name(),
- ent.getOldId().name(), toolName, prompt, gui,
- trustExitCode);
- if (ret != 0) {
+ ContentSource.Pair sourcePair = new ContentSource.Pair(source(oldTree),
+ source(newTree));
+ try {
+ for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
+ DiffEntry ent = files.get(fileIndex);
+ String mergedFilePath = ent.getNewPath();
+ if (mergedFilePath.equals(DiffEntry.DEV_NULL)) {
+ mergedFilePath = ent.getOldPath();
+ }
+ // check if user wants to launch compare
+ boolean launchCompare = true;
+ if (showPrompt) {
+ launchCompare = isLaunchCompare(fileIndex + 1, files.size(),
+ mergedFilePath, toolNamePrompt);
+ }
+ if (launchCompare) {
+ try {
+ FileElement local = createFileElement(
+ FileElement.Type.LOCAL, sourcePair, Side.OLD,
+ ent);
+ FileElement remote = createFileElement(
+ FileElement.Type.REMOTE, sourcePair, Side.NEW,
+ ent);
+ FileElement merged = new FileElement(mergedFilePath,
+ FileElement.Type.MERGED);
+ // TODO: check how to return the exit-code of the tool
+ // to jgit / java runtime ?
+ // int rc =...
+ ExecutionResult result = diffTools.compare(local,
+ remote, merged, toolName, prompt, gui,
+ trustExitCode);
+ outw.println(new String(result.getStdout().toByteArray()));
+ errw.println(
+ new String(result.getStderr().toByteArray()));
+ } catch (ToolException e) {
+ outw.println(e.getResultStdout());
+ outw.flush();
+ errw.println(e.getMessage());
throw die(MessageFormat.format(
- CLIText.get().diffToolDied, mergedFilePath));
+ CLIText.get().diffToolDied, mergedFilePath), e);
}
- break;
- default:
+ } else {
break;
}
- } else {
- break;
}
+ } finally {
+ sourcePair.close();
}
}
@@ -187,10 +221,9 @@ class DiffTool extends TextBuiltin {
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$
@@ -203,17 +236,18 @@ class DiffTool extends TextBuiltin {
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(
@@ -254,4 +288,53 @@ class DiffTool extends TextBuiltin {
return files;
}
+ private FileElement createFileElement(FileElement.Type elementType,
+ Pair pair, Side side, DiffEntry entry)
+ throws NoWorkTreeException, CorruptObjectException, IOException,
+ ToolException {
+ String entryPath = side == Side.NEW ? entry.getNewPath()
+ : entry.getOldPath();
+ FileElement fileElement = new FileElement(entryPath, elementType);
+ if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) {
+ try (RevWalk revWalk = new RevWalk(db);
+ TreeWalk treeWalk = new TreeWalk(db,
+ revWalk.getObjectReader())) {
+ treeWalk.setFilter(
+ PathFilterGroup.createFromStrings(entryPath));
+ if (side == Side.NEW) {
+ newTree.reset();
+ treeWalk.addTree(newTree);
+ } else {
+ oldTree.reset();
+ treeWalk.addTree(oldTree);
+ }
+ if (treeWalk.next()) {
+ final EolStreamType eolStreamType = treeWalk
+ .getEolStreamType(CHECKOUT_OP);
+ final String filterCommand = treeWalk.getFilterCommand(
+ Constants.ATTR_FILTER_TYPE_SMUDGE);
+ WorkingTreeOptions opt = db.getConfig()
+ .get(WorkingTreeOptions.KEY);
+ CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
+ eolStreamType, filterCommand);
+ DirCacheCheckout.getContent(db, entryPath,
+ checkoutMetadata, pair.open(side, entry), opt,
+ new FileOutputStream(
+ fileElement.createTempFile(null)));
+ } else {
+ throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$
+ + "' in staging area!", null); //$NON-NLS-1$
+ }
+ }
+ }
+ return fileElement;
+ }
+
+ private ContentSource source(AbstractTreeIterator iterator) {
+ if (iterator instanceof WorkingTreeIterator) {
+ return ContentSource.create((WorkingTreeIterator) iterator);
+ }
+ return ContentSource.create(db.newObjectReader());
+ }
+
}
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 0000000000..9712770758
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
@@ -0,0 +1,460 @@
+/*
+ * 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 static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+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.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.diff.ContentSource;
+import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
+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.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeOptions;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
+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;
+
+@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 BooleanTriState prompt = BooleanTriState.UNSET;
+
+ @Option(name = "--prompt", usage = "usage_prompt")
+ void setPrompt(@SuppressWarnings("unused") boolean on) {
+ prompt = BooleanTriState.TRUE;
+ }
+
+ @Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt")
+ void noPrompt(@SuppressWarnings("unused") boolean on) {
+ prompt = BooleanTriState.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;
+
+ 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
+ protected void run() {
+ try {
+ if (toolHelp) {
+ showToolHelp();
+ } else {
+ // get prompt
+ boolean showPrompt = mergeTools.isInteractive();
+ if (prompt != BooleanTriState.UNSET) {
+ showPrompt = prompt == BooleanTriState.TRUE;
+ }
+ // 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(CLIText.get().mergeToolNoFiles);
+ }
+ }
+ 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> mergedFilePaths = new ArrayList<>(files.keySet());
+ Collections.sort(mergedFilePaths);
+ // show the files
+ 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();
+ // 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;
+ }
+ }
+ // 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 MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
+ String toolNamePrompt) throws Exception {
+ outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict,
+ mergedFilePath));
+ outw.flush();
+ // 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());
+ // temporary directory if mergetool.writeToTemp == true
+ File tempDir = mergeTools.createTempDirectory();
+ // the parent directory for temp files (can be same as tempDir or just
+ // the worktree dir)
+ File tempFilesParent = tempDir != null ? tempDir : db.getWorkTree();
+ try {
+ FileElement base = null;
+ FileElement local = null;
+ FileElement remote = null;
+ FileElement merged = new FileElement(mergedFilePath,
+ Type.MERGED);
+ DirCache cache = db.readDirCache();
+ try (RevWalk revWalk = new RevWalk(db);
+ TreeWalk treeWalk = new TreeWalk(db,
+ revWalk.getObjectReader())) {
+ treeWalk.setFilter(
+ PathFilterGroup.createFromStrings(mergedFilePath));
+ DirCacheIterator cacheIter = new DirCacheIterator(cache);
+ treeWalk.addTree(cacheIter);
+ while (treeWalk.next()) {
+ if (treeWalk.isSubtree()) {
+ treeWalk.enterSubtree();
+ continue;
+ }
+ final EolStreamType eolStreamType = treeWalk
+ .getEolStreamType(CHECKOUT_OP);
+ final String filterCommand = treeWalk.getFilterCommand(
+ Constants.ATTR_FILTER_TYPE_SMUDGE);
+ WorkingTreeOptions opt = db.getConfig()
+ .get(WorkingTreeOptions.KEY);
+ CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
+ eolStreamType, filterCommand);
+ DirCacheEntry entry = treeWalk.getTree(DirCacheIterator.class).getDirCacheEntry();
+ if (entry == null) {
+ continue;
+ }
+ ObjectId id = entry.getObjectId();
+ switch (entry.getStage()) {
+ case DirCacheEntry.STAGE_1:
+ base = new FileElement(mergedFilePath, Type.BASE);
+ DirCacheCheckout.getContent(db, mergedFilePath,
+ checkoutMetadata,
+ baseSource.open(mergedFilePath, id), opt,
+ new FileOutputStream(
+ base.createTempFile(tempFilesParent)));
+ break;
+ case DirCacheEntry.STAGE_2:
+ local = new FileElement(mergedFilePath, Type.LOCAL);
+ DirCacheCheckout.getContent(db, mergedFilePath,
+ checkoutMetadata,
+ localSource.open(mergedFilePath, id), opt,
+ new FileOutputStream(
+ local.createTempFile(tempFilesParent)));
+ break;
+ case DirCacheEntry.STAGE_3:
+ remote = new FileElement(mergedFilePath, Type.REMOTE);
+ DirCacheCheckout.getContent(db, mergedFilePath,
+ checkoutMetadata,
+ remoteSource.open(mergedFilePath, id), opt,
+ new FileOutputStream(remote
+ .createTempFile(tempFilesParent)));
+ break;
+ }
+ }
+ }
+ if ((local == null) || (remote == null)) {
+ throw die(MessageFormat.format(CLIText.get().mergeToolDied,
+ mergedFilePath));
+ }
+ long modifiedBefore = merged.getFile().lastModified();
+ try {
+ // TODO: check how to return the exit-code of the
+ // tool to jgit / java runtime ?
+ // int rc =...
+ ExecutionResult executionResult = mergeTools.merge(local,
+ remote, merged, base, tempDir, 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.getFile().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$
+ launch = false;
+ }
+ }
+ return launch;
+ }
+
+ 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();
+ }
+ 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()) {
+ userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$
+ name, userTools.get(name).getCommand()));
+ }
+ outw.println(MessageFormat.format(
+ CLIText.get().mergeToolHelpSetToFollowing, availableToolNames,
+ userToolNames, notAvailableToolNames));
+ }
+
+ 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.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
index 7fe5b0fa45..989e649b72 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 class CLIText extends TranslationBundle {
/***/ 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.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
index c9ebec7638..4fd55c6cad 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
@@ -10,13 +10,17 @@
package org.eclipse.jgit.internal.diffmergetool;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_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;
@@ -25,6 +29,7 @@ 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;
/**
@@ -32,6 +37,54 @@ import org.junit.Test;
*/
public class ExternalDiffToolTest 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_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
+ command);
+
+ DiffTools manager = new DiffTools(db);
+
+ BooleanTriState prompt = BooleanTriState.UNSET;
+ BooleanTriState gui = BooleanTriState.UNSET;
+ BooleanTriState trustExitCode = BooleanTriState.TRUE;
+
+ manager.compare(local, remote, merged, toolName, prompt, gui,
+ trustExitCode);
+
+ 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_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
+ command);
+
+ DiffTools manager = new DiffTools(db);
+
+ BooleanTriState prompt = BooleanTriState.UNSET;
+ BooleanTriState gui = BooleanTriState.UNSET;
+ BooleanTriState trustExitCode = BooleanTriState.FALSE;
+
+ manager.compare(local, remote, merged, toolName, prompt, gui,
+ trustExitCode);
+
+ fail("Expected exception to be thrown due to external tool exiting with error code: "
+ + errorReturnCode);
+ }
+
@Test
public void testToolNames() {
DiffTools manager = new DiffTools(db);
@@ -86,11 +139,11 @@ public class ExternalDiffToolTest extends ExternalToolTestCase {
config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
CONFIG_KEY_PATH, "/usr/bin/echo");
config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
- CONFIG_KEY_PROMPT, "--no-prompt");
+ CONFIG_KEY_PROMPT, String.valueOf(false));
config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
- CONFIG_KEY_GUITOOL, "--no-gui");
+ CONFIG_KEY_GUITOOL, String.valueOf(false));
config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
- CONFIG_KEY_TRUST_EXIT_CODE, "--no-trust-exit-code");
+ CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false));
DiffTools manager = new DiffTools(db);
Set<String> actualToolNames = manager.getUserDefinedTools().keySet();
Set<String> expectedToolNames = new LinkedHashSet<>();
@@ -109,38 +162,50 @@ public class ExternalDiffToolTest extends ExternalToolTestCase {
}
@Test
- public void testCompare() {
- DiffTools manager = new DiffTools(db);
+ public void testCompare() throws ToolException {
+ String toolName = "customTool";
+
+ FileBasedConfig config = db.getConfig();
+ // the default diff tool is configured without a subsection
+ String subsection = null;
+ config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
+ toolName);
+
+ String command = getEchoCommand();
+
+ config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
+ command);
- String newPath = "";
- String oldPath = "";
- String newId = "";
- String oldId = "";
- String toolName = "";
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
BooleanTriState trustExitCode = BooleanTriState.UNSET;
+ DiffTools manager = new DiffTools(db);
+
int expectedCompareResult = 0;
- int compareResult = manager.compare(newPath, oldPath, newId, oldId,
+ ExecutionResult compareResult = manager.compare(local, remote, merged,
toolName, prompt, gui, trustExitCode);
assertEquals("Incorrect compare result for external diff tool",
- expectedCompareResult, compareResult);
+ 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
String subsection = null;
- config.setString("diff", subsection, "tool", "customTool");
+ config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
+ toolName);
DiffTools manager = new DiffTools(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);
+ toolName, defaultToolName);
gui = BooleanTriState.TRUE;
String defaultGuiToolName = manager.getDefaultToolName(gui);
@@ -148,11 +213,63 @@ public class ExternalDiffToolTest extends ExternalToolTestCase {
"Expected configured difftool to be the default external diff tool",
"my_gui_tool", defaultGuiToolName);
- config.setString("diff", subsection, "guitool", "customGuiTool");
+ config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL,
+ guiToolName);
manager = new DiffTools(db);
defaultGuiToolName = manager.getDefaultToolName(gui);
assertEquals(
"Expected configured difftool to be the default external diff guitool",
"my_gui_tool", defaultGuiToolName);
}
+
+ @Test
+ public void testOverridePreDefinedToolPath() {
+ String newToolPath = "/tmp/path/";
+
+ CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
+ assertTrue("Expected to find pre-defined external diff tools",
+ defaultTools.length > 0);
+
+ CommandLineDiffTool overridenTool = defaultTools[0];
+ String overridenToolName = overridenTool.name();
+ String overridenToolPath = newToolPath + overridenToolName;
+ FileBasedConfig config = db.getConfig();
+ config.setString(CONFIG_DIFFTOOL_SECTION, overridenToolName,
+ CONFIG_KEY_PATH, overridenToolPath);
+
+ DiffTools manager = new DiffTools(db);
+ Map<String, ExternalDiffTool> availableTools = manager
+ .getAvailableTools();
+ ExternalDiffTool externalDiffTool = availableTools
+ .get(overridenToolName);
+ String actualDiffToolPath = externalDiffTool.getPath();
+ assertEquals(
+ "Expected pre-defined external diff tool to have overriden path",
+ overridenToolPath, actualDiffToolPath);
+ String expectedDiffToolCommand = overridenToolPath + " "
+ + overridenTool.getParameters();
+ String actualDiffToolCommand = externalDiffTool.getCommand();
+ assertEquals(
+ "Expected pre-defined external diff tool to have overriden command",
+ expectedDiffToolCommand, actualDiffToolCommand);
+ }
+
+ @Test(expected = ToolException.class)
+ public void testUndefinedTool() throws Exception {
+ DiffTools manager = new DiffTools(db);
+
+ String toolName = "undefined";
+ BooleanTriState prompt = BooleanTriState.UNSET;
+ BooleanTriState gui = BooleanTriState.UNSET;
+ BooleanTriState trustExitCode = BooleanTriState.UNSET;
+
+ manager.compare(local, remote, merged, toolName, prompt, gui,
+ trustExitCode);
+ fail("Expected exception to be thrown due to not defined external diff tool");
+ }
+
+ private String getEchoCommand() {
+ return "(echo \"$LOCAL\" \"$REMOTE\") > "
+ + commandResult.getAbsolutePath();
+ }
}
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
new file mode 100644
index 0000000000..50576682eb
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2020-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.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;
+
+/**
+ * Testing external merge tools.
+ */
+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(local, remote, merged, base, null, 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(local, remote, merged, base, null, 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 merge tool names",
+ expectedToolNames, actualToolNames);
+ }
+
+ @Test
+ public void testAllTools() {
+ MergeTools manager = new MergeTools(db);
+ Set<String> actualToolNames = manager.getAvailableTools().keySet();
+ 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 = new LinkedHashSet<>();
+ expectedToolNames.add(customToolname);
+ assertEquals("Incorrect set of external merge tools", expectedToolNames,
+ actualToolNames);
+ }
+
+ @Test
+ 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 merge tools",
+ expectedToolNames, actualToolNames);
+ }
+
+ @Test
+ public void testCompare() throws ToolException {
+ String toolName = "customTool";
+
+ 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;
+
+ MergeTools manager = new MergeTools(db);
+
+ int expectedCompareResult = 0;
+ ExecutionResult compareResult = manager.merge(local, remote, merged,
+ base, null, 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 merge tool is configured without a subsection
+ String subsection = null;
+ 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 mergetool to be the default external merge tool",
+ toolName, defaultToolName);
+
+ gui = BooleanTriState.TRUE;
+ String defaultGuiToolName = manager.getDefaultToolName(gui);
+ assertEquals(
+ "Expected configured mergetool to be the default external merge tool",
+ "my_gui_tool", defaultGuiToolName);
+
+ config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL,
+ guiToolName);
+ manager = new MergeTools(db);
+ defaultGuiToolName = manager.getDefaultToolName(gui);
+ assertEquals(
+ "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(local, remote, merged, base, null, 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.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java
index 0cc12978a8..0fd85cb456 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java
@@ -36,6 +36,14 @@ public abstract class ExternalToolTestCase extends RepositoryTestCase {
protected File commandResult;
+ protected FileElement local;
+
+ protected FileElement remote;
+
+ protected FileElement merged;
+
+ protected FileElement base;
+
@Before
@Override
public void setUp() throws Exception {
@@ -51,6 +59,15 @@ public abstract class ExternalToolTestCase extends RepositoryTestCase {
baseFile.deleteOnExit();
commandResult = writeTrashFile("commandResult.txt", "");
commandResult.deleteOnExit();
+
+ local = new FileElement(localFile.getAbsolutePath(),
+ FileElement.Type.LOCAL);
+ remote = new FileElement(remoteFile.getAbsolutePath(),
+ FileElement.Type.REMOTE);
+ merged = new FileElement(mergedFile.getAbsolutePath(),
+ FileElement.Type.MERGED);
+ base = new FileElement(baseFile.getAbsolutePath(),
+ FileElement.Type.BASE);
}
@After
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java
index 1a41df3d0a..64ff19c9c3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010, 2020 Google Inc. and others
+ * Copyright (C) 2010, 2021 Google Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -91,6 +91,29 @@ public abstract class ContentSource {
public abstract ObjectLoader open(String path, ObjectId id)
throws IOException;
+ /**
+ * Closes the used resources like ObjectReader, TreeWalk etc. Default
+ * implementation does nothing.
+ *
+ * @since 6.2
+ */
+ public void close() {
+ // Do nothing
+ }
+
+ /**
+ * Checks if the source is from "working tree", so it can be accessed as a
+ * file directly.
+ *
+ * @since 6.2
+ *
+ * @return true if working tree source and false otherwise (loader must be
+ * used)
+ */
+ public boolean isWorkingTreeSource() {
+ return false;
+ }
+
private static class ObjectReaderSource extends ContentSource {
private final ObjectReader reader;
@@ -111,6 +134,16 @@ public abstract class ContentSource {
public ObjectLoader open(String path, ObjectId id) throws IOException {
return reader.open(id, Constants.OBJ_BLOB);
}
+
+ @Override
+ public void close() {
+ reader.close();
+ }
+
+ @Override
+ public boolean isWorkingTreeSource() {
+ return false;
+ }
}
private static class WorkingTreeSource extends ContentSource {
@@ -194,6 +227,16 @@ public abstract class ContentSource {
throw new FileNotFoundException(path);
}
}
+
+ @Override
+ public void close() {
+ tw.close();
+ }
+
+ @Override
+ public boolean isWorkingTreeSource() {
+ return true;
+ }
}
/** A pair of sources to access the old and new sides of a DiffEntry. */
@@ -261,5 +304,37 @@ public abstract class ContentSource {
throw new IllegalArgumentException();
}
}
+
+ /**
+ * Closes used resources.
+ *
+ * @since 6.2
+ */
+ public void close() {
+ oldSource.close();
+ newSource.close();
+ }
+
+ /**
+ * Checks if source (side) is a "working tree".
+ *
+ * @since 6.2
+ *
+ * @param side
+ * which side of the entry to read (OLD or NEW).
+ * @return is the source a "working tree"
+ *
+ */
+ public boolean isWorkingTreeSource(DiffEntry.Side side) {
+ switch (side) {
+ case OLD:
+ return oldSource.isWorkingTreeSource();
+ case NEW:
+ return newSource.isWorkingTreeSource();
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
}
}
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
new file mode 100644
index 0000000000..ad79fe8fc6
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2018-2021, 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 java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Map;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.FS_POSIX;
+import org.eclipse.jgit.util.FS_Win32;
+import org.eclipse.jgit.util.FS_Win32_Cygwin;
+
+/**
+ * Runs a command with help of FS.
+ */
+public class CommandExecutor {
+
+ private FS fs;
+
+ private boolean checkExitCode;
+
+ private File commandFile;
+
+ private boolean useMsys2;
+
+ /**
+ * @param fs
+ * the file system
+ * @param checkExitCode
+ * should the exit code be checked for errors ?
+ */
+ public CommandExecutor(FS fs, boolean checkExitCode) {
+ this.fs = fs;
+ this.checkExitCode = checkExitCode;
+ }
+
+ /**
+ * @param command
+ * the command string
+ * @param workingDir
+ * the working directory
+ * @param env
+ * the environment
+ * @return the execution result
+ * @throws ToolException
+ * @throws InterruptedException
+ * @throws IOException
+ */
+ public ExecutionResult run(String command, File workingDir,
+ Map<String, String> env)
+ throws ToolException, IOException, InterruptedException {
+ String[] commandArray = createCommandArray(command);
+ try {
+ ProcessBuilder pb = fs.runInShell(commandArray[0],
+ Arrays.copyOfRange(commandArray, 1, commandArray.length));
+ pb.directory(workingDir);
+ Map<String, String> envp = pb.environment();
+ if (env != null) {
+ envp.putAll(env);
+ }
+ ExecutionResult result = fs.execute(pb, null);
+ int rc = result.getRc();
+ 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 {
+ deleteCommandArray();
+ }
+ }
+
+ private void deleteCommandArray() {
+ deleteCommandFile();
+ }
+
+ private String[] createCommandArray(String command)
+ throws ToolException, IOException {
+ String[] commandArray = null;
+ checkUseMsys2(command);
+ createCommandFile(command);
+ if (fs instanceof FS_POSIX) {
+ commandArray = new String[1];
+ commandArray[0] = commandFile.getCanonicalPath();
+ } else if (fs instanceof FS_Win32) {
+ if (useMsys2) {
+ commandArray = new String[3];
+ commandArray[0] = "bash.exe"; //$NON-NLS-1$
+ commandArray[1] = "-c"; //$NON-NLS-1$
+ commandArray[2] = commandFile.getCanonicalPath().replace("\\", //$NON-NLS-1$
+ "/"); //$NON-NLS-1$
+ } else {
+ commandArray = new String[1];
+ commandArray[0] = commandFile.getCanonicalPath();
+ }
+ } else if (fs instanceof FS_Win32_Cygwin) {
+ commandArray = new String[1];
+ commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
+ } else {
+ throw new ToolException(
+ "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
+ }
+ return commandArray;
+ }
+
+ private void checkUseMsys2(String command) {
+ useMsys2 = false;
+ String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$
+ if (useMsys2Str != null && !useMsys2Str.isEmpty()) {
+ if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$
+ useMsys2 = command.contains(".sh"); //$NON-NLS-1$
+ } else {
+ useMsys2 = Boolean.parseBoolean(useMsys2Str);
+ }
+ }
+ }
+
+ private void createCommandFile(String command)
+ throws ToolException, IOException {
+ String fileExtension = null;
+ if (useMsys2 || fs instanceof FS_POSIX
+ || fs instanceof FS_Win32_Cygwin) {
+ fileExtension = ".sh"; //$NON-NLS-1$
+ } else if (fs instanceof FS_Win32) {
+ fileExtension = ".cmd"; //$NON-NLS-1$
+ command = "@echo off" + System.lineSeparator() + command //$NON-NLS-1$
+ + System.lineSeparator() + "exit /B %ERRORLEVEL%"; //$NON-NLS-1$
+ } else {
+ throw new ToolException(
+ "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
+ }
+ commandFile = File.createTempFile(".__", //$NON-NLS-1$
+ "__jgit_tool" + fileExtension); //$NON-NLS-1$
+ try (OutputStream outStream = new FileOutputStream(commandFile)) {
+ byte[] strToBytes = command.getBytes();
+ outStream.write(strToBytes);
+ outStream.close();
+ }
+ commandFile.setExecutable(true);
+ }
+
+ private void deleteCommandFile() {
+ if (commandFile != null && commandFile.exists()) {
+ commandFile.delete();
+ }
+ }
+
+ private boolean isCommandExecutionError(int rc) {
+ if (useMsys2 || fs instanceof FS_POSIX
+ || fs instanceof FS_Win32_Cygwin) {
+ // 126: permission for executing command denied
+ // 127: command not found
+ if ((rc == 126) || (rc == 127)) {
+ return true;
+ }
+ }
+ else if (fs instanceof FS_Win32) {
+ // 9009, 0x2331: Program is not recognized as an internal or
+ // external command, operable program or batch file. Indicates that
+ // command, application name or path has been misspelled when
+ // configuring the Action.
+ if (rc == 9009) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
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 0000000000..3a22124328
--- /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/DiffToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java
index 551f634f2d..c8b04f90f2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java
@@ -49,9 +49,10 @@ public class DiffToolConfig {
toolName = rc.getString(CONFIG_DIFF_SECTION, null, CONFIG_KEY_TOOL);
guiToolName = rc.getString(CONFIG_DIFF_SECTION, null,
CONFIG_KEY_GUITOOL);
- prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, CONFIG_KEY_PROMPT,
+ prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, toolName,
+ CONFIG_KEY_PROMPT,
true);
- String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, null,
+ String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, toolName,
CONFIG_KEY_TRUST_EXIT_CODE);
if (trustStr != null) {
trustExitCode = Boolean.parseBoolean(trustStr)
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 39729a4eec..1dcc523bf8 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
@@ -12,22 +12,27 @@ package org.eclipse.jgit.internal.diffmergetool;
import java.util.TreeMap;
import java.util.Collections;
+import java.io.IOException;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.StringUtils;
/**
* Manages diff tools.
*/
public class DiffTools {
+ private final Repository repo;
+
private final DiffToolConfig config;
- private Map<String, ExternalDiffTool> predefinedTools;
+ private final Map<String, ExternalDiffTool> predefinedTools;
- private Map<String, ExternalDiffTool> userDefinedTools;
+ private final Map<String, ExternalDiffTool> userDefinedTools;
/**
* Creates the external diff-tools manager for given repository.
@@ -36,22 +41,22 @@ public class DiffTools {
* the repository
*/
public DiffTools(Repository repo) {
+ this.repo = repo;
config = repo.getConfig().get(DiffToolConfig.KEY);
- setupPredefinedTools();
- setupUserDefinedTools();
+ predefinedTools = setupPredefinedTools();
+ userDefinedTools = setupUserDefinedTools(config, predefinedTools);
}
/**
* Compare two versions of a file.
*
- * @param newPath
- * the new file path
- * @param oldPath
- * the old file path
- * @param newId
- * the new object ID
- * @param oldId
- * the old object ID
+ * @param localFile
+ * the local file element
+ * @param remoteFile
+ * the remote file element
+ * @param mergedFile
+ * the merged file element, it's path equals local or remote
+ * element path
* @param toolName
* the selected tool name (can be null)
* @param prompt
@@ -60,12 +65,35 @@ public class DiffTools {
* 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 int compare(String newPath, String oldPath, String newId,
- String oldId, String toolName, BooleanTriState prompt,
- BooleanTriState gui, BooleanTriState trustExitCode) {
- return 0;
+ public ExecutionResult compare(FileElement localFile,
+ FileElement remoteFile, FileElement mergedFile, String toolName,
+ BooleanTriState prompt, BooleanTriState gui,
+ BooleanTriState trustExitCode) throws ToolException {
+ try {
+ // prepare the command (replace the file paths)
+ String command = ExternalToolUtils.prepareCommand(
+ guessTool(toolName, gui).getCommand(), localFile,
+ remoteFile, mergedFile, null);
+ // prepare the environment
+ Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo,
+ localFile, remoteFile, mergedFile, null);
+ boolean trust = config.isTrustExitCode();
+ if (trustExitCode != BooleanTriState.UNSET) {
+ trust = trustExitCode == BooleanTriState.TRUE;
+ }
+ // execute the tool
+ CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
+ return cmdExec.run(command, repo.getWorkTree(), env);
+ } catch (IOException | InterruptedException e) {
+ throw new ToolException(e);
+ } finally {
+ localFile.cleanTemporaries();
+ remoteFile.cleanTemporaries();
+ mergedFile.cleanTemporaries();
+ }
}
/**
@@ -103,41 +131,64 @@ public class DiffTools {
*/
public String getDefaultToolName(BooleanTriState gui) {
return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$
- : "my_default_toolname"; //$NON-NLS-1$
+ : config.getDefaultToolName();
}
/**
* @return is interactive (config prompt enabled) ?
*/
public boolean isInteractive() {
- return false;
+ return config.isPrompt();
+ }
+
+ private ExternalDiffTool guessTool(String toolName, BooleanTriState gui)
+ throws ToolException {
+ if (StringUtils.isEmptyOrNull(toolName)) {
+ toolName = getDefaultToolName(gui);
+ }
+ ExternalDiffTool tool = getTool(toolName);
+ if (tool == null) {
+ throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$
+ }
+ return tool;
+ }
+
+ private ExternalDiffTool getTool(final String name) {
+ ExternalDiffTool tool = userDefinedTools.get(name);
+ if (tool == null) {
+ tool = predefinedTools.get(name);
+ }
+ return tool;
}
- private void setupPredefinedTools() {
- predefinedTools = new TreeMap<>();
+ private static Map<String, ExternalDiffTool> setupPredefinedTools() {
+ Map<String, ExternalDiffTool> tools = new TreeMap<>();
for (CommandLineDiffTool tool : CommandLineDiffTool.values()) {
- predefinedTools.put(tool.name(), new PreDefinedDiffTool(tool));
+ tools.put(tool.name(), new PreDefinedDiffTool(tool));
}
+ return tools;
}
- private void setupUserDefinedTools() {
- userDefinedTools = new TreeMap<>();
- Map<String, ExternalDiffTool> userTools = config.getTools();
+ private static Map<String, ExternalDiffTool> setupUserDefinedTools(
+ DiffToolConfig cfg, Map<String, ExternalDiffTool> predefTools) {
+ Map<String, ExternalDiffTool> tools = new TreeMap<>();
+ Map<String, ExternalDiffTool> userTools = cfg.getTools();
for (String name : userTools.keySet()) {
ExternalDiffTool userTool = userTools.get(name);
// if difftool.<name>.cmd is defined we have user defined tool
if (userTool.getCommand() != null) {
- userDefinedTools.put(name, userTool);
+ tools.put(name, userTool);
} else if (userTool.getPath() != null) {
// if difftool.<name>.path is defined we just overload the path
// of predefined tool
- PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefinedTools
+ PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefTools
.get(name);
if (predefTool != null) {
predefTool.setPath(userTool.getPath());
}
}
}
+ return tools;
}
}
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
new file mode 100644
index 0000000000..0c3ddf9afe
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java
@@ -0,0 +1,33 @@
+/*
+ * 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 merge tool interface.
+ */
+public interface ExternalMergeTool extends ExternalDiffTool {
+
+ /**
+ * @return the tool "trust exit code" option
+ */
+ 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/ExternalToolUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java
new file mode 100644
index 0000000000..3efb90c490
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018-2021, 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 java.util.TreeMap;
+import java.io.IOException;
+import java.util.Map;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Utilities for diff- and merge-tools.
+ */
+public class ExternalToolUtils {
+
+ /**
+ * Prepare command for execution.
+ *
+ * @param command
+ * the input "command" string
+ * @param localFile
+ * the local file (ours)
+ * @param remoteFile
+ * the remote file (theirs)
+ * @param mergedFile
+ * the merged file (worktree)
+ * @param baseFile
+ * the base file (can be null)
+ * @return the prepared (with replaced variables) command string
+ * @throws IOException
+ */
+ public static String prepareCommand(String command, FileElement localFile,
+ FileElement remoteFile, FileElement mergedFile,
+ FileElement baseFile) throws IOException {
+ command = localFile.replaceVariable(command);
+ command = remoteFile.replaceVariable(command);
+ command = mergedFile.replaceVariable(command);
+ if (baseFile != null) {
+ command = baseFile.replaceVariable(command);
+ }
+ return command;
+ }
+
+ /**
+ * Prepare environment needed for execution.
+ *
+ * @param repo
+ * the repository
+ * @param localFile
+ * the local file (ours)
+ * @param remoteFile
+ * the remote file (theirs)
+ * @param mergedFile
+ * the merged file (worktree)
+ * @param baseFile
+ * the base file (can be null)
+ * @return the environment map with variables and values (file paths)
+ * @throws IOException
+ */
+ public static Map<String, String> prepareEnvironment(Repository repo,
+ FileElement localFile, FileElement remoteFile,
+ FileElement mergedFile, FileElement baseFile) throws IOException {
+ Map<String, String> env = new TreeMap<>();
+ env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath());
+ localFile.addToEnv(env);
+ remoteFile.addToEnv(env);
+ mergedFile.addToEnv(env);
+ if (baseFile != null) {
+ baseFile.addToEnv(env);
+ }
+ return env;
+ }
+
+}
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
new file mode 100644
index 0000000000..5902c1e1b8
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2018-2021, 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 java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+
+import org.eclipse.jgit.diff.DiffEntry;
+
+/**
+ * The element used as left or right file for compare.
+ *
+ */
+public class FileElement {
+
+ /**
+ * The file element type.
+ *
+ */
+ public enum Type {
+ /**
+ * The local file element (ours).
+ */
+ LOCAL,
+ /**
+ * The remote file element (theirs).
+ */
+ REMOTE,
+ /**
+ * The merged file element (path in worktree).
+ */
+ MERGED,
+ /**
+ * The base file element (of ours and theirs).
+ */
+ BASE,
+ /**
+ * The backup file element (copy of merged / conflicted).
+ */
+ BACKUP
+ }
+
+ private final String path;
+
+ private final Type type;
+
+ private InputStream stream;
+
+ private File tempFile;
+
+ /**
+ * Creates file element for path.
+ *
+ * @param path
+ * the file path
+ * @param type
+ * the element type
+ */
+ public FileElement(String path, Type type) {
+ this(path, type, null, null);
+ }
+
+ /**
+ * Creates file element for path.
+ *
+ * @param path
+ * the file path
+ * @param type
+ * the element type
+ * @param tempFile
+ * the temporary file to be used (can be null and will be created
+ * then)
+ * @param stream
+ * the object stream to load instead of file
+ */
+ public FileElement(String path, Type type, File tempFile,
+ InputStream stream) {
+ this.path = path;
+ this.type = type;
+ this.tempFile = tempFile;
+ this.stream = stream;
+ }
+
+ /**
+ * @return the file path
+ */
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * @return the element type
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Return a temporary file within passed directory and fills it with stream
+ * if valid.
+ *
+ * @param directory
+ * the 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 directory, String midName) throws IOException {
+ if ((tempFile != null) && (stream == null)) {
+ return tempFile;
+ }
+ tempFile = getTempFile(path, directory, midName);
+ return copyFromStream(tempFile, stream);
+ }
+
+ /**
+ * Return a real file from work tree or a temporary file with content if
+ * stream is valid or if path is "/dev/null"
+ *
+ * @return the object stream
+ * @throws IOException
+ */
+ public File getFile() throws IOException {
+ if ((tempFile != null) && (stream == null)) {
+ return tempFile;
+ }
+ File file = new File(path);
+ // if we have a stream or file is missing ("/dev/null") then create
+ // temporary file
+ if ((stream != null) || isNullPath()) {
+ tempFile = getTempFile(file);
+ return copyFromStream(tempFile, stream);
+ }
+ return file;
+ }
+
+ /**
+ * Check if path id "/dev/null"
+ *
+ * @return true if path is "/dev/null"
+ */
+ public boolean isNullPath() {
+ return path.equals(DiffEntry.DEV_NULL);
+ }
+
+ /**
+ * Create temporary file in given or system temporary directory
+ *
+ * @param directory
+ * the directory for the file (can be null); if null system
+ * temporary directory is used
+ * @return temporary file in directory or in the system temporary directory
+ * @throws IOException
+ */
+ public File createTempFile(File directory) throws IOException {
+ if (tempFile == null) {
+ File file = new File(path);
+ if (directory != null) {
+ tempFile = getTempFile(file, directory, type.name());
+ } else {
+ tempFile = getTempFile(file);
+ }
+ }
+ return tempFile;
+ }
+
+ private static File getTempFile(File file) throws IOException {
+ return File.createTempFile(".__", "__" + file.getName()); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ private static File getTempFile(File file, File directory, String midName)
+ throws IOException {
+ String[] fileNameAndExtension = splitBaseFileNameAndExtension(file);
+ return File.createTempFile(
+ fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
+ fileNameAndExtension[1], directory);
+ }
+
+ private static File getTempFile(String path, File directory, String midName)
+ throws IOException {
+ return getTempFile(new File(path), directory, midName);
+ }
+
+ /**
+ * Delete and invalidate temporary file if necessary.
+ */
+ public void cleanTemporaries() {
+ if (tempFile != null && tempFile.exists())
+ tempFile.delete();
+ tempFile = null;
+ }
+
+ private static File copyFromStream(File file, final InputStream stream)
+ throws IOException, FileNotFoundException {
+ if (stream != null) {
+ try (OutputStream outStream = new FileOutputStream(file)) {
+ 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();
+ }
+ }
+ return file;
+ }
+
+ private static String[] splitBaseFileNameAndExtension(File file) {
+ String[] result = new String[2];
+ result[0] = file.getName();
+ result[1] = ""; //$NON-NLS-1$
+ int idx = result[0].lastIndexOf("."); //$NON-NLS-1$
+ // if "." was found (>-1) and last-index is not first char (>0), then
+ // split (same behavior like cgit)
+ if (idx > 0) {
+ result[1] = result[0].substring(idx, result[0].length());
+ result[0] = result[0].substring(0, idx);
+ }
+ return result;
+ }
+
+ /**
+ * Replace variable in input
+ *
+ * @param input
+ * the input string
+ * @return the replaced input string
+ * @throws IOException
+ */
+ public String replaceVariable(String input) throws IOException {
+ return input.replace("$" + type.name(), getFile().getPath()); //$NON-NLS-1$
+ }
+
+ /**
+ * Add variable to environment map.
+ *
+ * @param env
+ * the environment where this element should be added
+ * @throws IOException
+ */
+ public void addToEnv(Map<String, String> env) throws IOException {
+ env.put(type.name(), getFile().getPath());
+ }
+
+}
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
new file mode 100644
index 0000000000..9be20b75ad
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java
@@ -0,0 +1,147 @@
+/*
+ * 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 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.internal.BooleanTriState;
+
+/**
+ * Keeps track of difftool related configuration options.
+ */
+public class MergeToolConfig {
+
+ /** Key for {@link Config#get(SectionParser)}. */
+ public static final Config.SectionParser<MergeToolConfig> KEY = MergeToolConfig::new;
+
+ private final String toolName;
+
+ private final String guiToolName;
+
+ private final boolean prompt;
+
+ private final boolean keepBackup;
+
+ private final boolean keepTemporaries;
+
+ private final boolean writeToTemp;
+
+ private final Map<String, ExternalMergeTool> tools;
+
+ private MergeToolConfig(Config rc) {
+ 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(CONFIG_MERGETOOL_SECTION);
+ for (String name : subsections) {
+ 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(CONFIG_MERGETOOL_SECTION, name,
+ CONFIG_KEY_TRUST_EXIT_CODE);
+ if (trustStr != null) {
+ trustExitCode = Boolean.valueOf(trustStr).booleanValue()
+ ? BooleanTriState.TRUE
+ : BooleanTriState.FALSE;
+ } else {
+ trustExitCode = BooleanTriState.UNSET;
+ }
+ if ((cmd != null) || (path != null)) {
+ tools.put(name, new UserDefinedMergeTool(name, path, cmd,
+ trustExitCode));
+ }
+ }
+ }
+
+ /**
+ * @return the default merge tool name (merge.tool)
+ */
+ public String getDefaultToolName() {
+ return toolName;
+ }
+
+ /**
+ * @return the default GUI merge tool name (merge.guitool)
+ */
+ public String getDefaultGuiToolName() {
+ return guiToolName;
+ }
+
+ /**
+ * @return the merge tool "prompt" option (mergetool.prompt)
+ */
+ public boolean isPrompt() {
+ return prompt;
+ }
+
+ /**
+ * @return the tool "keep backup" option
+ */
+ public boolean isKeepBackup() {
+ return keepBackup;
+ }
+
+ /**
+ * @return the tool "keepTemporaries" option
+ */
+ public boolean isKeepTemporaries() {
+ return keepTemporaries;
+ }
+
+ /**
+ * @return the tool "write to temp" option
+ */
+ public boolean isWriteToTemp() {
+ return writeToTemp;
+ }
+
+ /**
+ * @return the tools map
+ */
+ public Map<String, ExternalMergeTool> getTools() {
+ return tools;
+ }
+
+ /**
+ * @return the tool names
+ */
+ public Set<String> getToolNames() {
+ return tools.keySet();
+ }
+
+}
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
new file mode 100644
index 0000000000..9a2a8304eb
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
@@ -0,0 +1,259 @@
+/*
+ * 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 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;
+
+import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
+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 {
+
+ Repository repo;
+
+ private final MergeToolConfig config;
+
+ private final Map<String, ExternalMergeTool> predefinedTools;
+
+ private final Map<String, ExternalMergeTool> userDefinedTools;
+
+ /**
+ * @param repo
+ * the repository
+ */
+ public MergeTools(Repository repo) {
+ this.repo = repo;
+ config = repo.getConfig().get(MergeToolConfig.KEY);
+ predefinedTools = setupPredefinedTools();
+ userDefinedTools = setupUserDefinedTools(config, predefinedTools);
+ }
+
+ /**
+ * @param localFile
+ * the local file element
+ * @param remoteFile
+ * the remote file element
+ * @param mergedFile
+ * the merged file element
+ * @param baseFile
+ * the base file element (can be null)
+ * @param tempDir
+ * the temporary directory (needed for backup and auto-remove,
+ * can be null)
+ * @param toolName
+ * the selected tool name (can be null)
+ * @param prompt
+ * the prompt option
+ * @param gui
+ * the GUI option
+ * @return the execution result from tool
+ * @throws ToolException
+ */
+ public ExecutionResult merge(FileElement localFile, FileElement remoteFile,
+ FileElement mergedFile, FileElement baseFile, File tempDir,
+ String toolName, BooleanTriState prompt, BooleanTriState gui)
+ throws ToolException {
+ ExternalMergeTool tool = guessTool(toolName, gui);
+ FileElement backup = null;
+ ExecutionResult result = null;
+ try {
+ File workingDir = repo.getWorkTree();
+ // create additional backup file (copy worktree file)
+ backup = createBackupFile(mergedFile.getPath(),
+ tempDir != null ? tempDir : workingDir);
+ // prepare the command (replace the file paths)
+ boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
+ String command = ExternalToolUtils.prepareCommand(
+ tool.getCommand(baseFile != null), localFile, remoteFile,
+ mergedFile, baseFile);
+ // prepare the environment
+ Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo,
+ localFile, remoteFile, mergedFile, baseFile);
+ // execute the tool
+ CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
+ result = cmdExec.run(command, workingDir, env);
+ // keep backup as .orig file
+ if (backup != null) {
+ keepBackupFile(mergedFile.getPath(), backup);
+ }
+ return result;
+ } catch (IOException | InterruptedException e) {
+ throw new ToolException(e);
+ } finally {
+ // 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 FileElement createBackupFile(String filePath, File parentDir)
+ throws IOException {
+ FileElement backup = null;
+ Path path = Paths.get(filePath);
+ if (Files.exists(path)) {
+ backup = new FileElement(filePath, Type.BACKUP);
+ Files.copy(path, backup.createTempFile(parentDir).toPath(),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+ return backup;
+ }
+
+ /**
+ * @return the created temporary directory if (mergetol.writeToTemp == true)
+ * or null if not configured or false.
+ * @throws IOException
+ */
+ public File createTempDirectory() throws IOException {
+ return config.isWriteToTemp()
+ ? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$
+ : null;
+ }
+
+ /**
+ * @return the tool names
+ */
+ public Set<String> getToolNames() {
+ return config.getToolNames();
+ }
+
+ /**
+ * @return the user defined tools
+ */
+ public Map<String, ExternalMergeTool> getUserDefinedTools() {
+ return userDefinedTools;
+ }
+
+ /**
+ * @return the available predefined tools
+ */
+ public Map<String, ExternalMergeTool> getAvailableTools() {
+ return predefinedTools;
+ }
+
+ /**
+ * @return the NOT available predefined tools
+ */
+ public Map<String, ExternalMergeTool> getNotAvailableTools() {
+ return new TreeMap<>();
+ }
+
+ /**
+ * @param gui
+ * use the diff.guitool setting ?
+ * @return the default tool name
+ */
+ public String getDefaultToolName(BooleanTriState gui) {
+ return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$
+ : config.getDefaultToolName();
+ }
+
+ /**
+ * @return is interactive (config prompt enabled) ?
+ */
+ public boolean isInteractive() {
+ return config.isPrompt();
+ }
+
+ 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 ExternalMergeTool getTool(final String name) {
+ ExternalMergeTool tool = userDefinedTools.get(name);
+ if (tool == null) {
+ tool = predefinedTools.get(name);
+ }
+ return tool;
+ }
+
+ 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()) {
+ 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/PreDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java
index 1c69fb4911..092cb605be 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java
@@ -46,17 +46,6 @@ public class PreDefinedDiffTool extends UserDefinedDiffTool {
*/
@Override
public void setPath(String path) {
- // handling of spaces in path
- if (path.contains(" ")) { //$NON-NLS-1$
- // add quotes before if needed
- if (!path.startsWith("\"")) { //$NON-NLS-1$
- path = "\"" + path; //$NON-NLS-1$
- }
- // add quotes after if needed
- if (!path.endsWith("\"")) { //$NON-NLS-1$
- path = path + "\""; //$NON-NLS-1$
- }
- }
super.setPath(path);
}
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 0000000000..2c64c16667
--- /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/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
new file mode 100644
index 0000000000..27f7d12e66
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2018-2021, 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.util.FS.ExecutionResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tool exception for differentiation.
+ *
+ */
+public class ToolException extends Exception {
+
+ private final static Logger LOG = LoggerFactory
+ .getLogger(ToolException.class);
+
+ private final ExecutionResult result;
+
+ private final boolean commandExecutionError;
+
+ /**
+ * the serial version UID
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ */
+ public ToolException() {
+ this(null, null, false);
+ }
+
+ /**
+ * @param message
+ * the exception message
+ */
+ public ToolException(String message) {
+ this(message, null, false);
+ }
+
+ /**
+ * @param message
+ * the exception message
+ * @param result
+ * the execution result
+ * @param commandExecutionError
+ * is command execution error happened ?
+ */
+ public ToolException(String message, ExecutionResult result,
+ boolean commandExecutionError) {
+ super(message);
+ this.result = result;
+ this.commandExecutionError = commandExecutionError;
+ }
+
+ /**
+ * @param message
+ * the exception message
+ * @param cause
+ * the cause for throw
+ */
+ public ToolException(String message, Throwable cause) {
+ super(message, cause);
+ result = null;
+ commandExecutionError = false;
+ }
+
+ /**
+ * @param cause
+ * the cause for throw
+ */
+ public ToolException(Throwable cause) {
+ super(cause);
+ result = null;
+ commandExecutionError = false;
+ }
+
+ /**
+ * @return true if result is valid, false else
+ */
+ public boolean isResult() {
+ return result != null;
+ }
+
+ /**
+ * @return the execution result
+ */
+ public ExecutionResult getResult() {
+ return result;
+ }
+
+ /**
+ * @return true if command execution error appears, false otherwise
+ */
+ public boolean isCommandExecutionError() {
+ return commandExecutionError;
+ }
+
+ /**
+ * @return the result Stderr
+ */
+ public String getResultStderr() {
+ try {
+ return new String(result.getStderr().toByteArray());
+ } catch (Exception e) {
+ LOG.warn("Failed to retrieve standard error output", e); //$NON-NLS-1$
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+ /**
+ * @return the result Stdout
+ */
+ public String getResultStdout() {
+ try {
+ return new String(result.getStdout().toByteArray());
+ } catch (Exception e) {
+ LOG.warn("Failed to retrieve standard output", e); //$NON-NLS-1$
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+}
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
new file mode 100644
index 0000000000..1dd2f0d793
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java
@@ -0,0 +1,69 @@
+/*
+ * 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 user-defined merge tool.
+ */
+public class UserDefinedMergeTool extends UserDefinedDiffTool
+ implements ExternalMergeTool {
+
+ /**
+ * the merge tool "trust exit code" option
+ */
+ private BooleanTriState trustExitCode;
+
+ /**
+ * Creates the merge tool
+ *
+ * @param name
+ * the name
+ * @param path
+ * the path
+ * @param cmd
+ * the command
+ * @param trustExitCode
+ * the "trust exit code" option
+ */
+ public UserDefinedMergeTool(String name, String path, String cmd,
+ BooleanTriState trustExitCode) {
+ super(name, path, cmd);
+ this.trustExitCode = trustExitCode;
+ }
+ /**
+ * @return the "trust exit code" flag
+ */
+ @Override
+ 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 8ad32d41c7..2342cad0d7 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;
/**
@@ -31,14 +32,14 @@ public final class ConfigConstants {
public static final String CONFIG_DIFF_SECTION = "diff";
/**
- * The "tool" key within "diff" section
+ * The "tool" key within "diff" or "merge" section
*
* @since 6.1
*/
public static final String CONFIG_KEY_TOOL = "tool";
/**
- * The "guitool" key within "diff" section
+ * The "guitool" key within "diff" or "merge" section
*
* @since 6.1
*/
@@ -52,21 +53,21 @@ public final class ConfigConstants {
public static final String CONFIG_DIFFTOOL_SECTION = "difftool";
/**
- * The "prompt" key within "difftool" section
+ * The "prompt" key within "difftool" or "mergetool" section
*
* @since 6.1
*/
public static final String CONFIG_KEY_PROMPT = "prompt";
/**
- * The "trustExitCode" key within "difftool" section
+ * The "trustExitCode" key within "difftool" or "mergetool.<name>." section
*
* @since 6.1
*/
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
*/
@@ -124,6 +125,34 @@ public final class ConfigConstants {
public static final String CONFIG_MERGE_SECTION = "merge";
/**
+ * The "mergetool" section
+ *
+ * @since 6.2
+ */
+ public static final String CONFIG_MERGETOOL_SECTION = "mergetool";
+
+ /**
+ * The "keepBackup" key within "mergetool" section
+ *
+ * @since 6.2
+ */
+ public static final String CONFIG_KEY_KEEP_BACKUP = "keepBackup";
+
+ /**
+ * The "keepTemporaries" key within "mergetool" section
+ *
+ * @since 6.2
+ */
+ public static final String CONFIG_KEY_KEEP_TEMPORARIES = "keepTemporaries";
+
+ /**
+ * The "writeToTemp" key within "mergetool" section
+ *
+ * @since 6.2
+ */
+ public static final String CONFIG_KEY_WRITE_TO_TEMP = "writeToTemp";
+
+ /**
* The "filter" section
* @since 4.6
*/