import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.junit.Assert.fail;
+import java.io.File;
import java.io.InputStream;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.regex.Pattern;
import org.eclipse.jgit.internal.diffmergetool.DiffTools;
import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
configureEchoTool(TOOL_NAME);
}
+ @Test(expected = Die.class)
+ public void testUndefinedTool() throws Exception {
+ String toolName = "undefined";
+ String[] conflictingFilenames = createUnstagedChanges();
+
+ List<String> expectedErrors = new ArrayList<>();
+ for (String changedFilename : conflictingFilenames) {
+ expectedErrors.add("External diff tool is not defined: " + toolName);
+ expectedErrors.add("compare of " + changedFilename + " failed");
+ }
+
+ runAndCaptureUsingInitRaw(expectedErrors, DIFF_TOOL, "--no-prompt",
+ "--tool", toolName);
+ fail("Expected exception to be thrown due to undefined external tool");
+ }
+
+ @Test(expected = Die.class)
+ public void testUserToolWithCommandNotFoundError() throws Exception {
+ String toolName = "customTool";
+
+ int errorReturnCode = 127; // command not found
+ String command = "exit " + errorReturnCode;
+
+ StoredConfig config = db.getConfig();
+ config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
+ command);
+
+ createMergeConflict();
+ runAndCaptureUsingInitRaw(DIFF_TOOL, "--no-prompt", "--tool", toolName);
+
+ fail("Expected exception to be thrown due to external tool exiting with error code: "
+ + errorReturnCode);
+ }
+
+ @Test
+ public void testEmptyToolName() throws Exception {
+ String emptyToolName = "";
+
+ StoredConfig config = db.getConfig();
+ // the default diff tool is configured without a subsection
+ String subsection = null;
+ config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
+ emptyToolName);
+
+ createUnstagedChanges();
+
+ String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
+ String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
+ runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput), DIFF_TOOL,
+ "--no-prompt");
+ }
+
@Test
public void testToolWithPrompt() throws Exception {
String[] inputLines = {
@Test
public void testToolCached() throws Exception {
String[] conflictingFilenames = createStagedChanges();
- String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
+ Pattern[] expectedOutput = getExpectedCachedToolOutputNoPrompt(conflictingFilenames);
String[] options = { "--cached", "--staged", };
for (String option : options) {
- assertArrayOfLinesEquals("Incorrect output for option: " + option,
+ assertArrayOfMatchingLines("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
option, "--tool", TOOL_NAME));
}
String.valueOf(false));
}
- private static String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) {
+ private String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) {
String[] expectedToolOutput = new String[conflictingFilenames.length];
for (int i = 0; i < conflictingFilenames.length; ++i) {
String newPath = conflictingFilenames[i];
- String expectedLine = newPath;
- expectedToolOutput[i] = expectedLine;
+ Path fullPath = getFullPath(newPath);
+ expectedToolOutput[i] = fullPath.toString();
}
return expectedToolOutput;
}
- private static String[] getExpectedCompareOutput(String[] conflictingFilenames) {
+ private Pattern[] getExpectedCachedToolOutputNoPrompt(String[] conflictingFilenames) {
+ String tmpDir = System.getProperty("java.io.tmpdir");
+ if (tmpDir.endsWith(File.separator)) {
+ tmpDir = tmpDir.substring(0, tmpDir.length() - 1);
+ }
+ Pattern emptyPattern = Pattern.compile("");
+ List<Pattern> expectedToolOutput = new ArrayList<>();
+ for (int i = 0; i < conflictingFilenames.length; ++i) {
+ String changedFilename = conflictingFilenames[i];
+ Path fullPath = getFullPath(changedFilename);
+ String filename = fullPath.getFileName().toString();
+ String regexp = tmpDir + File.separatorChar + filename
+ + "_REMOTE_.*";
+ Pattern pattern = Pattern.compile(regexp);
+ expectedToolOutput.add(pattern);
+ expectedToolOutput.add(emptyPattern);
+ }
+ expectedToolOutput.add(emptyPattern);
+ return expectedToolOutput.toArray(new Pattern[0]);
+ }
+
+ private String[] getExpectedCompareOutput(String[] conflictingFilenames) {
List<String> expected = new ArrayList<>();
int n = conflictingFilenames.length;
for (int i = 0; i < n; ++i) {
- String newPath = conflictingFilenames[i];
+ String changedFilename = conflictingFilenames[i];
expected.add(
- "Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'");
+ "Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
+ + "'");
expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
- expected.add(newPath);
+ Path fullPath = getFullPath(changedFilename);
+ expected.add(fullPath.toString());
}
return expected.toArray(new String[0]);
}
- private static String[] getExpectedAbortOutput(String[] conflictingFilenames,
+ private String[] getExpectedAbortOutput(String[] conflictingFilenames,
int abortIndex) {
List<String> expected = new ArrayList<>();
int n = conflictingFilenames.length;
for (int i = 0; i < n; ++i) {
- String newPath = conflictingFilenames[i];
+ String changedFilename = conflictingFilenames[i];
expected.add(
- "Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'");
+ "Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
+ + "'");
expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
if (i == abortIndex) {
break;
}
- expected.add(newPath);
+ Path fullPath = getFullPath(changedFilename);
+ expected.add(fullPath.toString());
}
return expected.toArray(new String[0]);
}
+
+ private static String getEchoCommand() {
+ /*
+ * use 'REMOTE' placeholder, as it will be replaced by a file path
+ * within the repository.
+ */
+ return "(echo \"$REMOTE\")";
+ }
}
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
+import static org.junit.Assert.fail;
import java.io.InputStream;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
configureEchoTool(TOOL_NAME);
}
+ @Test
+ public void testUndefinedTool() throws Exception {
+ String toolName = "undefined";
+ String[] conflictingFilenames = createMergeConflict();
+
+ List<String> expectedErrors = new ArrayList<>();
+ for (String conflictingFilename : conflictingFilenames) {
+ expectedErrors.add("External merge tool is not defined: " + toolName);
+ expectedErrors.add("merge of " + conflictingFilename + " failed");
+ }
+
+ runAndCaptureUsingInitRaw(expectedErrors, MERGE_TOOL,
+ "--no-prompt", "--tool", toolName);
+ }
+
+ @Test(expected = Die.class)
+ public void testUserToolWithCommandNotFoundError() throws Exception {
+ String toolName = "customTool";
+
+ int errorReturnCode = 127; // command not found
+ String command = "exit " + errorReturnCode;
+
+ StoredConfig config = db.getConfig();
+ config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+ command);
+
+ createMergeConflict();
+ runAndCaptureUsingInitRaw(MERGE_TOOL, "--no-prompt", "--tool",
+ toolName);
+
+ fail("Expected exception to be thrown due to external tool exiting with error code: "
+ + errorReturnCode);
+ }
+
+ @Test
+ public void testEmptyToolName() throws Exception {
+ String emptyToolName = "";
+
+ StoredConfig config = db.getConfig();
+ // the default merge tool is configured without a subsection
+ String subsection = null;
+ config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
+ emptyToolName);
+
+ createMergeConflict();
+
+ String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
+ String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
+ runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput),
+ MERGE_TOOL, "--no-prompt");
+ }
+
@Test
public void testAbortMerge() throws Exception {
String[] inputLines = {
String.valueOf(false));
}
- private static String[] getExpectedMergeConflictOutputNoPrompt(
+ private String[] getExpectedMergeConflictOutputNoPrompt(
String[] conflictFilenames) {
List<String> expected = new ArrayList<>();
expected.add("Merging:");
+ "':");
expected.add("{local}: modified file");
expected.add("{remote}: modified file");
- expected.add(conflictFilename);
+ Path filePath = getFullPath(conflictFilename);
+ expected.add(filePath.toString());
expected.add(conflictFilename + " seems unchanged.");
}
return expected.toArray(new String[0]);
return expected.toArray(new String[0]);
}
- private static String[] getExpectedAbortMergeOutput(
+ private String[] getExpectedAbortMergeOutput(
String[] conflictFilenames, int abortIndex) {
List<String> expected = new ArrayList<>();
expected.add("Merging:");
"Normal merge conflict for '" + conflictFilename + "':");
expected.add("{local}: modified file");
expected.add("{remote}: modified file");
+ Path fullPath = getFullPath(conflictFilename);
expected.add("Hit return to start merge resolution tool ("
- + TOOL_NAME + "): " + conflictFilename);
+ + TOOL_NAME + "): " + fullPath);
expected.add(conflictFilename + " seems unchanged.");
expected.add("Was the merge successful [y/n]?");
if (i < conflictFilenames.length - 1) {
return expected.toArray(new String[0]);
}
- private static String[] getExpectedMergeConflictOutput(
+ private String[] getExpectedMergeConflictOutput(
String[] conflictFilenames) {
List<String> expected = new ArrayList<>();
expected.add("Merging:");
+ "':");
expected.add("{local}: modified file");
expected.add("{remote}: modified file");
+ Path filePath = getFullPath(conflictFilename);
expected.add("Hit return to start merge resolution tool ("
- + TOOL_NAME + "): " + conflictFilename);
+ + TOOL_NAME + "): " + filePath);
expected.add(conflictFilename + " seems unchanged.");
expected.add("Was the merge successful [y/n]?");
if (i < conflictFilenames.length - 1) {
}
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\")";
+ }
}
package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
+import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.Before;
import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
/**
* Base test case for the {@code difftool} and {@code mergetool} commands.
return runAndCaptureUsingInitRaw(inputStream, args);
}
+ protected String[] runAndCaptureUsingInitRaw(
+ List<String> expectedErrorOutput, String... args) throws Exception {
+ InputStream inputStream = null; // no input stream
+ return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput,
+ args);
+ }
+
protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
String... args) throws Exception {
+ List<String> expectedErrorOutput = Collections.emptyList();
+ return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput,
+ args);
+ }
+
+ protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
+ List<String> expectedErrorOutput, String... args)
+ throws CmdLineException, Exception, IOException {
CLIGitCommand.Result result = new CLIGitCommand.Result();
GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
.filter(l -> !l.isBlank()) // we care only about error messages
.collect(Collectors.toList());
assertEquals("Expected no standard error output from tool",
- Collections.EMPTY_LIST.toString(), errLines.toString());
+ expectedErrorOutput.toString(), errLines.toString());
return result.outLines().toArray(new String[0]);
}
return changes;
}
+ protected Path getFullPath(String repositoryFilename) {
+ Path dotGitPath = db.getDirectory().toPath();
+ Path repositoryRoot = dotGitPath.getParent();
+ Path repositoryFilePath = repositoryRoot.resolve(repositoryFilename);
+ return repositoryFilePath;
+ }
+
protected static InputStream createInputStream(String[] inputLines) {
return createInputStream(Arrays.asList(inputLines));
}
assertEquals(failMessage, toString(expected), toString(actual));
}
- protected static String getEchoCommand() {
- /*
- * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be
- * replaced with full paths to a temporary file during some of the tests
- */
- return "(echo \"$MERGED\")";
+ protected static void assertArrayOfMatchingLines(String failMessage,
+ Pattern[] expected, String[] actual) {
+ assertEquals(failMessage + System.lineSeparator()
+ + "Expected and actual lines count don't match. Expected: "
+ + Arrays.asList(expected) + ", actual: "
+ + Arrays.asList(actual), expected.length, actual.length);
+ int n = expected.length;
+ for (int i = 0; i < n; ++i) {
+ Pattern expectedPattern = expected[i];
+ String actualLine = actual[i];
+ Matcher matcher = expectedPattern.matcher(actualLine);
+ boolean matches = matcher.matches();
+ assertTrue(failMessage + System.lineSeparator() + "Line " + i + " '"
+ + actualLine + "' doesn't match expected pattern: "
+ + expectedPattern + System.lineSeparator() + "Expected: "
+ + Arrays.asList(expected) + ", actual: "
+ + Arrays.asList(actual),
+ matches);
+ }
}
}
/*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.diff.ContentSource;
import org.eclipse.jgit.internal.diffmergetool.DiffTools;
import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
import org.eclipse.jgit.internal.diffmergetool.FileElement;
+import org.eclipse.jgit.internal.diffmergetool.PromptContinueHandler;
import org.eclipse.jgit.internal.diffmergetool.ToolException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.FS.ExecutionResult;
-import org.eclipse.jgit.util.StringUtils;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@Argument(index = 1, metaVar = "metaVar_treeish")
private AbstractTreeIterator newTree;
+ private Optional<String> toolName = Optional.empty();
+
@Option(name = "--tool", aliases = {
"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForDiff")
- private String toolName;
+ void setToolName(String name) {
+ toolName = Optional.of(name);
+ }
@Option(name = "--cached", aliases = { "--staged" }, usage = "usage_cached")
private boolean cached;
@Option(name = "--tool-help", usage = "usage_toolHelp")
private boolean toolHelp;
- private BooleanTriState gui = BooleanTriState.UNSET;
+ private boolean gui = false;
@Option(name = "--gui", aliases = { "-g" }, usage = "usage_DiffGuiTool")
void setGui(@SuppressWarnings("unused") boolean on) {
- gui = BooleanTriState.TRUE;
+ gui = true;
}
@Option(name = "--no-gui", usage = "usage_noGui")
void noGui(@SuppressWarnings("unused") boolean on) {
- gui = BooleanTriState.FALSE;
+ gui = false;
}
private BooleanTriState trustExitCode = BooleanTriState.UNSET;
if (toolHelp) {
showToolHelp();
} else {
- boolean showPrompt = diffTools.isInteractive();
- if (prompt != BooleanTriState.UNSET) {
- showPrompt = prompt == BooleanTriState.TRUE;
- }
- // get passed or default tool name
- String toolNameToUse = promptToolName();
// get the changed files
List<DiffEntry> files = getFiles();
if (files.size() > 0) {
- compare(files, showPrompt, toolNameToUse);
+ compare(files);
}
}
} catch (RevisionSyntaxException | IOException e) {
}
}
- private String promptToolName() throws IOException {
- String toolNameToUse = toolName;
- if (StringUtils.isEmptyOrNull(toolNameToUse)) {
- toolNameToUse = diffTools.getDefaultToolName(gui);
- }
- if (StringUtils.isEmptyOrNull(toolNameToUse)) {
- Map<String, ExternalDiffTool> predefTools = diffTools
- .getPredefinedTools(false);
+ private void informUserNoTool(List<String> tools) {
+ try {
StringBuilder toolNames = new StringBuilder();
- for (String name : predefTools.keySet()) {
+ for (String name : tools) {
toolNames.append(name + " "); //$NON-NLS-1$
}
outw.println(MessageFormat.format(
CLIText.get().diffToolPromptToolName, toolNames));
outw.flush();
- toolNameToUse = diffTools.getFirstAvailableTool();
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
}
- if (StringUtils.isEmptyOrNull(toolNameToUse)) {
- throw new IOException(MessageFormat
- .format(CLIText.get().diffToolUnknownToolName, toolName));
+ }
+
+ private class CountingPromptContinueHandler
+ implements PromptContinueHandler {
+ private final int fileIndex;
+
+ private final int fileCount;
+
+ private final String fileName;
+
+ public CountingPromptContinueHandler(int fileIndex, int fileCount,
+ String fileName) {
+ this.fileIndex = fileIndex;
+ this.fileCount = fileCount;
+ this.fileName = fileName;
+ }
+
+ @SuppressWarnings("boxing")
+ @Override
+ public boolean prompt(String toolToLaunchName) {
+ try {
+ boolean launchCompare = true;
+ outw.println(MessageFormat.format(CLIText.get().diffToolLaunch,
+ fileIndex, fileCount, fileName, toolToLaunchName)
+ + " "); //$NON-NLS-1$
+ outw.flush();
+ BufferedReader br = inputReader;
+ String line = null;
+ if ((line = br.readLine()) != null) {
+ if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$
+ launchCompare = false;
+ }
+ }
+ return launchCompare;
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
+ }
}
- return toolNameToUse;
}
- private void compare(List<DiffEntry> files, boolean showPrompt,
- String toolNameToUse) throws IOException {
+ private void compare(List<DiffEntry> files) throws IOException {
ContentSource.Pair sourcePair = new ContentSource.Pair(source(oldTree),
source(newTree));
try {
for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
DiffEntry ent = files.get(fileIndex);
- String mergedFilePath = ent.getNewPath();
- if (mergedFilePath.equals(DiffEntry.DEV_NULL)) {
- mergedFilePath = ent.getOldPath();
- }
- // check if user wants to launch compare
- boolean launchCompare = true;
- if (showPrompt) {
- launchCompare = isLaunchCompare(fileIndex + 1, files.size(),
- mergedFilePath, toolNameToUse);
+
+ String filePath = ent.getNewPath();
+ if (filePath.equals(DiffEntry.DEV_NULL)) {
+ filePath = ent.getOldPath();
}
- if (launchCompare) {
- try {
- FileElement local = createFileElement(
- FileElement.Type.LOCAL, sourcePair, Side.OLD,
- ent);
- FileElement remote = createFileElement(
- FileElement.Type.REMOTE, sourcePair, Side.NEW,
- ent);
- FileElement merged = new FileElement(mergedFilePath,
- FileElement.Type.MERGED);
+
+ try {
+ FileElement local = createFileElement(
+ FileElement.Type.LOCAL, sourcePair, Side.OLD, ent);
+ FileElement remote = createFileElement(
+ FileElement.Type.REMOTE, sourcePair, Side.NEW, ent);
+
+ PromptContinueHandler promptContinueHandler = new CountingPromptContinueHandler(
+ fileIndex + 1, files.size(), filePath);
+
+ Optional<ExecutionResult> optionalResult = diffTools
+ .compare(local, remote, toolName, prompt, gui,
+ trustExitCode, promptContinueHandler,
+ this::informUserNoTool);
+
+ if (optionalResult.isPresent()) {
+ ExecutionResult result = optionalResult.get();
// TODO: check how to return the exit-code of the tool
// to jgit / java runtime ?
// int rc =...
- ExecutionResult result = diffTools.compare(local,
- remote, merged, toolNameToUse, prompt, gui,
- trustExitCode);
- outw.println(new String(result.getStdout().toByteArray()));
+ outw.println(
+ new String(result.getStdout().toByteArray()));
outw.flush();
errw.println(
new String(result.getStderr().toByteArray()));
errw.flush();
- } catch (ToolException e) {
- outw.println(e.getResultStdout());
- outw.flush();
- errw.println(e.getMessage());
- errw.flush();
- throw die(MessageFormat.format(
- CLIText.get().diffToolDied, mergedFilePath, e),
- e);
}
- } else {
- break;
+ } catch (ToolException e) {
+ outw.println(e.getResultStdout());
+ outw.flush();
+ errw.println(e.getMessage());
+ errw.flush();
+ throw die(MessageFormat.format(
+ CLIText.get().diffToolDied, filePath, e), e);
}
}
} finally {
}
}
- @SuppressWarnings("boxing")
- private boolean isLaunchCompare(int fileIndex, int fileCount,
- String fileName, String toolNamePrompt) throws IOException {
- boolean launchCompare = true;
- outw.println(MessageFormat.format(CLIText.get().diffToolLaunch,
- fileIndex, fileCount, fileName, toolNamePrompt) + " "); //$NON-NLS-1$
- outw.flush();
- BufferedReader br = inputReader;
- String line = null;
- if ((line = br.readLine()) != null) {
- if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$
- launchCompare = false;
- }
- }
- return launchCompare;
- }
private void showToolHelp() throws IOException {
Map<String, ExternalDiffTool> predefTools = diffTools
.getPredefinedTools(true);
}
private FileElement createFileElement(FileElement.Type elementType,
- Pair pair, Side side, DiffEntry entry)
- throws NoWorkTreeException, CorruptObjectException, IOException,
- ToolException {
+ Pair pair, Side side, DiffEntry entry) throws NoWorkTreeException,
+ CorruptObjectException, IOException, ToolException {
String entryPath = side == Side.NEW ? entry.getNewPath()
: entry.getOldPath();
- FileElement fileElement = new FileElement(entryPath, elementType);
+ FileElement fileElement = new FileElement(entryPath, elementType,
+ db.getWorkTree());
if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) {
try (RevWalk revWalk = new RevWalk(db);
TreeWalk treeWalk = new TreeWalk(db,
fileElement.createTempFile(null)));
} else {
throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$
- + "' in staging area!", null); //$NON-NLS-1$
+ + "' in staging area!", //$NON-NLS-1$
+ null);
}
}
}
/*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.TreeMap;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.StatusCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.ContentSource;
-import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator;
-import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
import org.eclipse.jgit.internal.diffmergetool.FileElement;
+import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
import org.eclipse.jgit.internal.diffmergetool.MergeTools;
import org.eclipse.jgit.internal.diffmergetool.ToolException;
-import org.eclipse.jgit.lib.IndexDiff.StageState;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.treewalk.WorkingTreeOptions;
-import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
+import org.eclipse.jgit.lib.IndexDiff.StageState;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.util.StringUtils;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.internal.BooleanTriState;
-import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.pgm.internal.CLIText;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeOptions;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
class MergeTool extends TextBuiltin {
private MergeTools mergeTools;
+ private Optional<String> toolName = Optional.empty();
+
@Option(name = "--tool", aliases = {
"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge")
- private String toolName;
+ void setToolName(String name) {
+ toolName = Optional.of(name);
+ }
private BooleanTriState prompt = BooleanTriState.UNSET;
@Option(name = "--tool-help", usage = "usage_toolHelp")
private boolean toolHelp;
- private BooleanTriState gui = BooleanTriState.UNSET;
+ private boolean gui = false;
@Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool")
void setGui(@SuppressWarnings("unused") boolean on) {
- gui = BooleanTriState.TRUE;
+ gui = true;
}
@Option(name = "--no-gui", usage = "usage_noGui")
void noGui(@SuppressWarnings("unused") boolean on) {
- gui = BooleanTriState.FALSE;
+ gui = false;
}
@Argument(required = false, index = 0, metaVar = "metaVar_paths")
if (toolHelp) {
showToolHelp();
} else {
- // get prompt
- boolean showPrompt = mergeTools.isInteractive();
- if (prompt != BooleanTriState.UNSET) {
- showPrompt = prompt == BooleanTriState.TRUE;
- }
- // get passed or default tool name
- String toolNameToUse = promptToolName();
// get the changed files
Map<String, StageState> files = getFiles();
if (files.size() > 0) {
- merge(files, showPrompt, toolNameToUse);
+ merge(files);
} else {
outw.println(CLIText.get().mergeToolNoFiles);
}
}
}
- private String promptToolName() throws IOException {
- String toolNameToUse = toolName;
- if (StringUtils.isEmptyOrNull(toolNameToUse)) {
- toolNameToUse = mergeTools.getDefaultToolName(gui);
- }
- if (StringUtils.isEmptyOrNull(toolNameToUse)) {
- Map<String, ExternalMergeTool> predefTools = mergeTools
- .getPredefinedTools(false);
+ private void informUserNoTool(List<String> tools) {
+ try {
StringBuilder toolNames = new StringBuilder();
- for (String name : predefTools.keySet()) {
+ for (String name : tools) {
toolNames.append(name + " "); //$NON-NLS-1$
}
outw.println(MessageFormat
.format(CLIText.get().mergeToolPromptToolName, toolNames));
outw.flush();
- toolNameToUse = mergeTools.getFirstAvailableTool();
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
}
- if (StringUtils.isEmptyOrNull(toolNameToUse)) {
- throw new IOException(MessageFormat
- .format(CLIText.get().mergeToolUnknownToolName, toolName));
- }
- return toolNameToUse;
}
- private void merge(Map<String, StageState> files, boolean showPrompt,
- String toolNamePrompt) throws Exception {
+ private void merge(Map<String, StageState> files) throws Exception {
// sort file names
List<String> mergedFilePaths = new ArrayList<>(files.keySet());
Collections.sort(mergedFilePaths);
outw.println(MessageFormat.format(CLIText.get().mergeToolMerging,
mergedFiles));
outw.flush();
+ boolean showPrompt = mergeTools.isInteractive();
+ if (prompt != BooleanTriState.UNSET) {
+ showPrompt = prompt == BooleanTriState.TRUE;
+ }
// merge the files
MergeResult mergeResult = MergeResult.SUCCESSFUL;
for (String mergedFilePath : mergedFilePaths) {
// get file stage state and merge
StageState fileState = files.get(mergedFilePath);
if (fileState == StageState.BOTH_MODIFIED) {
- mergeResult = mergeModified(mergedFilePath, showPrompt,
- toolNamePrompt);
+ mergeResult = mergeModified(mergedFilePath, showPrompt);
} else if ((fileState == StageState.DELETED_BY_US)
|| (fileState == StageState.DELETED_BY_THEM)) {
mergeResult = mergeDeleted(mergedFilePath,
}
}
- private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
- String toolNamePrompt) throws Exception {
+ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt)
+ throws Exception {
outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict,
mergedFilePath));
outw.flush();
- // check if user wants to launch merge resolution tool
- boolean launch = true;
- if (showPrompt) {
- launch = isLaunch(toolNamePrompt);
- }
- if (!launch) {
- return MergeResult.ABORTED; // abort
- }
boolean isMergeSuccessful = true;
ContentSource baseSource = ContentSource.create(db.newObjectReader());
ContentSource localSource = ContentSource.create(db.newObjectReader());
FileElement base = null;
FileElement local = null;
FileElement remote = null;
- FileElement merged = new FileElement(mergedFilePath,
- Type.MERGED);
+ FileElement merged = new FileElement(mergedFilePath, Type.MERGED,
+ db.getWorkTree());
DirCache cache = db.readDirCache();
try (RevWalk revWalk = new RevWalk(db);
TreeWalk treeWalk = new TreeWalk(db,
.get(WorkingTreeOptions.KEY);
CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
eolStreamType, filterCommand);
- DirCacheEntry entry = treeWalk.getTree(DirCacheIterator.class).getDirCacheEntry();
+ DirCacheEntry entry = treeWalk
+ .getTree(DirCacheIterator.class).getDirCacheEntry();
if (entry == null) {
continue;
}
// TODO: check how to return the exit-code of the
// tool to jgit / java runtime ?
// int rc =...
- ExecutionResult executionResult = mergeTools.merge(local,
- remote, merged, base, tempDir, toolName, prompt, gui);
- outw.println(
- new String(executionResult.getStdout().toByteArray()));
- outw.flush();
- errw.println(
- new String(executionResult.getStderr().toByteArray()));
- errw.flush();
+ Optional<ExecutionResult> optionalResult = mergeTools.merge(
+ local, remote, merged, base, tempDir, toolName, prompt,
+ gui, this::promptForLaunch, this::informUserNoTool);
+ if (optionalResult.isPresent()) {
+ ExecutionResult result = optionalResult.get();
+ outw.println(new String(result.getStdout().toByteArray()));
+ outw.flush();
+ errw.println(new String(result.getStderr().toByteArray()));
+ errw.flush();
+ } else {
+ return MergeResult.ABORTED;
+ }
} catch (ToolException e) {
isMergeSuccessful = false;
outw.println(e.getResultStdout());
outw.flush();
+ errw.println(e.getMessage());
errw.println(MessageFormat.format(
CLIText.get().mergeToolMergeFailed, mergedFilePath));
errw.flush();
if (e.isCommandExecutionError()) {
- errw.println(e.getMessage());
throw die(CLIText.get().mergeToolExecutionError, e);
}
}
return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull);
}
- private boolean isLaunch(String toolNamePrompt) throws IOException {
- boolean launch = true;
- outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch,
- toolNamePrompt) + " "); //$NON-NLS-1$
- outw.flush();
- BufferedReader br = inputReader;
- String line = null;
- if ((line = br.readLine()) != null) {
- if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$
- launch = false;
+ private boolean promptForLaunch(String toolNamePrompt) {
+ try {
+ boolean launch = true;
+ outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch,
+ toolNamePrompt) + " "); //$NON-NLS-1$
+ outw.flush();
+ BufferedReader br = inputReader;
+ String line = null;
+ if ((line = br.readLine()) != null) {
+ if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$
+ launch = false;
+ }
}
+ return launch;
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
}
- return launch;
}
private int getDeletedMergeDecision() throws IOException {
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.internal.BooleanTriState;
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
- DiffTools manager = new DiffTools(db);
-
- BooleanTriState prompt = BooleanTriState.UNSET;
- BooleanTriState gui = BooleanTriState.UNSET;
- BooleanTriState trustExitCode = BooleanTriState.TRUE;
-
- manager.compare(local, remote, merged, toolName, prompt, gui,
- trustExitCode);
+ invokeCompare(toolName);
fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode);
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
+ invokeCompare(toolName);
+ fail("Expected exception to be thrown due to external tool exiting with error code: "
+ + errorReturnCode);
+ }
+
+ @Test
+ public void testUserDefinedTool() throws Exception {
+ String command = getEchoCommand();
+
+ FileBasedConfig config = db.getConfig();
+ String customToolName = "customTool";
+ config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
+ CONFIG_KEY_CMD, command);
+
DiffTools manager = new DiffTools(db);
- BooleanTriState prompt = BooleanTriState.UNSET;
- BooleanTriState gui = BooleanTriState.UNSET;
- BooleanTriState trustExitCode = BooleanTriState.FALSE;
+ Map<String, ExternalDiffTool> tools = manager.getUserDefinedTools();
+ ExternalDiffTool externalTool = tools.get(customToolName);
+ boolean trustExitCode = true;
+ manager.compare(local, remote, externalTool, trustExitCode);
- manager.compare(local, remote, merged, toolName, prompt, gui,
- trustExitCode);
+ assertEchoCommandHasCorrectOutput();
+ }
- fail("Expected exception to be thrown due to external tool exiting with error code: "
- + errorReturnCode);
+ @Test
+ public void testUserDefinedToolWithPrompt() throws Exception {
+ String command = getEchoCommand();
+
+ FileBasedConfig config = db.getConfig();
+ String customToolName = "customTool";
+ config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
+ CONFIG_KEY_CMD, command);
+
+ DiffTools manager = new DiffTools(db);
+
+ PromptHandler promptHandler = PromptHandler.acceptPrompt();
+ MissingToolHandler noToolHandler = new MissingToolHandler();
+
+ manager.compare(local, remote, Optional.of(customToolName),
+ BooleanTriState.TRUE, false, BooleanTriState.TRUE,
+ promptHandler, noToolHandler);
+
+ assertEchoCommandHasCorrectOutput();
+
+ List<String> actualToolPrompts = promptHandler.toolPrompts;
+ List<String> expectedToolPrompts = Arrays.asList("customTool");
+ assertEquals("Expected a user prompt for custom tool call",
+ expectedToolPrompts, actualToolPrompts);
+
+ assertEquals("Expected to no informing about missing tools",
+ Collections.EMPTY_LIST, noToolHandler.missingTools);
}
@Test
- public void testToolNames() {
+ public void testUserDefinedToolWithCancelledPrompt() throws Exception {
DiffTools manager = new DiffTools(db);
- Set<String> actualToolNames = manager.getToolNames();
- Set<String> expectedToolNames = Collections.emptySet();
- assertEquals("Incorrect set of external diff tool names",
- expectedToolNames, actualToolNames);
+
+ PromptHandler promptHandler = PromptHandler.cancelPrompt();
+ MissingToolHandler noToolHandler = new MissingToolHandler();
+
+ Optional<ExecutionResult> result = manager.compare(local, remote,
+ Optional.empty(), BooleanTriState.TRUE, false,
+ BooleanTriState.TRUE, promptHandler, noToolHandler);
+ assertFalse("Expected no result if user cancels the operation",
+ result.isPresent());
}
@Test
public void testAllTools() {
+ FileBasedConfig config = db.getConfig();
+ String customToolName = "customTool";
+ config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
+ CONFIG_KEY_CMD, "echo");
+
DiffTools manager = new DiffTools(db);
- Set<String> actualToolNames = manager.getPredefinedTools(true).keySet();
+ Set<String> actualToolNames = manager.getAllToolNames();
Set<String> expectedToolNames = new LinkedHashSet<>();
+ expectedToolNames.add(customToolName);
CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
for (CommandLineDiffTool defaultTool : defaultTools) {
String toolName = defaultTool.name();
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
-
- BooleanTriState prompt = BooleanTriState.UNSET;
- BooleanTriState gui = BooleanTriState.UNSET;
- BooleanTriState trustExitCode = BooleanTriState.UNSET;
-
- DiffTools manager = new DiffTools(db);
-
+ Optional<ExecutionResult> result = invokeCompare(toolName);
+ assertTrue("Expected external diff tool result to be available",
+ result.isPresent());
int expectedCompareResult = 0;
- ExecutionResult compareResult = manager.compare(local, remote, merged,
- toolName, prompt, gui, trustExitCode);
assertEquals("Incorrect compare result for external diff tool",
- expectedCompareResult, compareResult.getRc());
+ expectedCompareResult, result.get().getRc());
}
@Test
toolName);
DiffTools manager = new DiffTools(db);
- BooleanTriState gui = BooleanTriState.UNSET;
+ boolean gui = false;
String defaultToolName = manager.getDefaultToolName(gui);
assertEquals(
"Expected configured difftool to be the default external diff tool",
toolName, defaultToolName);
- gui = BooleanTriState.TRUE;
+ gui = true;
String defaultGuiToolName = manager.getDefaultToolName(gui);
- assertEquals(
- "Expected configured difftool to be the default external diff tool",
- "my_gui_tool", defaultGuiToolName);
+ assertNull("Expected default difftool to not be set",
+ defaultGuiToolName);
config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL,
guiToolName);
defaultGuiToolName = manager.getDefaultToolName(gui);
assertEquals(
"Expected configured difftool to be the default external diff guitool",
- "my_gui_tool", defaultGuiToolName);
+ guiToolName, defaultGuiToolName);
}
@Test
@Test(expected = ToolException.class)
public void testUndefinedTool() throws Exception {
+ String toolName = "undefined";
+ invokeCompare(toolName);
+ fail("Expected exception to be thrown due to not defined external diff tool");
+ }
+
+ private Optional<ExecutionResult> invokeCompare(String toolName)
+ throws ToolException {
DiffTools manager = new DiffTools(db);
- String toolName = "undefined";
BooleanTriState prompt = BooleanTriState.UNSET;
- BooleanTriState gui = BooleanTriState.UNSET;
- BooleanTriState trustExitCode = BooleanTriState.UNSET;
+ boolean gui = false;
+ BooleanTriState trustExitCode = BooleanTriState.TRUE;
+ PromptHandler promptHandler = PromptHandler.acceptPrompt();
+ MissingToolHandler noToolHandler = new MissingToolHandler();
- manager.compare(local, remote, merged, toolName, prompt, gui,
- trustExitCode);
- fail("Expected exception to be thrown due to not defined external diff tool");
+ Optional<ExecutionResult> result = manager.compare(local, remote,
+ Optional.of(toolName), prompt, gui, trustExitCode,
+ promptHandler, noToolHandler);
+ return result;
}
private String getEchoCommand() {
return "(echo \"$LOCAL\" \"$REMOTE\") > "
+ commandResult.getAbsolutePath();
}
+
+ private void assertEchoCommandHasCorrectOutput() throws IOException {
+ List<String> actualLines = Files.readAllLines(commandResult.toPath());
+ String actualContent = String.join(System.lineSeparator(), actualLines);
+ actualLines = Arrays.asList(actualContent.split(" "));
+ List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(),
+ remoteFile.getAbsolutePath());
+ assertEquals("Dummy test tool called with unexpected arguments",
+ expectedLines, actualLines);
+ }
}
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.internal.BooleanTriState;
config.setString(CONFIG_MERGETOOL_SECTION, toolName,
CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(Boolean.TRUE));
- MergeTools manager = new MergeTools(db);
-
- BooleanTriState prompt = BooleanTriState.UNSET;
- BooleanTriState gui = BooleanTriState.UNSET;
-
- manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
+ invokeMerge(toolName);
fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode);
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
+ invokeMerge(toolName);
+
+ fail("Expected exception to be thrown due to external tool exiting with error code: "
+ + errorReturnCode);
+ }
+
+ @Test
+ public void testKdiff3() throws Exception {
+ assumePosixPlatform();
+
+ CommandLineMergeTool autoMergingTool = CommandLineMergeTool.kdiff3;
+ assumeMergeToolIsAvailable(autoMergingTool);
+
+ CommandLineMergeTool tool = autoMergingTool;
+ PreDefinedMergeTool externalTool = new PreDefinedMergeTool(tool.name(),
+ tool.getPath(), tool.getParameters(true),
+ tool.getParameters(false),
+ tool.isExitCodeTrustable() ? BooleanTriState.TRUE
+ : BooleanTriState.FALSE);
+
MergeTools manager = new MergeTools(db);
+ ExecutionResult result = manager.merge(local, remote, merged, null,
+ null, externalTool);
+ assertEquals("Expected merge tool to succeed", 0, result.getRc());
+
+ List<String> actualLines = Files.readAllLines(mergedFile.toPath());
+ String actualMergeResult = String.join(System.lineSeparator(),
+ actualLines);
+ String expectedMergeResult = DEFAULT_CONTENT;
+ assertEquals(
+ "Failed to merge equal local and remote versions with pre-defined tool: "
+ + tool.getPath(),
+ expectedMergeResult, actualMergeResult);
+ }
- BooleanTriState prompt = BooleanTriState.UNSET;
- BooleanTriState gui = BooleanTriState.UNSET;
+ @Test
+ public void testUserDefinedTool() throws Exception {
+ String customToolName = "customTool";
+ String command = getEchoCommand();
- manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
+ FileBasedConfig config = db.getConfig();
+ config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
+ CONFIG_KEY_CMD, command);
- fail("Expected exception to be thrown due to external tool exiting with error code: "
- + errorReturnCode);
+ MergeTools manager = new MergeTools(db);
+ Map<String, ExternalMergeTool> tools = manager.getUserDefinedTools();
+ ExternalMergeTool externalTool = tools.get(customToolName);
+ manager.merge(local, remote, merged, base, null, externalTool);
+
+ assertEchoCommandHasCorrectOutput();
}
@Test
- public void testToolNames() {
+ public void testUserDefinedToolWithPrompt() throws Exception {
+ String customToolName = "customTool";
+ String command = getEchoCommand();
+
+ FileBasedConfig config = db.getConfig();
+ config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
+ CONFIG_KEY_CMD, command);
+
MergeTools manager = new MergeTools(db);
- Set<String> actualToolNames = manager.getToolNames();
- Set<String> expectedToolNames = Collections.emptySet();
- assertEquals("Incorrect set of external merge tool names",
- expectedToolNames, actualToolNames);
+
+ PromptHandler promptHandler = PromptHandler.acceptPrompt();
+ MissingToolHandler noToolHandler = new MissingToolHandler();
+
+ manager.merge(local, remote, merged, base, null,
+ Optional.of(customToolName), BooleanTriState.TRUE, false,
+ promptHandler, noToolHandler);
+
+ assertEchoCommandHasCorrectOutput();
+
+ List<String> actualToolPrompts = promptHandler.toolPrompts;
+ List<String> expectedToolPrompts = Arrays.asList("customTool");
+ assertEquals("Expected a user prompt for custom tool call",
+ expectedToolPrompts, actualToolPrompts);
+
+ assertEquals("Expected to no informing about missing tools",
+ Collections.EMPTY_LIST, noToolHandler.missingTools);
+ }
+
+ @Test
+ public void testUserDefinedToolWithCancelledPrompt() throws Exception {
+ MergeTools manager = new MergeTools(db);
+
+ PromptHandler promptHandler = PromptHandler.cancelPrompt();
+ MissingToolHandler noToolHandler = new MissingToolHandler();
+
+ Optional<ExecutionResult> result = manager.merge(local, remote, merged,
+ base, null, Optional.empty(), BooleanTriState.TRUE, false,
+ promptHandler, noToolHandler);
+ assertFalse("Expected no result if user cancels the operation",
+ result.isPresent());
}
@Test
public void testAllTools() {
+ FileBasedConfig config = db.getConfig();
+ String customToolName = "customTool";
+ config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
+ CONFIG_KEY_CMD, "echo");
+
MergeTools manager = new MergeTools(db);
- Set<String> actualToolNames = manager.getPredefinedTools(true).keySet();
+ Set<String> actualToolNames = manager.getAllToolNames();
Set<String> expectedToolNames = new LinkedHashSet<>();
+ expectedToolNames.add(customToolName);
CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
for (CommandLineMergeTool defaultTool : defaultTools) {
String toolName = defaultTool.name();
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
- BooleanTriState prompt = BooleanTriState.UNSET;
- BooleanTriState gui = BooleanTriState.UNSET;
-
- MergeTools manager = new MergeTools(db);
-
+ Optional<ExecutionResult> result = invokeMerge(toolName);
+ assertTrue("Expected external merge tool result to be available",
+ result.isPresent());
int expectedCompareResult = 0;
- ExecutionResult compareResult = manager.merge(local, remote, merged,
- base, null, toolName, prompt, gui);
assertEquals("Incorrect compare result for external merge tool",
- expectedCompareResult, compareResult.getRc());
+ expectedCompareResult, result.get().getRc());
}
@Test
toolName);
MergeTools manager = new MergeTools(db);
- BooleanTriState gui = BooleanTriState.UNSET;
+ boolean gui = false;
String defaultToolName = manager.getDefaultToolName(gui);
assertEquals(
"Expected configured mergetool to be the default external merge tool",
toolName, defaultToolName);
- gui = BooleanTriState.TRUE;
+ gui = true;
String defaultGuiToolName = manager.getDefaultToolName(gui);
- assertEquals(
- "Expected configured mergetool to be the default external merge tool",
- "my_gui_tool", defaultGuiToolName);
+ assertNull("Expected default mergetool to not be set",
+ defaultGuiToolName);
config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL,
guiToolName);
defaultGuiToolName = manager.getDefaultToolName(gui);
assertEquals(
"Expected configured mergetool to be the default external merge guitool",
- "my_gui_tool", defaultGuiToolName);
+ guiToolName, defaultGuiToolName);
}
@Test
@Test(expected = ToolException.class)
public void testUndefinedTool() throws Exception {
- MergeTools manager = new MergeTools(db);
-
String toolName = "undefined";
+ invokeMerge(toolName);
+ fail("Expected exception to be thrown due to not defined external merge tool");
+ }
+
+ private Optional<ExecutionResult> invokeMerge(String toolName)
+ throws ToolException {
BooleanTriState prompt = BooleanTriState.UNSET;
- BooleanTriState gui = BooleanTriState.UNSET;
+ boolean gui = false;
- manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
- fail("Expected exception to be thrown due to not defined external merge tool");
+ MergeTools manager = new MergeTools(db);
+
+ PromptHandler promptHandler = PromptHandler.acceptPrompt();
+ MissingToolHandler noToolHandler = new MissingToolHandler();
+
+ Optional<ExecutionResult> result = manager.merge(local, remote, merged,
+ base, null, Optional.of(toolName), prompt, gui, promptHandler,
+ noToolHandler);
+ return result;
+ }
+
+ private void assumeMergeToolIsAvailable(
+ CommandLineMergeTool autoMergingTool) {
+ boolean isAvailable = ExternalToolUtils.isToolAvailable(db.getFS(),
+ db.getDirectory(), db.getWorkTree(), autoMergingTool.getPath());
+ assumeTrue("Assuming external tool is available: "
+ + autoMergingTool.name(), isAvailable);
}
private String getEchoCommand() {
- return "(echo \"$LOCAL\" \"$REMOTE\") > "
+ return "(echo $LOCAL $REMOTE $MERGED $BASE) > "
+ commandResult.getAbsolutePath();
}
+
+ private void assertEchoCommandHasCorrectOutput() throws IOException {
+ List<String> actualLines = Files.readAllLines(commandResult.toPath());
+ String actualContent = String.join(System.lineSeparator(), actualLines);
+ actualLines = Arrays.asList(actualContent.split(" "));
+ List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(),
+ remoteFile.getAbsolutePath(), mergedFile.getAbsolutePath(),
+ baseFile.getAbsolutePath());
+ assertEquals("Dummy test tool called with unexpected arguments",
+ expectedLines, actualLines);
+ }
}
import java.io.File;
import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.util.FS;
"This test can run only in Linux tests",
FS.DETECTED instanceof FS_POSIX);
}
+
+ protected static class PromptHandler implements PromptContinueHandler {
+
+ private final boolean promptResult;
+
+ final List<String> toolPrompts = new ArrayList<>();
+
+ private PromptHandler(boolean promptResult) {
+ this.promptResult = promptResult;
+ }
+
+ static PromptHandler acceptPrompt() {
+ return new PromptHandler(true);
+ }
+
+ static PromptHandler cancelPrompt() {
+ return new PromptHandler(false);
+ }
+
+ @Override
+ public boolean prompt(String toolName) {
+ toolPrompts.add(toolName);
+ return promptResult;
+ }
+ }
+
+ protected static class MissingToolHandler implements InformNoToolHandler {
+
+ final List<String> missingTools = new ArrayList<>();
+
+ @Override
+ public void inform(List<String> toolNames) {
+ missingTools.addAll(toolNames);
+ }
+ }
}
org.eclipse.jgit.internal.diffmergetool;version="6.2.0";
x-friends:="org.eclipse.jgit.test,
org.eclipse.jgit.pgm.test,
- org.eclipse.jgit.pgm",
+ org.eclipse.jgit.pgm,
+ org.eclipse.egit.ui",
org.eclipse.jgit.internal.fsck;version="6.2.0";
x-friends:="org.eclipse.jgit.test",
org.eclipse.jgit.internal.revwalk;version="6.2.0";
org.eclipse.jgit.util.time",
org.eclipse.jgit.lib.internal;version="6.2.0";
x-friends:="org.eclipse.jgit.test,
- org.eclipse.jgit.pgm",
+ org.eclipse.jgit.pgm,
+ org.eclipse.egit.ui",
org.eclipse.jgit.logging;version="6.2.0",
org.eclipse.jgit.merge;version="6.2.0";
uses:="org.eclipse.jgit.dircache,
/*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
package org.eclipse.jgit.internal.diffmergetool;
-import java.util.TreeMap;
-import java.util.Collections;
+import java.io.File;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
+import java.util.TreeMap;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult;
-import org.eclipse.jgit.util.StringUtils;
/**
* Manages diff tools.
*/
public class DiffTools {
- private final Repository repo;
+ private final FS fs;
+
+ private final File gitDir;
+
+ private final File workTree;
private final DiffToolConfig config;
* the repository
*/
public DiffTools(Repository repo) {
- this.repo = repo;
- config = repo.getConfig().get(DiffToolConfig.KEY);
+ this(repo, repo.getConfig());
+ }
+
+ /**
+ * Creates the external merge-tools manager for given configuration.
+ *
+ * @param config
+ * the git configuration
+ */
+ public DiffTools(StoredConfig config) {
+ this(null, config);
+ }
+
+ private DiffTools(Repository repo, StoredConfig config) {
+ this.config = config.get(DiffToolConfig.KEY);
+ this.gitDir = repo == null ? null : repo.getDirectory();
+ this.fs = repo == null ? FS.DETECTED : repo.getFS();
+ this.workTree = repo == null ? null : repo.getWorkTree();
predefinedTools = setupPredefinedTools();
- userDefinedTools = setupUserDefinedTools(config, predefinedTools);
+ userDefinedTools = setupUserDefinedTools(predefinedTools);
}
/**
* Compare two versions of a file.
*
* @param localFile
- * the local file element
+ * The local/left version of the file.
* @param remoteFile
- * the remote file element
- * @param mergedFile
- * the merged file element, it's path equals local or remote
- * element path
+ * The remote/right version of the file.
* @param toolName
- * the selected tool name (can be null)
+ * Optionally the name of the tool to use. If not given the
+ * default tool will be used.
* @param prompt
- * the prompt option
+ * Optionally a flag whether to prompt the user before compare.
+ * If not given the default will be used.
* @param gui
- * the GUI option
+ * A flag whether to prefer a gui tool.
+ * @param trustExitCode
+ * Optionally a flag whether to trust the exit code of the tool.
+ * If not given the default will be used.
+ * @param promptHandler
+ * The handler to use when needing to prompt the user if he wants
+ * to continue.
+ * @param noToolHandler
+ * The handler to use when needing to inform the user, that no
+ * tool is configured.
+ * @return the optioanl result of executing the tool if it was executed
+ * @throws ToolException
+ * when the tool fails
+ */
+ public Optional<ExecutionResult> compare(FileElement localFile,
+ FileElement remoteFile, Optional<String> toolName,
+ BooleanTriState prompt, boolean gui, BooleanTriState trustExitCode,
+ PromptContinueHandler promptHandler,
+ InformNoToolHandler noToolHandler) throws ToolException {
+
+ String toolNameToUse;
+
+ if (toolName.isPresent()) {
+ toolNameToUse = toolName.get();
+ } else {
+ toolNameToUse = getDefaultToolName(gui);
+
+ if (toolNameToUse == null || toolNameToUse.isEmpty()) {
+ noToolHandler.inform(new ArrayList<>(predefinedTools.keySet()));
+ toolNameToUse = getFirstAvailableTool();
+ }
+ }
+
+ boolean doPrompt;
+ if (prompt != BooleanTriState.UNSET) {
+ doPrompt = prompt == BooleanTriState.TRUE;
+ } else {
+ doPrompt = isInteractive();
+ }
+
+ if (doPrompt) {
+ if (!promptHandler.prompt(toolNameToUse)) {
+ return Optional.empty();
+ }
+ }
+
+ boolean trust;
+ if (trustExitCode != BooleanTriState.UNSET) {
+ trust = trustExitCode == BooleanTriState.TRUE;
+ } else {
+ trust = config.isTrustExitCode();
+ }
+
+ ExternalDiffTool tool = getTool(toolNameToUse);
+ if (tool == null) {
+ throw new ToolException(
+ "External diff tool is not defined: " + toolNameToUse); //$NON-NLS-1$
+ }
+
+ return Optional.of(
+ compare(localFile, remoteFile, tool, trust));
+ }
+
+ /**
+ * Compare two versions of a file.
+ *
+ * @param localFile
+ * the local file element
+ * @param remoteFile
+ * the remote file element
+ * @param tool
+ * the selected tool
* @param trustExitCode
* the "trust exit code" option
* @return the execution result from tool
* @throws ToolException
*/
public ExecutionResult compare(FileElement localFile,
- FileElement remoteFile, FileElement mergedFile, String toolName,
- BooleanTriState prompt, BooleanTriState gui,
- BooleanTriState trustExitCode) throws ToolException {
+ FileElement remoteFile, ExternalDiffTool tool,
+ boolean trustExitCode) throws ToolException {
try {
// prepare the command (replace the file paths)
- String command = ExternalToolUtils.prepareCommand(
- guessTool(toolName, gui).getCommand(), localFile,
- remoteFile, mergedFile, null);
+ String command = ExternalToolUtils.prepareCommand(tool.getCommand(),
+ localFile, remoteFile, null, null);
// prepare the environment
- Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo,
- localFile, remoteFile, mergedFile, null);
- boolean trust = config.isTrustExitCode();
- if (trustExitCode != BooleanTriState.UNSET) {
- trust = trustExitCode == BooleanTriState.TRUE;
- }
+ Map<String, String> env = ExternalToolUtils.prepareEnvironment(
+ gitDir, localFile, remoteFile, null, null);
// execute the tool
- CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
- return cmdExec.run(command, repo.getWorkTree(), env);
+ CommandExecutor cmdExec = new CommandExecutor(fs, trustExitCode);
+ return cmdExec.run(command, workTree, env);
} catch (IOException | InterruptedException e) {
throw new ToolException(e);
} finally {
localFile.cleanTemporaries();
remoteFile.cleanTemporaries();
- mergedFile.cleanTemporaries();
}
}
/**
- * @return the tool names
+ * Get user defined tool names.
+ *
+ * @return the user defined tool names
+ */
+ public Set<String> getUserDefinedToolNames() {
+ return userDefinedTools.keySet();
+ }
+
+ /**
+ * Get predefined tool names.
+ *
+ * @return the predefined tool names
+ */
+ public Set<String> getPredefinedToolNames() {
+ return predefinedTools.keySet();
+ }
+
+ /**
+ * Get all tool names.
+ *
+ * @return the all tool names (default or available tool name is the first
+ * in the set)
*/
- public Set<String> getToolNames() {
- return config.getToolNames();
+ public Set<String> getAllToolNames() {
+ String defaultName = getDefaultToolName(false);
+ if (defaultName == null) {
+ defaultName = getFirstAvailableTool();
+ }
+ return ExternalToolUtils.createSortedToolSet(defaultName,
+ getUserDefinedToolNames(), getPredefinedToolNames());
}
/**
+ * Get user defined tools map.
+ *
* @return the user defined tools
*/
public Map<String, ExternalDiffTool> getUserDefinedTools() {
}
/**
+ * 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
if (checkAvailability) {
for (ExternalDiffTool tool : predefinedTools.values()) {
PreDefinedDiffTool predefTool = (PreDefinedDiffTool) tool;
- predefTool.setAvailable(ExternalToolUtils.isToolAvailable(repo,
- predefTool.getPath()));
+ predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
+ gitDir, workTree, predefTool.getPath()));
}
}
return Collections.unmodifiableMap(predefinedTools);
}
/**
+ * Get first available tool name.
+ *
* @return the name of first available predefined tool or null
*/
public String getFirstAvailableTool() {
- String name = null;
for (ExternalDiffTool tool : predefinedTools.values()) {
- if (ExternalToolUtils.isToolAvailable(repo, tool.getPath())) {
- name = tool.getName();
- break;
+ if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
+ tool.getPath())) {
+ return tool.getName();
}
}
- return name;
+ return null;
}
/**
+ * Get default (gui-)tool name.
+ *
* @param gui
* use the diff.guitool setting ?
* @return the default tool name
*/
- public String getDefaultToolName(BooleanTriState gui) {
- return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$
+ public String getDefaultToolName(boolean gui) {
+ return gui ? config.getDefaultGuiToolName()
: config.getDefaultToolName();
}
/**
+ * Is interactive diff (prompt enabled) ?
+ *
* @return is interactive (config prompt enabled) ?
*/
public boolean isInteractive() {
return config.isPrompt();
}
- private ExternalDiffTool guessTool(String toolName, BooleanTriState gui)
- throws ToolException {
- if (StringUtils.isEmptyOrNull(toolName)) {
- toolName = getDefaultToolName(gui);
- }
- ExternalDiffTool tool = null;
- if (!StringUtils.isEmptyOrNull(toolName)) {
- tool = getTool(toolName);
- }
- if (tool == null) {
- throw new ToolException("Unknown diff tool '" + toolName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
- }
- return tool;
- }
-
private ExternalDiffTool getTool(final String name) {
ExternalDiffTool tool = userDefinedTools.get(name);
if (tool == null) {
return tools;
}
- private static Map<String, ExternalDiffTool> setupUserDefinedTools(
- DiffToolConfig cfg, Map<String, ExternalDiffTool> predefTools) {
+ private Map<String, ExternalDiffTool> setupUserDefinedTools(
+ Map<String, ExternalDiffTool> predefTools) {
Map<String, ExternalDiffTool> tools = new TreeMap<>();
- Map<String, ExternalDiffTool> userTools = cfg.getTools();
+ Map<String, ExternalDiffTool> userTools = config.getTools();
for (String name : userTools.keySet()) {
ExternalDiffTool userTool = userTools.get(name);
// if difftool.<name>.cmd is defined we have user defined tool
package org.eclipse.jgit.internal.diffmergetool;
import java.util.TreeMap;
+import java.io.File;
import java.io.IOException;
+import java.util.LinkedHashSet;
import java.util.Map;
+import java.util.Set;
+
import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.FS;
/**
* Utilities for diff- and merge-tools.
/**
* Prepare environment needed for execution.
*
- * @param repo
- * the repository
+ * @param gitDir
+ * the .git directory
* @param localFile
* the local file (ours)
* @param remoteFile
* @return the environment map with variables and values (file paths)
* @throws IOException
*/
- public static Map<String, String> prepareEnvironment(Repository repo,
+ public static Map<String, String> prepareEnvironment(File gitDir,
FileElement localFile, FileElement remoteFile,
FileElement mergedFile, FileElement baseFile) throws IOException {
Map<String, String> env = new TreeMap<>();
- env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath());
+ if (gitDir != null) {
+ env.put(Constants.GIT_DIR_KEY, gitDir.getAbsolutePath());
+ }
if (localFile != null) {
localFile.addToEnv(env);
}
}
/**
- * @param repo
- * the repository
+ * @param fs
+ * the file system abstraction
+ * @param gitDir
+ * the .git directory
+ * @param directory
+ * the working directory
* @param path
* the tool path
* @return true if tool available and false otherwise
*/
- public static boolean isToolAvailable(Repository repo, String path) {
+ public static boolean isToolAvailable(FS fs, File gitDir, File directory,
+ String path) {
boolean available = true;
try {
- CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), false);
- available = cmdExec.checkExecutable(path, repo.getWorkTree(),
- prepareEnvironment(repo, null, null, null, null));
+ CommandExecutor cmdExec = new CommandExecutor(fs, false);
+ available = cmdExec.checkExecutable(path, directory,
+ prepareEnvironment(gitDir, null, null, null, null));
} catch (Exception e) {
available = false;
}
return available;
}
+ /**
+ * @param defaultName
+ * the default tool name
+ * @param userDefinedNames
+ * the user defined tool names
+ * @param preDefinedNames
+ * the pre defined tool names
+ * @return the sorted tool names set: first element is default tool name if
+ * valid, then user defined tool names and then pre defined tool
+ * names
+ */
+ public static Set<String> createSortedToolSet(String defaultName,
+ Set<String> userDefinedNames, Set<String> preDefinedNames) {
+ Set<String> names = new LinkedHashSet<>();
+ if (defaultName != null) {
+ // remove defaultName from both sets
+ Set<String> namesPredef = new LinkedHashSet<>();
+ Set<String> namesUser = new LinkedHashSet<>();
+ namesUser.addAll(userDefinedNames);
+ namesUser.remove(defaultName);
+ namesPredef.addAll(preDefinedNames);
+ namesPredef.remove(defaultName);
+ // add defaultName as first in set
+ names.add(defaultName);
+ names.addAll(namesUser);
+ names.addAll(namesPredef);
+ } else {
+ names.addAll(userDefinedNames);
+ names.addAll(preDefinedNames);
+ }
+ return names;
+ }
+
}
private final Type type;
+ private final File workDir;
+
private InputStream stream;
private File tempFile;
* the element type
*/
public FileElement(String path, Type type) {
- this(path, type, null, null);
+ this(path, type, null);
}
/**
* the file path
* @param type
* the element type
- * @param tempFile
- * the temporary file to be used (can be null and will be created
- * then)
+ * @param workDir
+ * the working directory of the path (can be null, then current
+ * working dir is used)
+ */
+ public FileElement(String path, Type type, File workDir) {
+ this(path, type, workDir, null);
+ }
+
+ /**
+ * @param path
+ * the file path
+ * @param type
+ * the element type
+ * @param workDir
+ * the working directory of the path (can be null, then current
+ * working dir is used)
* @param stream
- * the object stream to load instead of file
+ * the object stream to load and write on demand, @see getFile(),
+ * to tempFile once (can be null)
*/
- public FileElement(String path, Type type, File tempFile,
+ public FileElement(String path, Type type, File workDir,
InputStream stream) {
this.path = path;
this.type = type;
- this.tempFile = tempFile;
+ this.workDir = workDir;
this.stream = stream;
}
}
/**
- * Return a temporary file within passed directory and fills it with stream
- * if valid.
- *
- * @param directory
- * the directory where the temporary file is created
- * @param midName
- * name added in the middle of generated temporary file name
- * @return the object stream
- * @throws IOException
- */
- public File getFile(File directory, String midName) throws IOException {
- if ((tempFile != null) && (stream == null)) {
- return tempFile;
- }
- tempFile = getTempFile(path, directory, midName);
- return copyFromStream(tempFile, stream);
- }
-
- /**
- * Return a real file from work tree or a temporary file with content if
- * stream is valid or if path is "/dev/null"
+ * Return
+ * <ul>
+ * <li>a temporary file if already created and stream is not valid</li>
+ * <li>OR a real file from work tree: if no temp file was created (@see
+ * createTempFile()) and if no stream was set</li>
+ * <li>OR an empty temporary file if path is "/dev/null"</li>
+ * <li>OR a temporary file with stream content if stream is valid (not
+ * null); stream is closed and invalidated (set to null) after write to temp
+ * file, so stream is used only once during first call!</li>
+ * </ul>
*
* @return the object stream
* @throws IOException
*/
public File getFile() throws IOException {
+ // if we have already temp file and no stream
+ // then just return this temp file (it was filled from outside)
if ((tempFile != null) && (stream == null)) {
return tempFile;
}
- File file = new File(path);
- // if we have a stream or file is missing ("/dev/null") then create
- // temporary file
+ File file = new File(workDir, path);
+ // if we have a stream or file is missing (path is "/dev/null")
+ // then optionally create temporary file and fill it with stream content
if ((stream != null) || isNullPath()) {
- tempFile = getTempFile(file);
- return copyFromStream(tempFile, stream);
+ if (tempFile == null) {
+ tempFile = getTempFile(file, type.name(), null);
+ }
+ if (stream != null) {
+ copyFromStream(tempFile, stream);
+ }
+ // invalidate the stream, because it is used once
+ stream = null;
+ return tempFile;
}
return file;
}
}
/**
- * Create temporary file in given or system temporary directory
+ * Create temporary file in given or system temporary directory.
*
* @param directory
* the directory for the file (can be null); if null system
*/
public File createTempFile(File directory) throws IOException {
if (tempFile == null) {
- File file = new File(path);
- if (directory != null) {
- tempFile = getTempFile(file, directory, type.name());
- } else {
- tempFile = getTempFile(file);
- }
+ tempFile = getTempFile(new File(path), type.name(), directory);
}
return tempFile;
}
- private static File getTempFile(File file) throws IOException {
- return File.createTempFile(".__", "__" + file.getName()); //$NON-NLS-1$ //$NON-NLS-2$
- }
-
- private static File getTempFile(File file, File directory, String midName)
- throws IOException {
- String[] fileNameAndExtension = splitBaseFileNameAndExtension(file);
- return File.createTempFile(
- fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
- fileNameAndExtension[1], directory);
- }
-
- private static File getTempFile(String path, File directory, String midName)
- throws IOException {
- return getTempFile(new File(path), directory, midName);
- }
-
/**
* Delete and invalidate temporary file if necessary.
*/
public void cleanTemporaries() {
- if (tempFile != null && tempFile.exists())
- tempFile.delete();
- tempFile = null;
- }
-
- private static File copyFromStream(File file, final InputStream stream)
- throws IOException, FileNotFoundException {
- if (stream != null) {
- try (OutputStream outStream = new FileOutputStream(file)) {
- int read = 0;
- byte[] bytes = new byte[8 * 1024];
- while ((read = stream.read(bytes)) != -1) {
- outStream.write(bytes, 0, read);
- }
- } finally {
- // stream can only be consumed once --> close it
- stream.close();
- }
- }
- return file;
- }
-
- private static String[] splitBaseFileNameAndExtension(File file) {
- String[] result = new String[2];
- result[0] = file.getName();
- result[1] = ""; //$NON-NLS-1$
- int idx = result[0].lastIndexOf("."); //$NON-NLS-1$
- // if "." was found (>-1) and last-index is not first char (>0), then
- // split (same behavior like cgit)
- if (idx > 0) {
- result[1] = result[0].substring(idx, result[0].length());
- result[0] = result[0].substring(0, idx);
+ if (tempFile != null && tempFile.exists()) {
+ tempFile.delete();
}
- return result;
+ tempFile = null;
}
/**
- * Replace variable in input
+ * Replace variable in input.
*
* @param input
* the input string
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;
+ }
+
}
--- /dev/null
+/*
+ * 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);
+}
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 {
/*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult;
-import org.eclipse.jgit.util.StringUtils;
/**
* Manages merge tools.
*/
public class MergeTools {
- Repository repo;
+ private final FS fs;
+
+ private final File gitDir;
+
+ private final File workTree;
private final MergeToolConfig config;
private final Map<String, ExternalMergeTool> userDefinedTools;
/**
+ * Creates the external merge-tools manager for given repository.
+ *
* @param repo
* the repository
*/
public MergeTools(Repository repo) {
- this.repo = repo;
- config = repo.getConfig().get(MergeToolConfig.KEY);
+ this(repo, repo.getConfig());
+ }
+
+ /**
+ * Creates the external diff-tools manager for given configuration.
+ *
+ * @param config
+ * the git configuration
+ */
+ public MergeTools(StoredConfig config) {
+ this(null, config);
+ }
+
+ private MergeTools(Repository repo, StoredConfig config) {
+ this.config = config.get(MergeToolConfig.KEY);
+ this.gitDir = repo == null ? null : repo.getDirectory();
+ this.fs = repo == null ? FS.DETECTED : repo.getFS();
+ this.workTree = repo == null ? null : repo.getWorkTree();
predefinedTools = setupPredefinedTools();
- userDefinedTools = setupUserDefinedTools(config, predefinedTools);
+ userDefinedTools = setupUserDefinedTools(predefinedTools);
+ }
+
+ /**
+ * Merge two versions of a file with optional base file.
+ *
+ * @param localFile
+ * The local/left version of the file.
+ * @param remoteFile
+ * The remote/right version of the file.
+ * @param mergedFile
+ * The file for the result.
+ * @param baseFile
+ * The base version of the file. May be null.
+ * @param tempDir
+ * The tmepDir used for the files. May be null.
+ * @param toolName
+ * Optionally the name of the tool to use. If not given the
+ * default tool will be used.
+ * @param prompt
+ * Optionally a flag whether to prompt the user before compare.
+ * If not given the default will be used.
+ * @param gui
+ * A flag whether to prefer a gui tool.
+ * @param promptHandler
+ * The handler to use when needing to prompt the user if he wants
+ * to continue.
+ * @param noToolHandler
+ * The handler to use when needing to inform the user, that no
+ * tool is configured.
+ * @return the optional result of executing the tool if it was executed
+ * @throws ToolException
+ * when the tool fails
+ */
+ public Optional<ExecutionResult> merge(FileElement localFile,
+ FileElement remoteFile, FileElement mergedFile,
+ FileElement baseFile, File tempDir, Optional<String> toolName,
+ BooleanTriState prompt, boolean gui,
+ PromptContinueHandler promptHandler,
+ InformNoToolHandler noToolHandler) throws ToolException {
+
+ String toolNameToUse;
+
+ if (toolName.isPresent()) {
+ toolNameToUse = toolName.get();
+ } else {
+ toolNameToUse = getDefaultToolName(gui);
+
+ if (toolNameToUse == null || toolNameToUse.isEmpty()) {
+ noToolHandler.inform(new ArrayList<>(predefinedTools.keySet()));
+ toolNameToUse = getFirstAvailableTool();
+ }
+ }
+
+ boolean doPrompt;
+ if (prompt != BooleanTriState.UNSET) {
+ doPrompt = prompt == BooleanTriState.TRUE;
+ } else {
+ doPrompt = isInteractive();
+ }
+
+ if (doPrompt) {
+ if (!promptHandler.prompt(toolNameToUse)) {
+ return Optional.empty();
+ }
+ }
+
+ ExternalMergeTool tool = getTool(toolNameToUse);
+ if (tool == null) {
+ throw new ToolException(
+ "External merge tool is not defined: " + toolNameToUse); //$NON-NLS-1$
+ }
+
+ return Optional.of(merge(localFile, remoteFile, mergedFile, baseFile,
+ tempDir, tool));
}
/**
+ * Merge two versions of a file with optional base file.
+ *
* @param localFile
* the local file element
* @param remoteFile
* @param tempDir
* the temporary directory (needed for backup and auto-remove,
* can be null)
- * @param toolName
- * the selected tool name (can be null)
- * @param prompt
- * the prompt option
- * @param gui
- * the GUI option
+ * @param tool
+ * the selected tool
* @return the execution result from tool
* @throws ToolException
*/
public ExecutionResult merge(FileElement localFile, FileElement remoteFile,
FileElement mergedFile, FileElement baseFile, File tempDir,
- String toolName, BooleanTriState prompt, BooleanTriState gui)
- throws ToolException {
- ExternalMergeTool tool = guessTool(toolName, gui);
+ ExternalMergeTool tool) throws ToolException {
FileElement backup = null;
ExecutionResult result = null;
try {
- File workingDir = repo.getWorkTree();
// create additional backup file (copy worktree file)
- backup = createBackupFile(mergedFile.getPath(),
- tempDir != null ? tempDir : workingDir);
+ backup = createBackupFile(mergedFile,
+ tempDir != null ? tempDir : workTree);
// prepare the command (replace the file paths)
- boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
String command = ExternalToolUtils.prepareCommand(
tool.getCommand(baseFile != null), localFile, remoteFile,
mergedFile, baseFile);
// prepare the environment
- Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo,
- localFile, remoteFile, mergedFile, baseFile);
+ Map<String, String> env = ExternalToolUtils.prepareEnvironment(
+ gitDir, localFile, remoteFile, mergedFile, baseFile);
+ boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
// execute the tool
- CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
- result = cmdExec.run(command, workingDir, env);
+ CommandExecutor cmdExec = new CommandExecutor(fs, trust);
+ result = cmdExec.run(command, workTree, env);
// keep backup as .orig file
if (backup != null) {
keepBackupFile(mergedFile.getPath(), backup);
}
}
- private FileElement createBackupFile(String filePath, File parentDir)
+ private FileElement createBackupFile(FileElement from, File toParentDir)
throws IOException {
FileElement backup = null;
- Path path = Paths.get(filePath);
+ Path path = Paths.get(from.getPath());
if (Files.exists(path)) {
- backup = new FileElement(filePath, Type.BACKUP);
- Files.copy(path, backup.createTempFile(parentDir).toPath(),
+ backup = new FileElement(from.getPath(), Type.BACKUP);
+ Files.copy(path, backup.createTempFile(toParentDir).toPath(),
StandardCopyOption.REPLACE_EXISTING);
}
return backup;
}
/**
+ * Create temporary directory.
+ *
* @return the created temporary directory if (mergetol.writeToTemp == true)
* or null if not configured or false.
* @throws IOException
}
/**
- * @return the tool names
+ * Get user defined tool names.
+ *
+ * @return the user defined tool names
*/
- public Set<String> getToolNames() {
- return config.getToolNames();
+ public Set<String> getUserDefinedToolNames() {
+ return userDefinedTools.keySet();
+ }
+
+ /**
+ * @return the predefined tool names
+ */
+ public Set<String> getPredefinedToolNames() {
+ return predefinedTools.keySet();
+ }
+
+ /**
+ * Get all tool names.
+ *
+ * @return the all tool names (default or available tool name is the first
+ * in the set)
+ */
+ public Set<String> getAllToolNames() {
+ String defaultName = getDefaultToolName(false);
+ if (defaultName == null) {
+ defaultName = getFirstAvailableTool();
+ }
+ return ExternalToolUtils.createSortedToolSet(defaultName,
+ getUserDefinedToolNames(), getPredefinedToolNames());
}
/**
* @return the user defined tools
*/
public Map<String, ExternalMergeTool> getUserDefinedTools() {
- return userDefinedTools;
+ return Collections.unmodifiableMap(userDefinedTools);
}
/**
+ * Get predefined tools map.
+ *
* @param checkAvailability
* true: for checking if tools can be executed; ATTENTION: this
* check took some time, do not execute often (store the map for
if (checkAvailability) {
for (ExternalMergeTool tool : predefinedTools.values()) {
PreDefinedMergeTool predefTool = (PreDefinedMergeTool) tool;
- predefTool.setAvailable(ExternalToolUtils.isToolAvailable(repo,
- predefTool.getPath()));
+ predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
+ gitDir, workTree, predefTool.getPath()));
}
}
- return predefinedTools;
+ return Collections.unmodifiableMap(predefinedTools);
}
/**
+ * Get first available tool name.
+ *
* @return the name of first available predefined tool or null
*/
public String getFirstAvailableTool() {
String name = null;
for (ExternalMergeTool tool : predefinedTools.values()) {
- if (ExternalToolUtils.isToolAvailable(repo, tool.getPath())) {
+ if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
+ tool.getPath())) {
name = tool.getName();
break;
}
}
/**
- * @param gui
- * use the diff.guitool setting ?
- * @return the default tool name
- */
- public String getDefaultToolName(BooleanTriState gui) {
- return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$
- : config.getDefaultToolName();
- }
-
- /**
+ * Is interactive merge (prompt enabled) ?
+ *
* @return is interactive (config prompt enabled) ?
*/
public boolean isInteractive() {
return config.isPrompt();
}
- private ExternalMergeTool guessTool(String toolName, BooleanTriState gui)
- throws ToolException {
- if (StringUtils.isEmptyOrNull(toolName)) {
- toolName = getDefaultToolName(gui);
- }
- ExternalMergeTool tool = null;
- if (!StringUtils.isEmptyOrNull(toolName)) {
- tool = getTool(toolName);
- }
- if (tool == null) {
- throw new ToolException("Unknown merge tool '" + toolName + "'"); //$NON-NLS-1$ //$NON-NLS-2$
- }
- return tool;
+ /**
+ * Get the default (gui-)tool name.
+ *
+ * @param gui
+ * use the diff.guitool setting ?
+ * @return the default tool name
+ */
+ public String getDefaultToolName(boolean gui) {
+ return gui ? config.getDefaultGuiToolName()
+ : config.getDefaultToolName();
}
private ExternalMergeTool getTool(final String name) {
}
private Map<String, ExternalMergeTool> setupUserDefinedTools(
- MergeToolConfig cfg, Map<String, ExternalMergeTool> predefTools) {
+ Map<String, ExternalMergeTool> predefTools) {
Map<String, ExternalMergeTool> tools = new TreeMap<>();
- Map<String, ExternalMergeTool> userTools = cfg.getTools();
+ Map<String, ExternalMergeTool> userTools = config.getTools();
for (String name : userTools.keySet()) {
ExternalMergeTool userTool = userTools.get(name);
// if mergetool.<name>.cmd is defined we have user defined tool
--- /dev/null
+/*
+ * 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);
+}