diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java')
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java | 458 |
1 files changed, 458 insertions, 0 deletions
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 new file mode 100644 index 0000000000..213ce6871e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -0,0 +1,458 @@ +/* + * 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.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.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 merge tools. + */ +public class MergeTools { + + 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; + 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(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 + * the remote file element + * @param mergedFile + * the merged file element + * @param baseFile + * the base file element (can be null) + * @param tempDir + * the temporary directory (needed for backup and auto-remove, + * can be null) + * @param tool + * the selected tool + * @return the execution result from tool + * @throws ToolException + * if the tool failed + */ + public ExecutionResult merge(FileElement localFile, FileElement remoteFile, + FileElement mergedFile, FileElement baseFile, File tempDir, + ExternalMergeTool tool) throws ToolException { + FileElement backup = null; + ExecutionResult result = null; + try { + // create additional backup file (copy worktree file) + backup = createBackupFile(mergedFile, + tempDir != null ? tempDir : workTree); + // prepare the command (replace the file paths) + String command = ExternalToolUtils.prepareCommand( + tool.getCommand(baseFile != null), localFile, remoteFile, + mergedFile, baseFile); + // prepare the environment + Map<String, String> env = ExternalToolUtils.prepareEnvironment( + gitDir, localFile, remoteFile, mergedFile, baseFile); + boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; + // execute the tool + CommandExecutor cmdExec = new CommandExecutor(fs, trust); + result = cmdExec.run(command, workTree, env); + // keep backup as .orig file + if (backup != null) { + keepBackupFile(mergedFile.getPath(), backup); + } + return result; + } catch (IOException | InterruptedException e) { + throw new ToolException(e); + } finally { + // always delete backup file (ignore that it was may be already + // moved to keep-backup file) + if (backup != null) { + backup.cleanTemporaries(); + } + // if the tool returns an error and keepTemporaries is set to true, + // then these temporary files will be preserved + if (!((result == null) && config.isKeepTemporaries())) { + // delete the files + localFile.cleanTemporaries(); + remoteFile.cleanTemporaries(); + if (baseFile != null) { + baseFile.cleanTemporaries(); + } + // delete temporary directory if needed + if (config.isWriteToTemp() && (tempDir != null) + && tempDir.exists()) { + tempDir.delete(); + } + } + } + } + + private FileElement createBackupFile(FileElement from, File toParentDir) + throws IOException { + FileElement backup = null; + Path path = Paths.get(from.getPath()); + if (Files.exists(path)) { + backup = new FileElement(from.getPath(), FileElement.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 + * if an IO error occurred + */ + public File createTempDirectory() throws IOException { + return config.isWriteToTemp() + ? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$ + : null; + } + + /** + * 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 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 + * if the tool failed + */ + public Optional<String> getExternalToolFromAttributes(final String path) + throws ToolException { + return ExternalToolUtils.getExternalToolFromAttributes(repo, path, + ExternalToolUtils.KEY_MERGE_TOOL); + } + + /** + * Checks the availability of the predefined tools in the system. + * + * @return set of predefined available tools + */ + 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; + } + + /** + * Get user defined tools + * + * @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(); + } + + /** + * 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) { + ExternalMergeTool tool = userDefinedTools.get(name); + if (tool == null) { + tool = predefinedTools.get(name); + } + return tool; + } + + private void keepBackupFile(String mergedFilePath, FileElement backup) + throws IOException { + if (config.isKeepBackup()) { + Path backupPath = backup.getFile().toPath(); + Files.move(backupPath, + backupPath.resolveSibling( + Paths.get(mergedFilePath).getFileName() + ".orig"), //$NON-NLS-1$ + StandardCopyOption.REPLACE_EXISTING); + } + } + + private Map<String, ExternalMergeTool> setupPredefinedTools() { + Map<String, ExternalMergeTool> tools = new TreeMap<>(); + for (CommandLineMergeTool tool : CommandLineMergeTool.values()) { + tools.put(tool.name(), new PreDefinedMergeTool(tool)); + } + return tools; + } + + private Map<String, ExternalMergeTool> setupUserDefinedTools( + Map<String, ExternalMergeTool> predefTools) { + Map<String, ExternalMergeTool> tools = new TreeMap<>(); + 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 + if (userTool.getCommand() != null) { + tools.put(name, userTool); + } else if (userTool.getPath() != null) { + // if mergetool.<name>.path is defined we just overload the path + // of predefined tool + PreDefinedMergeTool predefTool = (PreDefinedMergeTool) predefTools + .get(name); + if (predefTool != null) { + predefTool.setPath(userTool.getPath()); + if (userTool.getTrustExitCode() != BooleanTriState.UNSET) { + predefTool + .setTrustExitCode(userTool.getTrustExitCode()); + } + } + } + } + return tools; + } + +} |