Merge branch 'master' into stable-6.2

* master:
  Adapt diff- and merge tool code for PGM and EGit usage
  Teach JGit to handle external diff/merge tools defined in
.gitattributes

Change-Id: I3aefc14160caaac859bd3548460dd755ebe42fc5
This commit is contained in:
Andrey Loskutov 2022-06-03 15:49:45 +02:00
commit 2ef2a3562e
27 changed files with 1875 additions and 516 deletions

View File

@ -16,12 +16,17 @@
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool; import org.eclipse.jgit.internal.diffmergetool.DiffTools;
import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.lib.StoredConfig;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -40,6 +45,59 @@ public void setUp() throws Exception {
configureEchoTool(TOOL_NAME); configureEchoTool(TOOL_NAME);
} }
@Test(expected = Die.class)
public void testUndefinedTool() throws Exception {
String toolName = "undefined";
String[] conflictingFilenames = createUnstagedChanges();
List<String> expectedErrors = new ArrayList<>();
for (String changedFilename : conflictingFilenames) {
expectedErrors.add("External diff tool is not defined: " + toolName);
expectedErrors.add("compare of " + changedFilename + " failed");
}
runAndCaptureUsingInitRaw(expectedErrors, DIFF_TOOL, "--no-prompt",
"--tool", toolName);
fail("Expected exception to be thrown due to undefined external tool");
}
@Test(expected = Die.class)
public void testUserToolWithCommandNotFoundError() throws Exception {
String toolName = "customTool";
int errorReturnCode = 127; // command not found
String command = "exit " + errorReturnCode;
StoredConfig config = db.getConfig();
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
createMergeConflict();
runAndCaptureUsingInitRaw(DIFF_TOOL, "--no-prompt", "--tool", toolName);
fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode);
}
@Test(expected = Die.class)
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");
fail("Expected exception to be thrown due to external tool exiting with an error");
}
@Test @Test
public void testToolWithPrompt() throws Exception { public void testToolWithPrompt() throws Exception {
String[] inputLines = { String[] inputLines = {
@ -136,12 +194,12 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
@Test @Test
public void testToolCached() throws Exception { public void testToolCached() throws Exception {
String[] conflictingFilenames = createStagedChanges(); String[] conflictingFilenames = createStagedChanges();
String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames); Pattern[] expectedOutput = getExpectedCachedToolOutputNoPrompt(conflictingFilenames);
String[] options = { "--cached", "--staged", }; String[] options = { "--cached", "--staged", };
for (String option : options) { for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option, assertArrayOfMatchingLines("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
option, "--tool", TOOL_NAME)); option, "--tool", TOOL_NAME));
} }
@ -149,20 +207,38 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
@Test @Test
public void testToolHelp() throws Exception { public void testToolHelp() throws Exception {
CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
List<String> expectedOutput = new ArrayList<>(); List<String> expectedOutput = new ArrayList<>();
DiffTools diffTools = new DiffTools(db);
Map<String, ExternalDiffTool> predefinedTools = diffTools
.getPredefinedTools(true);
List<ExternalDiffTool> availableTools = new ArrayList<>();
List<ExternalDiffTool> notAvailableTools = new ArrayList<>();
for (ExternalDiffTool tool : predefinedTools.values()) {
if (tool.isAvailable()) {
availableTools.add(tool);
} else {
notAvailableTools.add(tool);
}
}
expectedOutput.add( expectedOutput.add(
"'git difftool --tool=<tool>' may be set to one of the following:"); "'git difftool --tool=<tool>' may be set to one of the following:");
for (CommandLineDiffTool defaultTool : defaultTools) { for (ExternalDiffTool tool : availableTools) {
String toolName = defaultTool.name(); String toolName = tool.getName();
expectedOutput.add(toolName); expectedOutput.add(toolName);
} }
String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " " String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
+ getEchoCommand(); + getEchoCommand();
expectedOutput.add("user-defined:"); expectedOutput.add("user-defined:");
expectedOutput.add(customToolHelpLine); expectedOutput.add(customToolHelpLine);
expectedOutput.add(
"The following tools are valid, but not currently available:");
for (ExternalDiffTool tool : notAvailableTools) {
String toolName = tool.getName();
expectedOutput.add(toolName);
}
String[] userDefinedToolsHelp = { String[] userDefinedToolsHelp = {
"The following tools are valid, but not currently available:",
"Some of the tools listed above only work in a windowed", "Some of the tools listed above only work in a windowed",
"environment. If run in a terminal-only session, they will fail.", "environment. If run in a terminal-only session, they will fail.",
}; };
@ -193,43 +269,76 @@ private void configureEchoTool(String toolName) {
String.valueOf(false)); String.valueOf(false));
} }
private static String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) { private String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) {
String[] expectedToolOutput = new String[conflictingFilenames.length]; String[] expectedToolOutput = new String[conflictingFilenames.length];
for (int i = 0; i < conflictingFilenames.length; ++i) { for (int i = 0; i < conflictingFilenames.length; ++i) {
String newPath = conflictingFilenames[i]; String newPath = conflictingFilenames[i];
String expectedLine = newPath; Path fullPath = getFullPath(newPath);
expectedToolOutput[i] = expectedLine; expectedToolOutput[i] = fullPath.toString();
} }
return expectedToolOutput; return expectedToolOutput;
} }
private static String[] getExpectedCompareOutput(String[] conflictingFilenames) { private Pattern[] getExpectedCachedToolOutputNoPrompt(String[] conflictingFilenames) {
String tmpDir = System.getProperty("java.io.tmpdir");
if (tmpDir.endsWith(File.separator)) {
tmpDir = tmpDir.substring(0, tmpDir.length() - 1);
}
Pattern emptyPattern = Pattern.compile("");
List<Pattern> expectedToolOutput = new ArrayList<>();
for (int i = 0; i < conflictingFilenames.length; ++i) {
String changedFilename = conflictingFilenames[i];
Path fullPath = getFullPath(changedFilename);
String filename = fullPath.getFileName().toString();
String regexp = tmpDir + File.separatorChar + filename
+ "_REMOTE_.*";
Pattern pattern = Pattern.compile(regexp);
expectedToolOutput.add(pattern);
expectedToolOutput.add(emptyPattern);
}
expectedToolOutput.add(emptyPattern);
return expectedToolOutput.toArray(new Pattern[0]);
}
private String[] getExpectedCompareOutput(String[] conflictingFilenames) {
List<String> expected = new ArrayList<>(); List<String> expected = new ArrayList<>();
int n = conflictingFilenames.length; int n = conflictingFilenames.length;
for (int i = 0; i < n; ++i) { for (int i = 0; i < n; ++i) {
String newPath = conflictingFilenames[i]; String changedFilename = conflictingFilenames[i];
expected.add( expected.add(
"Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'"); "Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
+ "'");
expected.add("Launch '" + TOOL_NAME + "' [Y/n]?"); expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
expected.add(newPath); Path fullPath = getFullPath(changedFilename);
expected.add(fullPath.toString());
} }
return expected.toArray(new String[0]); return expected.toArray(new String[0]);
} }
private static String[] getExpectedAbortOutput(String[] conflictingFilenames, private String[] getExpectedAbortOutput(String[] conflictingFilenames,
int abortIndex) { int abortIndex) {
List<String> expected = new ArrayList<>(); List<String> expected = new ArrayList<>();
int n = conflictingFilenames.length; int n = conflictingFilenames.length;
for (int i = 0; i < n; ++i) { for (int i = 0; i < n; ++i) {
String newPath = conflictingFilenames[i]; String changedFilename = conflictingFilenames[i];
expected.add( expected.add(
"Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'"); "Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
+ "'");
expected.add("Launch '" + TOOL_NAME + "' [Y/n]?"); expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
if (i == abortIndex) { if (i == abortIndex) {
break; break;
} }
expected.add(newPath); Path fullPath = getFullPath(changedFilename);
expected.add(fullPath.toString());
} }
return expected.toArray(new String[0]); 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\")";
}
} }

View File

@ -14,13 +14,17 @@
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; 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_MERGETOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
import static org.junit.Assert.fail;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import org.eclipse.jgit.internal.diffmergetool.CommandLineMergeTool; import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
import org.eclipse.jgit.internal.diffmergetool.MergeTools;
import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.lib.StoredConfig;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -39,6 +43,58 @@ public void setUp() throws Exception {
configureEchoTool(TOOL_NAME); configureEchoTool(TOOL_NAME);
} }
@Test
public void testUndefinedTool() throws Exception {
String toolName = "undefined";
String[] conflictingFilenames = createMergeConflict();
List<String> expectedErrors = new ArrayList<>();
for (String conflictingFilename : conflictingFilenames) {
expectedErrors.add("External merge tool is not defined: " + toolName);
expectedErrors.add("merge of " + conflictingFilename + " failed");
}
runAndCaptureUsingInitRaw(expectedErrors, MERGE_TOOL,
"--no-prompt", "--tool", toolName);
}
@Test(expected = Die.class)
public void testUserToolWithCommandNotFoundError() throws Exception {
String toolName = "customTool";
int errorReturnCode = 127; // command not found
String command = "exit " + errorReturnCode;
StoredConfig config = db.getConfig();
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
createMergeConflict();
runAndCaptureUsingInitRaw(MERGE_TOOL, "--no-prompt", "--tool",
toolName);
fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode);
}
@Test
public void testEmptyToolName() throws Exception {
String emptyToolName = "";
StoredConfig config = db.getConfig();
// the default merge tool is configured without a subsection
String subsection = null;
config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
emptyToolName);
createMergeConflict();
String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput),
MERGE_TOOL, "--no-prompt");
}
@Test @Test
public void testAbortMerge() throws Exception { public void testAbortMerge() throws Exception {
String[] inputLines = { String[] inputLines = {
@ -157,20 +213,38 @@ expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL,
@Test @Test
public void testToolHelp() throws Exception { public void testToolHelp() throws Exception {
CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
List<String> expectedOutput = new ArrayList<>(); List<String> expectedOutput = new ArrayList<>();
MergeTools diffTools = new MergeTools(db);
Map<String, ExternalMergeTool> predefinedTools = diffTools
.getPredefinedTools(true);
List<ExternalMergeTool> availableTools = new ArrayList<>();
List<ExternalMergeTool> notAvailableTools = new ArrayList<>();
for (ExternalMergeTool tool : predefinedTools.values()) {
if (tool.isAvailable()) {
availableTools.add(tool);
} else {
notAvailableTools.add(tool);
}
}
expectedOutput.add( expectedOutput.add(
"'git mergetool --tool=<tool>' may be set to one of the following:"); "'git mergetool --tool=<tool>' may be set to one of the following:");
for (CommandLineMergeTool defaultTool : defaultTools) { for (ExternalMergeTool tool : availableTools) {
String toolName = defaultTool.name(); String toolName = tool.getName();
expectedOutput.add(toolName); expectedOutput.add(toolName);
} }
String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " " String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
+ getEchoCommand(); + getEchoCommand();
expectedOutput.add("user-defined:"); expectedOutput.add("user-defined:");
expectedOutput.add(customToolHelpLine); expectedOutput.add(customToolHelpLine);
expectedOutput.add(
"The following tools are valid, but not currently available:");
for (ExternalMergeTool tool : notAvailableTools) {
String toolName = tool.getName();
expectedOutput.add(toolName);
}
String[] userDefinedToolsHelp = { String[] userDefinedToolsHelp = {
"The following tools are valid, but not currently available:",
"Some of the tools listed above only work in a windowed", "Some of the tools listed above only work in a windowed",
"environment. If run in a terminal-only session, they will fail.", }; "environment. If run in a terminal-only session, they will fail.", };
expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp)); expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));
@ -200,7 +274,7 @@ private void configureEchoTool(String toolName) {
String.valueOf(false)); String.valueOf(false));
} }
private static String[] getExpectedMergeConflictOutputNoPrompt( private String[] getExpectedMergeConflictOutputNoPrompt(
String[] conflictFilenames) { String[] conflictFilenames) {
List<String> expected = new ArrayList<>(); List<String> expected = new ArrayList<>();
expected.add("Merging:"); expected.add("Merging:");
@ -212,7 +286,8 @@ private static String[] getExpectedMergeConflictOutputNoPrompt(
+ "':"); + "':");
expected.add("{local}: modified file"); expected.add("{local}: modified file");
expected.add("{remote}: modified file"); expected.add("{remote}: modified file");
expected.add(conflictFilename); Path filePath = getFullPath(conflictFilename);
expected.add(filePath.toString());
expected.add(conflictFilename + " seems unchanged."); expected.add(conflictFilename + " seems unchanged.");
} }
return expected.toArray(new String[0]); return expected.toArray(new String[0]);
@ -237,7 +312,7 @@ private static String[] getExpectedAbortLaunchOutput(
return expected.toArray(new String[0]); return expected.toArray(new String[0]);
} }
private static String[] getExpectedAbortMergeOutput( private String[] getExpectedAbortMergeOutput(
String[] conflictFilenames, int abortIndex) { String[] conflictFilenames, int abortIndex) {
List<String> expected = new ArrayList<>(); List<String> expected = new ArrayList<>();
expected.add("Merging:"); expected.add("Merging:");
@ -254,8 +329,9 @@ private static String[] getExpectedAbortMergeOutput(
"Normal merge conflict for '" + conflictFilename + "':"); "Normal merge conflict for '" + conflictFilename + "':");
expected.add("{local}: modified file"); expected.add("{local}: modified file");
expected.add("{remote}: modified file"); expected.add("{remote}: modified file");
Path fullPath = getFullPath(conflictFilename);
expected.add("Hit return to start merge resolution tool (" expected.add("Hit return to start merge resolution tool ("
+ TOOL_NAME + "): " + conflictFilename); + TOOL_NAME + "): " + fullPath);
expected.add(conflictFilename + " seems unchanged."); expected.add(conflictFilename + " seems unchanged.");
expected.add("Was the merge successful [y/n]?"); expected.add("Was the merge successful [y/n]?");
if (i < conflictFilenames.length - 1) { if (i < conflictFilenames.length - 1) {
@ -266,7 +342,7 @@ private static String[] getExpectedAbortMergeOutput(
return expected.toArray(new String[0]); return expected.toArray(new String[0]);
} }
private static String[] getExpectedMergeConflictOutput( private String[] getExpectedMergeConflictOutput(
String[] conflictFilenames) { String[] conflictFilenames) {
List<String> expected = new ArrayList<>(); List<String> expected = new ArrayList<>();
expected.add("Merging:"); expected.add("Merging:");
@ -279,8 +355,9 @@ private static String[] getExpectedMergeConflictOutput(
+ "':"); + "':");
expected.add("{local}: modified file"); expected.add("{local}: modified file");
expected.add("{remote}: modified file"); expected.add("{remote}: modified file");
Path filePath = getFullPath(conflictFilename);
expected.add("Hit return to start merge resolution tool (" expected.add("Hit return to start merge resolution tool ("
+ TOOL_NAME + "): " + conflictFilename); + TOOL_NAME + "): " + filePath);
expected.add(conflictFilename + " seems unchanged."); expected.add(conflictFilename + " seems unchanged.");
expected.add("Was the merge successful [y/n]?"); expected.add("Was the merge successful [y/n]?");
if (i < conflictFilenames.length - 1) { if (i < conflictFilenames.length - 1) {
@ -307,4 +384,12 @@ private static String[] getExpectedDeletedConflictOutput(
} }
return expected.toArray(new String[0]); 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\")";
}
} }

View File

@ -10,13 +10,18 @@
package org.eclipse.jgit.pgm; package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
@ -29,6 +34,7 @@
import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.Before; import org.junit.Before;
import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
/** /**
* Base test case for the {@code difftool} and {@code mergetool} commands. * Base test case for the {@code difftool} and {@code mergetool} commands.
@ -64,8 +70,23 @@ protected String[] runAndCaptureUsingInitRaw(String... args)
return runAndCaptureUsingInitRaw(inputStream, args); return runAndCaptureUsingInitRaw(inputStream, args);
} }
protected String[] runAndCaptureUsingInitRaw(
List<String> expectedErrorOutput, String... args) throws Exception {
InputStream inputStream = null; // no input stream
return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput,
args);
}
protected String[] runAndCaptureUsingInitRaw(InputStream inputStream, protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
String... args) throws Exception { String... args) throws Exception {
List<String> expectedErrorOutput = Collections.emptyList();
return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput,
args);
}
protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
List<String> expectedErrorOutput, String... args)
throws CmdLineException, Exception, IOException {
CLIGitCommand.Result result = new CLIGitCommand.Result(); CLIGitCommand.Result result = new CLIGitCommand.Result();
GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
@ -86,7 +107,7 @@ protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
.filter(l -> !l.isBlank()) // we care only about error messages .filter(l -> !l.isBlank()) // we care only about error messages
.collect(Collectors.toList()); .collect(Collectors.toList());
assertEquals("Expected no standard error output from tool", 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]); return result.outLines().toArray(new String[0]);
} }
@ -177,6 +198,13 @@ protected List<DiffEntry> getRepositoryChanges(RevCommit commit)
return changes; 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) { protected static InputStream createInputStream(String[] inputLines) {
return createInputStream(Arrays.asList(inputLines)); return createInputStream(Arrays.asList(inputLines));
} }
@ -192,11 +220,24 @@ protected static void assertArrayOfLinesEquals(String failMessage,
assertEquals(failMessage, toString(expected), toString(actual)); assertEquals(failMessage, toString(expected), toString(actual));
} }
protected static String getEchoCommand() { protected static void assertArrayOfMatchingLines(String failMessage,
/* Pattern[] expected, String[] actual) {
* use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be assertEquals(failMessage + System.lineSeparator()
* replaced with full paths to a temporary file during some of the tests + "Expected and actual lines count don't match. Expected: "
*/ + Arrays.asList(expected) + ", actual: "
return "(echo \"$MERGED\")"; + 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);
}
} }
} }

