]> source.dussan.org Git - jgit.git/commitdiff
Adapt diff- and merge tool code for PGM and EGit usage 75/140275/45
authorAndre Bossert <andre.bossert@siemens.com>
Sun, 19 Jan 2020 19:57:23 +0000 (20:57 +0100)
committerAndrey Loskutov <loskutov@gmx.de>
Thu, 2 Jun 2022 08:36:31 +0000 (10:36 +0200)
see: https://git-scm.com/docs/git-mergetool

* DiffTools and MergeTools
  * store FS, gitDir and workTree for usage without
git repository (for EGit preferences)
  * add getUserDefinedToolNames() and getPredefinedToolNames()
  * replace getToolNames() with getAllToolNames() that combines the two
lists and put default tool name (diff.tool or merge.tool) as first
element (for EGit preferences)
* FileElement: refactoring of getFile() and friends to have midName
(LOCAL, REMOTE etc.) always added to the temp file name (also for EGit)
* FileElement: added directory attribute that is used in getFile() to
return path with workDir as parent
* DiffTool and MergeTool
  * added errw.flush(), because sometimes stderr is not printed in case
of die()
  * print e.getMessage() always to stderr
* Moved toolname and prompt logic into managers
* Exported internal packages required for egit.ui

Bug: 356832
Change-Id: I71e7f4dc362169a7612ca4f6546a021bc4b2b5f4
Signed-off-by: Andre Bossert <andre.bossert@siemens.com>
Signed-off-by: Tim Neumann <Tim.Neumann@advantest.com>
16 files changed:
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java
org.eclipse.jgit/META-INF/MANIFEST.MF
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java [new file with mode: 0644]

index a258821f0cabd7db608fcdd2c1145f666503b26c..ce4c004c09b434d821932e454ae185696c18a15e 100644 (file)
@@ -16,11 +16,14 @@ 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.File;
 import java.io.InputStream;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Pattern;
 
 import org.eclipse.jgit.internal.diffmergetool.DiffTools;
 import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
@@ -42,6 +45,58 @@ public class DiffToolTest extends ToolTestCase {
                configureEchoTool(TOOL_NAME);
        }
 
+       @Test(expected = Die.class)
+       public void testUndefinedTool() throws Exception {
+               String toolName = "undefined";
+               String[] conflictingFilenames = createUnstagedChanges();
+
+               List<String> expectedErrors = new ArrayList<>();
+               for (String changedFilename : conflictingFilenames) {
+                       expectedErrors.add("External diff tool is not defined: " + toolName);
+                       expectedErrors.add("compare of " + changedFilename + " failed");
+               }
+
+               runAndCaptureUsingInitRaw(expectedErrors, DIFF_TOOL, "--no-prompt",
+                               "--tool", toolName);
+               fail("Expected exception to be thrown due to undefined external tool");
+       }
+
+       @Test(expected = Die.class)
+       public void testUserToolWithCommandNotFoundError() throws Exception {
+               String toolName = "customTool";
+
+               int errorReturnCode = 127; // command not found
+               String command = "exit " + errorReturnCode;
+
+               StoredConfig config = db.getConfig();
+               config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
+                               command);
+
+               createMergeConflict();
+               runAndCaptureUsingInitRaw(DIFF_TOOL, "--no-prompt", "--tool", toolName);
+
+               fail("Expected exception to be thrown due to external tool exiting with error code: "
+                               + errorReturnCode);
+       }
+
+       @Test
+       public void testEmptyToolName() throws Exception {
+               String emptyToolName = "";
+
+               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,
+                               emptyToolName);
+
+               createUnstagedChanges();
+
+               String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
+               String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
+               runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput), DIFF_TOOL,
+                               "--no-prompt");
+       }
+
        @Test
        public void testToolWithPrompt() throws Exception {
                String[] inputLines = {
@@ -138,12 +193,12 @@ public class DiffToolTest extends ToolTestCase {
        @Test
        public void testToolCached() throws Exception {
                String[] conflictingFilenames = createStagedChanges();
-               String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
+               Pattern[] expectedOutput = getExpectedCachedToolOutputNoPrompt(conflictingFilenames);
 
                String[] options = { "--cached", "--staged", };
 
                for (String option : options) {
-                       assertArrayOfLinesEquals("Incorrect output for option: " + option,
+                       assertArrayOfMatchingLines("Incorrect output for option: " + option,
                                        expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
                                                        option, "--tool", TOOL_NAME));
                }
@@ -213,43 +268,76 @@ public class DiffToolTest extends ToolTestCase {
                                String.valueOf(false));
        }
 
-       private static String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) {
+       private 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;
+                       Path fullPath = getFullPath(newPath);
+                       expectedToolOutput[i] = fullPath.toString();
                }
                return expectedToolOutput;
        }
 
-       private static String[] getExpectedCompareOutput(String[] conflictingFilenames) {
+       private Pattern[] getExpectedCachedToolOutputNoPrompt(String[] conflictingFilenames) {
+               String tmpDir = System.getProperty("java.io.tmpdir");
+               if (tmpDir.endsWith(File.separator)) {
+                       tmpDir = tmpDir.substring(0, tmpDir.length() - 1);
+               }
+               Pattern emptyPattern = Pattern.compile("");
+               List<Pattern> expectedToolOutput = new ArrayList<>();
+               for (int i = 0; i < conflictingFilenames.length; ++i) {
+                       String changedFilename = conflictingFilenames[i];
+                       Path fullPath = getFullPath(changedFilename);
+                       String filename = fullPath.getFileName().toString();
+                       String regexp = tmpDir + File.separatorChar + filename
+                                       + "_REMOTE_.*";
+                       Pattern pattern = Pattern.compile(regexp);
+                       expectedToolOutput.add(pattern);
+                       expectedToolOutput.add(emptyPattern);
+               }
+               expectedToolOutput.add(emptyPattern);
+               return expectedToolOutput.toArray(new Pattern[0]);
+       }
+
+       private String[] getExpectedCompareOutput(String[] conflictingFilenames) {
                List<String> expected = new ArrayList<>();
                int n = conflictingFilenames.length;
                for (int i = 0; i < n; ++i) {
-                       String newPath = conflictingFilenames[i];
+                       String changedFilename = conflictingFilenames[i];
                        expected.add(
-                                       "Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'");
+                                       "Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
+                                                       + "'");
                        expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
-                       expected.add(newPath);
+                       Path fullPath = getFullPath(changedFilename);
+                       expected.add(fullPath.toString());
                }
                return expected.toArray(new String[0]);
        }
 
-       private static String[] getExpectedAbortOutput(String[] conflictingFilenames,
+       private 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];
+                       String changedFilename = conflictingFilenames[i];
                        expected.add(
-                                       "Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'");
+                                       "Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
+                                                       + "'");
                        expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
                        if (i == abortIndex) {
                                break;
                        }
-                       expected.add(newPath);
+                       Path fullPath = getFullPath(changedFilename);
+                       expected.add(fullPath.toString());
                }
                return expected.toArray(new String[0]);
        }
+
+       private static String getEchoCommand() {
+               /*
+                * use 'REMOTE' placeholder, as it will be replaced by a file path
+                * within the repository.
+                */
+               return "(echo \"$REMOTE\")";
+       }
 }
index e9d559e70e088fb19d56315ef22844009ea37f2b..1236dd30d3a306de9144b33dc8538599201fb8f3 100644 (file)
@@ -14,8 +14,10 @@ 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 static org.junit.Assert.fail;
 
 import java.io.InputStream;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -41,6 +43,58 @@ public class MergeToolTest extends ToolTestCase {
                configureEchoTool(TOOL_NAME);
        }
 
