aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
diff options
context:
space:
mode:
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.java458
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;
+ }
+
+}