View File

@ -61,6 +61,8 @@ deletedRemoteBranch=Deleted remote branch {0}
diffToolHelpSetToFollowing=''git difftool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail. diffToolHelpSetToFollowing=''git difftool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
diffToolLaunch=Viewing ({0}/{1}): ''{2}''\nLaunch ''{3}'' [Y/n]? diffToolLaunch=Viewing ({0}/{1}): ''{2}''\nLaunch ''{3}'' [Y/n]?
diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1} diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1}
diffToolPromptToolName=This message is displayed because 'diff.tool' is not configured.\nSee 'git difftool --tool-help' or 'git help config' for more details.\n'git difftool' will now attempt to use one of the following tools:\n{0}\n
diffToolUnknownToolName=Unknown diff tool '{0}'
doesNotExist={0} does not exist doesNotExist={0} does not exist
dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge: dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge:
everythingUpToDate=Everything up-to-date everythingUpToDate=Everything up-to-date
@ -107,6 +109,8 @@ mergeToolDeletedConflictByThem= {local}: modified file\n {remote}: deleted
mergeToolContinueUnresolvedPaths=\nContinue merging other unresolved paths [y/n]? mergeToolContinueUnresolvedPaths=\nContinue merging other unresolved paths [y/n]?
mergeToolWasMergeSuccessfull=Was the merge successful [y/n]? mergeToolWasMergeSuccessfull=Was the merge successful [y/n]?
mergeToolDeletedMergeDecision=Use (m)odified or (d)eleted file, or (a)bort? mergeToolDeletedMergeDecision=Use (m)odified or (d)eleted file, or (a)bort?
mergeToolPromptToolName=This message is displayed because 'merge.tool' is not configured.\nSee 'git mergetool --tool-help' or 'git help config' for more details.\n'git mergetool' will now attempt to use one of the following tools:\n{0}\n
mergeToolUnknownToolName=Unknown merge tool '{0}'
mergeFailed=Automatic merge failed; fix conflicts and then commit the result mergeFailed=Automatic merge failed; fix conflicts and then commit the result
mergeCheckoutFailed=Please, commit your changes or stash them before you can merge. mergeCheckoutFailed=Please, commit your changes or stash them before you can merge.
mergeMadeBy=Merge made by the ''{0}'' strategy. mergeMadeBy=Merge made by the ''{0}'' strategy.

View File

