DiffTools.java
/*
* 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
* 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<String, ExternalDiffTool> predefinedTools;
private final Map<String, ExternalDiffTool> 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<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, 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<String, String> 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<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> 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() {
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, 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);
}
/**
* 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<String, ExternalDiffTool> setupPredefinedTools() {
Map<String, ExternalDiffTool> tools = new TreeMap<>();
for (CommandLineDiffTool tool : CommandLineDiffTool.values()) {
tools.put(tool.name(), new PreDefinedDiffTool(tool));
}
return tools;
}
private Map<String, ExternalDiffTool> setupUserDefinedTools(
Map<String, ExternalDiffTool> predefTools) {
Map<String, ExternalDiffTool> tools = new TreeMap<>();
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
if (userTool.getCommand() != null) {
tools.put(name, userTool);
} else if (userTool.getPath() != null) {
// if difftool.<name>.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;
}
}