MergeTools.java

  1. /*
  2.  * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
  3.  * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */
  11. package org.eclipse.jgit.internal.diffmergetool;

  12. import java.io.File;
  13. import java.io.IOException;
  14. import java.nio.file.Files;
  15. import java.nio.file.Path;
  16. import java.nio.file.Paths;
  17. import java.nio.file.StandardCopyOption;
  18. import java.util.ArrayList;
  19. import java.util.Collections;
  20. import java.util.LinkedHashSet;
  21. import java.util.Map;
  22. import java.util.Map.Entry;
  23. import java.util.Optional;
  24. import java.util.Set;
  25. import java.util.TreeMap;

  26. import org.eclipse.jgit.internal.JGitText;
  27. import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
  28. import org.eclipse.jgit.lib.Repository;
  29. import org.eclipse.jgit.lib.StoredConfig;
  30. import org.eclipse.jgit.lib.internal.BooleanTriState;
  31. import org.eclipse.jgit.treewalk.TreeWalk;
  32. import org.eclipse.jgit.util.FS;
  33. import org.eclipse.jgit.util.StringUtils;
  34. import org.eclipse.jgit.util.FS.ExecutionResult;

  35. /**
  36.  * Manages merge tools.
  37.  */
  38. public class MergeTools {

  39.     private final FS fs;

  40.     private final File gitDir;

  41.     private final File workTree;

  42.     private final MergeToolConfig config;

  43.     private final Repository repo;

  44.     private final Map<String, ExternalMergeTool> predefinedTools;

  45.     private final Map<String, ExternalMergeTool> userDefinedTools;

  46.     /**
  47.      * Creates the external merge-tools manager for given repository.
  48.      *
  49.      * @param repo
  50.      *            the repository
  51.      */
  52.     public MergeTools(Repository repo) {
  53.         this(repo, repo.getConfig());
  54.     }

  55.     /**
  56.      * Creates the external diff-tools manager for given configuration.
  57.      *
  58.      * @param config
  59.      *            the git configuration
  60.      */
  61.     public MergeTools(StoredConfig config) {
  62.         this(null, config);
  63.     }

  64.     private MergeTools(Repository repo, StoredConfig config) {
  65.         this.repo = repo;
  66.         this.config = config.get(MergeToolConfig.KEY);
  67.         this.gitDir = repo == null ? null : repo.getDirectory();
  68.         this.fs = repo == null ? FS.DETECTED : repo.getFS();
  69.         this.workTree = repo == null ? null : repo.getWorkTree();
  70.         predefinedTools = setupPredefinedTools();
  71.         userDefinedTools = setupUserDefinedTools(predefinedTools);
  72.     }

  73.     /**
  74.      * Merge two versions of a file with optional base file.
  75.      *
  76.      * @param localFile
  77.      *            The local/left version of the file.
  78.      * @param remoteFile
  79.      *            The remote/right version of the file.
  80.      * @param mergedFile
  81.      *            The file for the result.
  82.      * @param baseFile
  83.      *            The base version of the file. May be null.
  84.      * @param tempDir
  85.      *            The tmepDir used for the files. May be null.
  86.      * @param toolName
  87.      *            Optionally the name of the tool to use. If not given the
  88.      *            default tool will be used.
  89.      * @param prompt
  90.      *            Optionally a flag whether to prompt the user before compare.
  91.      *            If not given the default will be used.
  92.      * @param gui
  93.      *            A flag whether to prefer a gui tool.
  94.      * @param promptHandler
  95.      *            The handler to use when needing to prompt the user if he wants
  96.      *            to continue.
  97.      * @param noToolHandler
  98.      *            The handler to use when needing to inform the user, that no
  99.      *            tool is configured.
  100.      * @return the optional result of executing the tool if it was executed
  101.      * @throws ToolException
  102.      *             when the tool fails
  103.      */
  104.     public Optional<ExecutionResult> merge(FileElement localFile,
  105.             FileElement remoteFile, FileElement mergedFile,
  106.             FileElement baseFile, File tempDir, Optional<String> toolName,
  107.             BooleanTriState prompt, boolean gui,
  108.             PromptContinueHandler promptHandler,
  109.             InformNoToolHandler noToolHandler) throws ToolException {

  110.         String toolNameToUse;

  111.         if (toolName == null) {
  112.             throw new ToolException(JGitText.get().diffToolNullError);
  113.         }

  114.         if (toolName.isPresent()) {
  115.             toolNameToUse = toolName.get();
  116.         } else {
  117.             toolNameToUse = getDefaultToolName(gui);

  118.             if (StringUtils.isEmptyOrNull(toolNameToUse)) {
  119.                 noToolHandler.inform(new ArrayList<>(predefinedTools.keySet()));
  120.                 toolNameToUse = getFirstAvailableTool();
  121.             }
  122.         }

  123.         if (StringUtils.isEmptyOrNull(toolNameToUse)) {
  124.             throw new ToolException(JGitText.get().diffToolNotGivenError);
  125.         }

  126.         boolean doPrompt;
  127.         if (prompt != BooleanTriState.UNSET) {
  128.             doPrompt = prompt == BooleanTriState.TRUE;
  129.         } else {
  130.             doPrompt = isInteractive();
  131.         }

  132.         if (doPrompt) {
  133.             if (!promptHandler.prompt(toolNameToUse)) {
  134.                 return Optional.empty();
  135.             }
  136.         }

  137.         ExternalMergeTool tool = getTool(toolNameToUse);
  138.         if (tool == null) {
  139.             throw new ToolException(
  140.                     "External merge tool is not defined: " + toolNameToUse); //$NON-NLS-1$
  141.         }

  142.         return Optional.of(merge(localFile, remoteFile, mergedFile, baseFile,
  143.                 tempDir, tool));
  144.     }

  145.     /**
  146.      * Merge two versions of a file with optional base file.
  147.      *
  148.      * @param localFile
  149.      *            the local file element
  150.      * @param remoteFile
  151.      *            the remote file element
  152.      * @param mergedFile
  153.      *            the merged file element
  154.      * @param baseFile
  155.      *            the base file element (can be null)
  156.      * @param tempDir
  157.      *            the temporary directory (needed for backup and auto-remove,
  158.      *            can be null)
  159.      * @param tool
  160.      *            the selected tool
  161.      * @return the execution result from tool
  162.      * @throws ToolException
  163.      */
  164.     public ExecutionResult merge(FileElement localFile, FileElement remoteFile,
  165.             FileElement mergedFile, FileElement baseFile, File tempDir,
  166.             ExternalMergeTool tool) throws ToolException {
  167.         FileElement backup = null;
  168.         ExecutionResult result = null;
  169.         try {
  170.             // create additional backup file (copy worktree file)
  171.             backup = createBackupFile(mergedFile,
  172.                     tempDir != null ? tempDir : workTree);
  173.             // prepare the command (replace the file paths)
  174.             String command = ExternalToolUtils.prepareCommand(
  175.                     tool.getCommand(baseFile != null), localFile, remoteFile,
  176.                     mergedFile, baseFile);
  177.             // prepare the environment
  178.             Map<String, String> env = ExternalToolUtils.prepareEnvironment(
  179.                     gitDir, localFile, remoteFile, mergedFile, baseFile);
  180.             boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
  181.             // execute the tool
  182.             CommandExecutor cmdExec = new CommandExecutor(fs, trust);
  183.             result = cmdExec.run(command, workTree, env);
  184.             // keep backup as .orig file
  185.             if (backup != null) {
  186.                 keepBackupFile(mergedFile.getPath(), backup);
  187.             }
  188.             return result;
  189.         } catch (IOException | InterruptedException e) {
  190.             throw new ToolException(e);
  191.         } finally {
  192.             // always delete backup file (ignore that it was may be already
  193.             // moved to keep-backup file)
  194.             if (backup != null) {
  195.                 backup.cleanTemporaries();
  196.             }
  197.             // if the tool returns an error and keepTemporaries is set to true,
  198.             // then these temporary files will be preserved
  199.             if (!((result == null) && config.isKeepTemporaries())) {
  200.                 // delete the files
  201.                 localFile.cleanTemporaries();
  202.                 remoteFile.cleanTemporaries();
  203.                 if (baseFile != null) {
  204.                     baseFile.cleanTemporaries();
  205.                 }
  206.                 // delete temporary directory if needed
  207.                 if (config.isWriteToTemp() && (tempDir != null)
  208.                         && tempDir.exists()) {
  209.                     tempDir.delete();
  210.                 }
  211.             }
  212.         }
  213.     }

  214.     private FileElement createBackupFile(FileElement from, File toParentDir)
  215.             throws IOException {
  216.         FileElement backup = null;
  217.         Path path = Paths.get(from.getPath());
  218.         if (Files.exists(path)) {
  219.             backup = new FileElement(from.getPath(), Type.BACKUP);
  220.             Files.copy(path, backup.createTempFile(toParentDir).toPath(),
  221.                     StandardCopyOption.REPLACE_EXISTING);
  222.         }
  223.         return backup;
  224.     }

  225.     /**
  226.      * Create temporary directory.
  227.      *
  228.      * @return the created temporary directory if (mergetol.writeToTemp == true)
  229.      *         or null if not configured or false.
  230.      * @throws IOException
  231.      */
  232.     public File createTempDirectory() throws IOException {
  233.         return config.isWriteToTemp()
  234.                 ? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$
  235.                 : null;
  236.     }

  237.     /**
  238.      * Get user defined tool names.
  239.      *
  240.      * @return the user defined tool names
  241.      */
  242.     public Set<String> getUserDefinedToolNames() {
  243.         return userDefinedTools.keySet();
  244.     }

  245.     /**
  246.      * @return the predefined tool names
  247.      */
  248.     public Set<String> getPredefinedToolNames() {
  249.         return predefinedTools.keySet();
  250.     }

  251.     /**
  252.      * Get all tool names.
  253.      *
  254.      * @return the all tool names (default or available tool name is the first
  255.      *         in the set)
  256.      */
  257.     public Set<String> getAllToolNames() {
  258.         String defaultName = getDefaultToolName(false);
  259.         if (defaultName == null) {
  260.             defaultName = getFirstAvailableTool();
  261.         }
  262.         return ExternalToolUtils.createSortedToolSet(defaultName,
  263.                 getUserDefinedToolNames(), getPredefinedToolNames());
  264.     }

  265.     /**
  266.      * Provides {@link Optional} with the name of an external merge tool if
  267.      * specified in git configuration for a path.
  268.      *
  269.      * The formed git configuration results from global rules as well as merged
  270.      * rules from info and worktree attributes.
  271.      *
  272.      * Triggers {@link TreeWalk} until specified path found in the tree.
  273.      *
  274.      * @param path
  275.      *            path to the node in repository to parse git attributes for
  276.      * @return name of the difftool if set
  277.      * @throws ToolException
  278.      */
  279.     public Optional<String> getExternalToolFromAttributes(final String path)
  280.             throws ToolException {
  281.         return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
  282.                 ExternalToolUtils.KEY_MERGE_TOOL);
  283.     }

  284.     /**
  285.      * Checks the availability of the predefined tools in the system.
  286.      *
  287.      * @return set of predefined available tools
  288.      */
  289.     public Set<String> getPredefinedAvailableTools() {
  290.         Map<String, ExternalMergeTool> defTools = getPredefinedTools(true);
  291.         Set<String> availableTools = new LinkedHashSet<>();
  292.         for (Entry<String, ExternalMergeTool> elem : defTools.entrySet()) {
  293.             if (elem.getValue().isAvailable()) {
  294.                 availableTools.add(elem.getKey());
  295.             }
  296.         }
  297.         return availableTools;
  298.     }

  299.     /**
  300.      * @return the user defined tools
  301.      */
  302.     public Map<String, ExternalMergeTool> getUserDefinedTools() {
  303.         return Collections.unmodifiableMap(userDefinedTools);
  304.     }

  305.     /**
  306.      * Get predefined tools map.
  307.      *
  308.      * @param checkAvailability
  309.      *            true: for checking if tools can be executed; ATTENTION: this
  310.      *            check took some time, do not execute often (store the map for
  311.      *            other actions); false: availability is NOT checked:
  312.      *            isAvailable() returns default false is this case!
  313.      * @return the predefined tools with optionally checked availability (long
  314.      *         running operation)
  315.      */
  316.     public Map<String, ExternalMergeTool> getPredefinedTools(
  317.             boolean checkAvailability) {
  318.         if (checkAvailability) {
  319.             for (ExternalMergeTool tool : predefinedTools.values()) {
  320.                 PreDefinedMergeTool predefTool = (PreDefinedMergeTool) tool;
  321.                 predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
  322.                         gitDir, workTree, predefTool.getPath()));
  323.             }
  324.         }
  325.         return Collections.unmodifiableMap(predefinedTools);
  326.     }

  327.     /**
  328.      * Get first available tool name.
  329.      *
  330.      * @return the name of first available predefined tool or null
  331.      */
  332.     public String getFirstAvailableTool() {
  333.         String name = null;
  334.         for (ExternalMergeTool tool : predefinedTools.values()) {
  335.             if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
  336.                     tool.getPath())) {
  337.                 name = tool.getName();
  338.                 break;
  339.             }
  340.         }
  341.         return name;
  342.     }

  343.     /**
  344.      * Is interactive merge (prompt enabled) ?
  345.      *
  346.      * @return is interactive (config prompt enabled) ?
  347.      */
  348.     public boolean isInteractive() {
  349.         return config.isPrompt();
  350.     }

  351.     /**
  352.      * Get the default (gui-)tool name.
  353.      *
  354.      * @param gui
  355.      *            use the diff.guitool setting ?
  356.      * @return the default tool name
  357.      */
  358.     public String getDefaultToolName(boolean gui) {
  359.         return gui ? config.getDefaultGuiToolName()
  360.                 : config.getDefaultToolName();
  361.     }

  362.     private ExternalMergeTool getTool(final String name) {
  363.         ExternalMergeTool tool = userDefinedTools.get(name);
  364.         if (tool == null) {
  365.             tool = predefinedTools.get(name);
  366.         }
  367.         return tool;
  368.     }

  369.     private void keepBackupFile(String mergedFilePath, FileElement backup)
  370.             throws IOException {
  371.         if (config.isKeepBackup()) {
  372.             Path backupPath = backup.getFile().toPath();
  373.             Files.move(backupPath,
  374.                     backupPath.resolveSibling(
  375.                             Paths.get(mergedFilePath).getFileName() + ".orig"), //$NON-NLS-1$
  376.                     StandardCopyOption.REPLACE_EXISTING);
  377.         }
  378.     }

  379.     private Map<String, ExternalMergeTool> setupPredefinedTools() {
  380.         Map<String, ExternalMergeTool> tools = new TreeMap<>();
  381.         for (CommandLineMergeTool tool : CommandLineMergeTool.values()) {
  382.             tools.put(tool.name(), new PreDefinedMergeTool(tool));
  383.         }
  384.         return tools;
  385.     }

  386.     private Map<String, ExternalMergeTool> setupUserDefinedTools(
  387.             Map<String, ExternalMergeTool> predefTools) {
  388.         Map<String, ExternalMergeTool> tools = new TreeMap<>();
  389.         Map<String, ExternalMergeTool> userTools = config.getTools();
  390.         for (String name : userTools.keySet()) {
  391.             ExternalMergeTool userTool = userTools.get(name);
  392.             // if mergetool.<name>.cmd is defined we have user defined tool
  393.             if (userTool.getCommand() != null) {
  394.                 tools.put(name, userTool);
  395.             } else if (userTool.getPath() != null) {
  396.                 // if mergetool.<name>.path is defined we just overload the path
  397.                 // of predefined tool
  398.                 PreDefinedMergeTool predefTool = (PreDefinedMergeTool) predefTools
  399.                         .get(name);
  400.                 if (predefTool != null) {
  401.                     predefTool.setPath(userTool.getPath());
  402.                     if (userTool.getTrustExitCode() != BooleanTriState.UNSET) {
  403.                         predefTool
  404.                                 .setTrustExitCode(userTool.getTrustExitCode());
  405.                     }
  406.                 }
  407.             }
  408.         }
  409.         return tools;
  410.     }

  411. }