From ff77d412a9f1a956b8d76b139489038ee3b0870c Mon Sep 17 00:00:00 2001 From: Andre Bossert Date: Sun, 19 Jan 2020 20:57:23 +0100 Subject: [PATCH] Adapt diff- and merge tool code for PGM and EGit usage 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 Signed-off-by: Tim Neumann --- .../org/eclipse/jgit/pgm/DiffToolTest.java | 114 ++++++++- .../org/eclipse/jgit/pgm/MergeToolTest.java | 77 +++++- .../org/eclipse/jgit/pgm/ToolTestCase.java | 55 +++- .../src/org/eclipse/jgit/pgm/DiffTool.java | 175 +++++++------ .../src/org/eclipse/jgit/pgm/MergeTool.java | 137 +++++----- .../diffmergetool/ExternalDiffToolTest.java | 153 ++++++++---- .../diffmergetool/ExternalMergeToolTest.java | 187 +++++++++++--- .../diffmergetool/ExternalToolTestCase.java | 37 +++ org.eclipse.jgit/META-INF/MANIFEST.MF | 6 +- .../internal/diffmergetool/DiffTools.java | 230 ++++++++++++----- .../diffmergetool/ExternalToolUtils.java | 66 ++++- .../internal/diffmergetool/FileElement.java | 185 +++++++------- .../diffmergetool/InformNoToolHandler.java | 28 +++ .../diffmergetool/MergeToolConfig.java | 2 +- .../internal/diffmergetool/MergeTools.java | 236 +++++++++++++----- .../diffmergetool/PromptContinueHandler.java | 27 ++ 16 files changed, 1222 insertions(+), 493 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java index a258821f0..ce4c004c0 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java @@ -16,11 +16,14 @@ 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 void setUp() throws Exception { configureEchoTool(TOOL_NAME); } + @Test(expected = Die.class) + public void testUndefinedTool() throws Exception { + String toolName = "undefined"; + String[] conflictingFilenames = createUnstagedChanges(); + + List 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 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, @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 @@ private void configureEchoTool(String toolName) { 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 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 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 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\")"; + } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java index e9d559e70..1236dd30d 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java @@ -14,8 +14,10 @@ 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 void setUp() throws Exception { configureEchoTool(TOOL_NAME); } + @Test + public void testUndefinedTool() throws Exception { + String toolName = "undefined"; + String[] conflictingFilenames = createMergeConflict(); + + List 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 @@ private void configureEchoTool(String toolName) { String.valueOf(false)); } - private static String[] getExpectedMergeConflictOutputNoPrompt( + private String[] getExpectedMergeConflictOutputNoPrompt( String[] conflictFilenames) { List expected = new ArrayList<>(); expected.add("Merging:"); @@ -232,7 +286,8 @@ private static String[] getExpectedMergeConflictOutputNoPrompt( + "':"); 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 @@ private static String[] getExpectedAbortLaunchOutput( return expected.toArray(new String[0]); } - private static String[] getExpectedAbortMergeOutput( + private String[] getExpectedAbortMergeOutput( String[] conflictFilenames, int abortIndex) { List expected = new ArrayList<>(); expected.add("Merging:"); @@ -274,8 +329,9 @@ private static String[] getExpectedAbortMergeOutput( "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 @@ private static String[] getExpectedAbortMergeOutput( return expected.toArray(new String[0]); } - private static String[] getExpectedMergeConflictOutput( + private String[] getExpectedMergeConflictOutput( String[] conflictFilenames) { List expected = new ArrayList<>(); expected.add("Merging:"); @@ -299,8 +355,9 @@ private static String[] getExpectedMergeConflictOutput( + "':"); 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 @@ private static String[] getExpectedDeletedConflictOutput( } 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\")"; + } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java index 933f19bcc..a3c41f0fe 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java @@ -10,13 +10,18 @@ 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.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 @@ protected String[] runAndCaptureUsingInitRaw(String... args) return runAndCaptureUsingInitRaw(inputStream, args); } + protected String[] runAndCaptureUsingInitRaw( + List 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 expectedErrorOutput = Collections.emptyList(); + return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput, + args); + } + + protected String[] runAndCaptureUsingInitRaw(InputStream inputStream, + List expectedErrorOutput, String... args) + throws CmdLineException, Exception, IOException { CLIGitCommand.Result result = new CLIGitCommand.Result(); GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); @@ -86,7 +107,7 @@ protected String[] runAndCaptureUsingInitRaw(InputStream inputStream, .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 @@ protected List getRepositoryChanges(RevCommit commit) 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 @@ protected static void assertArrayOfLinesEquals(String failMessage, 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); + } } } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java index e5a3c53e3..3e6042afe 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2018-2022, Andre Bossert + * Copyright (C) 2019, Tim Neumann * * 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.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.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.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 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 @@ void noPrompt(@SuppressWarnings("unused") boolean on) { @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 @@ protected void run() { 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 files = getFiles(); if (files.size() > 0) { - compare(files, showPrompt, toolNameToUse); + compare(files); } } } catch (RevisionSyntaxException | IOException e) { @@ -160,79 +160,103 @@ protected void run() { } } - private String promptToolName() throws IOException { - String toolNameToUse = toolName; - if (StringUtils.isEmptyOrNull(toolNameToUse)) { - toolNameToUse = diffTools.getDefaultToolName(gui); - } - if (StringUtils.isEmptyOrNull(toolNameToUse)) { - Map predefTools = diffTools - .getPredefinedTools(false); + private void informUserNoTool(List 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)); - } - return toolNameToUse; } - private void compare(List files, boolean showPrompt, - String toolNameToUse) throws IOException { + 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$ + } + } + } + + private void compare(List 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(); + + String filePath = ent.getNewPath(); + if (filePath.equals(DiffEntry.DEV_NULL)) { + filePath = ent.getOldPath(); } - // check if user wants to launch compare - boolean launchCompare = true; - if (showPrompt) { - launchCompare = isLaunchCompare(fileIndex + 1, files.size(), - mergedFilePath, toolNameToUse); - } - 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 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 @@ private void compare(List files, boolean showPrompt, } } - @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 predefTools = diffTools .getPredefinedTools(true); @@ -314,12 +322,12 @@ private List getFiles() } 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 @@ private FileElement createFileElement(FileElement.Type elementType, 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); } } } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java index f5884c44d..2a411b81f 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2018-2022, Andre Bossert + * Copyright (C) 2019, Tim Neumann * * 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.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.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.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.lib.Repository; +import org.eclipse.jgit.lib.internal.BooleanTriState; +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.lib.Constants; -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.util.FS.ExecutionResult; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; @@ -62,9 +63,13 @@ class MergeTool extends TextBuiltin { private MergeTools mergeTools; + private Optional 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 @@ void noPrompt(@SuppressWarnings("unused") boolean on) { @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 @@ protected void run() { 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 files = getFiles(); if (files.size() > 0) { - merge(files, showPrompt, toolNameToUse); + merge(files); } else { outw.println(CLIText.get().mergeToolNoFiles); } @@ -137,32 +135,21 @@ protected void run() { } } - private String promptToolName() throws IOException { - String toolNameToUse = toolName; - if (StringUtils.isEmptyOrNull(toolNameToUse)) { - toolNameToUse = mergeTools.getDefaultToolName(gui); - } - if (StringUtils.isEmptyOrNull(toolNameToUse)) { - Map predefTools = mergeTools - .getPredefinedTools(false); + private void informUserNoTool(List 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 files, boolean showPrompt, - String toolNamePrompt) throws Exception { + private void merge(Map files) throws Exception { // sort file names List mergedFilePaths = new ArrayList<>(files.keySet()); Collections.sort(mergedFilePaths); @@ -174,6 +161,10 @@ private void merge(Map files, boolean showPrompt, 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 @@ private void merge(Map files, boolean showPrompt, // 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 @@ private void merge(Map files, boolean showPrompt, } } - 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 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt, 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 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt, .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 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt, // 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 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 @@ private boolean isMergeSuccessful() throws IOException { return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull); } - private boolean isLaunch(String toolNamePrompt) throws IOException { - boolean launch = true; - outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch, - toolNamePrompt) + " "); //$NON-NLS-1$ - outw.flush(); - BufferedReader br = inputReader; - String line = null; - if ((line = br.readLine()) != null) { - if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$ - launch = false; + 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 { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java index 1b501c25b..222608e31 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java @@ -18,13 +18,20 @@ 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 void testUserToolWithError() throws Exception { 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 void testUserToolWithCommandNotFoundError() throws Exception { config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, command); - DiffTools manager = new DiffTools(db); - - BooleanTriState prompt = BooleanTriState.UNSET; - BooleanTriState gui = BooleanTriState.UNSET; - BooleanTriState trustExitCode = BooleanTriState.FALSE; - - manager.compare(local, remote, merged, toolName, prompt, gui, - trustExitCode); - + invokeCompare(toolName); fail("Expected exception to be thrown due to external tool exiting with error code: " + errorReturnCode); } @Test - public void testToolNames() { + 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); - Set actualToolNames = manager.getToolNames(); - Set expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of external diff tool names", - expectedToolNames, actualToolNames); + + Map tools = manager.getUserDefinedTools(); + ExternalDiffTool externalTool = tools.get(customToolName); + boolean trustExitCode = true; + manager.compare(local, remote, externalTool, trustExitCode); + + assertEchoCommandHasCorrectOutput(); + } + + @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 actualToolPrompts = promptHandler.toolPrompts; + List 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 { + DiffTools manager = new DiffTools(db); + + PromptHandler promptHandler = PromptHandler.cancelPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + Optional 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 actualToolNames = manager.getPredefinedTools(true).keySet(); + Set actualToolNames = manager.getAllToolNames(); Set expectedToolNames = new LinkedHashSet<>(); + expectedToolNames.add(customToolName); CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); for (CommandLineDiffTool defaultTool : defaultTools) { String toolName = defaultTool.name(); @@ -166,18 +217,12 @@ public void testCompare() throws ToolException { 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 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 void testDefaultTool() throws Exception { 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 void testDefaultTool() throws Exception { 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 void testOverridePreDefinedToolPath() { @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 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 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 actualLines = Files.readAllLines(commandResult.toPath()); + String actualContent = String.join(System.lineSeparator(), actualLines); + actualLines = Arrays.asList(actualContent.split(" ")); + List expectedLines = Arrays.asList(localFile.getAbsolutePath(), + remoteFile.getAbsolutePath()); + assertEquals("Dummy test tool called with unexpected arguments", + expectedLines, actualLines); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java index 305f2b470..130b42a92 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java @@ -18,13 +18,21 @@ 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 void testUserToolWithError() throws Exception { 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 void testUserToolWithCommandNotFoundError() throws Exception { config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, command); - MergeTools manager = new MergeTools(db); - - BooleanTriState prompt = BooleanTriState.UNSET; - BooleanTriState gui = BooleanTriState.UNSET; - - manager.merge(local, remote, merged, base, null, toolName, prompt, gui); + invokeMerge(toolName); fail("Expected exception to be thrown due to external tool exiting with error code: " + errorReturnCode); } @Test - public void testToolNames() { + 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); - Set actualToolNames = manager.getToolNames(); - Set expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of external merge tool names", - expectedToolNames, actualToolNames); + ExecutionResult result = manager.merge(local, remote, merged, null, + null, externalTool); + assertEquals("Expected merge tool to succeed", 0, result.getRc()); + + List 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); + } + + @Test + public void testUserDefinedTool() 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); + Map tools = manager.getUserDefinedTools(); + ExternalMergeTool externalTool = tools.get(customToolName); + manager.merge(local, remote, merged, base, null, externalTool); + + assertEchoCommandHasCorrectOutput(); + } + + @Test + 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); + + 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 actualToolPrompts = promptHandler.toolPrompts; + List 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 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 actualToolNames = manager.getPredefinedTools(true).keySet(); + Set actualToolNames = manager.getAllToolNames(); Set expectedToolNames = new LinkedHashSet<>(); + expectedToolNames.add(customToolName); CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); for (CommandLineMergeTool defaultTool : defaultTools) { String toolName = defaultTool.name(); @@ -165,16 +249,12 @@ public void testCompare() throws ToolException { config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, command); - BooleanTriState prompt = BooleanTriState.UNSET; - BooleanTriState gui = BooleanTriState.UNSET; - - MergeTools manager = new MergeTools(db); - + Optional 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 void testDefaultTool() throws Exception { 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 void testDefaultTool() throws Exception { 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 void testOverridePreDefinedToolPath() { @Test(expected = ToolException.class) public void testUndefinedTool() throws Exception { - MergeTools manager = new MergeTools(db); - String toolName = "undefined"; - BooleanTriState prompt = BooleanTriState.UNSET; - BooleanTriState gui = BooleanTriState.UNSET; - - manager.merge(local, remote, merged, base, null, toolName, prompt, gui); + invokeMerge(toolName); fail("Expected exception to be thrown due to not defined external merge tool"); } + private Optional invokeMerge(String toolName) + throws ToolException { + BooleanTriState prompt = BooleanTriState.UNSET; + boolean gui = false; + + MergeTools manager = new MergeTools(db); + + PromptHandler promptHandler = PromptHandler.acceptPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + Optional 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 actualLines = Files.readAllLines(commandResult.toPath()); + String actualContent = String.join(System.lineSeparator(), actualLines); + actualLines = Arrays.asList(actualContent.split(" ")); + List expectedLines = Arrays.asList(localFile.getAbsolutePath(), + remoteFile.getAbsolutePath(), mergedFile.getAbsolutePath(), + baseFile.getAbsolutePath()); + assertEquals("Dummy test tool called with unexpected arguments", + expectedLines, actualLines); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java index 0fd85cb45..7a6ff4657 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java @@ -11,6 +11,8 @@ 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 @@ protected static void assumePosixPlatform() { "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 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 missingTools = new ArrayList<>(); + + @Override + public void inform(List toolNames) { + missingTools.addAll(toolNames); + } + } } diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index e72f00f0b..a25685073 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -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, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index 6c1a780a3..5fe263a4e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2018-2022, Andre Bossert + * Copyright (C) 2019, Tim Neumann * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -10,23 +11,31 @@ 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,10 +50,103 @@ 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/left version of the file. + * @param remoteFile + * The remote/right version of the file. + * @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 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 compare(FileElement localFile, + FileElement remoteFile, Optional 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)); } /** @@ -54,56 +156,70 @@ public DiffTools(Repository repo) { * the local file element * @param remoteFile * the remote file element - * @param mergedFile - * the merged file element, it's path equals local or remote - * element path - * @param toolName - * the selected tool name (can be null) - * @param prompt - * the prompt option - * @param gui - * the GUI option + * @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 env = ExternalToolUtils.prepareEnvironment(repo, - localFile, remoteFile, mergedFile, null); - boolean trust = config.isTrustExitCode(); - if (trustExitCode != BooleanTriState.UNSET) { - trust = trustExitCode == BooleanTriState.TRUE; - } + Map 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 getToolNames() { - return config.getToolNames(); + public Set getUserDefinedToolNames() { + return userDefinedTools.keySet(); } /** + * Get predefined tool names. + * + * @return the predefined tool names + */ + public Set 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 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 getUserDefinedTools() { @@ -111,6 +227,8 @@ public Map getUserDefinedTools() { } /** + * 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 Map getPredefinedTools( 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 @@ private static Map setupPredefinedTools() { return tools; } - private static Map setupUserDefinedTools( - DiffToolConfig cfg, Map predefTools) { + private Map setupUserDefinedTools( + Map predefTools) { Map tools = new TreeMap<>(); - Map userTools = cfg.getTools(); + Map userTools = config.getTools(); for (String name : userTools.keySet()) { ExternalDiffTool userTool = userTools.get(name); // if difftool..cmd is defined we have user defined tool diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java index e10607d2f..9a6968113 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java @@ -10,10 +10,14 @@ 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 static String prepareCommand(String command, FileElement localFile, /** * 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 static String prepareCommand(String command, FileElement localFile, * @return the environment map with variables and values (file paths) * @throws IOException */ - public static Map prepareEnvironment(Repository repo, + public static Map prepareEnvironment(File gitDir, FileElement localFile, FileElement remoteFile, FileElement mergedFile, FileElement baseFile) throws IOException { Map 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 static String quotePath(String path) { } /** - * @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 createSortedToolSet(String defaultName, + Set userDefinedNames, Set preDefinedNames) { + Set names = new LinkedHashSet<>(); + if (defaultName != null) { + // remove defaultName from both sets + Set namesPredef = new LinkedHashSet<>(); + Set 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; + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java index 5902c1e1b..ba8ca54c5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java @@ -57,6 +57,8 @@ public enum Type { private final Type type; + private final File workDir; + private InputStream stream; private File tempFile; @@ -70,7 +72,7 @@ public enum Type { * the element type */ public FileElement(String path, Type type) { - this(path, type, null, null); + this(path, type, null); } /** @@ -80,17 +82,31 @@ public FileElement(String path, Type type) { * the file path * @param type * the element type - * @param tempFile - * the temporary file to be used (can be null and will be created - * then) - * @param stream - * the object stream to load instead of file + * @param workDir + * the working directory of the path (can be null, then current + * working dir is used) */ - public FileElement(String path, Type type, File tempFile, + 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 and write on demand, @see getFile(), + * to tempFile once (can be null) + */ + 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 Type getType() { } /** - * 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 + *
    + *
  • a temporary file if already created and stream is not valid
  • + *
  • OR a real file from work tree: if no temp file was created (@see + * createTempFile()) and if no stream was set
  • + *
  • OR an empty temporary file if path is "/dev/null"
  • + *
  • 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!
  • + *
* * @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 boolean isNullPath() { } /** - * 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 boolean isNullPath() { */ 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(); + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } tempFile = null; } - private static File copyFromStream(File file, final InputStream stream) - throws IOException, FileNotFoundException { - if (stream != null) { - try (OutputStream outStream = new FileOutputStream(file)) { - int read = 0; - byte[] bytes = new byte[8 * 1024]; - while ((read = stream.read(bytes)) != -1) { - outStream.write(bytes, 0, read); - } - } finally { - // stream can only be consumed once --> close it - stream.close(); - } - } - return file; - } - - private static String[] splitBaseFileNameAndExtension(File file) { - String[] result = new String[2]; - result[0] = file.getName(); - result[1] = ""; //$NON-NLS-1$ - int idx = result[0].lastIndexOf("."); //$NON-NLS-1$ - // if "." was found (>-1) and last-index is not first char (>0), then - // split (same behavior like cgit) - if (idx > 0) { - result[1] = result[0].substring(idx, result[0].length()); - result[0] = result[0].substring(0, idx); - } - return result; - } - /** - * Replace variable in input + * Replace variable in input. * * @param input * the input string @@ -258,4 +220,43 @@ public void addToEnv(Map env) throws IOException { 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 index 000000000..36b290d37 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018-2019, Tim Neumann + * + * 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 toolNames); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java index 9be20b75a..9625d5f10 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java @@ -31,7 +31,7 @@ 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 { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java index d91d57f1a..d2055272e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2018-2022, Andre Bossert + * Copyright (C) 2019, Tim Neumann * * 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.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 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); - predefinedTools = setupPredefinedTools(); - userDefinedTools = setupUserDefinedTools(config, predefinedTools); + 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(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 merge(FileElement localFile, + FileElement remoteFile, FileElement mergedFile, + FileElement baseFile, File tempDir, Optional 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 MergeTools(Repository repo) { * @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 env = ExternalToolUtils.prepareEnvironment(repo, - localFile, remoteFile, mergedFile, baseFile); + Map 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 ExecutionResult merge(FileElement localFile, FileElement remoteFile, } } - 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 File createTempDirectory() throws IOException { } /** - * @return the tool names + * Get user defined tool names. + * + * @return the user defined tool names */ - public Set getToolNames() { - return config.getToolNames(); + public Set getUserDefinedToolNames() { + return userDefinedTools.keySet(); + } + + /** + * @return the predefined tool names + */ + public Set 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 getAllToolNames() { + String defaultName = getDefaultToolName(false); + if (defaultName == null) { + defaultName = getFirstAvailableTool(); + } + return ExternalToolUtils.createSortedToolSet(defaultName, + getUserDefinedToolNames(), getPredefinedToolNames()); } /** * @return the user defined tools */ public Map 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 Map getPredefinedTools( 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 String getFirstAvailableTool() { } /** - * @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 @@ private Map setupPredefinedTools() { } private Map setupUserDefinedTools( - MergeToolConfig cfg, Map predefTools) { + Map predefTools) { Map tools = new TreeMap<>(); - Map userTools = cfg.getTools(); + Map userTools = config.getTools(); for (String name : userTools.keySet()) { ExternalMergeTool userTool = userTools.get(name); // if mergetool..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 index 000000000..6ad33df2a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018-2019, Tim Neumann + * + * 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); +}