@ -1,5 +1,6 @@
/* /*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
* Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
* *
* This program and the accompanying materials are made available under the * This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at * terms of the Eclipse Distribution License v. 1.0 which is available at
@ -22,30 +23,33 @@
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.diff.ContentSource; import org.eclipse.jgit.diff.ContentSource;
import org.eclipse.jgit.diff.ContentSource.Pair; import org.eclipse.jgit.diff.ContentSource.Pair;
import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.Side; import org.eclipse.jgit.diff.DiffEntry.Side;
import org.eclipse.jgit.internal.diffmergetool.ToolException;
import org.eclipse.jgit.internal.diffmergetool.DiffTools;
import org.eclipse.jgit.internal.diffmergetool.FileElement;
import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.errors.AmbiguousObjectException; import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.internal.diffmergetool.DiffTools;
import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
import org.eclipse.jgit.internal.diffmergetool.FileElement;
import org.eclipse.jgit.internal.diffmergetool.PromptContinueHandler;
import org.eclipse.jgit.internal.diffmergetool.ToolException;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler; import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
@ -58,7 +62,6 @@
import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.treewalk.WorkingTreeOptions;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.StringUtils;
import org.eclipse.jgit.util.FS.ExecutionResult; import org.eclipse.jgit.util.FS.ExecutionResult;
import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option; import org.kohsuke.args4j.Option;
@ -75,9 +78,13 @@ class DiffTool extends TextBuiltin {
@Argument(index = 1, metaVar = "metaVar_treeish") @Argument(index = 1, metaVar = "metaVar_treeish")
private AbstractTreeIterator newTree; private AbstractTreeIterator newTree;
private Optional<String> toolName = Optional.empty();
@Option(name = "--tool", aliases = { @Option(name = "--tool", aliases = {
"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForDiff") "-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") @Option(name = "--cached", aliases = { "--staged" }, usage = "usage_cached")
private boolean cached; private boolean cached;
@ -97,16 +104,16 @@ void noPrompt(@SuppressWarnings("unused") boolean on) {
@Option(name = "--tool-help", usage = "usage_toolHelp") @Option(name = "--tool-help", usage = "usage_toolHelp")
private boolean toolHelp; private boolean toolHelp;
private BooleanTriState gui = BooleanTriState.UNSET; private boolean gui = false;
@Option(name = "--gui", aliases = { "-g" }, usage = "usage_DiffGuiTool") @Option(name = "--gui", aliases = { "-g" }, usage = "usage_DiffGuiTool")
void setGui(@SuppressWarnings("unused") boolean on) { void setGui(@SuppressWarnings("unused") boolean on) {
gui = BooleanTriState.TRUE; gui = true;
} }
@Option(name = "--no-gui", usage = "usage_noGui") @Option(name = "--no-gui", usage = "usage_noGui")
void noGui(@SuppressWarnings("unused") boolean on) { void noGui(@SuppressWarnings("unused") boolean on) {
gui = BooleanTriState.FALSE; gui = false;
} }
private BooleanTriState trustExitCode = BooleanTriState.UNSET; private BooleanTriState trustExitCode = BooleanTriState.UNSET;
@ -140,23 +147,12 @@ protected void run() {
if (toolHelp) { if (toolHelp) {
showToolHelp(); showToolHelp();
} else { } else {
boolean showPrompt = diffTools.isInteractive();
if (prompt != BooleanTriState.UNSET) {
showPrompt = prompt == BooleanTriState.TRUE;
}
String toolNamePrompt = toolName;
if (showPrompt) {
if (StringUtils.isEmptyOrNull(toolNamePrompt)) {
toolNamePrompt = diffTools.getDefaultToolName(gui);
}
}
// get the changed files // get the changed files
List<DiffEntry> files = getFiles(); List<DiffEntry> files = getFiles();
if (files.size() > 0) { if (files.size() > 0) {
compare(files, showPrompt, toolNamePrompt); compare(files);
} }
} }
outw.flush();
} catch (RevisionSyntaxException | IOException e) { } catch (RevisionSyntaxException | IOException e) {
throw die(e.getMessage(), e); throw die(e.getMessage(), e);
} finally { } finally {
@ -164,51 +160,103 @@ protected void run() {
} }
} }
private void compare(List<DiffEntry> files, boolean showPrompt, private void informUserNoTool(List<String> tools) {
String toolNamePrompt) throws IOException { try {
StringBuilder toolNames = new StringBuilder();
for (String name : tools) {
toolNames.append(name + " "); //$NON-NLS-1$
}
outw.println(MessageFormat.format(
CLIText.get().diffToolPromptToolName, toolNames));
outw.flush();
} catch (IOException e) {
throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
}
}
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<DiffEntry> files) throws IOException {
ContentSource.Pair sourcePair = new ContentSource.Pair(source(oldTree), ContentSource.Pair sourcePair = new ContentSource.Pair(source(oldTree),
source(newTree)); source(newTree));
try { try {
for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) { for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
DiffEntry ent = files.get(fileIndex); DiffEntry ent = files.get(fileIndex);
String mergedFilePath = ent.getNewPath();
if (mergedFilePath.equals(DiffEntry.DEV_NULL)) { String filePath = ent.getNewPath();
mergedFilePath = ent.getOldPath(); if (filePath.equals(DiffEntry.DEV_NULL)) {
filePath = ent.getOldPath();
} }
// check if user wants to launch compare
boolean launchCompare = true; try {
if (showPrompt) { FileElement local = createFileElement(
launchCompare = isLaunchCompare(fileIndex + 1, files.size(), FileElement.Type.LOCAL, sourcePair, Side.OLD, ent);
mergedFilePath, toolNamePrompt); FileElement remote = createFileElement(
} FileElement.Type.REMOTE, sourcePair, Side.NEW, ent);
if (launchCompare) {
try { PromptContinueHandler promptContinueHandler = new CountingPromptContinueHandler(
FileElement local = createFileElement( fileIndex + 1, files.size(), filePath);
FileElement.Type.LOCAL, sourcePair, Side.OLD,
ent); Optional<ExecutionResult> optionalResult = diffTools
FileElement remote = createFileElement( .compare(local, remote, toolName, prompt, gui,
FileElement.Type.REMOTE, sourcePair, Side.NEW, trustExitCode, promptContinueHandler,
ent); this::informUserNoTool);
FileElement merged = new FileElement(mergedFilePath,
FileElement.Type.MERGED); if (optionalResult.isPresent()) {
ExecutionResult result = optionalResult.get();
// TODO: check how to return the exit-code of the tool // TODO: check how to return the exit-code of the tool
// to jgit / java runtime ? // to jgit / java runtime ?
// int rc =... // int rc =...
ExecutionResult result = diffTools.compare(local, outw.println(
remote, merged, toolName, prompt, gui, new String(result.getStdout().toByteArray()));
trustExitCode); outw.flush();
outw.println(new String(result.getStdout().toByteArray()));
errw.println( errw.println(
new String(result.getStderr().toByteArray())); new String(result.getStderr().toByteArray()));
} catch (ToolException e) { errw.flush();
outw.println(e.getResultStdout());
outw.flush();
errw.println(e.getMessage());
throw die(MessageFormat.format(
CLIText.get().diffToolDied, mergedFilePath), e);
} }
} else { } catch (ToolException e) {
break; outw.println(e.getResultStdout());
outw.flush();
errw.println(e.getMessage());
errw.flush();
throw die(MessageFormat.format(
CLIText.get().diffToolDied, filePath, e), e);
} }
} }
} finally { } finally {
@ -216,32 +264,17 @@ private void compare(List<DiffEntry> 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 { private void showToolHelp() throws IOException {
Map<String, ExternalDiffTool> predefTools = diffTools
.getPredefinedTools(true);
StringBuilder availableToolNames = new StringBuilder(); StringBuilder availableToolNames = new StringBuilder();
for (String name : diffTools.getAvailableTools().keySet()) {
availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
}
StringBuilder notAvailableToolNames = new StringBuilder(); StringBuilder notAvailableToolNames = new StringBuilder();
for (String name : diffTools.getNotAvailableTools().keySet()) { for (String name : predefTools.keySet()) {
notAvailableToolNames if (predefTools.get(name).isAvailable()) {
.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
} else {
notAvailableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
}
} }
StringBuilder userToolNames = new StringBuilder(); StringBuilder userToolNames = new StringBuilder();
Map<String, ExternalDiffTool> userTools = diffTools Map<String, ExternalDiffTool> userTools = diffTools
@ -289,12 +322,12 @@ private List<DiffEntry> getFiles()
} }
private FileElement createFileElement(FileElement.Type elementType, private FileElement createFileElement(FileElement.Type elementType,
Pair pair, Side side, DiffEntry entry) Pair pair, Side side, DiffEntry entry) throws NoWorkTreeException,
throws NoWorkTreeException, CorruptObjectException, IOException, CorruptObjectException, IOException, ToolException {
ToolException {
String entryPath = side == Side.NEW ? entry.getNewPath() String entryPath = side == Side.NEW ? entry.getNewPath()
: entry.getOldPath(); : entry.getOldPath();
FileElement fileElement = new FileElement(entryPath, elementType); FileElement fileElement = new FileElement(entryPath, elementType,
db.getWorkTree());
if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) { if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) {
try (RevWalk revWalk = new RevWalk(db); try (RevWalk revWalk = new RevWalk(db);
TreeWalk treeWalk = new TreeWalk(db, TreeWalk treeWalk = new TreeWalk(db,
@ -323,7 +356,8 @@ private FileElement createFileElement(FileElement.Type elementType,
fileElement.createTempFile(null))); fileElement.createTempFile(null)));
} else { } else {
throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$ throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$
+ "' in staging area!", null); //$NON-NLS-1$ + "' in staging area!", //$NON-NLS-1$
null);
} }
} }
} }

View File

@ -1,5 +1,6 @@
/* /*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
* Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
* *
* This program and the accompanying materials are made available under the * This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at * terms of the Eclipse Distribution License v. 1.0 which is available at
@ -22,6 +23,7 @@
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.TreeMap; import java.util.TreeMap;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
@ -29,29 +31,29 @@
import org.eclipse.jgit.api.StatusCommand; import org.eclipse.jgit.api.StatusCommand;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.ContentSource; 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.DirCache;
import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool; import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
import org.eclipse.jgit.internal.diffmergetool.FileElement; 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.MergeTools;
import org.eclipse.jgit.internal.diffmergetool.ToolException; 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.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.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.treewalk.WorkingTreeOptions;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.util.FS.ExecutionResult; import org.eclipse.jgit.util.FS.ExecutionResult;
import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option; import org.kohsuke.args4j.Option;
@ -61,9 +63,13 @@
class MergeTool extends TextBuiltin { class MergeTool extends TextBuiltin {
private MergeTools mergeTools; private MergeTools mergeTools;
private Optional<String> toolName = Optional.empty();
@Option(name = "--tool", aliases = { @Option(name = "--tool", aliases = {
"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge") "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge")
private String toolName; void setToolName(String name) {
toolName = Optional.of(name);
}
private BooleanTriState prompt = BooleanTriState.UNSET; private BooleanTriState prompt = BooleanTriState.UNSET;
@ -80,16 +86,16 @@ void noPrompt(@SuppressWarnings("unused") boolean on) {
@Option(name = "--tool-help", usage = "usage_toolHelp") @Option(name = "--tool-help", usage = "usage_toolHelp")
private boolean toolHelp; private boolean toolHelp;
private BooleanTriState gui = BooleanTriState.UNSET; private boolean gui = false;
@Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool") @Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool")
void setGui(@SuppressWarnings("unused") boolean on) { void setGui(@SuppressWarnings("unused") boolean on) {
gui = BooleanTriState.TRUE; gui = true;
} }
@Option(name = "--no-gui", usage = "usage_noGui") @Option(name = "--no-gui", usage = "usage_noGui")
void noGui(@SuppressWarnings("unused") boolean on) { void noGui(@SuppressWarnings("unused") boolean on) {
gui = BooleanTriState.FALSE; gui = false;
} }
@Argument(required = false, index = 0, metaVar = "metaVar_paths") @Argument(required = false, index = 0, metaVar = "metaVar_paths")
@ -115,20 +121,10 @@ protected void run() {
if (toolHelp) { if (toolHelp) {
showToolHelp(); showToolHelp();
} else { } else {
// get prompt
boolean showPrompt = mergeTools.isInteractive();
if (prompt != BooleanTriState.UNSET) {
showPrompt = prompt == BooleanTriState.TRUE;
}
// get passed or default tool name
String toolNameSelected = toolName;
if ((toolNameSelected == null) || toolNameSelected.isEmpty()) {
toolNameSelected = mergeTools.getDefaultToolName(gui);
}
// get the changed files // get the changed files
Map<String, StageState> files = getFiles(); Map<String, StageState> files = getFiles();
if (files.size() > 0) { if (files.size() > 0) {
merge(files, showPrompt, toolNameSelected); merge(files);
} else { } else {
outw.println(CLIText.get().mergeToolNoFiles); outw.println(CLIText.get().mergeToolNoFiles);
} }
@ -139,8 +135,21 @@ protected void run() {
} }
} }
private void merge(Map<String, StageState> files, boolean showPrompt, private void informUserNoTool(List<String> tools) {
String toolNamePrompt) throws Exception { try {
StringBuilder toolNames = new StringBuilder();
for (String name : tools) {
toolNames.append(name + " "); //$NON-NLS-1$
}
outw.println(MessageFormat
.format(CLIText.get().mergeToolPromptToolName, toolNames));
outw.flush();
} catch (IOException e) {
throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
}
}
private void merge(Map<String, StageState> files) throws Exception {
// sort file names // sort file names
List<String> mergedFilePaths = new ArrayList<>(files.keySet()); List<String> mergedFilePaths = new ArrayList<>(files.keySet());
Collections.sort(mergedFilePaths); Collections.sort(mergedFilePaths);
@ -152,6 +161,10 @@ private void merge(Map<String, StageState> files, boolean showPrompt,
outw.println(MessageFormat.format(CLIText.get().mergeToolMerging, outw.println(MessageFormat.format(CLIText.get().mergeToolMerging,
mergedFiles)); mergedFiles));
outw.flush(); outw.flush();
boolean showPrompt = mergeTools.isInteractive();
if (prompt != BooleanTriState.UNSET) {
showPrompt = prompt == BooleanTriState.TRUE;
}
// merge the files // merge the files
MergeResult mergeResult = MergeResult.SUCCESSFUL; MergeResult mergeResult = MergeResult.SUCCESSFUL;
for (String mergedFilePath : mergedFilePaths) { for (String mergedFilePath : mergedFilePaths) {
@ -169,8 +182,7 @@ private void merge(Map<String, StageState> files, boolean showPrompt,
// get file stage state and merge // get file stage state and merge
StageState fileState = files.get(mergedFilePath); StageState fileState = files.get(mergedFilePath);
if (fileState == StageState.BOTH_MODIFIED) { if (fileState == StageState.BOTH_MODIFIED) {
mergeResult = mergeModified(mergedFilePath, showPrompt, mergeResult = mergeModified(mergedFilePath, showPrompt);
toolNamePrompt);
} else if ((fileState == StageState.DELETED_BY_US) } else if ((fileState == StageState.DELETED_BY_US)
|| (fileState == StageState.DELETED_BY_THEM)) { || (fileState == StageState.DELETED_BY_THEM)) {
mergeResult = mergeDeleted(mergedFilePath, mergeResult = mergeDeleted(mergedFilePath,
@ -184,19 +196,11 @@ private void merge(Map<String, StageState> files, boolean showPrompt,
} }
} }
private MergeResult mergeModified(String mergedFilePath, boolean showPrompt, private MergeResult mergeModified(String mergedFilePath, boolean showPrompt)
String toolNamePrompt) throws Exception { throws Exception {
outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict, outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict,
mergedFilePath)); mergedFilePath));
outw.flush(); 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; boolean isMergeSuccessful = true;
ContentSource baseSource = ContentSource.create(db.newObjectReader()); ContentSource baseSource = ContentSource.create(db.newObjectReader());
ContentSource localSource = ContentSource.create(db.newObjectReader()); ContentSource localSource = ContentSource.create(db.newObjectReader());
@ -210,8 +214,8 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
FileElement base = null; FileElement base = null;
FileElement local = null; FileElement local = null;
FileElement remote = null; FileElement remote = null;
FileElement merged = new FileElement(mergedFilePath, FileElement merged = new FileElement(mergedFilePath, Type.MERGED,
Type.MERGED); db.getWorkTree());
DirCache cache = db.readDirCache(); DirCache cache = db.readDirCache();
try (RevWalk revWalk = new RevWalk(db); try (RevWalk revWalk = new RevWalk(db);
TreeWalk treeWalk = new TreeWalk(db, TreeWalk treeWalk = new TreeWalk(db,
@ -233,7 +237,8 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
.get(WorkingTreeOptions.KEY); .get(WorkingTreeOptions.KEY);
CheckoutMetadata checkoutMetadata = new CheckoutMetadata( CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
eolStreamType, filterCommand); eolStreamType, filterCommand);
DirCacheEntry entry = treeWalk.getTree(DirCacheIterator.class).getDirCacheEntry(); DirCacheEntry entry = treeWalk
.getTree(DirCacheIterator.class).getDirCacheEntry();
if (entry == null) { if (entry == null) {
continue; continue;
} }
@ -275,23 +280,27 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
// TODO: check how to return the exit-code of the // TODO: check how to return the exit-code of the
// tool to jgit / java runtime ? // tool to jgit / java runtime ?
// int rc =... // int rc =...
ExecutionResult executionResult = mergeTools.merge(local, Optional<ExecutionResult> optionalResult = mergeTools.merge(
remote, merged, base, tempDir, toolName, prompt, gui); local, remote, merged, base, tempDir, toolName, prompt,
outw.println( gui, this::promptForLaunch, this::informUserNoTool);
new String(executionResult.getStdout().toByteArray())); if (optionalResult.isPresent()) {
outw.flush(); ExecutionResult result = optionalResult.get();
errw.println( outw.println(new String(result.getStdout().toByteArray()));
new String(executionResult.getStderr().toByteArray())); outw.flush();
errw.flush(); errw.println(new String(result.getStderr().toByteArray()));
errw.flush();
} else {
return MergeResult.ABORTED;
}
} catch (ToolException e) { } catch (ToolException e) {
isMergeSuccessful = false; isMergeSuccessful = false;
outw.println(e.getResultStdout()); outw.println(e.getResultStdout());
outw.flush(); outw.flush();
errw.println(e.getMessage());
errw.println(MessageFormat.format( errw.println(MessageFormat.format(
CLIText.get().mergeToolMergeFailed, mergedFilePath)); CLIText.get().mergeToolMergeFailed, mergedFilePath));
errw.flush(); errw.flush();
if (e.isCommandExecutionError()) { if (e.isCommandExecutionError()) {
errw.println(e.getMessage());
throw die(CLIText.get().mergeToolExecutionError, e); throw die(CLIText.get().mergeToolExecutionError, e);
} }
} }
@ -380,19 +389,23 @@ private boolean isMergeSuccessful() throws IOException {
return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull); return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull);
} }
private boolean isLaunch(String toolNamePrompt) throws IOException { private boolean promptForLaunch(String toolNamePrompt) {
boolean launch = true; try {
outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch, boolean launch = true;
toolNamePrompt) + " "); //$NON-NLS-1$ outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch,
outw.flush(); toolNamePrompt) + " "); //$NON-NLS-1$
BufferedReader br = inputReader; outw.flush();
String line = null; BufferedReader br = inputReader;
if ((line = br.readLine()) != null) { String line = null;
if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$ if ((line = br.readLine()) != null) {
launch = false; 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 { private int getDeletedMergeDecision() throws IOException {
@ -420,14 +433,16 @@ private int getDeletedMergeDecision() throws IOException {
} }
private void showToolHelp() throws IOException { private void showToolHelp() throws IOException {
Map<String, ExternalMergeTool> predefTools = mergeTools
.getPredefinedTools(true);
StringBuilder availableToolNames = new StringBuilder(); StringBuilder availableToolNames = new StringBuilder();
for (String name : mergeTools.getAvailableTools().keySet()) {
availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
}
StringBuilder notAvailableToolNames = new StringBuilder(); StringBuilder notAvailableToolNames = new StringBuilder();
for (String name : mergeTools.getNotAvailableTools().keySet()) { for (String name : predefTools.keySet()) {
notAvailableToolNames if (predefTools.get(name).isAvailable()) {
.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
} else {
notAvailableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
}
} }
StringBuilder userToolNames = new StringBuilder(); StringBuilder userToolNames = new StringBuilder();
Map<String, ExternalMergeTool> userTools = mergeTools Map<String, ExternalMergeTool> userTools = mergeTools

View File

@ -139,6 +139,8 @@ public static String fatalError(String message) {
/***/ public String diffToolHelpSetToFollowing; /***/ public String diffToolHelpSetToFollowing;
/***/ public String diffToolLaunch; /***/ public String diffToolLaunch;
/***/ public String diffToolDied; /***/ public String diffToolDied;
/***/ public String diffToolPromptToolName;
/***/ public String diffToolUnknownToolName;
/***/ public String doesNotExist; /***/ public String doesNotExist;
/***/ public String dontOverwriteLocalChanges; /***/ public String dontOverwriteLocalChanges;
/***/ public String everythingUpToDate; /***/ public String everythingUpToDate;
@ -185,6 +187,8 @@ public static String fatalError(String message) {
/***/ public String mergeToolContinueUnresolvedPaths; /***/ public String mergeToolContinueUnresolvedPaths;
/***/ public String mergeToolWasMergeSuccessfull; /***/ public String mergeToolWasMergeSuccessfull;
/***/ public String mergeToolDeletedMergeDecision; /***/ public String mergeToolDeletedMergeDecision;
/***/ public String mergeToolPromptToolName;
/***/ public String mergeToolUnknownToolName;
/***/ public String mergeFailed; /***/ public String mergeFailed;
/***/ public String mergeCheckoutFailed; /***/ public String mergeCheckoutFailed;
/***/ public String mergeMadeBy; /***/ public String mergeMadeBy;

View File

@ -18,15 +18,25 @@
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS.ExecutionResult; import org.eclipse.jgit.util.FS.ExecutionResult;
@ -48,14 +58,7 @@ public void testUserToolWithError() throws Exception {
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command); command);
DiffTools manager = new DiffTools(db); invokeCompare(toolName);
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
BooleanTriState trustExitCode = BooleanTriState.TRUE;
manager.compare(local, remote, merged, toolName, prompt, gui,
trustExitCode);
fail("Expected exception to be thrown due to external tool exiting with error code: " fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode); + errorReturnCode);
@ -72,33 +75,91 @@ public void testUserToolWithCommandNotFoundError() throws Exception {
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command); command);
DiffTools manager = new DiffTools(db); invokeCompare(toolName);
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
BooleanTriState trustExitCode = BooleanTriState.FALSE;
manager.compare(local, remote, merged, toolName, prompt, gui,
trustExitCode);
fail("Expected exception to be thrown due to external tool exiting with error code: " fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode); + errorReturnCode);
} }
@Test @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); DiffTools manager = new DiffTools(db);
Set<String> actualToolNames = manager.getToolNames();
Set<String> expectedToolNames = Collections.emptySet(); Map<String, ExternalDiffTool> tools = manager.getUserDefinedTools();
assertEquals("Incorrect set of external diff tool names", ExternalDiffTool externalTool = tools.get(customToolName);
expectedToolNames, actualToolNames); 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<String> actualToolPrompts = promptHandler.toolPrompts;
List<String> expectedToolPrompts = Arrays.asList("customTool");
assertEquals("Expected a user prompt for custom tool call",
expectedToolPrompts, actualToolPrompts);
assertEquals("Expected to no informing about missing tools",
Collections.EMPTY_LIST, noToolHandler.missingTools);
}
@Test
public void testUserDefinedToolWithCancelledPrompt() throws Exception {
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.cancelPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
Optional<ExecutionResult> result = manager.compare(local, remote,
Optional.of(customToolName), BooleanTriState.TRUE, false,
BooleanTriState.TRUE, promptHandler, noToolHandler);
assertFalse("Expected no result if user cancels the operation",
result.isPresent());
} }
@Test @Test
public void testAllTools() { 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); DiffTools manager = new DiffTools(db);
Set<String> actualToolNames = manager.getAvailableTools().keySet(); Set<String> actualToolNames = manager.getAllToolNames();
Set<String> expectedToolNames = new LinkedHashSet<>(); Set<String> expectedToolNames = new LinkedHashSet<>();
expectedToolNames.add(customToolName);
CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
for (CommandLineDiffTool defaultTool : defaultTools) { for (CommandLineDiffTool defaultTool : defaultTools) {
String toolName = defaultTool.name(); String toolName = defaultTool.name();
@ -152,15 +213,6 @@ public void testUserDefinedTools() {
actualToolNames); actualToolNames);
} }
@Test
public void testNotAvailableTools() {
DiffTools manager = new DiffTools(db);
Set<String> actualToolNames = manager.getNotAvailableTools().keySet();
Set<String> expectedToolNames = Collections.emptySet();
assertEquals("Incorrect set of not available external diff tools",
expectedToolNames, actualToolNames);
}
@Test @Test
public void testCompare() throws ToolException { public void testCompare() throws ToolException {
String toolName = "customTool"; String toolName = "customTool";
@ -175,18 +227,12 @@ public void testCompare() throws ToolException {
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command); command);
Optional<ExecutionResult> result = invokeCompare(toolName);
BooleanTriState prompt = BooleanTriState.UNSET; assertTrue("Expected external diff tool result to be available",
BooleanTriState gui = BooleanTriState.UNSET; result.isPresent());
BooleanTriState trustExitCode = BooleanTriState.UNSET;
DiffTools manager = new DiffTools(db);
int expectedCompareResult = 0; int expectedCompareResult = 0;
ExecutionResult compareResult = manager.compare(local, remote, merged,
toolName, prompt, gui, trustExitCode);
assertEquals("Incorrect compare result for external diff tool", assertEquals("Incorrect compare result for external diff tool",
expectedCompareResult, compareResult.getRc()); expectedCompareResult, result.get().getRc());
} }
@Test @Test
@ -201,17 +247,17 @@ public void testDefaultTool() throws Exception {
toolName); toolName);
DiffTools manager = new DiffTools(db); DiffTools manager = new DiffTools(db);
BooleanTriState gui = BooleanTriState.UNSET; boolean gui = false;
String defaultToolName = manager.getDefaultToolName(gui); String defaultToolName = manager.getDefaultToolName(gui);
assertEquals( assertEquals(
"Expected configured difftool to be the default external diff tool", "Expected configured difftool to be the default external diff tool",
toolName, defaultToolName); toolName, defaultToolName);
gui = BooleanTriState.TRUE; gui = true;
String defaultGuiToolName = manager.getDefaultToolName(gui); String defaultGuiToolName = manager.getDefaultToolName(gui);
assertEquals( assertEquals(
"Expected configured difftool to be the default external diff tool", "Expected default gui difftool to be the default tool if no gui tool is set",
"my_gui_tool", defaultGuiToolName); toolName, defaultGuiToolName);
config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL, config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL,
guiToolName); guiToolName);
@ -219,7 +265,7 @@ public void testDefaultTool() throws Exception {
defaultGuiToolName = manager.getDefaultToolName(gui); defaultGuiToolName = manager.getDefaultToolName(gui);
assertEquals( assertEquals(
"Expected configured difftool to be the default external diff guitool", "Expected configured difftool to be the default external diff guitool",
"my_gui_tool", defaultGuiToolName); guiToolName, defaultGuiToolName);
} }
@Test @Test
@ -239,7 +285,7 @@ public void testOverridePreDefinedToolPath() {
DiffTools manager = new DiffTools(db); DiffTools manager = new DiffTools(db);
Map<String, ExternalDiffTool> availableTools = manager Map<String, ExternalDiffTool> availableTools = manager
.getAvailableTools(); .getPredefinedTools(true);
ExternalDiffTool externalDiffTool = availableTools ExternalDiffTool externalDiffTool = availableTools
.get(overridenToolName); .get(overridenToolName);
String actualDiffToolPath = externalDiffTool.getPath(); String actualDiffToolPath = externalDiffTool.getPath();
@ -256,20 +302,152 @@ public void testOverridePreDefinedToolPath() {
@Test(expected = ToolException.class) @Test(expected = ToolException.class)
public void testUndefinedTool() throws Exception { public void testUndefinedTool() throws Exception {
String toolName = "undefined";
invokeCompare(toolName);
fail("Expected exception to be thrown due to not defined external diff tool");
}
@Test
public void testDefaultToolExecutionWithPrompt() throws Exception {
FileBasedConfig config = db.getConfig();
// the default diff tool is configured without a subsection
String subsection = null;
config.setString("diff", subsection, "tool", "customTool");
String command = getEchoCommand();
config.setString("difftool", "customTool", "cmd", command);
DiffTools manager = new DiffTools(db); DiffTools manager = new DiffTools(db);
String toolName = "undefined"; PromptHandler promptHandler = PromptHandler.acceptPrompt();
BooleanTriState prompt = BooleanTriState.UNSET; MissingToolHandler noToolHandler = new MissingToolHandler();
BooleanTriState gui = BooleanTriState.UNSET;
BooleanTriState trustExitCode = BooleanTriState.UNSET;
manager.compare(local, remote, merged, toolName, prompt, gui, manager.compare(local, remote, Optional.empty(), BooleanTriState.TRUE,
trustExitCode); false, BooleanTriState.TRUE, promptHandler, noToolHandler);
fail("Expected exception to be thrown due to not defined external diff tool");
assertEchoCommandHasCorrectOutput();
}
@Test
public void testNoDefaultToolName() {
DiffTools manager = new DiffTools(db);
boolean gui = false;
String defaultToolName = manager.getDefaultToolName(gui);
assertNull("Expected no default tool when none is configured",
defaultToolName);
gui = true;
defaultToolName = manager.getDefaultToolName(gui);
assertNull("Expected no default tool when none is configured",
defaultToolName);
}
@Test
public void testExternalToolInGitAttributes() throws Exception {
String content = "attributes:\n*.txt difftool=customTool";
File gitattributes = writeTrashFile(".gitattributes", content);
gitattributes.deleteOnExit();
try (TestRepository<Repository> testRepository = new TestRepository<>(
db)) {
FileBasedConfig config = db.getConfig();
config.setString("difftool", "customTool", "cmd", "echo");
testRepository.git().add().addFilepattern(localFile.getName())
.call();
testRepository.git().add().addFilepattern(".gitattributes").call();
testRepository.branch("master").commit().message("first commit")
.create();
DiffTools manager = new DiffTools(db);
Optional<String> tool = manager
.getExternalToolFromAttributes(localFile.getName());
assertTrue("Failed to find user defined tool", tool.isPresent());
assertEquals("Failed to find user defined tool", "customTool",
tool.get());
} finally {
Files.delete(gitattributes.toPath());
}
}
@Test
public void testNotExternalToolInGitAttributes() throws Exception {
String content = "";
File gitattributes = writeTrashFile(".gitattributes", content);
gitattributes.deleteOnExit();
try (TestRepository<Repository> testRepository = new TestRepository<>(
db)) {
FileBasedConfig config = db.getConfig();
config.setString("difftool", "customTool", "cmd", "echo");
testRepository.git().add().addFilepattern(localFile.getName())
.call();
testRepository.git().add().addFilepattern(".gitattributes").call();
testRepository.branch("master").commit().message("first commit")
.create();
DiffTools manager = new DiffTools(db);
Optional<String> tool = manager
.getExternalToolFromAttributes(localFile.getName());
assertFalse(
"Expected no external tool if no default tool is specified in .gitattributes",
tool.isPresent());
} finally {
Files.delete(gitattributes.toPath());
}
}
@Test(expected = ToolException.class)
public void testNullTool() throws Exception {
DiffTools manager = new DiffTools(db);
boolean trustExitCode = true;
ExternalDiffTool tool = null;
manager.compare(local, remote, tool, trustExitCode);
}
@Test(expected = ToolException.class)
public void testNullToolWithPrompt() throws Exception {
DiffTools manager = new DiffTools(db);
PromptHandler promptHandler = PromptHandler.cancelPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
Optional<String> tool = null;
manager.compare(local, remote, tool, BooleanTriState.TRUE, false,
BooleanTriState.TRUE, promptHandler, noToolHandler);
}
private Optional<ExecutionResult> invokeCompare(String toolName)
throws ToolException {
DiffTools manager = new DiffTools(db);
BooleanTriState prompt = BooleanTriState.UNSET;
boolean gui = false;
BooleanTriState trustExitCode = BooleanTriState.TRUE;
PromptHandler promptHandler = PromptHandler.acceptPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
Optional<ExecutionResult> result = manager.compare(local, remote,
Optional.of(toolName), prompt, gui, trustExitCode,
promptHandler, noToolHandler);
return result;
} }
private String getEchoCommand() { private String getEchoCommand() {
return "(echo \"$LOCAL\" \"$REMOTE\") > " return "(echo \"$LOCAL\" \"$REMOTE\") > "
+ commandResult.getAbsolutePath(); + commandResult.getAbsolutePath();
} }
private void assertEchoCommandHasCorrectOutput() throws IOException {
List<String> actualLines = Files.readAllLines(commandResult.toPath());
String actualContent = String.join(System.lineSeparator(), actualLines);
actualLines = Arrays.asList(actualContent.split(" "));
List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(),
remoteFile.getAbsolutePath());
assertEquals("Dummy test tool called with unexpected arguments",
expectedLines, actualLines);
}
} }

