| /* |
| * 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; |
| } |
| |
| } |