diff options
Diffstat (limited to 'org.eclipse.jgit')
8 files changed, 550 insertions, 224 deletions
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index e72f00f0bf..a25685073d 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -73,7 +73,8 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.0", org.eclipse.jgit.internal.diffmergetool;version="6.2.0"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.pgm.test, - org.eclipse.jgit.pgm", + org.eclipse.jgit.pgm, + org.eclipse.egit.ui", org.eclipse.jgit.internal.fsck;version="6.2.0"; x-friends:="org.eclipse.jgit.test", org.eclipse.jgit.internal.revwalk;version="6.2.0"; @@ -133,7 +134,8 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.0", org.eclipse.jgit.util.time", org.eclipse.jgit.lib.internal;version="6.2.0"; x-friends:="org.eclipse.jgit.test, - org.eclipse.jgit.pgm", + org.eclipse.jgit.pgm, + org.eclipse.egit.ui", org.eclipse.jgit.logging;version="6.2.0", org.eclipse.jgit.merge;version="6.2.0"; uses:="org.eclipse.jgit.dircache, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index 6c1a780a35..5fe263a4ef 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,23 +11,31 @@ package org.eclipse.jgit.internal.diffmergetool; -import java.util.TreeMap; -import java.util.Collections; +import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS.ExecutionResult; -import org.eclipse.jgit.util.StringUtils; /** * Manages diff tools. */ public class DiffTools { - private final Repository repo; + private final FS fs; + + private final File gitDir; + + private final File workTree; private final DiffToolConfig config; @@ -41,69 +50,176 @@ public class DiffTools { * the repository */ public DiffTools(Repository repo) { - this.repo = repo; - config = repo.getConfig().get(DiffToolConfig.KEY); + this(repo, repo.getConfig()); + } + + /** + * Creates the external merge-tools manager for given configuration. + * + * @param config + * the git configuration + */ + public DiffTools(StoredConfig config) { + this(null, config); + } + + private DiffTools(Repository repo, StoredConfig config) { + this.config = config.get(DiffToolConfig.KEY); + this.gitDir = repo == null ? null : repo.getDirectory(); + this.fs = repo == null ? FS.DETECTED : repo.getFS(); + this.workTree = repo == null ? null : repo.getWorkTree(); predefinedTools = setupPredefinedTools(); - userDefinedTools = setupUserDefinedTools(config, predefinedTools); + userDefinedTools = setupUserDefinedTools(predefinedTools); } /** * Compare two versions of a file. * * @param localFile - * the local file element + * The local/left version of the file. * @param remoteFile - * the remote file element - * @param mergedFile - * the merged file element, it's path equals local or remote - * element path + * The remote/right version of the file. * @param toolName - * the selected tool name (can be null) + * Optionally the name of the tool to use. If not given the + * default tool will be used. * @param prompt - * the prompt option + * Optionally a flag whether to prompt the user before compare. + * If not given the default will be used. * @param gui - * the GUI option + * A flag whether to prefer a gui tool. + * @param trustExitCode + * Optionally a flag whether to trust the exit code of the tool. + * If not given the default will be used. + * @param promptHandler + * The handler to use when needing to prompt the user if he wants + * to continue. + * @param noToolHandler + * The handler to use when needing to inform the user, that no + * tool is configured. + * @return the optioanl result of executing the tool if it was executed + * @throws ToolException + * when the tool fails + */ + public Optional<ExecutionResult> compare(FileElement localFile, + FileElement remoteFile, Optional<String> toolName, + BooleanTriState prompt, boolean gui, BooleanTriState trustExitCode, + PromptContinueHandler promptHandler, + InformNoToolHandler noToolHandler) throws ToolException { + + String toolNameToUse; + + if (toolName.isPresent()) { + toolNameToUse = toolName.get(); + } else { + toolNameToUse = getDefaultToolName(gui); + + if (toolNameToUse == null || toolNameToUse.isEmpty()) { + noToolHandler.inform(new ArrayList<>(predefinedTools.keySet())); + toolNameToUse = getFirstAvailableTool(); + } + } + + boolean doPrompt; + if (prompt != BooleanTriState.UNSET) { + doPrompt = prompt == BooleanTriState.TRUE; + } else { + doPrompt = isInteractive(); + } + + if (doPrompt) { + if (!promptHandler.prompt(toolNameToUse)) { + return Optional.empty(); + } + } + + boolean trust; + if (trustExitCode != BooleanTriState.UNSET) { + trust = trustExitCode == BooleanTriState.TRUE; + } else { + trust = config.isTrustExitCode(); + } + + ExternalDiffTool tool = getTool(toolNameToUse); + if (tool == null) { + throw new ToolException( + "External diff tool is not defined: " + toolNameToUse); //$NON-NLS-1$ + } + + return Optional.of( + compare(localFile, remoteFile, tool, trust)); + } + + /** + * Compare two versions of a file. + * + * @param localFile + * the local file element + * @param remoteFile + * the remote file element + * @param tool + * the selected tool * @param trustExitCode * the "trust exit code" option * @return the execution result from tool * @throws ToolException */ public ExecutionResult compare(FileElement localFile, - FileElement remoteFile, FileElement mergedFile, String toolName, - BooleanTriState prompt, BooleanTriState gui, - BooleanTriState trustExitCode) throws ToolException { + FileElement remoteFile, ExternalDiffTool tool, + boolean trustExitCode) throws ToolException { try { // prepare the command (replace the file paths) - String command = ExternalToolUtils.prepareCommand( - guessTool(toolName, gui).getCommand(), localFile, - remoteFile, mergedFile, null); + String command = ExternalToolUtils.prepareCommand(tool.getCommand(), + localFile, remoteFile, null, null); // prepare the environment - Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo, - localFile, remoteFile, mergedFile, null); - boolean trust = config.isTrustExitCode(); - if (trustExitCode != BooleanTriState.UNSET) { - trust = trustExitCode == BooleanTriState.TRUE; - } + Map<String, String> env = ExternalToolUtils.prepareEnvironment( + gitDir, localFile, remoteFile, null, null); // execute the tool - CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); - return cmdExec.run(command, repo.getWorkTree(), env); + CommandExecutor cmdExec = new CommandExecutor(fs, trustExitCode); + return cmdExec.run(command, workTree, env); } catch (IOException | InterruptedException e) { throw new ToolException(e); } finally { localFile.cleanTemporaries(); remoteFile.cleanTemporaries(); - mergedFile.cleanTemporaries(); } } /** - * @return the tool names + * Get user defined tool names. + * + * @return the user defined tool names + */ + public Set<String> getUserDefinedToolNames() { + return userDefinedTools.keySet(); + } + + /** + * Get predefined tool names. + * + * @return the predefined tool names + */ + public Set<String> getPredefinedToolNames() { + return predefinedTools.keySet(); + } + + /** + * Get all tool names. + * + * @return the all tool names (default or available tool name is the first + * in the set) */ - public Set<String> getToolNames() { - return config.getToolNames(); + public Set<String> getAllToolNames() { + String defaultName = getDefaultToolName(false); + if (defaultName == null) { + defaultName = getFirstAvailableTool(); + } + return ExternalToolUtils.createSortedToolSet(defaultName, + getUserDefinedToolNames(), getPredefinedToolNames()); } /** + * Get user defined tools map. + * * @return the user defined tools */ public Map<String, ExternalDiffTool> getUserDefinedTools() { @@ -111,6 +227,8 @@ public class DiffTools { } /** + * Get predefined tools map. + * * @param checkAvailability * true: for checking if tools can be executed; ATTENTION: this * check took some time, do not execute often (store the map for @@ -124,59 +242,49 @@ public class DiffTools { if (checkAvailability) { for (ExternalDiffTool tool : predefinedTools.values()) { PreDefinedDiffTool predefTool = (PreDefinedDiffTool) tool; - predefTool.setAvailable(ExternalToolUtils.isToolAvailable(repo, - predefTool.getPath())); + predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs, + gitDir, workTree, predefTool.getPath())); } } return Collections.unmodifiableMap(predefinedTools); } /** + * Get first available tool name. + * * @return the name of first available predefined tool or null */ public String getFirstAvailableTool() { - String name = null; for (ExternalDiffTool tool : predefinedTools.values()) { - if (ExternalToolUtils.isToolAvailable(repo, tool.getPath())) { - name = tool.getName(); - break; + if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree, + tool.getPath())) { + return tool.getName(); } } - return name; + return null; } /** + * Get default (gui-)tool name. + * * @param gui * use the diff.guitool setting ? * @return the default tool name */ - public String getDefaultToolName(BooleanTriState gui) { - return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ + public String getDefaultToolName(boolean gui) { + return gui ? config.getDefaultGuiToolName() : config.getDefaultToolName(); } /** + * Is interactive diff (prompt enabled) ? + * * @return is interactive (config prompt enabled) ? */ public boolean isInteractive() { return config.isPrompt(); } - private ExternalDiffTool guessTool(String toolName, BooleanTriState gui) - throws ToolException { - if (StringUtils.isEmptyOrNull(toolName)) { - toolName = getDefaultToolName(gui); - } - ExternalDiffTool tool = null; - if (!StringUtils.isEmptyOrNull(toolName)) { - tool = getTool(toolName); - } - if (tool == null) { - throw new ToolException("Unknown diff tool '" + toolName + "'"); //$NON-NLS-1$ //$NON-NLS-2$ - } - return tool; - } - private ExternalDiffTool getTool(final String name) { ExternalDiffTool tool = userDefinedTools.get(name); if (tool == null) { @@ -193,10 +301,10 @@ public class DiffTools { return tools; } - private static Map<String, ExternalDiffTool> setupUserDefinedTools( - DiffToolConfig cfg, Map<String, ExternalDiffTool> predefTools) { + private Map<String, ExternalDiffTool> setupUserDefinedTools( + Map<String, ExternalDiffTool> predefTools) { Map<String, ExternalDiffTool> tools = new TreeMap<>(); - Map<String, ExternalDiffTool> userTools = cfg.getTools(); + Map<String, ExternalDiffTool> userTools = config.getTools(); for (String name : userTools.keySet()) { ExternalDiffTool userTool = userTools.get(name); // if difftool.<name>.cmd is defined we have user defined tool 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 e10607d2fd..9a69681133 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java @@ -10,10 +10,14 @@ package org.eclipse.jgit.internal.diffmergetool; import java.util.TreeMap; +import java.io.File; import java.io.IOException; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; + import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.FS; /** * Utilities for diff- and merge-tools. @@ -57,8 +61,8 @@ public class ExternalToolUtils { /** * Prepare environment needed for execution. * - * @param repo - * the repository + * @param gitDir + * the .git directory * @param localFile * the local file (ours) * @param remoteFile @@ -70,11 +74,13 @@ public class ExternalToolUtils { * @return the environment map with variables and values (file paths) * @throws IOException */ - public static Map<String, String> prepareEnvironment(Repository repo, + public static Map<String, String> prepareEnvironment(File gitDir, FileElement localFile, FileElement remoteFile, FileElement mergedFile, FileElement baseFile) throws IOException { Map<String, String> env = new TreeMap<>(); - env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath()); + if (gitDir != null) { + env.put(Constants.GIT_DIR_KEY, gitDir.getAbsolutePath()); + } if (localFile != null) { localFile.addToEnv(env); } @@ -112,22 +118,60 @@ public class ExternalToolUtils { } /** - * @param repo - * the repository + * @param fs + * the file system abstraction + * @param gitDir + * the .git directory + * @param directory + * the working directory * @param path * the tool path * @return true if tool available and false otherwise */ - public static boolean isToolAvailable(Repository repo, String path) { + public static boolean isToolAvailable(FS fs, File gitDir, File directory, + String path) { boolean available = true; try { - CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), false); - available = cmdExec.checkExecutable(path, repo.getWorkTree(), - prepareEnvironment(repo, null, null, null, null)); + CommandExecutor cmdExec = new CommandExecutor(fs, false); + available = cmdExec.checkExecutable(path, directory, + prepareEnvironment(gitDir, null, null, null, null)); } catch (Exception e) { available = false; } return available; } + /** + * @param defaultName + * the default tool name + * @param userDefinedNames + * the user defined tool names + * @param preDefinedNames + * the pre defined tool names + * @return the sorted tool names set: first element is default tool name if + * valid, then user defined tool names and then pre defined tool + * names + */ + public static Set<String> createSortedToolSet(String defaultName, + Set<String> userDefinedNames, Set<String> preDefinedNames) { + Set<String> names = new LinkedHashSet<>(); + if (defaultName != null) { + // remove defaultName from both sets + Set<String> namesPredef = new LinkedHashSet<>(); + Set<String> namesUser = new LinkedHashSet<>(); + namesUser.addAll(userDefinedNames); + namesUser.remove(defaultName); + namesPredef.addAll(preDefinedNames); + namesPredef.remove(defaultName); + // add defaultName as first in set + names.add(defaultName); + names.addAll(namesUser); + names.addAll(namesPredef); + } else { + names.addAll(userDefinedNames); + names.addAll(preDefinedNames); + } + return names; + } + } 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 d91d57f1a8..d2055272e0 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,22 +16,30 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.TreeMap; import org.eclipse.jgit.internal.diffmergetool.FileElement.Type; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS.ExecutionResult; -import org.eclipse.jgit.util.StringUtils; /** * Manages merge tools. */ public class MergeTools { - Repository repo; + private final FS fs; + + private final File gitDir; + + private final File workTree; private final MergeToolConfig config; @@ -39,17 +48,111 @@ public class MergeTools { private final Map<String, ExternalMergeTool> userDefinedTools; /** + * Creates the external merge-tools manager for given repository. + * * @param repo * the repository */ public MergeTools(Repository repo) { - this.repo = repo; - config = repo.getConfig().get(MergeToolConfig.KEY); + this(repo, repo.getConfig()); + } + + /** + * Creates the external diff-tools manager for given configuration. + * + * @param config + * the git configuration + */ + public MergeTools(StoredConfig config) { + this(null, config); + } + + private MergeTools(Repository repo, StoredConfig config) { + this.config = config.get(MergeToolConfig.KEY); + this.gitDir = repo == null ? null : repo.getDirectory(); + this.fs = repo == null ? FS.DETECTED : repo.getFS(); + this.workTree = repo == null ? null : repo.getWorkTree(); predefinedTools = setupPredefinedTools(); - userDefinedTools = setupUserDefinedTools(config, predefinedTools); + userDefinedTools = setupUserDefinedTools(predefinedTools); + } + + /** + * Merge two versions of a file with optional base file. + * + * @param localFile + * The local/left version of the file. + * @param remoteFile + * The remote/right version of the file. + * @param mergedFile + * The file for the result. + * @param baseFile + * The base version of the file. May be null. + * @param tempDir + * The tmepDir used for the files. May be null. + * @param toolName + * Optionally the name of the tool to use. If not given the + * default tool will be used. + * @param prompt + * Optionally a flag whether to prompt the user before compare. + * If not given the default will be used. + * @param gui + * A flag whether to prefer a gui tool. + * @param promptHandler + * The handler to use when needing to prompt the user if he wants + * to continue. + * @param noToolHandler + * The handler to use when needing to inform the user, that no + * tool is configured. + * @return the optional result of executing the tool if it was executed + * @throws ToolException + * when the tool fails + */ + public Optional<ExecutionResult> merge(FileElement localFile, + FileElement remoteFile, FileElement mergedFile, + FileElement baseFile, File tempDir, Optional<String> toolName, + BooleanTriState prompt, boolean gui, + PromptContinueHandler promptHandler, + InformNoToolHandler noToolHandler) throws ToolException { + + String toolNameToUse; + + if (toolName.isPresent()) { + toolNameToUse = toolName.get(); + } else { + toolNameToUse = getDefaultToolName(gui); + + if (toolNameToUse == null || toolNameToUse.isEmpty()) { + noToolHandler.inform(new ArrayList<>(predefinedTools.keySet())); + toolNameToUse = getFirstAvailableTool(); + } + } + + boolean doPrompt; + if (prompt != BooleanTriState.UNSET) { + doPrompt = prompt == BooleanTriState.TRUE; + } else { + doPrompt = isInteractive(); + } + + if (doPrompt) { + if (!promptHandler.prompt(toolNameToUse)) { + return Optional.empty(); + } + } + + ExternalMergeTool tool = getTool(toolNameToUse); + if (tool == null) { + throw new ToolException( + "External merge tool is not defined: " + toolNameToUse); //$NON-NLS-1$ + } + + return Optional.of(merge(localFile, remoteFile, mergedFile, baseFile, + tempDir, tool)); } /** + * Merge two versions of a file with optional base file. + * * @param localFile * the local file element * @param remoteFile @@ -61,38 +164,31 @@ public class MergeTools { * @param tempDir * the temporary directory (needed for backup and auto-remove, * can be null) - * @param toolName - * the selected tool name (can be null) - * @param prompt - * the prompt option - * @param gui - * the GUI option + * @param tool + * the selected tool * @return the execution result from tool * @throws ToolException */ public ExecutionResult merge(FileElement localFile, FileElement remoteFile, FileElement mergedFile, FileElement baseFile, File tempDir, - String toolName, BooleanTriState prompt, BooleanTriState gui) - throws ToolException { - ExternalMergeTool tool = guessTool(toolName, gui); + ExternalMergeTool tool) throws ToolException { FileElement backup = null; ExecutionResult result = null; try { - File workingDir = repo.getWorkTree(); // create additional backup file (copy worktree file) - backup = createBackupFile(mergedFile.getPath(), - tempDir != null ? tempDir : workingDir); + backup = createBackupFile(mergedFile, + tempDir != null ? tempDir : workTree); // prepare the command (replace the file paths) - boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; String command = ExternalToolUtils.prepareCommand( tool.getCommand(baseFile != null), localFile, remoteFile, mergedFile, baseFile); // prepare the environment - Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo, - localFile, remoteFile, mergedFile, baseFile); + Map<String, String> env = ExternalToolUtils.prepareEnvironment( + gitDir, localFile, remoteFile, mergedFile, baseFile); + boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; // execute the tool - CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); - result = cmdExec.run(command, workingDir, env); + CommandExecutor cmdExec = new CommandExecutor(fs, trust); + result = cmdExec.run(command, workTree, env); // keep backup as .orig file if (backup != null) { keepBackupFile(mergedFile.getPath(), backup); @@ -124,19 +220,21 @@ public class MergeTools { } } - private FileElement createBackupFile(String filePath, File parentDir) + private FileElement createBackupFile(FileElement from, File toParentDir) throws IOException { FileElement backup = null; - Path path = Paths.get(filePath); + Path path = Paths.get(from.getPath()); if (Files.exists(path)) { - backup = new FileElement(filePath, Type.BACKUP); - Files.copy(path, backup.createTempFile(parentDir).toPath(), + backup = new FileElement(from.getPath(), Type.BACKUP); + Files.copy(path, backup.createTempFile(toParentDir).toPath(), StandardCopyOption.REPLACE_EXISTING); } return backup; } /** + * Create temporary directory. + * * @return the created temporary directory if (mergetol.writeToTemp == true) * or null if not configured or false. * @throws IOException @@ -148,20 +246,46 @@ public class MergeTools { } /** - * @return the tool names + * Get user defined tool names. + * + * @return the user defined tool names */ - public Set<String> getToolNames() { - return config.getToolNames(); + public Set<String> getUserDefinedToolNames() { + return userDefinedTools.keySet(); + } + + /** + * @return the predefined tool names + */ + public Set<String> getPredefinedToolNames() { + return predefinedTools.keySet(); + } + + /** + * Get all tool names. + * + * @return the all tool names (default or available tool name is the first + * in the set) + */ + public Set<String> getAllToolNames() { + String defaultName = getDefaultToolName(false); + if (defaultName == null) { + defaultName = getFirstAvailableTool(); + } + return ExternalToolUtils.createSortedToolSet(defaultName, + getUserDefinedToolNames(), getPredefinedToolNames()); } /** * @return the user defined tools */ public Map<String, ExternalMergeTool> getUserDefinedTools() { - return userDefinedTools; + return Collections.unmodifiableMap(userDefinedTools); } /** + * Get predefined tools map. + * * @param checkAvailability * true: for checking if tools can be executed; ATTENTION: this * check took some time, do not execute often (store the map for @@ -175,20 +299,23 @@ public class MergeTools { if (checkAvailability) { for (ExternalMergeTool tool : predefinedTools.values()) { PreDefinedMergeTool predefTool = (PreDefinedMergeTool) tool; - predefTool.setAvailable(ExternalToolUtils.isToolAvailable(repo, - predefTool.getPath())); + predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs, + gitDir, workTree, predefTool.getPath())); } } - return predefinedTools; + return Collections.unmodifiableMap(predefinedTools); } /** + * Get first available tool name. + * * @return the name of first available predefined tool or null */ public String getFirstAvailableTool() { String name = null; for (ExternalMergeTool tool : predefinedTools.values()) { - if (ExternalToolUtils.isToolAvailable(repo, tool.getPath())) { + if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree, + tool.getPath())) { name = tool.getName(); break; } @@ -197,35 +324,24 @@ public class MergeTools { } /** - * @param gui - * use the diff.guitool setting ? - * @return the default tool name - */ - public String getDefaultToolName(BooleanTriState gui) { - return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ - : config.getDefaultToolName(); - } - - /** + * Is interactive merge (prompt enabled) ? + * * @return is interactive (config prompt enabled) ? */ public boolean isInteractive() { return config.isPrompt(); } - private ExternalMergeTool guessTool(String toolName, BooleanTriState gui) - throws ToolException { - if (StringUtils.isEmptyOrNull(toolName)) { - toolName = getDefaultToolName(gui); - } - ExternalMergeTool tool = null; - if (!StringUtils.isEmptyOrNull(toolName)) { - tool = getTool(toolName); - } - if (tool == null) { - throw new ToolException("Unknown merge tool '" + toolName + "'"); //$NON-NLS-1$ //$NON-NLS-2$ - } - return tool; + /** + * Get the default (gui-)tool name. + * + * @param gui + * use the diff.guitool setting ? + * @return the default tool name + */ + public String getDefaultToolName(boolean gui) { + return gui ? config.getDefaultGuiToolName() + : config.getDefaultToolName(); } private ExternalMergeTool getTool(final String name) { @@ -256,9 +372,9 @@ public class MergeTools { } private Map<String, ExternalMergeTool> setupUserDefinedTools( - MergeToolConfig cfg, Map<String, ExternalMergeTool> predefTools) { + Map<String, ExternalMergeTool> predefTools) { Map<String, ExternalMergeTool> tools = new TreeMap<>(); - Map<String, ExternalMergeTool> userTools = cfg.getTools(); + Map<String, ExternalMergeTool> userTools = config.getTools(); for (String name : userTools.keySet()) { ExternalMergeTool userTool = userTools.get(name); // if mergetool.<name>.cmd is defined we have user defined tool diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java new file mode 100644 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); +} |