View File

@ -9,22 +9,30 @@
*/ */
package org.eclipse.jgit.internal.diffmergetool; package org.eclipse.jgit.internal.diffmergetool;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; 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.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.lib.internal.BooleanTriState;
@ -50,12 +58,7 @@ public void testUserToolWithError() throws Exception {
config.setString(CONFIG_MERGETOOL_SECTION, toolName, config.setString(CONFIG_MERGETOOL_SECTION, toolName,
CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(Boolean.TRUE)); CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(Boolean.TRUE));
MergeTools manager = new MergeTools(db); invokeMerge(toolName);
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
fail("Expected exception to be thrown due to external tool exiting with error code: " fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode); + errorReturnCode);
@ -72,31 +75,112 @@ public void testUserToolWithCommandNotFoundError() throws Exception {
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
command); command);
MergeTools manager = new MergeTools(db); invokeMerge(toolName);
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
fail("Expected exception to be thrown due to external tool exiting with error code: " fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode); + errorReturnCode);
} }
@Test @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); MergeTools manager = new MergeTools(db);
Set<String> actualToolNames = manager.getToolNames(); ExecutionResult result = manager.merge(local, remote, merged, null,
Set<String> expectedToolNames = Collections.emptySet(); null, externalTool);
assertEquals("Incorrect set of external merge tool names", assertEquals("Expected merge tool to succeed", 0, result.getRc());
expectedToolNames, actualToolNames);
List<String> actualLines = Files.readAllLines(mergedFile.toPath());
String actualMergeResult = String.join(System.lineSeparator(),
actualLines);
String expectedMergeResult = DEFAULT_CONTENT;
assertEquals(
"Failed to merge equal local and remote versions with pre-defined tool: "
+ tool.getPath(),
expectedMergeResult, actualMergeResult);
}
@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<String, ExternalMergeTool> 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<String> actualToolPrompts = promptHandler.toolPrompts;
List<String> expectedToolPrompts = Arrays.asList("customTool");
assertEquals("Expected a user prompt for custom tool call",
expectedToolPrompts, actualToolPrompts);
assertEquals("Expected to no informing about missing tools",
Collections.EMPTY_LIST, noToolHandler.missingTools);
}
@Test
public void testUserDefinedToolWithCancelledPrompt() throws Exception {
MergeTools manager = new MergeTools(db);
PromptHandler promptHandler = PromptHandler.cancelPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
Optional<ExecutionResult> result = manager.merge(local, remote, merged,
base, null, Optional.empty(), BooleanTriState.TRUE, false,
promptHandler, noToolHandler);
assertFalse("Expected no result if user cancels the operation",
result.isPresent());
} }
@Test @Test
public void testAllTools() { 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); MergeTools manager = new MergeTools(db);
Set<String> actualToolNames = manager.getAvailableTools().keySet(); Set<String> actualToolNames = manager.getAllToolNames();
Set<String> expectedToolNames = new LinkedHashSet<>(); Set<String> expectedToolNames = new LinkedHashSet<>();
expectedToolNames.add(customToolName);
CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
for (CommandLineMergeTool defaultTool : defaultTools) { for (CommandLineMergeTool defaultTool : defaultTools) {
String toolName = defaultTool.name(); String toolName = defaultTool.name();
@ -150,15 +234,6 @@ public void testUserDefinedTools() {
actualToolNames); actualToolNames);
} }
@Test
public void testNotAvailableTools() {
MergeTools manager = new MergeTools(db);
Set<String> actualToolNames = manager.getNotAvailableTools().keySet();
Set<String> expectedToolNames = Collections.emptySet();
assertEquals("Incorrect set of not available external merge tools",
expectedToolNames, actualToolNames);
}
@Test @Test
public void testCompare() throws ToolException { public void testCompare() throws ToolException {
String toolName = "customTool"; String toolName = "customTool";
@ -174,16 +249,12 @@ public void testCompare() throws ToolException {
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
command); command);
BooleanTriState prompt = BooleanTriState.UNSET; Optional<ExecutionResult> result = invokeMerge(toolName);
BooleanTriState gui = BooleanTriState.UNSET; assertTrue("Expected external merge tool result to be available",
result.isPresent());
MergeTools manager = new MergeTools(db);
int expectedCompareResult = 0; int expectedCompareResult = 0;
ExecutionResult compareResult = manager.merge(local, remote, merged,
base, null, toolName, prompt, gui);
assertEquals("Incorrect compare result for external merge tool", assertEquals("Incorrect compare result for external merge tool",
expectedCompareResult, compareResult.getRc()); expectedCompareResult, result.get().getRc());
} }
@Test @Test
@ -198,17 +269,16 @@ public void testDefaultTool() throws Exception {
toolName); toolName);
MergeTools manager = new MergeTools(db); MergeTools manager = new MergeTools(db);
BooleanTriState gui = BooleanTriState.UNSET; boolean gui = false;
String defaultToolName = manager.getDefaultToolName(gui); String defaultToolName = manager.getDefaultToolName(gui);
assertEquals( assertEquals(
"Expected configured mergetool to be the default external merge tool", "Expected configured mergetool to be the default external merge tool",
toolName, defaultToolName); toolName, defaultToolName);
gui = BooleanTriState.TRUE; gui = true;
String defaultGuiToolName = manager.getDefaultToolName(gui); String defaultGuiToolName = manager.getDefaultToolName(gui);
assertEquals( assertNull("Expected default mergetool to not be set",
"Expected configured mergetool to be the default external merge tool", defaultGuiToolName);
"my_gui_tool", defaultGuiToolName);
config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL, config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL,
guiToolName); guiToolName);
@ -216,7 +286,7 @@ public void testDefaultTool() throws Exception {
defaultGuiToolName = manager.getDefaultToolName(gui); defaultGuiToolName = manager.getDefaultToolName(gui);
assertEquals( assertEquals(
"Expected configured mergetool to be the default external merge guitool", "Expected configured mergetool to be the default external merge guitool",
"my_gui_tool", defaultGuiToolName); guiToolName, defaultGuiToolName);
} }
@Test @Test
@ -236,7 +306,7 @@ public void testOverridePreDefinedToolPath() {
MergeTools manager = new MergeTools(db); MergeTools manager = new MergeTools(db);
Map<String, ExternalMergeTool> availableTools = manager Map<String, ExternalMergeTool> availableTools = manager
.getAvailableTools(); .getPredefinedTools(true);
ExternalMergeTool externalMergeTool = availableTools ExternalMergeTool externalMergeTool = availableTools
.get(overridenToolName); .get(overridenToolName);
String actualMergeToolPath = externalMergeTool.getPath(); String actualMergeToolPath = externalMergeTool.getPath();
@ -254,18 +324,110 @@ public void testOverridePreDefinedToolPath() {
@Test(expected = ToolException.class) @Test(expected = ToolException.class)
public void testUndefinedTool() throws Exception { public void testUndefinedTool() throws Exception {
MergeTools manager = new MergeTools(db);
String toolName = "undefined"; String toolName = "undefined";
BooleanTriState prompt = BooleanTriState.UNSET; invokeMerge(toolName);
BooleanTriState gui = BooleanTriState.UNSET;
manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
fail("Expected exception to be thrown due to not defined external merge tool"); fail("Expected exception to be thrown due to not defined external merge tool");
} }
@Test
public void testDefaultToolExecutionWithPrompt() throws Exception {
FileBasedConfig config = db.getConfig();
// the default diff tool is configured without a subsection
String subsection = null;
config.setString("merge", subsection, "tool", "customTool");
String command = getEchoCommand();
config.setString("mergetool", "customTool", "cmd", command);
MergeTools manager = new MergeTools(db);
PromptHandler promptHandler = PromptHandler.acceptPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
manager.merge(local, remote, merged, base, null, Optional.empty(),
BooleanTriState.TRUE, false, promptHandler, noToolHandler);
assertEchoCommandHasCorrectOutput();
}
@Test
public void testNoDefaultToolName() {
MergeTools manager = new MergeTools(db);
boolean gui = false;
String defaultToolName = manager.getDefaultToolName(gui);
assertNull("Expected no default tool when none is configured",
defaultToolName);
gui = true;
defaultToolName = manager.getDefaultToolName(gui);
assertNull("Expected no default tool when none is configured",
defaultToolName);
}
@Test(expected = ToolException.class)
public void testNullTool() throws Exception {
MergeTools manager = new MergeTools(db);
PromptHandler promptHandler = null;
MissingToolHandler noToolHandler = null;
Optional<String> tool = null;
manager.merge(local, remote, merged, base, null, tool,
BooleanTriState.TRUE, false, promptHandler, noToolHandler);
}
@Test(expected = ToolException.class)
public void testNullToolWithPrompt() throws Exception {
MergeTools manager = new MergeTools(db);
PromptHandler promptHandler = PromptHandler.cancelPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
Optional<String> tool = null;
manager.merge(local, remote, merged, base, null, tool,
BooleanTriState.TRUE, false, promptHandler, noToolHandler);
}
private Optional<ExecutionResult> 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<ExecutionResult> result = manager.merge(local, remote, merged,
base, null, Optional.of(toolName), prompt, gui, promptHandler,
noToolHandler);
return result;
}
private void assumeMergeToolIsAvailable(
CommandLineMergeTool autoMergingTool) {
boolean isAvailable = ExternalToolUtils.isToolAvailable(db.getFS(),
db.getDirectory(), db.getWorkTree(), autoMergingTool.getPath());
assumeTrue("Assuming external tool is available: "
+ autoMergingTool.name(), isAvailable);
}
private String getEchoCommand() { private String getEchoCommand() {
return "(echo \"$LOCAL\" \"$REMOTE\") > " return "(echo $LOCAL $REMOTE $MERGED $BASE) > "
+ commandResult.getAbsolutePath(); + commandResult.getAbsolutePath();
} }
private void assertEchoCommandHasCorrectOutput() throws IOException {
List<String> actualLines = Files.readAllLines(commandResult.toPath());
String actualContent = String.join(System.lineSeparator(), actualLines);
actualLines = Arrays.asList(actualContent.split(" "));
List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(),
remoteFile.getAbsolutePath(), mergedFile.getAbsolutePath(),
baseFile.getAbsolutePath());
assertEquals("Dummy test tool called with unexpected arguments",
expectedLines, actualLines);
}
} }

