/* * Copyright (C) 2018-2022, Andre Bossert * Copyright (C) 2019, Tim Neumann * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.diffmergetool; 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; /** * Manages diff tools. */ public class DiffTools { private final FS fs; private final File gitDir; private final File workTree; private final DiffToolConfig config; private final Repository repo; private final Map predefinedTools; private final Map userDefinedTools; /** * Creates the external diff-tools manager for given repository. * * @param repo * 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; 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(predefinedTools); } /** * Compare two versions of a file. * * @param localFile * The local/left version of the file. * @param remoteFile * The remote/right version of the file. * @param toolName * Optionally the name of the tool to use. If not given the * default tool will be used. * @param prompt * Optionally a flag whether to prompt the user before compare. * If not given the default will be used. * @param gui * A flag whether to prefer a gui tool. * @param trustExitCode * Optionally a flag whether to trust the exit code of the tool. * If not given the default will be used. * @param promptHandler * The handler to use when needing to prompt the user if he wants * to continue. * @param noToolHandler * The handler to use when needing to inform the user, that no * tool is configured. * @return the optional result of executing the tool if it was executed * @throws ToolException * when the tool fails */ public Optional compare(FileElement localFile, FileElement remoteFile, Optional toolName, BooleanTriState prompt, boolean gui, BooleanTriState trustExitCode, PromptContinueHandler promptHandler, InformNoToolHandler noToolHandler) throws ToolException { String toolNameToUse; if (toolName == 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 * when the tool fails */ public ExecutionResult compare(FileElement localFile, 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(tool.getCommand(), localFile, remoteFile, null, null); // prepare the environment Map env = ExternalToolUtils.prepareEnvironment( gitDir, localFile, remoteFile, null, null); // execute the tool 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(); } } /** * Get user defined tool names. * * @return the user defined tool names */ public Set getUserDefinedToolNames() { return userDefinedTools.keySet(); } /** * Get predefined tool names. * * @return the predefined tool names */ public Set getPredefinedToolNames() { return predefinedTools.keySet(); } /** * Get all tool names. * * @return the all tool names (default or available tool name is the first * in the set) */ public Set getAllToolNames() { String defaultName = getDefaultToolName(false); if (defaultName == null) { defaultName = getFirstAvailableTool(); } return ExternalToolUtils.createSortedToolSet(defaultName, getUserDefinedToolNames(), getPredefinedToolNames()); } /** * 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 * when the tool failed */ public Optional 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 getPredefinedAvailableTools() { Map defTools = getPredefinedTools(true); Set availableTools = new LinkedHashSet<>(); for (Entry 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 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 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); } /** * Get first available tool name. * * @return the name of first available predefined tool or null */ 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(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 getTool(final String name) { ExternalDiffTool tool = userDefinedTools.get(name); if (tool == null) { tool = predefinedTools.get(name); } return tool; } private static Map setupPredefinedTools() { Map tools = new TreeMap<>(); for (CommandLineDiffTool tool : CommandLineDiffTool.values()) { tools.put(tool.name(), new PreDefinedDiffTool(tool)); } return tools; } private Map setupUserDefinedTools( Map predefTools) { Map tools = new TreeMap<>(); Map userTools = config.getTools(); for (String name : userTools.keySet()) { ExternalDiffTool userTool = userTools.get(name); // if difftool..cmd is defined we have user defined tool if (userTool.getCommand() != null) { tools.put(name, userTool); } else if (userTool.getPath() != null) { // if difftool..path is defined we just overload the path // of predefined tool PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefTools .get(name); if (predefTool != null) { predefTool.setPath(userTool.getPath()); } } } return tools; } }