+       @Test
+       public void testUndefinedTool() throws Exception {
+               String toolName = "undefined";
+               String[] conflictingFilenames = createMergeConflict();
+
+               List<String> expectedErrors = new ArrayList<>();
+               for (String conflictingFilename : conflictingFilenames) {
+                       expectedErrors.add("External merge tool is not defined: " + toolName);
+                       expectedErrors.add("merge of " + conflictingFilename + " failed");
+               }
+
+               runAndCaptureUsingInitRaw(expectedErrors, MERGE_TOOL,
+                               "--no-prompt", "--tool", toolName);
+       }
+
+       @Test(expected = Die.class)
+       public void testUserToolWithCommandNotFoundError() throws Exception {
+               String toolName = "customTool";
+
+               int errorReturnCode = 127; // command not found
+               String command = "exit " + errorReturnCode;
+
+               StoredConfig config = db.getConfig();
+               config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+                               command);
+
+               createMergeConflict();
+               runAndCaptureUsingInitRaw(MERGE_TOOL, "--no-prompt", "--tool",
+                               toolName);
+
+               fail("Expected exception to be thrown due to external tool exiting with error code: "
+                               + errorReturnCode);
+       }
+
+       @Test
+       public void testEmptyToolName() throws Exception {
+               String emptyToolName = "";
+
+               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,
+                               emptyToolName);
+
+               createMergeConflict();
+
+               String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
+               String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
+               runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput),
+                               MERGE_TOOL, "--no-prompt");
+       }
+
        @Test
        public void testAbortMerge() throws Exception {
                String[] inputLines = {
@@ -220,7 +274,7 @@ public class MergeToolTest extends ToolTestCase {
                                String.valueOf(false));
        }
 
-       private static String[] getExpectedMergeConflictOutputNoPrompt(
+       private String[] getExpectedMergeConflictOutputNoPrompt(
                        String[] conflictFilenames) {
                List<String> expected = new ArrayList<>();
                expected.add("Merging:");
@@ -232,7 +286,8 @@ public class MergeToolTest extends ToolTestCase {
                                        + "':");
                        expected.add("{local}: modified file");
                        expected.add("{remote}: modified file");
-                       expected.add(conflictFilename);
+                       Path filePath = getFullPath(conflictFilename);
+                       expected.add(filePath.toString());
                        expected.add(conflictFilename + " seems unchanged.");
                }
                return expected.toArray(new String[0]);
@@ -257,7 +312,7 @@ public class MergeToolTest extends ToolTestCase {
                return expected.toArray(new String[0]);
        }
 
-       private static String[] getExpectedAbortMergeOutput(
+       private String[] getExpectedAbortMergeOutput(
                        String[] conflictFilenames, int abortIndex) {
                List<String> expected = new ArrayList<>();
                expected.add("Merging:");
@@ -274,8 +329,9 @@ public class MergeToolTest extends ToolTestCase {
                                        "Normal merge conflict for '" + conflictFilename + "':");
                        expected.add("{local}: modified file");
                        expected.add("{remote}: modified file");
+                       Path fullPath = getFullPath(conflictFilename);
                        expected.add("Hit return to start merge resolution tool ("
-                                       + TOOL_NAME + "): " + conflictFilename);
+                                       + TOOL_NAME + "): " + fullPath);
                        expected.add(conflictFilename + " seems unchanged.");
                        expected.add("Was the merge successful [y/n]?");
                        if (i < conflictFilenames.length - 1) {
@@ -286,7 +342,7 @@ public class MergeToolTest extends ToolTestCase {
                return expected.toArray(new String[0]);
        }
 
-       private static String[] getExpectedMergeConflictOutput(
+       private String[] getExpectedMergeConflictOutput(
                        String[] conflictFilenames) {
                List<String> expected = new ArrayList<>();
                expected.add("Merging:");
@@ -299,8 +355,9 @@ public class MergeToolTest extends ToolTestCase {
                                        + "':");
                        expected.add("{local}: modified file");
                        expected.add("{remote}: modified file");
+                       Path filePath = getFullPath(conflictFilename);
                        expected.add("Hit return to start merge resolution tool ("
-                                       + TOOL_NAME + "): " + conflictFilename);
+                                       + TOOL_NAME + "): " + filePath);
                        expected.add(conflictFilename + " seems unchanged.");
                        expected.add("Was the merge successful [y/n]?");
                        if (i < conflictFilenames.length - 1) {
@@ -327,4 +384,12 @@ public class MergeToolTest extends ToolTestCase {
                }
                return expected.toArray(new String[0]);
        }
+
+       private static String getEchoCommand() {
+               /*
+                * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be
+                * replaced with full paths to a temporary file during some of the tests
+                */
+               return "(echo \"$MERGED\")";
+       }
 }
index 933f19bcc44aed7ffdb613f3e0debebcaa96f1e6..a3c41f0feddcd2a073b568c82d25944c8acf9e91 100644 (file)
 package org.eclipse.jgit.pgm;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import java.io.ByteArrayInputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import org.eclipse.jgit.api.Git;
@@ -29,6 +34,7 @@ import org.eclipse.jgit.treewalk.FileTreeIterator;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.junit.Before;
 import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
 
 /**
  * Base test case for the {@code difftool} and {@code mergetool} commands.
@@ -64,8 +70,23 @@ public abstract class ToolTestCase extends CLIRepositoryTestCase {
                return runAndCaptureUsingInitRaw(inputStream, args);
        }
 
+       protected String[] runAndCaptureUsingInitRaw(
+                       List<String> expectedErrorOutput, String... args) throws Exception {
+               InputStream inputStream = null; // no input stream
+               return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput,
+                               args);
+       }
+
        protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
                        String... args) throws Exception {
+               List<String> expectedErrorOutput = Collections.emptyList();
+               return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput,
+                               args);
+       }
+
+       protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
+                       List<String> expectedErrorOutput, String... args)
+                       throws CmdLineException, Exception, IOException {
                CLIGitCommand.Result result = new CLIGitCommand.Result();
 
                GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
@@ -86,7 +107,7 @@ public abstract class ToolTestCase extends CLIRepositoryTestCase {
                                .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());
+                               expectedErrorOutput.toString(), errLines.toString());
 
                return result.outLines().toArray(new String[0]);
        }
@@ -177,6 +198,13 @@ public abstract class ToolTestCase extends CLIRepositoryTestCase {
                return changes;
        }
 
+       protected Path getFullPath(String repositoryFilename) {
+               Path dotGitPath = db.getDirectory().toPath();
+               Path repositoryRoot = dotGitPath.getParent();
+               Path repositoryFilePath = repositoryRoot.resolve(repositoryFilename);
+               return repositoryFilePath;
+       }
+
        protected static InputStream createInputStream(String[] inputLines) {
                return createInputStream(Arrays.asList(inputLines));
        }
@@ -192,11 +220,24 @@ public abstract class ToolTestCase extends CLIRepositoryTestCase {
                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\")";
+       protected static void assertArrayOfMatchingLines(String failMessage,
+                       Pattern[] expected, String[] actual) {
+               assertEquals(failMessage + System.lineSeparator()
+                               + "Expected and actual lines count don't match. Expected: "
+                               + Arrays.asList(expected) + ", actual: "
+                               + Arrays.asList(actual), expected.length, actual.length);
+               int n = expected.length;
+               for (int i = 0; i < n; ++i) {
+                       Pattern expectedPattern = expected[i];
+                       String actualLine = actual[i];
+                       Matcher matcher = expectedPattern.matcher(actualLine);
+                       boolean matches = matcher.matches();
+                       assertTrue(failMessage + System.lineSeparator() + "Line " + i + " '"
+                                       + actualLine + "' doesn't match expected pattern: "
+                                       + expectedPattern + System.lineSeparator() + "Expected: "
+                                       + Arrays.asList(expected) + ", actual: "
+                                       + Arrays.asList(actual),
+                                       matches);
+               }
        }
 }
index e5a3c53e3f6465a3aec996bae8df37f0720823d9..3e6042afee399739291aacc5e7fa66dfe4ed4dbd 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.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
@@ -22,6 +23,7 @@ import java.nio.charset.StandardCharsets;
 import java.text.MessageFormat;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.diff.ContentSource;
@@ -40,6 +42,7 @@ import org.eclipse.jgit.errors.RevisionSyntaxException;
 import org.eclipse.jgit.internal.diffmergetool.DiffTools;
 import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
 import org.eclipse.jgit.internal.diffmergetool.FileElement;
+import org.eclipse.jgit.internal.diffmergetool.PromptContinueHandler;
 import org.eclipse.jgit.internal.diffmergetool.ToolException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
@@ -60,7 +63,6 @@ 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.FS.ExecutionResult;
-import org.eclipse.jgit.util.StringUtils;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -76,9 +78,13 @@ class DiffTool extends TextBuiltin {
        @Argument(index = 1, metaVar = "metaVar_treeish")
        private AbstractTreeIterator newTree;
 
+       private Optional<String> toolName = Optional.empty();
+
        @Option(name = "--tool", aliases = {
                        "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForDiff")
-       private String toolName;
+       void setToolName(String name) {
+               toolName = Optional.of(name);
+       }
 
        @Option(name = "--cached", aliases = { "--staged" }, usage = "usage_cached")
        private boolean cached;
@@ -98,16 +104,16 @@ class DiffTool extends TextBuiltin {
        @Option(name = "--tool-help", usage = "usage_toolHelp")
        private boolean toolHelp;
 
-       private BooleanTriState gui = BooleanTriState.UNSET;
+       private boolean gui = false;
 
        @Option(name = "--gui", aliases = { "-g" }, usage = "usage_DiffGuiTool")
        void setGui(@SuppressWarnings("unused") boolean on) {
-               gui = BooleanTriState.TRUE;
+               gui = true;
        }
 
        @Option(name = "--no-gui", usage = "usage_noGui")
        void noGui(@SuppressWarnings("unused") boolean on) {
-               gui = BooleanTriState.FALSE;
+               gui = false;
        }
 
        private BooleanTriState trustExitCode = BooleanTriState.UNSET;
@@ -141,16 +147,10 @@ class DiffTool extends TextBuiltin {
                        if (toolHelp) {
                                showToolHelp();
                        } else {
-                               boolean showPrompt = diffTools.isInteractive();
-                               if (prompt != BooleanTriState.UNSET) {
-                                       showPrompt = prompt == BooleanTriState.TRUE;
-                               }
-                               // get passed or default tool name
-                               String toolNameToUse = promptToolName();
                                // get the changed files
                                List<DiffEntry> files = getFiles();
                                if (files.size() > 0) {
-                                       compare(files, showPrompt, toolNameToUse);
+                                       compare(files);
                                }
                        }
                } catch (RevisionSyntaxException | IOException e) {
@@ -160,79 +160,103 @@ class DiffTool extends TextBuiltin {
                }
        }
 
-       private String promptToolName() throws IOException {
-               String toolNameToUse = toolName;
-               if (StringUtils.isEmptyOrNull(toolNameToUse)) {
-                       toolNameToUse = diffTools.getDefaultToolName(gui);
-               }
-               if (StringUtils.isEmptyOrNull(toolNameToUse)) {
-                       Map<String, ExternalDiffTool> predefTools = diffTools
-                                       .getPredefinedTools(false);
+       private void informUserNoTool(List<String> tools) {
+               try {
                        StringBuilder toolNames = new StringBuilder();
-                       for (String name : predefTools.keySet()) {
+                       for (String name : tools) {
                                toolNames.append(name + " "); //$NON-NLS-1$
                        }
                        outw.println(MessageFormat.format(
                                        CLIText.get().diffToolPromptToolName, toolNames));
                        outw.flush();
-                       toolNameToUse = diffTools.getFirstAvailableTool();
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
                }
-               if (StringUtils.isEmptyOrNull(toolNameToUse)) {
-                       throw new IOException(MessageFormat
-                                       .format(CLIText.get().diffToolUnknownToolName, toolName));
+       }
+
+       private class CountingPromptContinueHandler
+                       implements PromptContinueHandler {
+               private final int fileIndex;
+
+               private final int fileCount;
+
+               private final String fileName;
+
+               public CountingPromptContinueHandler(int fileIndex, int fileCount,
+                               String fileName) {
+                       this.fileIndex = fileIndex;
+                       this.fileCount = fileCount;
+                       this.fileName = fileName;
+               }
+
+               @SuppressWarnings("boxing")
+               @Override
+               public boolean prompt(String toolToLaunchName) {
+                       try {
+                               boolean launchCompare = true;
+                               outw.println(MessageFormat.format(CLIText.get().diffToolLaunch,
+                                               fileIndex, fileCount, fileName, toolToLaunchName)
+                                               + " "); //$NON-NLS-1$
+                               outw.flush();
+                               BufferedReader br = inputReader;
+                               String line = null;
+                               if ((line = br.readLine()) != null) {
+                                       if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$
+                                               launchCompare = false;
+                                       }
+                               }
+                               return launchCompare;
+                       } catch (IOException e) {
+                               throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
+                       }
                }
-               return toolNameToUse;
        }
 
-       private void compare(List<DiffEntry> files, boolean showPrompt,
-                       String toolNameToUse) throws IOException {
+       private void compare(List<DiffEntry> files) throws IOException {
                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, toolNameToUse);
+
+                               String filePath = ent.getNewPath();
+                               if (filePath.equals(DiffEntry.DEV_NULL)) {
+                                       filePath = ent.getOldPath();
                                }
-                               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);
+
+                               try {
+                                       FileElement local = createFileElement(
+                                                       FileElement.Type.LOCAL, sourcePair, Side.OLD, ent);
+                                       FileElement remote = createFileElement(
+                                                       FileElement.Type.REMOTE, sourcePair, Side.NEW, ent);
+
+                                       PromptContinueHandler promptContinueHandler = new CountingPromptContinueHandler(
+                                                       fileIndex + 1, files.size(), filePath);
+
+                                       Optional<ExecutionResult> optionalResult = diffTools
+                                                       .compare(local, remote, toolName, prompt, gui,
+                                                                       trustExitCode, promptContinueHandler,
+                                                                       this::informUserNoTool);
+
+                                       if (optionalResult.isPresent()) {
+                                               ExecutionResult result = optionalResult.get();
                                                // TODO: check how to return the exit-code of the tool
                                                // to jgit / java runtime ?
                                                // int rc =...
-                                               ExecutionResult result = diffTools.compare(local,
-                                                               remote, merged, toolNameToUse, prompt, gui,
-                                                               trustExitCode);
-                                               outw.println(new String(result.getStdout().toByteArray()));
+                                               outw.println(
+                                                               new String(result.getStdout().toByteArray()));
                                                outw.flush();
                                                errw.println(
                                                                new String(result.getStderr().toByteArray()));
                                                errw.flush();
-                                       } catch (ToolException e) {
-                                               outw.println(e.getResultStdout());
-                                               outw.flush();
-                                               errw.println(e.getMessage());
-                                               errw.flush();
-                                               throw die(MessageFormat.format(
-                                                               CLIText.get().diffToolDied, mergedFilePath, e),
-                                                               e);
                                        }
-                               } else {
-                                       break;
+                               } catch (ToolException e) {
+                                       outw.println(e.getResultStdout());
+                                       outw.flush();
+                                       errw.println(e.getMessage());
+                                       errw.flush();
+                                       throw die(MessageFormat.format(
+                                                       CLIText.get().diffToolDied, filePath, e), e);
                                }
                        }
                } finally {
@@ -240,22 +264,6 @@ class DiffTool extends TextBuiltin {
                }
        }
 
-       @SuppressWarnings("boxing")
-       private boolean isLaunchCompare(int fileIndex, int fileCount,
-                       String fileName, String toolNamePrompt) throws IOException {
-               boolean launchCompare = true;
-               outw.println(MessageFormat.format(CLIText.get().diffToolLaunch,
-                               fileIndex, fileCount, fileName, toolNamePrompt) + " "); //$NON-NLS-1$
-               outw.flush();
-               BufferedReader br = inputReader;
-               String line = null;
-               if ((line = br.readLine()) != null) {
-                       if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$
-                               launchCompare = false;
-                       }
-               }
-               return launchCompare;
-       }
        private void showToolHelp() throws IOException {
                Map<String, ExternalDiffTool> predefTools = diffTools
                                .getPredefinedTools(true);
@@ -314,12 +322,12 @@ class DiffTool extends TextBuiltin {
        }
 
        private FileElement createFileElement(FileElement.Type elementType,
-                       Pair pair, Side side, DiffEntry entry)
-                       throws NoWorkTreeException, CorruptObjectException, IOException,
-                       ToolException {
+                       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);
+               FileElement fileElement = new FileElement(entryPath, elementType,
+                               db.getWorkTree());
                if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) {
                        try (RevWalk revWalk = new RevWalk(db);
                                        TreeWalk treeWalk = new TreeWalk(db,
@@ -348,7 +356,8 @@ class DiffTool extends TextBuiltin {
                                                                        fileElement.createTempFile(null)));
                                } else {
                                        throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$
-                                                       + "' in staging area!", null); //$NON-NLS-1$
+                                                       + "' in staging area!", //$NON-NLS-1$
+                                                       null);
                                }
                        }
                }
index f5884c44da9b2fcb6ef4fb9f35e780d559087495..2a411b81fe6c28f4a97dad0592a4fce771dd0b4e 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.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
@@ -22,6 +23,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.TreeMap;
 
 import org.eclipse.jgit.api.Git;
@@ -29,30 +31,29 @@ 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.DirCacheCheckout.CheckoutMetadata;
 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.FileElement.Type;
 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.CoreConfig.EolStreamType;
+import org.eclipse.jgit.lib.IndexDiff.StageState;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.util.StringUtils;
 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.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.util.FS.ExecutionResult;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
@@ -62,9 +63,13 @@ import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
 class MergeTool extends TextBuiltin {
        private MergeTools mergeTools;
 
+       private Optional<String> toolName = Optional.empty();
+
        @Option(name = "--tool", aliases = {
                        "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge")
-       private String toolName;
+       void setToolName(String name) {
+               toolName = Optional.of(name);
+       }
 
        private BooleanTriState prompt = BooleanTriState.UNSET;
 
@@ -81,16 +86,16 @@ class MergeTool extends TextBuiltin {
        @Option(name = "--tool-help", usage = "usage_toolHelp")
        private boolean toolHelp;
 
-       private BooleanTriState gui = BooleanTriState.UNSET;
+       private boolean gui = false;
 
        @Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool")
        void setGui(@SuppressWarnings("unused") boolean on) {
-               gui = BooleanTriState.TRUE;
+               gui = true;
        }
 
        @Option(name = "--no-gui", usage = "usage_noGui")
        void noGui(@SuppressWarnings("unused") boolean on) {
-               gui = BooleanTriState.FALSE;
+               gui = false;
        }
 
        @Argument(required = false, index = 0, metaVar = "metaVar_paths")
@@ -116,17 +121,10 @@ class MergeTool extends TextBuiltin {
                        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 toolNameToUse = promptToolName();
                                // get the changed files
                                Map<String, StageState> files = getFiles();
                                if (files.size() > 0) {
-                                       merge(files, showPrompt, toolNameToUse);
+                                       merge(files);
                                } else {
                                        outw.println(CLIText.get().mergeToolNoFiles);
                                }
@@ -137,32 +135,21 @@ class MergeTool extends TextBuiltin {
                }
        }
 
-       private String promptToolName() throws IOException {
-               String toolNameToUse = toolName;
-               if (StringUtils.isEmptyOrNull(toolNameToUse)) {
-                       toolNameToUse = mergeTools.getDefaultToolName(gui);
-               }
-               if (StringUtils.isEmptyOrNull(toolNameToUse)) {
-                       Map<String, ExternalMergeTool> predefTools = mergeTools
-                                       .getPredefinedTools(false);
+       private void informUserNoTool(List<String> tools) {
+               try {
                        StringBuilder toolNames = new StringBuilder();
-                       for (String name : predefTools.keySet()) {
+                       for (String name : tools) {
                                toolNames.append(name + " "); //$NON-NLS-1$
                        }
                        outw.println(MessageFormat
                                        .format(CLIText.get().mergeToolPromptToolName, toolNames));
                        outw.flush();
-                       toolNameToUse = mergeTools.getFirstAvailableTool();
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
                }
-               if (StringUtils.isEmptyOrNull(toolNameToUse)) {
-                       throw new IOException(MessageFormat
-                                       .format(CLIText.get().mergeToolUnknownToolName, toolName));
-               }
-               return toolNameToUse;
        }
 
-       private void merge(Map<String, StageState> files, boolean showPrompt,
-                       String toolNamePrompt) throws Exception {
+       private void merge(Map<String, StageState> files) throws Exception {
                // sort file names
                List<String> mergedFilePaths = new ArrayList<>(files.keySet());
                Collections.sort(mergedFilePaths);
@@ -174,6 +161,10 @@ class MergeTool extends TextBuiltin {
                outw.println(MessageFormat.format(CLIText.get().mergeToolMerging,
                                mergedFiles));
                outw.flush();
+               boolean showPrompt = mergeTools.isInteractive();
+               if (prompt != BooleanTriState.UNSET) {
+                       showPrompt = prompt == BooleanTriState.TRUE;
+               }
                // merge the files
                MergeResult mergeResult = MergeResult.SUCCESSFUL;
                for (String mergedFilePath : mergedFilePaths) {
@@ -191,8 +182,7 @@ class MergeTool extends TextBuiltin {
                        // get file stage state and merge
                        StageState fileState = files.get(mergedFilePath);
                        if (fileState == StageState.BOTH_MODIFIED) {
-                               mergeResult = mergeModified(mergedFilePath, showPrompt,
-                                               toolNamePrompt);
+                               mergeResult = mergeModified(mergedFilePath, showPrompt);
                        } else if ((fileState == StageState.DELETED_BY_US)
                                        || (fileState == StageState.DELETED_BY_THEM)) {
                                mergeResult = mergeDeleted(mergedFilePath,
@@ -206,19 +196,11 @@ class MergeTool extends TextBuiltin {
                }
        }
 
-       private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
-                       String toolNamePrompt) throws Exception {
+       private MergeResult mergeModified(String mergedFilePath, boolean showPrompt)
+                       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());
@@ -232,8 +214,8 @@ class MergeTool extends TextBuiltin {
                        FileElement base = null;
                        FileElement local = null;
                        FileElement remote = null;
-                       FileElement merged = new FileElement(mergedFilePath,
-                                       Type.MERGED);
+                       FileElement merged = new FileElement(mergedFilePath, Type.MERGED,
+                                       db.getWorkTree());
                        DirCache cache = db.readDirCache();
                        try (RevWalk revWalk = new RevWalk(db);
                                        TreeWalk treeWalk = new TreeWalk(db,
@@ -255,7 +237,8 @@ class MergeTool extends TextBuiltin {
                                                        .get(WorkingTreeOptions.KEY);
                                        CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
                                                        eolStreamType, filterCommand);
-                                       DirCacheEntry entry = treeWalk.getTree(DirCacheIterator.class).getDirCacheEntry();
+                                       DirCacheEntry entry = treeWalk
+                                                       .getTree(DirCacheIterator.class).getDirCacheEntry();
                                        if (entry == null) {
                                                continue;
                                        }
@@ -297,23 +280,27 @@ class MergeTool extends TextBuiltin {
                                // 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();
+                               Optional<ExecutionResult> optionalResult = mergeTools.merge(
+                                               local, remote, merged, base, tempDir, toolName, prompt,
+                                               gui, this::promptForLaunch, this::informUserNoTool);
+                               if (optionalResult.isPresent()) {
+                                       ExecutionResult result = optionalResult.get();
+                                       outw.println(new String(result.getStdout().toByteArray()));
+                                       outw.flush();
+                                       errw.println(new String(result.getStderr().toByteArray()));
+                                       errw.flush();
+                               } else {
+                                       return MergeResult.ABORTED;
+                               }
                        } catch (ToolException e) {
                                isMergeSuccessful = false;
                                outw.println(e.getResultStdout());
                                outw.flush();
+                               errw.println(e.getMessage());
                                errw.println(MessageFormat.format(
                                                CLIText.get().mergeToolMergeFailed, mergedFilePath));
                                errw.flush();
                                if (e.isCommandExecutionError()) {
-                                       errw.println(e.getMessage());
                                        throw die(CLIText.get().mergeToolExecutionError, e);
                                }
                        }
@@ -402,19 +389,23 @@ class MergeTool extends TextBuiltin {
                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;
+       private boolean promptForLaunch(String toolNamePrompt) {
+               try {
+                       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;
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
                }
-               return launch;
        }
 
        private int getDeletedMergeDecision() throws IOException {
index 1b501c25b01ff5edcce6cbd59e3648c9e2028321..222608e314ea269d820211dfb606c17070caae44 100644 (file)
@@ -18,13 +18,20 @@ 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.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 import org.eclipse.jgit.lib.internal.BooleanTriState;
@@ -48,14 +55,7 @@ public class ExternalDiffToolTest extends ExternalToolTestCase {
                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);
+               invokeCompare(toolName);
 
                fail("Expected exception to be thrown due to external tool exiting with error code: "
                                + errorReturnCode);
@@ -72,33 +72,84 @@ public class ExternalDiffToolTest extends ExternalToolTestCase {
                config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
                                command);
 
+               invokeCompare(toolName);
+               fail("Expected exception to be thrown due to external tool exiting with error code: "
+                               + errorReturnCode);
+       }
+
+       @Test
+       public void testUserDefinedTool() throws Exception {
+               String command = getEchoCommand();
+
+               FileBasedConfig config = db.getConfig();
+               String customToolName = "customTool";
+               config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
+                               CONFIG_KEY_CMD, command);
+
                DiffTools manager = new DiffTools(db);
 
-               BooleanTriState prompt = BooleanTriState.UNSET;
-               BooleanTriState gui = BooleanTriState.UNSET;
-               BooleanTriState trustExitCode = BooleanTriState.FALSE;
+               Map<String, ExternalDiffTool> tools = manager.getUserDefinedTools();
+               ExternalDiffTool externalTool = tools.get(customToolName);
+               boolean trustExitCode = true;
+               manager.compare(local, remote, externalTool, trustExitCode);
 
-               manager.compare(local, remote, merged, toolName, prompt, gui,
-                               trustExitCode);
+               assertEchoCommandHasCorrectOutput();
+       }
 
-               fail("Expected exception to be thrown due to external tool exiting with error code: "
-                               + errorReturnCode);
+       @Test
+       public void testUserDefinedToolWithPrompt() throws Exception {
+               String command = getEchoCommand();
+
+               FileBasedConfig config = db.getConfig();
+               String customToolName = "customTool";
+               config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
+                               CONFIG_KEY_CMD, command);
+
+               DiffTools manager = new DiffTools(db);
+
+               PromptHandler promptHandler = PromptHandler.acceptPrompt();
+               MissingToolHandler noToolHandler = new MissingToolHandler();
+
+               manager.compare(local, remote, Optional.of(customToolName),
+                               BooleanTriState.TRUE, false, BooleanTriState.TRUE,
+                               promptHandler, noToolHandler);
+
+               assertEchoCommandHasCorrectOutput();
+
+               List<String> actualToolPrompts = promptHandler.toolPrompts;
+               List<String> expectedToolPrompts = Arrays.asList("customTool");
+               assertEquals("Expected a user prompt for custom tool call",
+                               expectedToolPrompts, actualToolPrompts);
+
+               assertEquals("Expected to no informing about missing tools",
+                               Collections.EMPTY_LIST, noToolHandler.missingTools);
        }
 
        @Test
-       public void testToolNames() {
+       public void testUserDefinedToolWithCancelledPrompt() throws Exception {
                DiffTools manager = new DiffTools(db);
-               Set<String> actualToolNames = manager.getToolNames();
-               Set<String> expectedToolNames = Collections.emptySet();
-               assertEquals("Incorrect set of external diff tool names",
-                               expectedToolNames, actualToolNames);
+
+               PromptHandler promptHandler = PromptHandler.cancelPrompt();
+               MissingToolHandler noToolHandler = new MissingToolHandler();
+
+               Optional<ExecutionResult> result = manager.compare(local, remote,
+                               Optional.empty(), BooleanTriState.TRUE, false,
+                               BooleanTriState.TRUE, promptHandler, noToolHandler);
+               assertFalse("Expected no result if user cancels the operation",
+                               result.isPresent());
        }
 
        @Test
        public void testAllTools() {
+               FileBasedConfig config = db.getConfig();
+               String customToolName = "customTool";
+               config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
+                               CONFIG_KEY_CMD, "echo");
+
                DiffTools manager = new DiffTools(db);
-               Set<String> actualToolNames = manager.getPredefinedTools(true).keySet();
+               Set<String> actualToolNames = manager.getAllToolNames();
                Set<String> expectedToolNames = new LinkedHashSet<>();
+               expectedToolNames.add(customToolName);
                CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
                for (CommandLineDiffTool defaultTool : defaultTools) {
                        String toolName = defaultTool.name();
@@ -166,18 +217,12 @@ public class ExternalDiffToolTest extends ExternalToolTestCase {
 
                config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
                                command);
-
-               BooleanTriState prompt = BooleanTriState.UNSET;
-               BooleanTriState gui = BooleanTriState.UNSET;
-               BooleanTriState trustExitCode = BooleanTriState.UNSET;
-
-               DiffTools manager = new DiffTools(db);
-
+               Optional<ExecutionResult> result = invokeCompare(toolName);
+               assertTrue("Expected external diff tool result to be available",
+                               result.isPresent());
                int expectedCompareResult = 0;
-               ExecutionResult compareResult = manager.compare(local, remote, merged,
-                               toolName, prompt, gui, trustExitCode);
                assertEquals("Incorrect compare result for external diff tool",
-                               expectedCompareResult, compareResult.getRc());
+                               expectedCompareResult, result.get().getRc());
        }
 
        @Test
@@ -192,17 +237,16 @@ public class ExternalDiffToolTest extends ExternalToolTestCase {
                                toolName);
 
                DiffTools manager = new DiffTools(db);
-               BooleanTriState gui = BooleanTriState.UNSET;
+               boolean gui = false;
                String defaultToolName = manager.getDefaultToolName(gui);
                assertEquals(
                                "Expected configured difftool to be the default external diff tool",
                                toolName, defaultToolName);
 
-               gui = BooleanTriState.TRUE;
+               gui = true;
                String defaultGuiToolName = manager.getDefaultToolName(gui);
-               assertEquals(
-                               "Expected configured difftool to be the default external diff tool",
-                               "my_gui_tool", defaultGuiToolName);
+               assertNull("Expected default difftool to not be set",
+                               defaultGuiToolName);
 
                config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL,
                                guiToolName);
@@ -210,7 +254,7 @@ public class ExternalDiffToolTest extends ExternalToolTestCase {
                defaultGuiToolName = manager.getDefaultToolName(gui);
                assertEquals(
                                "Expected configured difftool to be the default external diff guitool",
-                               "my_gui_tool", defaultGuiToolName);
+                               guiToolName, defaultGuiToolName);
        }
 
        @Test
@@ -247,20 +291,39 @@ public class ExternalDiffToolTest extends ExternalToolTestCase {
 
        @Test(expected = ToolException.class)
        public void testUndefinedTool() throws Exception {
+               String toolName = "undefined";
+               invokeCompare(toolName);
+               fail("Expected exception to be thrown due to not defined external diff tool");
+       }
+
+       private Optional<ExecutionResult> invokeCompare(String toolName)
+                       throws ToolException {
                DiffTools manager = new DiffTools(db);
 
-               String toolName = "undefined";
                BooleanTriState prompt = BooleanTriState.UNSET;
-               BooleanTriState gui = BooleanTriState.UNSET;
-               BooleanTriState trustExitCode = BooleanTriState.UNSET;
+               boolean gui = false;
+               BooleanTriState trustExitCode = BooleanTriState.TRUE;
+               PromptHandler promptHandler = PromptHandler.acceptPrompt();
+               MissingToolHandler noToolHandler = new MissingToolHandler();
 
-               manager.compare(local, remote, merged, toolName, prompt, gui,
-                               trustExitCode);
-               fail("Expected exception to be thrown due to not defined external diff tool");
+               Optional<ExecutionResult> result = manager.compare(local, remote,
+                               Optional.of(toolName), prompt, gui, trustExitCode,
+                               promptHandler, noToolHandler);
+               return result;
        }
 
        private String getEchoCommand() {
                return "(echo \"$LOCAL\" \"$REMOTE\") > "
                                + commandResult.getAbsolutePath();
        }
+
+       private void assertEchoCommandHasCorrectOutput() throws IOException {
+               List<String> actualLines = Files.readAllLines(commandResult.toPath());
+               String actualContent = String.join(System.lineSeparator(), actualLines);
+               actualLines = Arrays.asList(actualContent.split(" "));
+               List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(),
+                               remoteFile.getAbsolutePath());
+               assertEquals("Dummy test tool called with unexpected arguments",
+                               expectedLines, actualLines);
+       }
 }
index 305f2b470ea4eb9c05ce75cd8032d8fbaeac468d..130b42a92a362e8cf4cf4397035256aca6cefa30 100644 (file)
@@ -18,13 +18,21 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 import org.eclipse.jgit.lib.internal.BooleanTriState;
@@ -50,12 +58,7 @@ public class ExternalMergeToolTest extends ExternalToolTestCase {
                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);
+               invokeMerge(toolName);
 
                fail("Expected exception to be thrown due to external tool exiting with error code: "
                                + errorReturnCode);
@@ -72,31 +75,112 @@ public class ExternalMergeToolTest extends ExternalToolTestCase {
                config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
                                command);
 
+               invokeMerge(toolName);
+
+               fail("Expected exception to be thrown due to external tool exiting with error code: "
+                               + errorReturnCode);
+       }
+
+       @Test
+       public void testKdiff3() throws Exception {
+               assumePosixPlatform();
+
+               CommandLineMergeTool autoMergingTool = CommandLineMergeTool.kdiff3;
+               assumeMergeToolIsAvailable(autoMergingTool);
+
+               CommandLineMergeTool tool = autoMergingTool;
+               PreDefinedMergeTool externalTool = new PreDefinedMergeTool(tool.name(),
+                               tool.getPath(), tool.getParameters(true),
+                               tool.getParameters(false),
+                               tool.isExitCodeTrustable() ? BooleanTriState.TRUE
+                                               : BooleanTriState.FALSE);
+
                MergeTools manager = new MergeTools(db);
+               ExecutionResult result = manager.merge(local, remote, merged, null,
+                               null, externalTool);
+               assertEquals("Expected merge tool to succeed", 0, result.getRc());
+
+               List<String> actualLines = Files.readAllLines(mergedFile.toPath());
+               String actualMergeResult = String.join(System.lineSeparator(),
+                               actualLines);
+               String expectedMergeResult = DEFAULT_CONTENT;
+               assertEquals(
+                               "Failed to merge equal local and remote versions with pre-defined tool: "
+                                               + tool.getPath(),
+                               expectedMergeResult, actualMergeResult);
+       }
 
-               BooleanTriState prompt = BooleanTriState.UNSET;
-               BooleanTriState gui = BooleanTriState.UNSET;
+       @Test
+       public void testUserDefinedTool() throws Exception {
+               String customToolName = "customTool";
+               String command = getEchoCommand();
 
-               manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
+               FileBasedConfig config = db.getConfig();
+               config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
+                               CONFIG_KEY_CMD, command);
 
-               fail("Expected exception to be thrown due to external tool exiting with error code: "
-                               + errorReturnCode);
+               MergeTools manager = new MergeTools(db);
+               Map<String, ExternalMergeTool> tools = manager.getUserDefinedTools();
+               ExternalMergeTool externalTool = tools.get(customToolName);
+               manager.merge(local, remote, merged, base, null, externalTool);
+
+               assertEchoCommandHasCorrectOutput();
        }
 
        @Test
-       public void testToolNames() {
+       public void testUserDefinedToolWithPrompt() throws Exception {
+               String customToolName = "customTool";
+               String command = getEchoCommand();
+
+               FileBasedConfig config = db.getConfig();
+               config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
+                               CONFIG_KEY_CMD, command);
+
                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);
+
+               PromptHandler promptHandler = PromptHandler.acceptPrompt();
+               MissingToolHandler noToolHandler = new MissingToolHandler();
+
+               manager.merge(local, remote, merged, base, null,
+                               Optional.of(customToolName), BooleanTriState.TRUE, false,
+                               promptHandler, noToolHandler);
+
+               assertEchoCommandHasCorrectOutput();
+
+               List<String> actualToolPrompts = promptHandler.toolPrompts;
+               List<String> expectedToolPrompts = Arrays.asList("customTool");
+               assertEquals("Expected a user prompt for custom tool call",
+                               expectedToolPrompts, actualToolPrompts);
+
+               assertEquals("Expected to no informing about missing tools",
+                               Collections.EMPTY_LIST, noToolHandler.missingTools);
+       }
+
+       @Test
+       public void testUserDefinedToolWithCancelledPrompt() throws Exception {
+               MergeTools manager = new MergeTools(db);
+
+               PromptHandler promptHandler = PromptHandler.cancelPrompt();
+               MissingToolHandler noToolHandler = new MissingToolHandler();
+
+               Optional<ExecutionResult> result = manager.merge(local, remote, merged,
+                               base, null, Optional.empty(), BooleanTriState.TRUE, false,
+                               promptHandler, noToolHandler);
+               assertFalse("Expected no result if user cancels the operation",
+                               result.isPresent());
        }
 
        @Test
        public void testAllTools() {
+               FileBasedConfig config = db.getConfig();
+               String customToolName = "customTool";
+               config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
+                               CONFIG_KEY_CMD, "echo");
+
                MergeTools manager = new MergeTools(db);
-               Set<String> actualToolNames = manager.getPredefinedTools(true).keySet();
+               Set<String> actualToolNames = manager.getAllToolNames();
                Set<String> expectedToolNames = new LinkedHashSet<>();
+               expectedToolNames.add(customToolName);
                CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
                for (CommandLineMergeTool defaultTool : defaultTools) {
                        String toolName = defaultTool.name();
@@ -165,16 +249,12 @@ public class ExternalMergeToolTest extends ExternalToolTestCase {
                config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
                                command);
 
-               BooleanTriState prompt = BooleanTriState.UNSET;
-               BooleanTriState gui = BooleanTriState.UNSET;
-
-               MergeTools manager = new MergeTools(db);
-
+               Optional<ExecutionResult> result = invokeMerge(toolName);
+               assertTrue("Expected external merge tool result to be available",
+                               result.isPresent());
                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());
+                               expectedCompareResult, result.get().getRc());
        }
 
        @Test
@@ -189,17 +269,16 @@ public class ExternalMergeToolTest extends ExternalToolTestCase {
                                toolName);
 
                MergeTools manager = new MergeTools(db);
-               BooleanTriState gui = BooleanTriState.UNSET;
+               boolean gui = false;
                String defaultToolName = manager.getDefaultToolName(gui);
                assertEquals(
                                "Expected configured mergetool to be the default external merge tool",
                                toolName, defaultToolName);
 
-               gui = BooleanTriState.TRUE;
+               gui = true;
                String defaultGuiToolName = manager.getDefaultToolName(gui);
-               assertEquals(
-                               "Expected configured mergetool to be the default external merge tool",
-                               "my_gui_tool", defaultGuiToolName);
+               assertNull("Expected default mergetool to not be set",
+                               defaultGuiToolName);
 
                config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL,
                                guiToolName);
@@ -207,7 +286,7 @@ public class ExternalMergeToolTest extends ExternalToolTestCase {
                defaultGuiToolName = manager.getDefaultToolName(gui);
                assertEquals(
                                "Expected configured mergetool to be the default external merge guitool",
-                               "my_gui_tool", defaultGuiToolName);
+                               guiToolName, defaultGuiToolName);
        }
 
        @Test
@@ -245,18 +324,48 @@ public class ExternalMergeToolTest extends ExternalToolTestCase {
 
        @Test(expected = ToolException.class)
        public void testUndefinedTool() throws Exception {
-               MergeTools manager = new MergeTools(db);
-
                String toolName = "undefined";
+               invokeMerge(toolName);
+               fail("Expected exception to be thrown due to not defined external merge tool");
+       }
+
+       private Optional<ExecutionResult> invokeMerge(String toolName)
+                       throws ToolException {
                BooleanTriState prompt = BooleanTriState.UNSET;
-               BooleanTriState gui = BooleanTriState.UNSET;
+               boolean gui = false;
 
-               manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
-               fail("Expected exception to be thrown due to not defined external merge tool");
+               MergeTools manager = new MergeTools(db);
+
+               PromptHandler promptHandler = PromptHandler.acceptPrompt();
+               MissingToolHandler noToolHandler = new MissingToolHandler();
+
+               Optional<ExecutionResult> result = manager.merge(local, remote, merged,
+                               base, null, Optional.of(toolName), prompt, gui, promptHandler,
+                               noToolHandler);
+               return result;
+       }
+
+       private void assumeMergeToolIsAvailable(
+                       CommandLineMergeTool autoMergingTool) {
+               boolean isAvailable = ExternalToolUtils.isToolAvailable(db.getFS(),
+                               db.getDirectory(), db.getWorkTree(), autoMergingTool.getPath());
+               assumeTrue("Assuming external tool is available: "
+                               + autoMergingTool.name(), isAvailable);
        }
 
        private String getEchoCommand() {
-               return "(echo \"$LOCAL\" \"$REMOTE\") > "
+               return "(echo $LOCAL $REMOTE $MERGED $BASE) > "
                                + commandResult.getAbsolutePath();
        }
+
+       private void assertEchoCommandHasCorrectOutput() throws IOException {
+               List<String> actualLines = Files.readAllLines(commandResult.toPath());
+               String actualContent = String.join(System.lineSeparator(), actualLines);
+               actualLines = Arrays.asList(actualContent.split(" "));
+               List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(),
+                               remoteFile.getAbsolutePath(), mergedFile.getAbsolutePath(),
+                               baseFile.getAbsolutePath());
+               assertEquals("Dummy test tool called with unexpected arguments",
+                               expectedLines, actualLines);
+       }
 }
index 0fd85cb4568edbaf4caac068c0cecab8502b7099..7a6ff46578d5d2a88bd9f19935f2fe8d4c86aa4b 100644 (file)
@@ -11,6 +11,8 @@ package org.eclipse.jgit.internal.diffmergetool;
 
 import java.io.File;
 import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
 
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.util.FS;
@@ -88,4 +90,39 @@ public abstract class ExternalToolTestCase extends RepositoryTestCase {
                                "This test can run only in Linux tests",
                                FS.DETECTED instanceof FS_POSIX);
        }
+
+       protected static class PromptHandler implements PromptContinueHandler {
+
+               private final boolean promptResult;
+
+               final List<String> toolPrompts = new ArrayList<>();
+
+               private PromptHandler(boolean promptResult) {
+                       this.promptResult = promptResult;
+               }
+
+               static PromptHandler acceptPrompt() {
+                       return new PromptHandler(true);
+               }
+
+               static PromptHandler cancelPrompt() {
+                       return new PromptHandler(false);
+               }
+
+               @Override
+               public boolean prompt(String toolName) {
+                       toolPrompts.add(toolName);
+                       return promptResult;
+               }
+       }
+
+       protected static class MissingToolHandler implements InformNoToolHandler {
+
+               final List<String> missingTools = new ArrayList<>();
+
+               @Override
+               public void inform(List<String> toolNames) {
+                       missingTools.addAll(toolNames);
+               }
+       }
 }
index e72f00f0bfb19c74ca96e8ea54dd575d6bbe9226..a25685073d12c322cf114a9fa8556cfb6cbe10d9 100644 (file)
@@ -73,7 +73,8 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.0",
  org.eclipse.jgit.internal.diffmergetool;version="6.2.0";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.pgm.test,
-   org.eclipse.jgit.pgm",
+   org.eclipse.jgit.pgm,
+   org.eclipse.egit.ui",
  org.eclipse.jgit.internal.fsck;version="6.2.0";
   x-friends:="org.eclipse.jgit.test",
  org.eclipse.jgit.internal.revwalk;version="6.2.0";
@@ -133,7 +134,8 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.0",
    org.eclipse.jgit.util.time",
  org.eclipse.jgit.lib.internal;version="6.2.0";
   x-friends:="org.eclipse.jgit.test,
-   org.eclipse.jgit.pgm",
+   org.eclipse.jgit.pgm,
+   org.eclipse.egit.ui",
  org.eclipse.jgit.logging;version="6.2.0",
  org.eclipse.jgit.merge;version="6.2.0";
   uses:="org.eclipse.jgit.dircache,
index 6c1a780a355d334afdbcd7273908a0626db31af5..5fe263a4efad6d2ba737bfb7fabc7de76bf52711 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.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
 
 package org.eclipse.jgit.internal.diffmergetool;
 
-import java.util.TreeMap;
-import java.util.Collections;
+import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
+import java.util.TreeMap;
 
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.util.FS;
 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 FS fs;
+
+       private final File gitDir;
+
+       private final File workTree;
 
        private final DiffToolConfig config;
 
@@ -41,69 +50,176 @@ public class DiffTools {
         *            the repository
         */
        public DiffTools(Repository repo) {
-               this.repo = repo;
-               config = repo.getConfig().get(DiffToolConfig.KEY);
+               this(repo, repo.getConfig());
+       }
+
+       /**
+        * Creates the external merge-tools manager for given configuration.
+        *
+        * @param config
+        *            the git configuration
+        */
+       public DiffTools(StoredConfig config) {
+               this(null, config);
+       }
+
+       private DiffTools(Repository repo, StoredConfig config) {
+               this.config = config.get(DiffToolConfig.KEY);
+               this.gitDir = repo == null ? null : repo.getDirectory();
+               this.fs = repo == null ? FS.DETECTED : repo.getFS();
+               this.workTree = repo == null ? null : repo.getWorkTree();
                predefinedTools = setupPredefinedTools();
-               userDefinedTools = setupUserDefinedTools(config, predefinedTools);
+               userDefinedTools = setupUserDefinedTools(predefinedTools);
        }
 
        /**
         * Compare two versions of a file.
         *
         * @param localFile
-        *            the local file element
+        *            The local/left version of the file.
         * @param remoteFile
-        *            the remote file element
-        * @param mergedFile
-        *            the merged file element, it's path equals local or remote
-        *            element path
+        *            The remote/right version of the file.
         * @param toolName
-        *            the selected tool name (can be null)
+        *            Optionally the name of the tool to use. If not given the
+        *            default tool will be used.
         * @param prompt
-        *            the prompt option
+        *            Optionally a flag whether to prompt the user before compare.
+        *            If not given the default will be used.
         * @param gui
-        *            the GUI option
+        *            A flag whether to prefer a gui tool.
+        * @param trustExitCode
+        *            Optionally a flag whether to trust the exit code of the tool.
+        *            If not given the default will be used.
+        * @param promptHandler
+        *            The handler to use when needing to prompt the user if he wants
+        *            to continue.
+        * @param noToolHandler
+        *            The handler to use when needing to inform the user, that no
+        *            tool is configured.
+        * @return the optioanl result of executing the tool if it was executed
+        * @throws ToolException
+        *             when the tool fails
+        */
+       public Optional<ExecutionResult> compare(FileElement localFile,
+                       FileElement remoteFile, Optional<String> toolName,
+                       BooleanTriState prompt, boolean gui, BooleanTriState trustExitCode,
+                       PromptContinueHandler promptHandler,
+                       InformNoToolHandler noToolHandler) throws ToolException {
+
+               String toolNameToUse;
+
+               if (toolName.isPresent()) {
+                       toolNameToUse = toolName.get();
+               } else {
+                       toolNameToUse = getDefaultToolName(gui);
+
+                       if (toolNameToUse == null || toolNameToUse.isEmpty()) {
+                               noToolHandler.inform(new ArrayList<>(predefinedTools.keySet()));
+                               toolNameToUse = getFirstAvailableTool();
+                       }
+               }
+
+               boolean doPrompt;
+               if (prompt != BooleanTriState.UNSET) {
+                       doPrompt = prompt == BooleanTriState.TRUE;
+               } else {
+                       doPrompt = isInteractive();
+               }
+
+               if (doPrompt) {
+                       if (!promptHandler.prompt(toolNameToUse)) {
+                               return Optional.empty();
+                       }
+               }
+
+               boolean trust;
+               if (trustExitCode != BooleanTriState.UNSET) {
+                       trust = trustExitCode == BooleanTriState.TRUE;
+               } else {
+                       trust = config.isTrustExitCode();
+               }
+
+               ExternalDiffTool tool = getTool(toolNameToUse);
+               if (tool == null) {
+                       throw new ToolException(
+                                       "External diff tool is not defined: " + toolNameToUse); //$NON-NLS-1$
+               }
+
+               return Optional.of(
+                               compare(localFile, remoteFile, tool, trust));
+       }
+
+       /**
+        * Compare two versions of a file.
+        *
+        * @param localFile
+        *            the local file element
+        * @param remoteFile
+        *            the remote file element
+        * @param tool
+        *            the selected tool
         * @param trustExitCode
         *            the "trust exit code" option
         * @return the execution result from tool
         * @throws ToolException
         */
        public ExecutionResult compare(FileElement localFile,
-                       FileElement remoteFile, FileElement mergedFile, String toolName,
-                       BooleanTriState prompt, BooleanTriState gui,
-                       BooleanTriState trustExitCode) throws ToolException {
+                       FileElement remoteFile, ExternalDiffTool tool,
+                       boolean trustExitCode) throws ToolException {
                try {
                        // prepare the command (replace the file paths)
-                       String command = ExternalToolUtils.prepareCommand(
-                                       guessTool(toolName, gui).getCommand(), localFile,
-                                       remoteFile, mergedFile, null);
+                       String command = ExternalToolUtils.prepareCommand(tool.getCommand(),
+                                       localFile, remoteFile, null, 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;
-                       }
+                       Map<String, String> env = ExternalToolUtils.prepareEnvironment(
+                                       gitDir, localFile, remoteFile, null, null);
                        // execute the tool
-                       CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
-                       return cmdExec.run(command, repo.getWorkTree(), env);
+                       CommandExecutor cmdExec = new CommandExecutor(fs, trustExitCode);
+                       return cmdExec.run(command, workTree, env);
                } catch (IOException | InterruptedException e) {
                        throw new ToolException(e);
                } finally {
                        localFile.cleanTemporaries();
                        remoteFile.cleanTemporaries();
-                       mergedFile.cleanTemporaries();
                }
        }
 
        /**
-        * @return the tool names
+        * Get user defined tool names.
+        *
+        * @return the user defined tool names
+        */
+       public Set<String> getUserDefinedToolNames() {
+               return userDefinedTools.keySet();
+       }
+
+       /**
+        * Get predefined tool names.
+        *
+        * @return the predefined tool names
+        */
+       public Set<String> getPredefinedToolNames() {
+               return predefinedTools.keySet();
+       }
+
+       /**
+        * Get all tool names.
+        *
+        * @return the all tool names (default or available tool name is the first
+        *         in the set)
         */
-       public Set<String> getToolNames() {
-               return config.getToolNames();
+       public Set<String> getAllToolNames() {
+               String defaultName = getDefaultToolName(false);
+               if (defaultName == null) {
+                       defaultName = getFirstAvailableTool();
+               }
+               return ExternalToolUtils.createSortedToolSet(defaultName,
+                               getUserDefinedToolNames(), getPredefinedToolNames());
        }
 
        /**
+        * Get user defined tools map.
+        *
         * @return the user defined tools
         */
        public Map<String, ExternalDiffTool> getUserDefinedTools() {
@@ -111,6 +227,8 @@ public class DiffTools {
        }
 
        /**
+        * Get predefined tools map.
+        *
         * @param checkAvailability
         *            true: for checking if tools can be executed; ATTENTION: this
         *            check took some time, do not execute often (store the map for
@@ -124,59 +242,49 @@ public class DiffTools {
                if (checkAvailability) {
                        for (ExternalDiffTool tool : predefinedTools.values()) {
                                PreDefinedDiffTool predefTool = (PreDefinedDiffTool) tool;
-                               predefTool.setAvailable(ExternalToolUtils.isToolAvailable(repo,
-                                               predefTool.getPath()));
+                               predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
+                                               gitDir, workTree, predefTool.getPath()));
                        }
                }
                return Collections.unmodifiableMap(predefinedTools);
        }
 
        /**
+        * Get first available tool name.
+        *
         * @return the name of first available predefined tool or null
         */
        public String getFirstAvailableTool() {
-               String name = null;
                for (ExternalDiffTool tool : predefinedTools.values()) {
-                       if (ExternalToolUtils.isToolAvailable(repo, tool.getPath())) {
-                               name = tool.getName();
-                               break;
+                       if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
+                                       tool.getPath())) {
+                               return tool.getName();
                        }
                }
-               return name;
+               return null;
        }
 
        /**
+        * Get default (gui-)tool name.
+        *
         * @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$
+       public String getDefaultToolName(boolean gui) {
+               return gui ? config.getDefaultGuiToolName()
                                : config.getDefaultToolName();
        }
 
        /**
+        * Is interactive diff (prompt enabled) ?
+        *
         * @return is interactive (config prompt enabled) ?
         */
        public boolean isInteractive() {
                return config.isPrompt();
        }
 
-       private ExternalDiffTool guessTool(String toolName, BooleanTriState gui)
-                       throws ToolException {
-               if (StringUtils.isEmptyOrNull(toolName)) {
-                       toolName = getDefaultToolName(gui);
-               }
-               ExternalDiffTool tool = null;
-               if (!StringUtils.isEmptyOrNull(toolName)) {
-                       tool = getTool(toolName);
-               }
-               if (tool == null) {
-                       throw new ToolException("Unknown diff tool '" + toolName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
-               }
-               return tool;
-       }
-
        private ExternalDiffTool getTool(final String name) {
                ExternalDiffTool tool = userDefinedTools.get(name);
                if (tool == null) {
@@ -193,10 +301,10 @@ public class DiffTools {
                return tools;
        }
 
-       private static Map<String, ExternalDiffTool> setupUserDefinedTools(
-                       DiffToolConfig cfg, Map<String, ExternalDiffTool> predefTools) {
+       private Map<String, ExternalDiffTool> setupUserDefinedTools(
+                       Map<String, ExternalDiffTool> predefTools) {
                Map<String, ExternalDiffTool> tools = new TreeMap<>();
-               Map<String, ExternalDiffTool> userTools = cfg.getTools();
+               Map<String, ExternalDiffTool> userTools = config.getTools();
                for (String name : userTools.keySet()) {
                        ExternalDiffTool userTool = userTools.get(name);
                        // if difftool.<name>.cmd is defined we have user defined tool
index e10607d2fd4041d03e43f141f978ffa0acbd014f..9a69681133b81ae59bf2f0e25c56a4d7683531c8 100644 (file)
 package org.eclipse.jgit.internal.diffmergetool;
 
 import java.util.TreeMap;
+import java.io.File;
 import java.io.IOException;
+import java.util.LinkedHashSet;
 import java.util.Map;
+import java.util.Set;
+
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.FS;
 
 /**
  * Utilities for diff- and merge-tools.
@@ -57,8 +61,8 @@ public class ExternalToolUtils {
        /**
         * Prepare environment needed for execution.
         *
-        * @param repo
-        *            the repository
+        * @param gitDir
+        *            the .git directory
         * @param localFile
         *            the local file (ours)
         * @param remoteFile
@@ -70,11 +74,13 @@ public class ExternalToolUtils {
         * @return the environment map with variables and values (file paths)
         * @throws IOException
         */
-       public static Map<String, String> prepareEnvironment(Repository repo,
+       public static Map<String, String> prepareEnvironment(File gitDir,
                        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());
+               if (gitDir != null) {
+                       env.put(Constants.GIT_DIR_KEY, gitDir.getAbsolutePath());
+               }
                if (localFile != null) {
                        localFile.addToEnv(env);
                }
@@ -112,22 +118,60 @@ public class ExternalToolUtils {
        }
 
        /**
-        * @param repo
-        *            the repository
+        * @param fs
+        *            the file system abstraction
+        * @param gitDir
+        *            the .git directory
+        * @param directory
+        *            the working directory
         * @param path
         *            the tool path
         * @return true if tool available and false otherwise
         */
-       public static boolean isToolAvailable(Repository repo, String path) {
+       public static boolean isToolAvailable(FS fs, File gitDir, File directory,
+                       String path) {
                boolean available = true;
                try {
-                       CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), false);
-                       available = cmdExec.checkExecutable(path, repo.getWorkTree(),
-                                       prepareEnvironment(repo, null, null, null, null));
+                       CommandExecutor cmdExec = new CommandExecutor(fs, false);
+                       available = cmdExec.checkExecutable(path, directory,
+                                       prepareEnvironment(gitDir, null, null, null, null));
                } catch (Exception e) {
                        available = false;
                }
                return available;
        }
 
+       /**
+        * @param defaultName
+        *            the default tool name
+        * @param userDefinedNames
+        *            the user defined tool names
+        * @param preDefinedNames
+        *            the pre defined tool names
+        * @return the sorted tool names set: first element is default tool name if
+        *         valid, then user defined tool names and then pre defined tool
+        *         names
+        */
+       public static Set<String> createSortedToolSet(String defaultName,
+                       Set<String> userDefinedNames, Set<String> preDefinedNames) {
+               Set<String> names = new LinkedHashSet<>();
+               if (defaultName != null) {
+                       // remove defaultName from both sets
+                       Set<String> namesPredef = new LinkedHashSet<>();
+                       Set<String> namesUser = new LinkedHashSet<>();
+                       namesUser.addAll(userDefinedNames);
+                       namesUser.remove(defaultName);
+                       namesPredef.addAll(preDefinedNames);
+                       namesPredef.remove(defaultName);
+                       // add defaultName as first in set
+                       names.add(defaultName);
+                       names.addAll(namesUser);
+                       names.addAll(namesPredef);
+               } else {
+                       names.addAll(userDefinedNames);
+                       names.addAll(preDefinedNames);
+               }
+               return names;
+       }
+
 }
index 5902c1e1b8a30783215a03fcca02b2ec0dfd77e4..ba8ca54c58f46d5ea8137581b79a587da1c6cadb 100644 (file)
@@ -57,6 +57,8 @@ public class FileElement {
 
        private final Type type;
 
+       private final File workDir;
+
        private InputStream stream;
 
        private File tempFile;
@@ -70,7 +72,7 @@ public class FileElement {
         *            the element type
         */
        public FileElement(String path, Type type) {
-               this(path, type, null, null);
+               this(path, type, null);
        }
 
        /**
@@ -80,17 +82,31 @@ public class FileElement {
         *            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 workDir
+        *            the working directory of the path (can be null, then current
+        *            working dir is used)
+        */
+       public FileElement(String path, Type type, File workDir) {
+               this(path, type, workDir, null);
+       }
+
+       /**
+        * @param path
+        *            the file path
+        * @param type
+        *            the element type
+        * @param workDir
+        *            the working directory of the path (can be null, then current
+        *            working dir is used)
         * @param stream
-        *            the object stream to load instead of file
+        *            the object stream to load and write on demand, @see getFile(),
+        *            to tempFile once (can be null)
         */
-       public FileElement(String path, Type type, File tempFile,
+       public FileElement(String path, Type type, File workDir,
                        InputStream stream) {
                this.path = path;
                this.type = type;
-               this.tempFile = tempFile;
+               this.workDir = workDir;
                this.stream = stream;
        }
 
@@ -109,41 +125,39 @@ public class FileElement {
        }
 
        /**
-        * 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
+        * <ul>
+        * <li>a temporary file if already created and stream is not valid</li>
+        * <li>OR a real file from work tree: if no temp file was created (@see
+        * createTempFile()) and if no stream was set</li>
+        * <li>OR an empty temporary file if path is "/dev/null"</li>
+        * <li>OR a temporary file with stream content if stream is valid (not
+        * null); stream is closed and invalidated (set to null) after write to temp
+        * file, so stream is used only once during first call!</li>
+        * </ul>
         *
         * @return the object stream
         * @throws IOException
         */
        public File getFile() throws IOException {
+               // if we have already temp file and no stream
+               // then just return this temp file (it was filled from outside)
                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
+               File file = new File(workDir, path);
+               // if we have a stream or file is missing (path is "/dev/null")
+               // then optionally create temporary file and fill it with stream content
                if ((stream != null) || isNullPath()) {
-                       tempFile = getTempFile(file);
-                       return copyFromStream(tempFile, stream);
+                       if (tempFile == null) {
+                               tempFile = getTempFile(file, type.name(), null);
+                       }
+                       if (stream != null) {
+                               copyFromStream(tempFile, stream);
+                       }
+                       // invalidate the stream, because it is used once
+                       stream = null;
+                       return tempFile;
                }
                return file;
        }
@@ -158,7 +172,7 @@ public class FileElement {
        }
 
        /**
-        * Create temporary file in given or system temporary directory
+        * Create temporary file in given or system temporary directory.
         *
         * @param directory
         *            the directory for the file (can be null); if null system
@@ -168,75 +182,23 @@ public class FileElement {
         */
        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);
-                       }
+                       tempFile = getTempFile(new File(path), type.name(), directory);
                }
                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);
+               if (tempFile != null && tempFile.exists()) {
+                       tempFile.delete();
                }
-               return result;
+               tempFile = null;
        }
 
        /**
-        * Replace variable in input
+        * Replace variable in input.
         *
         * @param input
         *            the input string
@@ -258,4 +220,43 @@ public class FileElement {
                env.put(type.name(), getFile().getPath());
        }
 
+       private static File getTempFile(final File file, final String midName,
+                       final File workingDir) throws IOException {
+               String[] fileNameAndExtension = splitBaseFileNameAndExtension(file);
+               // TODO: avoid long random file name (number generated by
+               // createTempFile)
+               return File.createTempFile(
+                               fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
+                               fileNameAndExtension[1], workingDir);
+       }
+
+       private static void copyFromStream(final File file,
+                       final InputStream stream)
+                       throws IOException, FileNotFoundException {
+               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 and invalidate
+                       stream.close();
+               }
+       }
+
+       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;
+       }
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java
new file mode 100644 (file)
index 0000000..36b290d
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018-2019, Tim Neumann <Tim.Neumann@advantest.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.List;
+
+/**
+ * A handler for when the diff/merge tool manager wants to inform the user that
+ * no tool has been configured and one of the default tools will be used.
+ */
+public interface InformNoToolHandler {
+       /**
+        * Inform the user, that no tool is configured and that one of the given
+        * tools is used.
+        *
+        * @param toolNames
+        *            The tools which are tried
+        */
+       void inform(List<String> toolNames);
+}
index 9be20b75ad6825251b884f354b0f4f0286cec580..9625d5f101505a3e8cb2a12d6f6a4174fc30b05a 100644 (file)
@@ -31,7 +31,7 @@ import org.eclipse.jgit.lib.Config.SectionParser;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
 
 /**
- * Keeps track of difftool related configuration options.
+ * Keeps track of merge tool related configuration options.
  */
 public class MergeToolConfig {
 
index d91d57f1a8feea59e629b010ef3e51e0d3d74244..d2055272e011687aa0dff3fe44e24fc62534de8e 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.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
@@ -15,22 +16,30 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Map;
+import java.util.Optional;
 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.StoredConfig;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FS.ExecutionResult;
-import org.eclipse.jgit.util.StringUtils;
 
 /**
  * Manages merge tools.
  */
 public class MergeTools {
 
-       Repository repo;
+       private final FS fs;
+
+       private final File gitDir;
+
+       private final File workTree;
 
        private final MergeToolConfig config;
 
@@ -39,17 +48,111 @@ public class MergeTools {
        private final Map<String, ExternalMergeTool> userDefinedTools;
 
        /**
+        * Creates the external merge-tools manager for given repository.
+        *
         * @param repo
         *            the repository
         */
        public MergeTools(Repository repo) {
-               this.repo = repo;
-               config = repo.getConfig().get(MergeToolConfig.KEY);
+               this(repo, repo.getConfig());
+       }
+
+       /**
+        * Creates the external diff-tools manager for given configuration.
+        *
+        * @param config
+        *            the git configuration
+        */
+       public MergeTools(StoredConfig config) {
+               this(null, config);
+       }
+
+       private MergeTools(Repository repo, StoredConfig config) {
+               this.config = config.get(MergeToolConfig.KEY);
+               this.gitDir = repo == null ? null : repo.getDirectory();
+               this.fs = repo == null ? FS.DETECTED : repo.getFS();
+               this.workTree = repo == null ? null : repo.getWorkTree();
                predefinedTools = setupPredefinedTools();
-               userDefinedTools = setupUserDefinedTools(config, predefinedTools);
+               userDefinedTools = setupUserDefinedTools(predefinedTools);
+       }
+
+       /**
+        * Merge two versions of a file with optional base file.
+        *
+        * @param localFile
+        *            The local/left version of the file.
+        * @param remoteFile
+        *            The remote/right version of the file.
+        * @param mergedFile
+        *            The file for the result.
+        * @param baseFile
+        *            The base version of the file. May be null.
+        * @param tempDir
+        *            The tmepDir used for the files. May be null.
+        * @param toolName
+        *            Optionally the name of the tool to use. If not given the
+        *            default tool will be used.
+        * @param prompt
+        *            Optionally a flag whether to prompt the user before compare.
+        *            If not given the default will be used.
+        * @param gui
+        *            A flag whether to prefer a gui tool.
+        * @param promptHandler
+        *            The handler to use when needing to prompt the user if he wants
+        *            to continue.
+        * @param noToolHandler
+        *            The handler to use when needing to inform the user, that no
+        *            tool is configured.
+        * @return the optional result of executing the tool if it was executed
+        * @throws ToolException
+        *             when the tool fails
+        */
+       public Optional<ExecutionResult> merge(FileElement localFile,
+                       FileElement remoteFile, FileElement mergedFile,
+                       FileElement baseFile, File tempDir, Optional<String> toolName,
+                       BooleanTriState prompt, boolean gui,
+                       PromptContinueHandler promptHandler,
+                       InformNoToolHandler noToolHandler) throws ToolException {
+
+               String toolNameToUse;
+
+               if (toolName.isPresent()) {
+                       toolNameToUse = toolName.get();
+               } else {
+                       toolNameToUse = getDefaultToolName(gui);
+
+                       if (toolNameToUse == null || toolNameToUse.isEmpty()) {
+                               noToolHandler.inform(new ArrayList<>(predefinedTools.keySet()));
+                               toolNameToUse = getFirstAvailableTool();
+                       }
+               }
+
+               boolean doPrompt;
+               if (prompt != BooleanTriState.UNSET) {
+                       doPrompt = prompt == BooleanTriState.TRUE;
+               } else {
+                       doPrompt = isInteractive();
+               }
+
+               if (doPrompt) {
+                       if (!promptHandler.prompt(toolNameToUse)) {
+                               return Optional.empty();
+                       }
+               }
+
+               ExternalMergeTool tool = getTool(toolNameToUse);
+               if (tool == null) {
+                       throw new ToolException(
+                                       "External merge tool is not defined: " + toolNameToUse); //$NON-NLS-1$
+               }
+
+               return Optional.of(merge(localFile, remoteFile, mergedFile, baseFile,
+                               tempDir, tool));
        }
 
        /**
+        * Merge two versions of a file with optional base file.
+        *
         * @param localFile
         *            the local file element
         * @param remoteFile
@@ -61,38 +164,31 @@ public class MergeTools {
         * @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
+        * @param tool
+        *            the selected tool
         * @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);
+                       ExternalMergeTool tool) throws ToolException {
                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);
+                       backup = createBackupFile(mergedFile,
+                                       tempDir != null ? tempDir : workTree);
                        // 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);
+                       Map<String, String> env = ExternalToolUtils.prepareEnvironment(
+                                       gitDir, localFile, remoteFile, mergedFile, baseFile);
+                       boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
                        // execute the tool
-                       CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
-                       result = cmdExec.run(command, workingDir, env);
+                       CommandExecutor cmdExec = new CommandExecutor(fs, trust);
+                       result = cmdExec.run(command, workTree, env);
                        // keep backup as .orig file
                        if (backup != null) {
                                keepBackupFile(mergedFile.getPath(), backup);
@@ -124,19 +220,21 @@ public class MergeTools {
                }
        }
 
-       private FileElement createBackupFile(String filePath, File parentDir)
+       private FileElement createBackupFile(FileElement from, File toParentDir)
                        throws IOException {
                FileElement backup = null;
-               Path path = Paths.get(filePath);
+               Path path = Paths.get(from.getPath());
                if (Files.exists(path)) {
-                       backup = new FileElement(filePath, Type.BACKUP);
-                       Files.copy(path, backup.createTempFile(parentDir).toPath(),
+                       backup = new FileElement(from.getPath(), Type.BACKUP);
+                       Files.copy(path, backup.createTempFile(toParentDir).toPath(),
                                        StandardCopyOption.REPLACE_EXISTING);
                }
                return backup;
        }
 
        /**
+        * Create temporary directory.
+        *
         * @return the created temporary directory if (mergetol.writeToTemp == true)
         *         or null if not configured or false.
         * @throws IOException
@@ -148,20 +246,46 @@ public class MergeTools {
        }
 
        /**
-        * @return the tool names
+        * Get user defined tool names.
+        *
+        * @return the user defined tool names
         */
-       public Set<String> getToolNames() {
-               return config.getToolNames();
+       public Set<String> getUserDefinedToolNames() {
+               return userDefinedTools.keySet();
+       }
+
+       /**
+        * @return the predefined tool names
+        */
+       public Set<String> getPredefinedToolNames() {
+               return predefinedTools.keySet();
+       }
+
+       /**
+        * Get all tool names.
+        *
+        * @return the all tool names (default or available tool name is the first
+        *         in the set)
+        */
+       public Set<String> getAllToolNames() {
+               String defaultName = getDefaultToolName(false);
+               if (defaultName == null) {
+                       defaultName = getFirstAvailableTool();
+               }
+               return ExternalToolUtils.createSortedToolSet(defaultName,
+                               getUserDefinedToolNames(), getPredefinedToolNames());
        }
 
        /**
         * @return the user defined tools
         */
        public Map<String, ExternalMergeTool> getUserDefinedTools() {
-               return userDefinedTools;
+               return Collections.unmodifiableMap(userDefinedTools);
        }
 
        /**
+        * Get predefined tools map.
+        *
         * @param checkAvailability
         *            true: for checking if tools can be executed; ATTENTION: this
         *            check took some time, do not execute often (store the map for
@@ -175,20 +299,23 @@ public class MergeTools {
                if (checkAvailability) {
                        for (ExternalMergeTool tool : predefinedTools.values()) {
                                PreDefinedMergeTool predefTool = (PreDefinedMergeTool) tool;
-                               predefTool.setAvailable(ExternalToolUtils.isToolAvailable(repo,
-                                               predefTool.getPath()));
+                               predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
+                                               gitDir, workTree, predefTool.getPath()));
                        }
                }
-               return predefinedTools;
+               return Collections.unmodifiableMap(predefinedTools);
        }
 
        /**
+        * Get first available tool name.
+        *
         * @return the name of first available predefined tool or null
         */
        public String getFirstAvailableTool() {
                String name = null;
                for (ExternalMergeTool tool : predefinedTools.values()) {
-                       if (ExternalToolUtils.isToolAvailable(repo, tool.getPath())) {
+                       if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
+                                       tool.getPath())) {
                                name = tool.getName();
                                break;
                        }
@@ -197,35 +324,24 @@ public class MergeTools {
        }
 
        /**
-        * @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();
-       }
-
-       /**
+        * Is interactive merge (prompt enabled) ?
+        *
         * @return is interactive (config prompt enabled) ?
         */
        public boolean isInteractive() {
                return config.isPrompt();
        }
 
-       private ExternalMergeTool guessTool(String toolName, BooleanTriState gui)
-                       throws ToolException {
-               if (StringUtils.isEmptyOrNull(toolName)) {
-                       toolName = getDefaultToolName(gui);
-               }
-               ExternalMergeTool tool = null;
-               if (!StringUtils.isEmptyOrNull(toolName)) {
-                       tool = getTool(toolName);
-               }
-               if (tool == null) {
-                       throw new ToolException("Unknown merge tool '" + toolName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
-               }
-               return tool;
+       /**
+        * Get the default (gui-)tool name.
+        *
+        * @param gui
+        *            use the diff.guitool setting ?
+        * @return the default tool name
+        */
+       public String getDefaultToolName(boolean gui) {
+               return gui ? config.getDefaultGuiToolName()
+                               : config.getDefaultToolName();
        }
 
        private ExternalMergeTool getTool(final String name) {
@@ -256,9 +372,9 @@ public class MergeTools {
        }
 
        private Map<String, ExternalMergeTool> setupUserDefinedTools(
-                       MergeToolConfig cfg, Map<String, ExternalMergeTool> predefTools) {
+                       Map<String, ExternalMergeTool> predefTools) {
                Map<String, ExternalMergeTool> tools = new TreeMap<>();
-               Map<String, ExternalMergeTool> userTools = cfg.getTools();
+               Map<String, ExternalMergeTool> userTools = config.getTools();
                for (String name : userTools.keySet()) {
                        ExternalMergeTool userTool = userTools.get(name);
                        // if mergetool.<name>.cmd is defined we have user defined tool
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java
new file mode 100644 (file)
index 0000000..6ad33df
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018-2019, Tim Neumann <Tim.Neumann@advantest.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;
+
+/**
+ * A handler for when the diff/merge tool manager wants to prompt the user
+ * whether to continue
+ */
+public interface PromptContinueHandler {
+       /**
+        * Prompt the user whether to continue with the next file by opening a given
+        * tool.
+        *
+        * @param toolName
+        *            The name of the tool to open
+        * @return Whether the user wants to continue
+        */
+       boolean prompt(String toolName);
+}