View File

@ -11,6 +11,8 @@
import java.io.File; import java.io.File;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
@ -88,4 +90,39 @@ protected static void assumePosixPlatform() {
"This test can run only in Linux tests", "This test can run only in Linux tests",
FS.DETECTED instanceof FS_POSIX); FS.DETECTED instanceof FS_POSIX);
} }
protected static class PromptHandler implements PromptContinueHandler {
private final boolean promptResult;
final List<String> toolPrompts = new ArrayList<>();
private PromptHandler(boolean promptResult) {
this.promptResult = promptResult;
}
static PromptHandler acceptPrompt() {
return new PromptHandler(true);
}
static PromptHandler cancelPrompt() {
return new PromptHandler(false);
}
@Override
public boolean prompt(String toolName) {
toolPrompts.add(toolName);
return promptResult;
}
}
protected static class MissingToolHandler implements InformNoToolHandler {
final List<String> missingTools = new ArrayList<>();
@Override
public void inform(List<String> toolNames) {
missingTools.addAll(toolNames);
}
}
} }

View File

@ -73,7 +73,8 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.0",
org.eclipse.jgit.internal.diffmergetool;version="6.2.0"; org.eclipse.jgit.internal.diffmergetool;version="6.2.0";
x-friends:="org.eclipse.jgit.test, x-friends:="org.eclipse.jgit.test,
org.eclipse.jgit.pgm.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"; org.eclipse.jgit.internal.fsck;version="6.2.0";
x-friends:="org.eclipse.jgit.test", x-friends:="org.eclipse.jgit.test",
org.eclipse.jgit.internal.revwalk;version="6.2.0"; 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.util.time",
org.eclipse.jgit.lib.internal;version="6.2.0"; org.eclipse.jgit.lib.internal;version="6.2.0";
x-friends:="org.eclipse.jgit.test, 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.logging;version="6.2.0",
org.eclipse.jgit.merge;version="6.2.0"; org.eclipse.jgit.merge;version="6.2.0";
uses:="org.eclipse.jgit.dircache, uses:="org.eclipse.jgit.dircache,

View File

