diff options
author | Andrey Loskutov <loskutov@gmx.de> | 2022-06-03 15:49:45 +0200 |
---|---|---|
committer | Andrey Loskutov <loskutov@gmx.de> | 2022-06-03 15:51:54 +0200 |
commit | 2ef2a3562e9aa5c826ce063a2f1d3d8ffef1f0ce (patch) | |
tree | b0f2988a40474499808220cd7d5fe677d87b6600 /org.eclipse.jgit | |
parent | 7b1ade51d24f6efacfddd91fcfed7137c27642d3 (diff) | |
parent | c32694e5ae98e5c1373afcb535a76d37e5d82a29 (diff) | |
download | jgit-2ef2a3562e9aa5c826ce063a2f1d3d8ffef1f0ce.tar.gz jgit-2ef2a3562e9aa5c826ce063a2f1d3d8ffef1f0ce.zip |
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
Diffstat (limited to 'org.eclipse.jgit')
17 files changed, 907 insertions, 217 deletions
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 242b3a4c92..fda154f93e 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -73,7 +73,8 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.0", org.eclipse.jgit.internal.diffmergetool;version="6.2.0"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.pgm.test, - org.eclipse.jgit.pgm", + org.eclipse.jgit.pgm, + org.eclipse.egit.ui", org.eclipse.jgit.internal.fsck;version="6.2.0"; x-friends:="org.eclipse.jgit.test", org.eclipse.jgit.internal.revwalk;version="6.2.0"; @@ -133,7 +134,8 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.0", org.eclipse.jgit.util.time", org.eclipse.jgit.lib.internal;version="6.2.0"; x-friends:="org.eclipse.jgit.test, - org.eclipse.jgit.pgm", + org.eclipse.jgit.pgm, + org.eclipse.egit.ui", org.eclipse.jgit.logging;version="6.2.0", org.eclipse.jgit.merge;version="6.2.0"; uses:="org.eclipse.jgit.dircache, diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 9f264fca34..f0bb6c6c99 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -237,6 +237,9 @@ deleteTagUnexpectedResult=Delete tag returned unexpected result {0} deletingNotSupported=Deleting {0} not supported. destinationIsNotAWildcard=Destination is not a wildcard. 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 dirCacheFileIsNotLocked=DirCache {0} 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} 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}" +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 minutesAgo={0} minutes ago mismatchOffset=mismatch offset for object {0} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index b81e605c13..17e359de49 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -265,6 +265,9 @@ public class JGitText extends TranslationBundle { /***/ public String deletingNotSupported; /***/ public String destinationIsNotAWildcard; /***/ public String detachedHeadDetected; + /***/ public String diffToolNotGivenError; + /***/ public String diffToolNotSpecifiedInGitAttributesError; + /***/ public String diffToolNullError; /***/ public String dirCacheDoesNotHaveABackingFile; /***/ public String dirCacheFileIsNotLocked; /***/ public String dirCacheIsNotLocked; @@ -485,6 +488,8 @@ public class JGitText extends TranslationBundle { /***/ public String mergeUsingStrategyResultedInDescription; /***/ public String mergeRecursiveConflictsWhenMergingCommonAncestors; /***/ public String mergeRecursiveTooManyMergeBasesFor; + /***/ public String mergeToolNotGivenError; + /***/ public String mergeToolNullError; /***/ public String messageAndTaggerNotAllowedInUnannotatedTags; /***/ public String minutesAgo; /***/ public String mismatchOffset; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java index ad79fe8fc6..668adeab65 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java @@ -14,13 +14,19 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; 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.Map; + +import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS.ExecutionResult; import org.eclipse.jgit.util.FS_POSIX; import org.eclipse.jgit.util.FS_Win32; import org.eclipse.jgit.util.FS_Win32_Cygwin; +import org.eclipse.jgit.util.StringUtils; /** * Runs a command with help of FS. @@ -91,6 +97,49 @@ public class CommandExecutor { } } + /** + * @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() { deleteCommandFile(); } @@ -127,7 +176,7 @@ public class CommandExecutor { private void checkUseMsys2(String command) { useMsys2 = false; 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$ useMsys2 = command.contains(".sh"); //$NON-NLS-1$ } else { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java index 509515c37a..00dec32718 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java @@ -111,7 +111,7 @@ public enum CommandLineDiffTool { * See: <a href= * "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= * "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= * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> */ - vimdiff("viewdiff", gvimdiff), + vimdiff("vimdiff", gvimdiff), /** * See: <a href= * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index 1dcc523bf8..7cedd82995 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2018-2022, Andre Bossert <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 @@ -10,14 +11,22 @@ 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.Collections; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; 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.StoredConfig; 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.StringUtils; @@ -26,10 +35,16 @@ import org.eclipse.jgit.util.StringUtils; */ 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 Repository repo; + private final Map<String, ExternalDiffTool> predefinedTools; private final Map<String, ExternalDiffTool> userDefinedTools; @@ -41,69 +56,220 @@ public class DiffTools { * the repository */ 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; - 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(); - 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 == 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)); + } + + /** + * 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 { + if (tool == null) { + throw new ToolException(JGitText + .get().diffToolNotSpecifiedInGitAttributesError); + } // 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> 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> getToolNames() { - return config.getToolNames(); + 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 */ public Map<String, ExternalDiffTool> getUserDefinedTools() { @@ -111,48 +277,70 @@ public class DiffTools { } /** - * @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 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() { - return Collections.unmodifiableMap(new TreeMap<>()); + public String getFirstAvailableTool() { + 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 * 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(); + public String getDefaultToolName(boolean gui) { + String guiToolName; + if (gui) { + guiToolName = config.getDefaultGuiToolName(); + if (guiToolName != null) { + return guiToolName; + } + } + return 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 = getTool(toolName); - if (tool == null) { - throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$ - } - return tool; - } - private ExternalDiffTool getTool(final String name) { ExternalDiffTool tool = userDefinedTools.get(name); if (tool == null) { @@ -169,10 +357,10 @@ public class DiffTools { return tools; } - private static Map<String, ExternalDiffTool> setupUserDefinedTools( - DiffToolConfig cfg, Map<String, ExternalDiffTool> predefTools) { + private Map<String, ExternalDiffTool> setupUserDefinedTools( + Map<String, ExternalDiffTool> predefTools) { Map<String, ExternalDiffTool> tools = new TreeMap<>(); - Map<String, ExternalDiffTool> userTools = cfg.getTools(); + Map<String, ExternalDiffTool> userTools = config.getTools(); for (String name : userTools.keySet()) { ExternalDiffTool userTool = userTools.get(name); // if difftool.<name>.cmd is defined we have user defined tool diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java index f2d7e828cb..e01b892a53 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java @@ -30,4 +30,10 @@ public interface ExternalDiffTool { */ String getCommand(); + /** + * @return availability of the tool: true if tool can be executed and false + * if not + */ + boolean isAvailable(); + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java index 3efb90c490..b2dd846d70 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java @@ -10,10 +10,22 @@ 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.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.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. @@ -21,6 +33,16 @@ import org.eclipse.jgit.lib.Repository; 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. * * @param command @@ -39,9 +61,15 @@ public class ExternalToolUtils { public static String prepareCommand(String command, FileElement localFile, FileElement remoteFile, FileElement mergedFile, FileElement baseFile) throws IOException { - command = localFile.replaceVariable(command); - command = remoteFile.replaceVariable(command); - command = mergedFile.replaceVariable(command); + if (localFile != null) { + command = localFile.replaceVariable(command); + } + if (remoteFile != null) { + command = remoteFile.replaceVariable(command); + } + if (mergedFile != null) { + command = mergedFile.replaceVariable(command); + } if (baseFile != null) { command = baseFile.replaceVariable(command); } @@ -51,8 +79,8 @@ public class ExternalToolUtils { /** * Prepare environment needed for execution. * - * @param repo - * the repository + * @param gitDir + * the .git directory * @param localFile * the local file (ours) * @param remoteFile @@ -64,18 +92,151 @@ public class ExternalToolUtils { * @return the environment map with variables and values (file paths) * @throws IOException */ - public static Map<String, String> prepareEnvironment(Repository repo, + public static Map<String, String> prepareEnvironment(File gitDir, FileElement localFile, FileElement remoteFile, FileElement mergedFile, FileElement baseFile) throws IOException { Map<String, String> env = new TreeMap<>(); - env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath()); - localFile.addToEnv(env); - remoteFile.addToEnv(env); - mergedFile.addToEnv(env); + if (gitDir != null) { + env.put(Constants.GIT_DIR_KEY, gitDir.getAbsolutePath()); + } + if (localFile != null) { + localFile.addToEnv(env); + } + if (remoteFile != null) { + remoteFile.addToEnv(env); + } + if (mergedFile != null) { + mergedFile.addToEnv(env); + } if (baseFile != null) { baseFile.addToEnv(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); + } + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java index 5902c1e1b8..ba8ca54c58 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java @@ -57,6 +57,8 @@ public class FileElement { private final Type type; + private final File workDir; + private InputStream stream; private File tempFile; @@ -70,7 +72,7 @@ public class FileElement { * the element type */ public FileElement(String path, Type type) { - this(path, type, null, null); + this(path, type, null); } /** @@ -80,17 +82,31 @@ public class FileElement { * the file path * @param type * the element type - * @param tempFile - * the temporary file to be used (can be null and will be created - * then) + * @param workDir + * the working directory of the path (can be null, then current + * working dir is used) + */ + public FileElement(String path, Type type, File workDir) { + this(path, type, workDir, null); + } + + /** + * @param path + * the file path + * @param type + * the element type + * @param workDir + * the working directory of the path (can be null, then current + * working dir is used) * @param stream - * the object stream to load instead of file + * the object stream to load and write on demand, @see getFile(), + * to tempFile once (can be null) */ - public FileElement(String path, Type type, File tempFile, + public FileElement(String path, Type type, File workDir, InputStream stream) { this.path = path; this.type = type; - this.tempFile = tempFile; + this.workDir = workDir; this.stream = stream; } @@ -109,41 +125,39 @@ public class FileElement { } /** - * Return a temporary file within passed directory and fills it with stream - * if valid. - * - * @param directory - * the directory where the temporary file is created - * @param midName - * name added in the middle of generated temporary file name - * @return the object stream - * @throws IOException - */ - public File getFile(File directory, String midName) throws IOException { - if ((tempFile != null) && (stream == null)) { - return tempFile; - } - tempFile = getTempFile(path, directory, midName); - return copyFromStream(tempFile, stream); - } - - /** - * Return a real file from work tree or a temporary file with content if - * stream is valid or if path is "/dev/null" + * Return + * <ul> + * <li>a temporary file if already created and stream is not valid</li> + * <li>OR a real file from work tree: if no temp file was created (@see + * createTempFile()) and if no stream was set</li> + * <li>OR an empty temporary file if path is "/dev/null"</li> + * <li>OR a temporary file with stream content if stream is valid (not + * null); stream is closed and invalidated (set to null) after write to temp + * file, so stream is used only once during first call!</li> + * </ul> * * @return the object stream * @throws IOException */ public File getFile() throws IOException { + // if we have already temp file and no stream + // then just return this temp file (it was filled from outside) if ((tempFile != null) && (stream == null)) { return tempFile; } - File file = new File(path); - // if we have a stream or file is missing ("/dev/null") then create - // temporary file + File file = new File(workDir, path); + // if we have a stream or file is missing (path is "/dev/null") + // then optionally create temporary file and fill it with stream content if ((stream != null) || isNullPath()) { - tempFile = getTempFile(file); - return copyFromStream(tempFile, stream); + if (tempFile == null) { + tempFile = getTempFile(file, type.name(), null); + } + if (stream != null) { + copyFromStream(tempFile, stream); + } + // invalidate the stream, because it is used once + stream = null; + return tempFile; } return file; } @@ -158,7 +172,7 @@ public class FileElement { } /** - * Create temporary file in given or system temporary directory + * Create temporary file in given or system temporary directory. * * @param directory * the directory for the file (can be null); if null system @@ -168,75 +182,23 @@ public class FileElement { */ public File createTempFile(File directory) throws IOException { if (tempFile == null) { - File file = new File(path); - if (directory != null) { - tempFile = getTempFile(file, directory, type.name()); - } else { - tempFile = getTempFile(file); - } + tempFile = getTempFile(new File(path), type.name(), directory); } return tempFile; } - private static File getTempFile(File file) throws IOException { - return File.createTempFile(".__", "__" + file.getName()); //$NON-NLS-1$ //$NON-NLS-2$ - } - - private static File getTempFile(File file, File directory, String midName) - throws IOException { - String[] fileNameAndExtension = splitBaseFileNameAndExtension(file); - return File.createTempFile( - fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$ - fileNameAndExtension[1], directory); - } - - private static File getTempFile(String path, File directory, String midName) - throws IOException { - return getTempFile(new File(path), directory, midName); - } - /** * Delete and invalidate temporary file if necessary. */ public void cleanTemporaries() { - if (tempFile != null && tempFile.exists()) - tempFile.delete(); - tempFile = null; - } - - private static File copyFromStream(File file, final InputStream stream) - throws IOException, FileNotFoundException { - if (stream != null) { - try (OutputStream outStream = new FileOutputStream(file)) { - int read = 0; - byte[] bytes = new byte[8 * 1024]; - while ((read = stream.read(bytes)) != -1) { - outStream.write(bytes, 0, read); - } - } finally { - // stream can only be consumed once --> close it - stream.close(); - } - } - return file; - } - - private static String[] splitBaseFileNameAndExtension(File file) { - String[] result = new String[2]; - result[0] = file.getName(); - result[1] = ""; //$NON-NLS-1$ - int idx = result[0].lastIndexOf("."); //$NON-NLS-1$ - // if "." was found (>-1) and last-index is not first char (>0), then - // split (same behavior like cgit) - if (idx > 0) { - result[1] = result[0].substring(idx, result[0].length()); - result[0] = result[0].substring(0, idx); + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); } - return result; + tempFile = null; } /** - * Replace variable in input + * Replace variable in input. * * @param input * the input string @@ -258,4 +220,43 @@ public class FileElement { env.put(type.name(), getFile().getPath()); } + private static File getTempFile(final File file, final String midName, + final File workingDir) throws IOException { + String[] fileNameAndExtension = splitBaseFileNameAndExtension(file); + // TODO: avoid long random file name (number generated by + // createTempFile) + return File.createTempFile( + fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$ + fileNameAndExtension[1], workingDir); + } + + private static void copyFromStream(final File file, + final InputStream stream) + throws IOException, FileNotFoundException { + try (OutputStream outStream = new FileOutputStream(file)) { + int read = 0; + byte[] bytes = new byte[8 * 1024]; + while ((read = stream.read(bytes)) != -1) { + outStream.write(bytes, 0, read); + } + } finally { + // stream can only be consumed once --> close it and invalidate + stream.close(); + } + } + + private static String[] splitBaseFileNameAndExtension(File file) { + String[] result = new String[2]; + result[0] = file.getName(); + result[1] = ""; //$NON-NLS-1$ + int idx = result[0].lastIndexOf("."); //$NON-NLS-1$ + // if "." was found (>-1) and last-index is not first char (>0), then + // split (same behavior like cgit) + if (idx > 0) { + result[1] = result[0].substring(idx, result[0].length()); + result[0] = result[0].substring(0, idx); + } + return result; + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java new file mode 100644 index 0000000000..36b290d37d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java @@ -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); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java index 9be20b75ad..9625d5f101 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java @@ -31,7 +31,7 @@ import org.eclipse.jgit.lib.Config.SectionParser; import org.eclipse.jgit.lib.internal.BooleanTriState; /** - * Keeps track of difftool related configuration options. + * Keeps track of merge tool related configuration options. */ public class MergeToolConfig { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java index 9a2a8304eb..b903201264 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com> * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -15,13 +16,23 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import org.eclipse.jgit.internal.JGitText; 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.treewalk.TreeWalk; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.FS.ExecutionResult; /** @@ -29,26 +40,135 @@ import org.eclipse.jgit.util.FS.ExecutionResult; */ public class MergeTools { - Repository repo; + private final FS fs; + + private final File gitDir; + + private final File workTree; private final MergeToolConfig config; + private final Repository repo; + private final Map<String, ExternalMergeTool> predefinedTools; 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.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.repo = repo; - config = repo.getConfig().get(MergeToolConfig.KEY); + 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 == 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 * the local file element * @param remoteFile @@ -60,38 +180,31 @@ public class MergeTools { * @param tempDir * the temporary directory (needed for backup and auto-remove, * can be null) - * @param toolName - * the selected tool name (can be null) - * @param prompt - * the prompt option - * @param gui - * the GUI option + * @param tool + * the selected tool * @return the execution result from tool * @throws ToolException */ public ExecutionResult merge(FileElement localFile, FileElement remoteFile, FileElement mergedFile, FileElement baseFile, File tempDir, - String toolName, BooleanTriState prompt, BooleanTriState gui) - throws ToolException { - ExternalMergeTool tool = guessTool(toolName, gui); + ExternalMergeTool tool) throws ToolException { FileElement backup = null; ExecutionResult result = null; try { - File workingDir = repo.getWorkTree(); // create additional backup file (copy worktree file) - backup = createBackupFile(mergedFile.getPath(), - tempDir != null ? tempDir : workingDir); + backup = createBackupFile(mergedFile, + tempDir != null ? tempDir : workTree); // prepare the command (replace the file paths) - boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; String command = ExternalToolUtils.prepareCommand( tool.getCommand(baseFile != null), localFile, remoteFile, mergedFile, baseFile); // prepare the environment - Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo, - localFile, remoteFile, mergedFile, baseFile); + Map<String, String> env = ExternalToolUtils.prepareEnvironment( + gitDir, localFile, remoteFile, mergedFile, baseFile); + boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; // execute the tool - CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); - result = cmdExec.run(command, workingDir, env); + CommandExecutor cmdExec = new CommandExecutor(fs, trust); + result = cmdExec.run(command, workTree, env); // keep backup as .orig file if (backup != null) { keepBackupFile(mergedFile.getPath(), backup); @@ -123,19 +236,21 @@ public class MergeTools { } } - private FileElement createBackupFile(String filePath, File parentDir) + private FileElement createBackupFile(FileElement from, File toParentDir) throws IOException { FileElement backup = null; - Path path = Paths.get(filePath); + Path path = Paths.get(from.getPath()); if (Files.exists(path)) { - backup = new FileElement(filePath, Type.BACKUP); - Files.copy(path, backup.createTempFile(parentDir).toPath(), + backup = new FileElement(from.getPath(), Type.BACKUP); + Files.copy(path, backup.createTempFile(toParentDir).toPath(), StandardCopyOption.REPLACE_EXISTING); } return backup; } /** + * Create temporary directory. + * * @return the created temporary directory if (mergetol.writeToTemp == true) * or null if not configured or false. * @throws IOException @@ -147,60 +262,138 @@ public class MergeTools { } /** - * @return the tool names + * Get user defined tool names. + * + * @return the user defined tool names */ - public Set<String> getToolNames() { - return config.getToolNames(); + public Set<String> getUserDefinedToolNames() { + return userDefinedTools.keySet(); } /** - * @return the user defined tools + * @return the predefined tool names */ - public Map<String, ExternalMergeTool> getUserDefinedTools() { - return userDefinedTools; + public Set<String> getPredefinedToolNames() { + return predefinedTools.keySet(); } /** - * @return the available predefined tools + * Get all tool names. + * + * @return the all tool names (default or available tool name is the first + * in the set) */ - public Map<String, ExternalMergeTool> getAvailableTools() { - return predefinedTools; + public Set<String> getAllToolNames() { + String defaultName = getDefaultToolName(false); + if (defaultName == null) { + defaultName = getFirstAvailableTool(); + } + return ExternalToolUtils.createSortedToolSet(defaultName, + getUserDefinedToolNames(), getPredefinedToolNames()); } /** - * @return the NOT available predefined tools + * 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 Map<String, ExternalMergeTool> getNotAvailableTools() { - return new TreeMap<>(); + public Optional<String> getExternalToolFromAttributes(final String path) + throws ToolException { + return ExternalToolUtils.getExternalToolFromAttributes(repo, path, + ExternalToolUtils.KEY_MERGE_TOOL); } /** - * @param gui - * use the diff.guitool setting ? - * @return the default tool name + * Checks the availability of the predefined tools in the system. + * + * @return set of predefined available tools */ - public String getDefaultToolName(BooleanTriState gui) { - return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ - : config.getDefaultToolName(); + 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 + */ + public Map<String, ExternalMergeTool> getUserDefinedTools() { + 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 + * 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> getPredefinedTools( + 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); + } + + /** + * 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(fs, gitDir, workTree, + tool.getPath())) { + name = tool.getName(); + break; + } + } + return name; + } + + /** + * 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 ((toolName == null) || toolName.isEmpty()) { - toolName = getDefaultToolName(gui); - } - ExternalMergeTool tool = getTool(toolName); - if (tool == null) { - throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$ - } - return tool; + /** + * 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) { @@ -231,9 +424,9 @@ public class MergeTools { } private Map<String, ExternalMergeTool> setupUserDefinedTools( - MergeToolConfig cfg, Map<String, ExternalMergeTool> predefTools) { + Map<String, ExternalMergeTool> predefTools) { Map<String, ExternalMergeTool> tools = new TreeMap<>(); - Map<String, ExternalMergeTool> userTools = cfg.getTools(); + Map<String, ExternalMergeTool> userTools = config.getTools(); for (String name : userTools.keySet()) { ExternalMergeTool userTool = userTools.get(name); // if mergetool.<name>.cmd is defined we have user defined tool diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java index 092cb605be..e1169a2d60 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java @@ -56,7 +56,7 @@ public class PreDefinedDiffTool extends UserDefinedDiffTool { */ @Override public String getCommand() { - return getPath() + " " + super.getCommand(); //$NON-NLS-1$ + return ExternalToolUtils.quotePath(getPath()) + " " + super.getCommand(); //$NON-NLS-1$ } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java index 2c64c16667..7b28d32820 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java @@ -84,7 +84,7 @@ public class PreDefinedMergeTool extends UserDefinedMergeTool { */ @Override public String getCommand(boolean withBase) { - return getPath() + " " //$NON-NLS-1$ + return ExternalToolUtils.quotePath(getPath()) + " " //$NON-NLS-1$ + (withBase ? super.getCommand() : parametersWithoutBase); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java new file mode 100644 index 0000000000..6ad33df2a0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java @@ -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); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java index 27f7d12e66..7cc5bb50d9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java @@ -110,6 +110,9 @@ public class ToolException extends Exception { * @return the result Stderr */ public String getResultStderr() { + if (result == null) { + return ""; //$NON-NLS-1$ + } try { return new String(result.getStderr().toByteArray()); } catch (Exception e) { @@ -122,6 +125,9 @@ public class ToolException extends Exception { * @return the result Stdout */ public String getResultStdout() { + if (result == null) { + return ""; //$NON-NLS-1$ + } try { return new String(result.getStdout().toByteArray()); } catch (Exception e) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java index 012296eb35..eb72d01cdb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java @@ -15,6 +15,8 @@ package org.eclipse.jgit.internal.diffmergetool; */ public class UserDefinedDiffTool implements ExternalDiffTool { + private boolean available; + /** * the diff tool name */ @@ -99,6 +101,23 @@ public class UserDefinedDiffTool implements ExternalDiffTool { } /** + * @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 * {@code difftool.<tool>.path}. * |