MergeTools.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.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.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.treewalk.TreeWalk;
- import org.eclipse.jgit.util.FS;
- import org.eclipse.jgit.util.StringUtils;
- import org.eclipse.jgit.util.FS.ExecutionResult;
- /**
- * 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
- */
- 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(), 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
- */
- 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();
- }
- /**
- * @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
- */
- 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;
- }
- /**
- * @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;
- }
- }