@ -237,6 +237,9 @@ deleteTagUnexpectedResult=Delete tag returned unexpected result {0}
deletingNotSupported=Deleting {0} not supported. deletingNotSupported=Deleting {0} not supported.
destinationIsNotAWildcard=Destination is not a wildcard. destinationIsNotAWildcard=Destination is not a wildcard.
detachedHeadDetected=HEAD is detached detachedHeadDetected=HEAD is detached
diffToolNotGivenError=No diff tool provided and no defaults configured.
diffToolNotSpecifiedInGitAttributesError=Diff tool specified in git attributes cannot be found.
diffToolNullError=Parameter for diff tool cannot be null.
dirCacheDoesNotHaveABackingFile=DirCache does not have a backing file dirCacheDoesNotHaveABackingFile=DirCache does not have a backing file
dirCacheFileIsNotLocked=DirCache {0} not locked dirCacheFileIsNotLocked=DirCache {0} not locked
dirCacheIsNotLocked=DirCache is not locked dirCacheIsNotLocked=DirCache is not locked
@ -457,6 +460,8 @@ mergeStrategyDoesNotSupportHeads=merge strategy {0} does not support {1} heads t
mergeUsingStrategyResultedInDescription=Merge of revisions {0} with base {1} using strategy {2} resulted in: {3}. {4} mergeUsingStrategyResultedInDescription=Merge of revisions {0} with base {1} using strategy {2} resulted in: {3}. {4}
mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1} mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1}
mergeRecursiveTooManyMergeBasesFor = "More than {0} merge bases for:\n a {1}\n b {2} found:\n count {3}" mergeRecursiveTooManyMergeBasesFor = "More than {0} merge bases for:\n a {1}\n b {2} found:\n count {3}"
mergeToolNotGivenError=No merge tool provided and no defaults configured.
mergeToolNullError=Parameter for merge tool cannot be null.
messageAndTaggerNotAllowedInUnannotatedTags = Unannotated tags cannot have a message or tagger messageAndTaggerNotAllowedInUnannotatedTags = Unannotated tags cannot have a message or tagger
minutesAgo={0} minutes ago minutesAgo={0} minutes ago
mismatchOffset=mismatch offset for object {0} mismatchOffset=mismatch offset for object {0}

View File

@ -265,6 +265,9 @@ public static JGitText get() {
/***/ public String deletingNotSupported; /***/ public String deletingNotSupported;
/***/ public String destinationIsNotAWildcard; /***/ public String destinationIsNotAWildcard;
/***/ public String detachedHeadDetected; /***/ public String detachedHeadDetected;
/***/ public String diffToolNotGivenError;
/***/ public String diffToolNotSpecifiedInGitAttributesError;
/***/ public String diffToolNullError;
/***/ public String dirCacheDoesNotHaveABackingFile; /***/ public String dirCacheDoesNotHaveABackingFile;
/***/ public String dirCacheFileIsNotLocked; /***/ public String dirCacheFileIsNotLocked;
/***/ public String dirCacheIsNotLocked; /***/ public String dirCacheIsNotLocked;
@ -485,6 +488,8 @@ public static JGitText get() {
/***/ public String mergeUsingStrategyResultedInDescription; /***/ public String mergeUsingStrategyResultedInDescription;
/***/ public String mergeRecursiveConflictsWhenMergingCommonAncestors; /***/ public String mergeRecursiveConflictsWhenMergingCommonAncestors;
/***/ public String mergeRecursiveTooManyMergeBasesFor; /***/ public String mergeRecursiveTooManyMergeBasesFor;
/***/ public String mergeToolNotGivenError;
/***/ public String mergeToolNullError;
/***/ public String messageAndTaggerNotAllowedInUnannotatedTags; /***/ public String messageAndTaggerNotAllowedInUnannotatedTags;
/***/ public String minutesAgo; /***/ public String minutesAgo;
/***/ public String mismatchOffset; /***/ public String mismatchOffset;

View File

@ -14,13 +14,19 @@
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult; import org.eclipse.jgit.util.FS.ExecutionResult;
import org.eclipse.jgit.util.FS_POSIX; import org.eclipse.jgit.util.FS_POSIX;
import org.eclipse.jgit.util.FS_Win32; import org.eclipse.jgit.util.FS_Win32;
import org.eclipse.jgit.util.FS_Win32_Cygwin; import org.eclipse.jgit.util.FS_Win32_Cygwin;
import org.eclipse.jgit.util.StringUtils;
/** /**
* Runs a command with help of FS. * Runs a command with help of FS.
@ -91,6 +97,49 @@ public ExecutionResult run(String command, File workingDir,
} }
} }
/**
* @param path
* the executable path
* @param workingDir
* the working directory
* @param env
* the environment
* @return the execution result
* @throws ToolException
* @throws InterruptedException
* @throws IOException
*/
public boolean checkExecutable(String path, File workingDir,
Map<String, String> env)
throws ToolException, IOException, InterruptedException {
checkUseMsys2(path);
String command = null;
if (fs instanceof FS_Win32 && !useMsys2) {
Path p = Paths.get(path);
// Win32 (and not cygwin or MSYS2) where accepts only command / exe
// name as parameter
// so check if exists and executable in this case
if (p.isAbsolute() && Files.isExecutable(p)) {
return true;
}
// try where command for all other cases
command = "where " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
} else {
command = "which " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
}
boolean available = true;
try {
ExecutionResult rc = run(command, workingDir, env);
if (rc.getRc() != 0) {
available = false;
}
} catch (IOException | InterruptedException | NoWorkTreeException
| ToolException e) {
// no op: is true to not hide possible tools from user
}
return available;
}
private void deleteCommandArray() { private void deleteCommandArray() {
deleteCommandFile(); deleteCommandFile();
} }
@ -127,7 +176,7 @@ private String[] createCommandArray(String command)
private void checkUseMsys2(String command) { private void checkUseMsys2(String command) {
useMsys2 = false; useMsys2 = false;
String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$ String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$
if (useMsys2Str != null && !useMsys2Str.isEmpty()) { if (!StringUtils.isEmptyOrNull(useMsys2Str)) {
if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$ if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$
useMsys2 = command.contains(".sh"); //$NON-NLS-1$ useMsys2 = command.contains(".sh"); //$NON-NLS-1$
} else { } else {

View File

@ -111,7 +111,7 @@ public enum CommandLineDiffTool {
* See: <a href= * See: <a href=
* "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
*/ */
gvimdiff("gviewdiff", "\"$LOCAL\" \"$REMOTE\""), gvimdiff("gvimdiff", "\"$LOCAL\" \"$REMOTE\""),
/** /**
* See: <a href= * See: <a href=
* "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
@ -160,7 +160,7 @@ public enum CommandLineDiffTool {
* See: <a href= * See: <a href=
* "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
*/ */
vimdiff("viewdiff", gvimdiff), vimdiff("vimdiff", gvimdiff),
/** /**
* See: <a href= * See: <a href=
* "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>

View File

@ -1,5 +1,6 @@
/* /*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
* Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
* *
* This program and the accompanying materials are made available under the * This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at * terms of the Eclipse Distribution License v. 1.0 which is available at
@ -10,14 +11,22 @@
package org.eclipse.jgit.internal.diffmergetool; package org.eclipse.jgit.internal.diffmergetool;
import java.util.TreeMap; import java.io.File;
import java.util.Collections;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult; import org.eclipse.jgit.util.FS.ExecutionResult;
import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.StringUtils;
@ -26,10 +35,16 @@
*/ */
public class DiffTools { public class DiffTools {
private final Repository repo; private final FS fs;
private final File gitDir;
private final File workTree;
private final DiffToolConfig config; private final DiffToolConfig config;
private final Repository repo;
private final Map<String, ExternalDiffTool> predefinedTools; private final Map<String, ExternalDiffTool> predefinedTools;
private final Map<String, ExternalDiffTool> userDefinedTools; private final Map<String, ExternalDiffTool> userDefinedTools;
@ -41,10 +56,107 @@ public class DiffTools {
* the repository * the repository
*/ */
public DiffTools(Repository repo) { public DiffTools(Repository repo) {
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.repo = repo; this.repo = repo;
config = repo.getConfig().get(DiffToolConfig.KEY); 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(); 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<ExecutionResult> compare(FileElement localFile,
FileElement remoteFile, Optional<String> toolName,
BooleanTriState prompt, boolean gui, BooleanTriState trustExitCode,
PromptContinueHandler promptHandler,
InformNoToolHandler noToolHandler) throws ToolException {
String toolNameToUse;
if (toolName == null) {
throw new ToolException(JGitText.get().diffToolNullError);
}
if (toolName.isPresent()) {
toolNameToUse = toolName.get();
} else {
toolNameToUse = getDefaultToolName(gui);
}
if (StringUtils.isEmptyOrNull(toolNameToUse)) {
throw new ToolException(JGitText.get().diffToolNotGivenError);
}
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 +166,110 @@ public DiffTools(Repository repo) {
* the local file element * the local file element
* @param remoteFile * @param remoteFile
* the remote file element * the remote file element
* @param mergedFile * @param tool
* the merged file element, it's path equals local or remote * the selected tool
* element path
* @param toolName
* the selected tool name (can be null)
* @param prompt
* the prompt option
* @param gui
* the GUI option
* @param trustExitCode * @param trustExitCode
* the "trust exit code" option * the "trust exit code" option
* @return the execution result from tool * @return the execution result from tool
* @throws ToolException * @throws ToolException
*/ */
public ExecutionResult compare(FileElement localFile, public ExecutionResult compare(FileElement localFile,
FileElement remoteFile, FileElement mergedFile, String toolName, FileElement remoteFile, ExternalDiffTool tool,
BooleanTriState prompt, BooleanTriState gui, boolean trustExitCode) throws ToolException {
BooleanTriState trustExitCode) throws ToolException {
try { try {
// prepare the command (replace the file paths) if (tool == null) {
String command = ExternalToolUtils.prepareCommand( throw new ToolException(JGitText
guessTool(toolName, gui).getCommand(), localFile, .get().diffToolNotSpecifiedInGitAttributesError);
remoteFile, mergedFile, null);
// prepare the environment
Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo,
localFile, remoteFile, mergedFile, null);
boolean trust = config.isTrustExitCode();
if (trustExitCode != BooleanTriState.UNSET) {
trust = trustExitCode == BooleanTriState.TRUE;
} }
// prepare the command (replace the file paths)
String command = ExternalToolUtils.prepareCommand(tool.getCommand(),
localFile, remoteFile, null, null);
// prepare the environment
Map<String, String> env = ExternalToolUtils.prepareEnvironment(
gitDir, localFile, remoteFile, null, null);
// execute the tool // execute the tool
CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); CommandExecutor cmdExec = new CommandExecutor(fs, trustExitCode);
return cmdExec.run(command, repo.getWorkTree(), env); return cmdExec.run(command, workTree, env);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
throw new ToolException(e); throw new ToolException(e);
} finally { } finally {
localFile.cleanTemporaries(); localFile.cleanTemporaries();
remoteFile.cleanTemporaries(); remoteFile.cleanTemporaries();
mergedFile.cleanTemporaries();
} }
} }
/** /**
* @return the tool names * Get user defined tool names.
*
* @return the user defined tool names
*/ */
public Set<String> getToolNames() { public Set<String> getUserDefinedToolNames() {
return config.getToolNames(); return userDefinedTools.keySet();
} }
/** /**
* Get predefined tool names.
*
* @return the predefined tool names
*/
public Set<String> getPredefinedToolNames() {
return predefinedTools.keySet();
}
/**
* Get all tool names.
*
* @return the all tool names (default or available tool name is the first
* in the set)
*/
public Set<String> getAllToolNames() {
String defaultName = getDefaultToolName(false);
if (defaultName == null) {
defaultName = getFirstAvailableTool();
}
return ExternalToolUtils.createSortedToolSet(defaultName,
getUserDefinedToolNames(), getPredefinedToolNames());
}
/**
* Provides {@link Optional} with the name of an external diff tool if
* specified in git configuration for a path.
*
* The formed git configuration results from global rules as well as merged
* rules from info and worktree attributes.
*
* Triggers {@link TreeWalk} until specified path found in the tree.
*
* @param path
* path to the node in repository to parse git attributes for
* @return name of the difftool if set
* @throws ToolException
*/
public Optional<String> getExternalToolFromAttributes(final String path)
throws ToolException {
return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
ExternalToolUtils.KEY_DIFF_TOOL);
}
/**
* Checks the availability of the predefined tools in the system.
*
* @return set of predefined available tools
*/
public Set<String> getPredefinedAvailableTools() {
Map<String, ExternalDiffTool> defTools = getPredefinedTools(true);
Set<String> availableTools = new LinkedHashSet<>();
for (Entry<String, ExternalDiffTool> elem : defTools.entrySet()) {
if (elem.getValue().isAvailable()) {
availableTools.add(elem.getKey());
}
}
return availableTools;
}
/**
* Get user defined tools map.
*
* @return the user defined tools * @return the user defined tools
*/ */
public Map<String, ExternalDiffTool> getUserDefinedTools() { public Map<String, ExternalDiffTool> getUserDefinedTools() {
@ -111,48 +277,70 @@ public Map<String, ExternalDiffTool> getUserDefinedTools() {
} }
/** /**
* @return the available predefined tools * 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
* other actions); false: availability is NOT checked:
* isAvailable() returns default false is this case!
* @return the predefined tools with optionally checked availability (long
* running operation)
*/ */
public Map<String, ExternalDiffTool> getAvailableTools() { public Map<String, ExternalDiffTool> getPredefinedTools(
boolean checkAvailability) {
if (checkAvailability) {
for (ExternalDiffTool tool : predefinedTools.values()) {
PreDefinedDiffTool predefTool = (PreDefinedDiffTool) tool;
predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
gitDir, workTree, predefTool.getPath()));
}
}
return Collections.unmodifiableMap(predefinedTools); return Collections.unmodifiableMap(predefinedTools);
} }
/** /**
* @return the NOT available predefined tools * Get first available tool name.
*
* @return the name of first available predefined tool or null
*/ */
public Map<String, ExternalDiffTool> getNotAvailableTools() { public String getFirstAvailableTool() {
return Collections.unmodifiableMap(new TreeMap<>()); for (ExternalDiffTool tool : predefinedTools.values()) {
if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
tool.getPath())) {
return tool.getName();
}
}
return null;
} }
/** /**
* Get default (gui-)tool name.
*
* @param gui * @param gui
* use the diff.guitool setting ? * use the diff.guitool setting ?
* @return the default tool name * @return the default tool name
*/ */
public String getDefaultToolName(BooleanTriState gui) { public String getDefaultToolName(boolean gui) {
return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ String guiToolName;
: config.getDefaultToolName(); if (gui) {
guiToolName = config.getDefaultGuiToolName();
if (guiToolName != null) {
return guiToolName;
}
}
return config.getDefaultToolName();
} }
/** /**
* Is interactive diff (prompt enabled) ?
*
* @return is interactive (config prompt enabled) ? * @return is interactive (config prompt enabled) ?
*/ */
public boolean isInteractive() { public boolean isInteractive() {
return config.isPrompt(); return config.isPrompt();
} }
private ExternalDiffTool guessTool(String toolName, BooleanTriState gui)
throws ToolException {
if (StringUtils.isEmptyOrNull(toolName)) {
toolName = getDefaultToolName(gui);
}
ExternalDiffTool tool = getTool(toolName);
if (tool == null) {
throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$
}
return tool;
}
private ExternalDiffTool getTool(final String name) { private ExternalDiffTool getTool(final String name) {
ExternalDiffTool tool = userDefinedTools.get(name); ExternalDiffTool tool = userDefinedTools.get(name);
if (tool == null) { if (tool == null) {
@ -169,10 +357,10 @@ private static Map<String, ExternalDiffTool> setupPredefinedTools() {
return tools; return tools;
} }
private static Map<String, ExternalDiffTool> setupUserDefinedTools( private Map<String, ExternalDiffTool> setupUserDefinedTools(
DiffToolConfig cfg, Map<String, ExternalDiffTool> predefTools) { Map<String, ExternalDiffTool> predefTools) {
Map<String, ExternalDiffTool> tools = new TreeMap<>(); Map<String, ExternalDiffTool> tools = new TreeMap<>();
Map<String, ExternalDiffTool> userTools = cfg.getTools(); Map<String, ExternalDiffTool> userTools = config.getTools();
for (String name : userTools.keySet()) { for (String name : userTools.keySet()) {
ExternalDiffTool userTool = userTools.get(name); ExternalDiffTool userTool = userTools.get(name);
// if difftool.<name>.cmd is defined we have user defined tool // if difftool.<name>.cmd is defined we have user defined tool

View File

@ -30,4 +30,10 @@ public interface ExternalDiffTool {
*/ */
String getCommand(); String getCommand();
/**
* @return availability of the tool: true if tool can be executed and false
* if not
*/
boolean isAvailable();
} }

View File

@ -10,16 +10,38 @@
package org.eclipse.jgit.internal.diffmergetool; package org.eclipse.jgit.internal.diffmergetool;
import java.util.TreeMap; import java.util.TreeMap;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.attributes.Attributes;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
import org.eclipse.jgit.util.FS;
/** /**
* Utilities for diff- and merge-tools. * Utilities for diff- and merge-tools.
*/ */
public class ExternalToolUtils { public class ExternalToolUtils {
/**
* Key for merge tool git configuration section
*/
public static final String KEY_MERGE_TOOL = "mergetool"; //$NON-NLS-1$
/**
* Key for diff tool git configuration section
*/
public static final String KEY_DIFF_TOOL = "difftool"; //$NON-NLS-1$
/** /**
* Prepare command for execution. * Prepare command for execution.
* *
@ -39,9 +61,15 @@ public class ExternalToolUtils {
public static String prepareCommand(String command, FileElement localFile, public static String prepareCommand(String command, FileElement localFile,
FileElement remoteFile, FileElement mergedFile, FileElement remoteFile, FileElement mergedFile,
FileElement baseFile) throws IOException { FileElement baseFile) throws IOException {
command = localFile.replaceVariable(command); if (localFile != null) {
command = remoteFile.replaceVariable(command); command = localFile.replaceVariable(command);
command = mergedFile.replaceVariable(command); }
if (remoteFile != null) {
command = remoteFile.replaceVariable(command);
}
if (mergedFile != null) {
command = mergedFile.replaceVariable(command);
}
if (baseFile != null) { if (baseFile != null) {
command = baseFile.replaceVariable(command); command = baseFile.replaceVariable(command);
} }
@ -51,8 +79,8 @@ public static String prepareCommand(String command, FileElement localFile,
/** /**
* Prepare environment needed for execution. * Prepare environment needed for execution.
* *
* @param repo * @param gitDir
* the repository * the .git directory
* @param localFile * @param localFile
* the local file (ours) * the local file (ours)
* @param remoteFile * @param remoteFile
@ -64,18 +92,151 @@ public static String prepareCommand(String command, FileElement localFile,
* @return the environment map with variables and values (file paths) * @return the environment map with variables and values (file paths)
* @throws IOException * @throws IOException
*/ */
public static Map<String, String> prepareEnvironment(Repository repo, public static Map<String, String> prepareEnvironment(File gitDir,
FileElement localFile, FileElement remoteFile, FileElement localFile, FileElement remoteFile,
FileElement mergedFile, FileElement baseFile) throws IOException { FileElement mergedFile, FileElement baseFile) throws IOException {
Map<String, String> env = new TreeMap<>(); Map<String, String> env = new TreeMap<>();
env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath()); if (gitDir != null) {
localFile.addToEnv(env); env.put(Constants.GIT_DIR_KEY, gitDir.getAbsolutePath());
remoteFile.addToEnv(env); }
mergedFile.addToEnv(env); if (localFile != null) {
localFile.addToEnv(env);
}
if (remoteFile != null) {
remoteFile.addToEnv(env);
}
if (mergedFile != null) {
mergedFile.addToEnv(env);
}
if (baseFile != null) { if (baseFile != null) {
baseFile.addToEnv(env); baseFile.addToEnv(env);
} }
return env; return env;
} }
/**
* @param path
* the path to be quoted
* @return quoted path if it contains spaces
*/
@SuppressWarnings("nls")
public static String quotePath(String path) {
// handling of spaces in path
if (path.contains(" ")) {
// add quotes before if needed
if (!path.startsWith("\"")) {
path = "\"" + path;
}
// add quotes after if needed
if (!path.endsWith("\"")) {
path = path + "\"";
}
}
return path;
}
/**
* @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(FS fs, File gitDir, File directory,
String path) {
boolean available = true;
try {
CommandExecutor cmdExec = new CommandExecutor(fs, false);
available = cmdExec.checkExecutable(path, directory,
prepareEnvironment(gitDir, null, null, null, null));
} catch (Exception e) {
available = false;
}
return available;
}
/**
* @param defaultName
* the default tool name
* @param userDefinedNames
* the user defined tool names
* @param preDefinedNames
* the pre defined tool names
* @return the sorted tool names set: first element is default tool name if
* valid, then user defined tool names and then pre defined tool
* names
*/
public static Set<String> createSortedToolSet(String defaultName,
Set<String> userDefinedNames, Set<String> preDefinedNames) {
Set<String> names = new LinkedHashSet<>();
if (defaultName != null) {
// remove defaultName from both sets
Set<String> namesPredef = new LinkedHashSet<>();
Set<String> namesUser = new LinkedHashSet<>();
namesUser.addAll(userDefinedNames);
namesUser.remove(defaultName);
namesPredef.addAll(preDefinedNames);
namesPredef.remove(defaultName);
// add defaultName as first in set
names.add(defaultName);
names.addAll(namesUser);
names.addAll(namesPredef);
} else {
names.addAll(userDefinedNames);
names.addAll(preDefinedNames);
}
return names;
}
/**
* Provides {@link Optional} with the name of an external tool if specified
* in git configuration for a path.
*
* The formed git configuration results from global rules as well as merged
* rules from info and worktree attributes.
*
* Triggers {@link TreeWalk} until specified path found in the tree.
*
* @param repository
* target repository to traverse into
* @param path
* path to the node in repository to parse git attributes for
* @param toolKey
* config key name for the tool
* @return attribute value for the given tool key if set
* @throws ToolException
*/
public static Optional<String> getExternalToolFromAttributes(
final Repository repository, final String path,
final String toolKey) throws ToolException {
try {
WorkingTreeIterator treeIterator = new FileTreeIterator(repository);
try (TreeWalk walk = new TreeWalk(repository)) {
walk.addTree(treeIterator);
walk.setFilter(new NotIgnoredFilter(0));
while (walk.next()) {
String treePath = walk.getPathString();
if (treePath.equals(path)) {
Attributes attrs = walk.getAttributes();
if (attrs.containsKey(toolKey)) {
return Optional.of(attrs.getValue(toolKey));
}
}
if (walk.isSubtree()) {
walk.enterSubtree();
}
}
// no external tool specified
return Optional.empty();
}
} catch (RevisionSyntaxException | IOException e) {
throw new ToolException(e);
}
}
} }

View File

@ -57,6 +57,8 @@ public enum Type {
private final Type type; private final Type type;
private final File workDir;
private InputStream stream; private InputStream stream;
private File tempFile; private File tempFile;
@ -70,7 +72,7 @@ public enum Type {
* the element type * the element type
*/ */
public FileElement(String path, Type 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 * the file path
* @param type * @param type
* the element type * the element type
* @param tempFile * @param workDir
* the temporary file to be used (can be null and will be created * the working directory of the path (can be null, then current
* then) * working dir is used)
* @param stream
* the object stream to load instead of file
*/ */
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) { InputStream stream) {
this.path = path; this.path = path;
this.type = type; this.type = type;
this.tempFile = tempFile; this.workDir = workDir;
this.stream = stream; this.stream = stream;
} }
@ -109,41 +125,39 @@ public Type getType() {
} }
/** /**
* Return a temporary file within passed directory and fills it with stream * Return
* if valid. * <ul>
* * <li>a temporary file if already created and stream is not valid</li>
* @param directory * <li>OR a real file from work tree: if no temp file was created (@see
* the directory where the temporary file is created * createTempFile()) and if no stream was set</li>
* @param midName * <li>OR an empty temporary file if path is "/dev/null"</li>
* name added in the middle of generated temporary file name * <li>OR a temporary file with stream content if stream is valid (not
* @return the object stream * null); stream is closed and invalidated (set to null) after write to temp
* @throws IOException * file, so stream is used only once during first call!</li>
*/ * </ul>
public File getFile(File directory, String midName) throws IOException {
if ((tempFile != null) && (stream == null)) {
return tempFile;
}
tempFile = getTempFile(path, directory, midName);
return copyFromStream(tempFile, stream);
}
/**
* Return a real file from work tree or a temporary file with content if
* stream is valid or if path is "/dev/null"
* *
* @return the object stream * @return the object stream
* @throws IOException * @throws IOException
*/ */
public File getFile() 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)) { if ((tempFile != null) && (stream == null)) {
return tempFile; return tempFile;
} }
File file = new File(path); File file = new File(workDir, path);
// if we have a stream or file is missing ("/dev/null") then create // if we have a stream or file is missing (path is "/dev/null")
// temporary file // then optionally create temporary file and fill it with stream content
if ((stream != null) || isNullPath()) { if ((stream != null) || isNullPath()) {
tempFile = getTempFile(file); if (tempFile == null) {
return copyFromStream(tempFile, stream); 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; 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 * @param directory
* the directory for the file (can be null); if null system * 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 { public File createTempFile(File directory) throws IOException {
if (tempFile == null) { if (tempFile == null) {
File file = new File(path); tempFile = getTempFile(new File(path), type.name(), directory);
if (directory != null) {
tempFile = getTempFile(file, directory, type.name());
} else {
tempFile = getTempFile(file);
}
} }
return tempFile; 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. * Delete and invalidate temporary file if necessary.
*/ */
public void cleanTemporaries() { public void cleanTemporaries() {
if (tempFile != null && tempFile.exists()) if (tempFile != null && tempFile.exists()) {
tempFile.delete(); tempFile.delete();
}
tempFile = null; 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 * @param input
* the input string * the input string
@ -258,4 +220,43 @@ public void addToEnv(Map<String, String> env) throws IOException {
env.put(type.name(), getFile().getPath()); 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;
}
} }

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2018-2019, Tim Neumann <Tim.Neumann@advantest.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
import java.util.List;
/**
* A handler for when the diff/merge tool manager wants to inform the user that
* no tool has been configured and one of the default tools will be used.
*/
public interface InformNoToolHandler {
/**
* Inform the user, that no tool is configured and that one of the given
* tools is used.
*
* @param toolNames
* The tools which are tried
*/
void inform(List<String> toolNames);
}

View File

@ -31,7 +31,7 @@
import org.eclipse.jgit.lib.internal.BooleanTriState; 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 { public class MergeToolConfig {

View File

@ -1,5 +1,6 @@
/* /*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
* Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
* *
* This program and the accompanying materials are made available under the * This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at * terms of the Eclipse Distribution License v. 1.0 which is available at
@ -15,13 +16,23 @@
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.diffmergetool.FileElement.Type; import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.StringUtils;
import org.eclipse.jgit.util.FS.ExecutionResult; import org.eclipse.jgit.util.FS.ExecutionResult;
/** /**
@ -29,26 +40,135 @@
*/ */
public class MergeTools { public class MergeTools {
Repository repo; private final FS fs;
private final File gitDir;
private final File workTree;
private final MergeToolConfig config; private final MergeToolConfig config;
private final Repository repo;
private final Map<String, ExternalMergeTool> predefinedTools; private final Map<String, ExternalMergeTool> predefinedTools;
private final Map<String, ExternalMergeTool> userDefinedTools; private final Map<String, ExternalMergeTool> userDefinedTools;
/** /**
* Creates the external merge-tools manager for given repository.
*
* @param repo * @param repo
* the repository * the repository
*/ */
public MergeTools(Repository repo) { public MergeTools(Repository repo) {
this.repo = repo; this(repo, repo.getConfig());
config = repo.getConfig().get(MergeToolConfig.KEY);
predefinedTools = setupPredefinedTools();
userDefinedTools = setupUserDefinedTools(config, predefinedTools);
} }
/** /**
* 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.repo = repo;
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<ExecutionResult> merge(FileElement localFile,
FileElement remoteFile, FileElement mergedFile,
FileElement baseFile, File tempDir, Optional<String> toolName,
BooleanTriState prompt, boolean gui,
PromptContinueHandler promptHandler,
InformNoToolHandler noToolHandler) throws ToolException {
String toolNameToUse;
if (toolName == null) {
throw new ToolException(JGitText.get().diffToolNullError);
}
if (toolName.isPresent()) {
toolNameToUse = toolName.get();
} else {
toolNameToUse = getDefaultToolName(gui);
if (StringUtils.isEmptyOrNull(toolNameToUse)) {
noToolHandler.inform(new ArrayList<>(predefinedTools.keySet()));
toolNameToUse = getFirstAvailableTool();
}
}
if (StringUtils.isEmptyOrNull(toolNameToUse)) {
throw new ToolException(JGitText.get().diffToolNotGivenError);
}
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 * @param localFile
* the local file element * the local file element
* @param remoteFile * @param remoteFile
@ -60,38 +180,31 @@ public MergeTools(Repository repo) {
* @param tempDir * @param tempDir
* the temporary directory (needed for backup and auto-remove, * the temporary directory (needed for backup and auto-remove,
* can be null) * can be null)
* @param toolName * @param tool
* the selected tool name (can be null) * the selected tool
* @param prompt
* the prompt option
* @param gui
* the GUI option
* @return the execution result from tool * @return the execution result from tool
* @throws ToolException * @throws ToolException
*/ */
public ExecutionResult merge(FileElement localFile, FileElement remoteFile, public ExecutionResult merge(FileElement localFile, FileElement remoteFile,
FileElement mergedFile, FileElement baseFile, File tempDir, FileElement mergedFile, FileElement baseFile, File tempDir,
String toolName, BooleanTriState prompt, BooleanTriState gui) ExternalMergeTool tool) throws ToolException {
throws ToolException {
ExternalMergeTool tool = guessTool(toolName, gui);
FileElement backup = null; FileElement backup = null;
ExecutionResult result = null; ExecutionResult result = null;
try { try {
File workingDir = repo.getWorkTree();
// create additional backup file (copy worktree file) // create additional backup file (copy worktree file)
backup = createBackupFile(mergedFile.getPath(), backup = createBackupFile(mergedFile,
tempDir != null ? tempDir : workingDir); tempDir != null ? tempDir : workTree);
// prepare the command (replace the file paths) // prepare the command (replace the file paths)
boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
String command = ExternalToolUtils.prepareCommand( String command = ExternalToolUtils.prepareCommand(
tool.getCommand(baseFile != null), localFile, remoteFile, tool.getCommand(baseFile != null), localFile, remoteFile,
mergedFile, baseFile); mergedFile, baseFile);
// prepare the environment // prepare the environment
Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo, Map<String, String> env = ExternalToolUtils.prepareEnvironment(
localFile, remoteFile, mergedFile, baseFile); gitDir, localFile, remoteFile, mergedFile, baseFile);
boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
// execute the tool // execute the tool
CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); CommandExecutor cmdExec = new CommandExecutor(fs, trust);
result = cmdExec.run(command, workingDir, env); result = cmdExec.run(command, workTree, env);
// keep backup as .orig file // keep backup as .orig file
if (backup != null) { if (backup != null) {
keepBackupFile(mergedFile.getPath(), backup); keepBackupFile(mergedFile.getPath(), backup);
@ -123,19 +236,21 @@ public ExecutionResult merge(FileElement localFile, FileElement remoteFile,
} }
} }
private FileElement createBackupFile(String filePath, File parentDir) private FileElement createBackupFile(FileElement from, File toParentDir)
throws IOException { throws IOException {
FileElement backup = null; FileElement backup = null;
Path path = Paths.get(filePath); Path path = Paths.get(from.getPath());
if (Files.exists(path)) { if (Files.exists(path)) {
backup = new FileElement(filePath, Type.BACKUP); backup = new FileElement(from.getPath(), Type.BACKUP);
Files.copy(path, backup.createTempFile(parentDir).toPath(), Files.copy(path, backup.createTempFile(toParentDir).toPath(),
StandardCopyOption.REPLACE_EXISTING); StandardCopyOption.REPLACE_EXISTING);
} }
return backup; return backup;
} }
/** /**
* Create temporary directory.
*
* @return the created temporary directory if (mergetol.writeToTemp == true) * @return the created temporary directory if (mergetol.writeToTemp == true)
* or null if not configured or false. * or null if not configured or false.
* @throws IOException * @throws IOException
@ -147,60 +262,138 @@ public File createTempDirectory() throws IOException {
} }
/** /**
* @return the tool names * Get user defined tool names.
*
* @return the user defined tool names
*/ */
public Set<String> getToolNames() { public Set<String> getUserDefinedToolNames() {
return config.getToolNames(); return userDefinedTools.keySet();
}
/**
* @return the predefined tool names
*/
public Set<String> getPredefinedToolNames() {
return predefinedTools.keySet();
}
/**
* Get all tool names.
*
* @return the all tool names (default or available tool name is the first
* in the set)
*/
public Set<String> getAllToolNames() {
String defaultName = getDefaultToolName(false);
if (defaultName == null) {
defaultName = getFirstAvailableTool();
}
return ExternalToolUtils.createSortedToolSet(defaultName,
getUserDefinedToolNames(), getPredefinedToolNames());
}
/**
* Provides {@link Optional} with the name of an external merge tool if
* specified in git configuration for a path.
*
* The formed git configuration results from global rules as well as merged
* rules from info and worktree attributes.
*
* Triggers {@link TreeWalk} until specified path found in the tree.
*
* @param path
* path to the node in repository to parse git attributes for
* @return name of the difftool if set
* @throws ToolException
*/
public Optional<String> getExternalToolFromAttributes(final String path)
throws ToolException {
return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
ExternalToolUtils.KEY_MERGE_TOOL);
}
/**
* Checks the availability of the predefined tools in the system.
*
* @return set of predefined available tools
*/
public Set<String> getPredefinedAvailableTools() {
Map<String, ExternalMergeTool> defTools = getPredefinedTools(true);
Set<String> availableTools = new LinkedHashSet<>();
for (Entry<String, ExternalMergeTool> elem : defTools.entrySet()) {
if (elem.getValue().isAvailable()) {
availableTools.add(elem.getKey());
}
}
return availableTools;
} }
/** /**
* @return the user defined tools * @return the user defined tools
*/ */
public Map<String, ExternalMergeTool> getUserDefinedTools() { public Map<String, ExternalMergeTool> getUserDefinedTools() {
return userDefinedTools; return Collections.unmodifiableMap(userDefinedTools);
} }
/** /**
* @return the available predefined tools * 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
* other actions); false: availability is NOT checked:
* isAvailable() returns default false is this case!
* @return the predefined tools with optionally checked availability (long
* running operation)
*/ */
public Map<String, ExternalMergeTool> getAvailableTools() { public Map<String, ExternalMergeTool> getPredefinedTools(
return predefinedTools; boolean checkAvailability) {
if (checkAvailability) {
for (ExternalMergeTool tool : predefinedTools.values()) {
PreDefinedMergeTool predefTool = (PreDefinedMergeTool) tool;
predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
gitDir, workTree, predefTool.getPath()));
}
}
return Collections.unmodifiableMap(predefinedTools);
} }
/** /**
* @return the NOT available predefined tools * Get first available tool name.
*
* @return the name of first available predefined tool or null
*/ */
public Map<String, ExternalMergeTool> getNotAvailableTools() { public String getFirstAvailableTool() {
return new TreeMap<>(); String name = null;
} for (ExternalMergeTool tool : predefinedTools.values()) {
if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
/** tool.getPath())) {
* @param gui name = tool.getName();
* use the diff.guitool setting ? break;
* @return the default tool name }
*/ }
public String getDefaultToolName(BooleanTriState gui) { return name;
return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$
: config.getDefaultToolName();
} }
/** /**
* Is interactive merge (prompt enabled) ?
*
* @return is interactive (config prompt enabled) ? * @return is interactive (config prompt enabled) ?
*/ */
public boolean isInteractive() { public boolean isInteractive() {
return config.isPrompt(); return config.isPrompt();
} }
private ExternalMergeTool guessTool(String toolName, BooleanTriState gui) /**
throws ToolException { * Get the default (gui-)tool name.
if ((toolName == null) || toolName.isEmpty()) { *
toolName = getDefaultToolName(gui); * @param gui
} * use the diff.guitool setting ?
ExternalMergeTool tool = getTool(toolName); * @return the default tool name
if (tool == null) { */
throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$ public String getDefaultToolName(boolean gui) {
} return gui ? config.getDefaultGuiToolName()
return tool; : config.getDefaultToolName();
} }
private ExternalMergeTool getTool(final String name) { private ExternalMergeTool getTool(final String name) {
@ -231,9 +424,9 @@ private Map<String, ExternalMergeTool> setupPredefinedTools() {
} }
private Map<String, ExternalMergeTool> setupUserDefinedTools( private Map<String, ExternalMergeTool> setupUserDefinedTools(
MergeToolConfig cfg, Map<String, ExternalMergeTool> predefTools) { Map<String, ExternalMergeTool> predefTools) {
Map<String, ExternalMergeTool> tools = new TreeMap<>(); Map<String, ExternalMergeTool> tools = new TreeMap<>();
Map<String, ExternalMergeTool> userTools = cfg.getTools(); Map<String, ExternalMergeTool> userTools = config.getTools();
for (String name : userTools.keySet()) { for (String name : userTools.keySet()) {
ExternalMergeTool userTool = userTools.get(name); ExternalMergeTool userTool = userTools.get(name);
// if mergetool.<name>.cmd is defined we have user defined tool // if mergetool.<name>.cmd is defined we have user defined tool

View File

@ -56,7 +56,7 @@ public void setPath(String path) {
*/ */
@Override @Override
public String getCommand() { public String getCommand() {
return getPath() + " " + super.getCommand(); //$NON-NLS-1$ return ExternalToolUtils.quotePath(getPath()) + " " + super.getCommand(); //$NON-NLS-1$
} }
} }

View File

@ -84,7 +84,7 @@ public String getCommand() {
*/ */
@Override @Override
public String getCommand(boolean withBase) { public String getCommand(boolean withBase) {
return getPath() + " " //$NON-NLS-1$ return ExternalToolUtils.quotePath(getPath()) + " " //$NON-NLS-1$
+ (withBase ? super.getCommand() : parametersWithoutBase); + (withBase ? super.getCommand() : parametersWithoutBase);
} }

View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2018-2019, Tim Neumann <Tim.Neumann@advantest.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
/**
* A handler for when the diff/merge tool manager wants to prompt the user
* whether to continue
*/
public interface PromptContinueHandler {
/**
* Prompt the user whether to continue with the next file by opening a given
* tool.
*
* @param toolName
* The name of the tool to open
* @return Whether the user wants to continue
*/
boolean prompt(String toolName);
}

View File

@ -110,6 +110,9 @@ public boolean isCommandExecutionError() {
* @return the result Stderr * @return the result Stderr
*/ */
public String getResultStderr() { public String getResultStderr() {
if (result == null) {
return ""; //$NON-NLS-1$
}
try { try {
return new String(result.getStderr().toByteArray()); return new String(result.getStderr().toByteArray());
} catch (Exception e) { } catch (Exception e) {
@ -122,6 +125,9 @@ public String getResultStderr() {
* @return the result Stdout * @return the result Stdout
*/ */
public String getResultStdout() { public String getResultStdout() {
if (result == null) {
return ""; //$NON-NLS-1$
}
try { try {
return new String(result.getStdout().toByteArray()); return new String(result.getStdout().toByteArray());
} catch (Exception e) { } catch (Exception e) {

View File

@ -15,6 +15,8 @@
*/ */
public class UserDefinedDiffTool implements ExternalDiffTool { public class UserDefinedDiffTool implements ExternalDiffTool {
private boolean available;
/** /**
* the diff tool name * the diff tool name
*/ */
@ -98,6 +100,23 @@ public String getCommand() {
return cmd; return cmd;
} }
/**
* @return availability of the tool: true if tool can be executed and false
* if not
*/
@Override
public boolean isAvailable() {
return available;
}
/**
* @param available
* true if tool can be found and false if not
*/
public void setAvailable(boolean available) {
this.available = available;
}
/** /**
* Overrides the path for the given tool. Equivalent to setting * Overrides the path for the given tool. Equivalent to setting
* {@code difftool.<tool>.path}. * {@code difftool.<tool>.path}.