DiffTools.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.util.Collections;
  15. import java.util.LinkedHashSet;
  16. import java.util.Map;
  17. import java.util.Map.Entry;
  18. import java.util.Optional;
  19. import java.util.Set;
  20. import java.util.TreeMap;

  21. import org.eclipse.jgit.internal.JGitText;
  22. import org.eclipse.jgit.lib.Repository;
  23. import org.eclipse.jgit.lib.StoredConfig;
  24. import org.eclipse.jgit.lib.internal.BooleanTriState;
  25. import org.eclipse.jgit.treewalk.TreeWalk;
  26. import org.eclipse.jgit.util.FS;
  27. import org.eclipse.jgit.util.FS.ExecutionResult;
  28. import org.eclipse.jgit.util.StringUtils;

  29. /**
  30.  * Manages diff tools.
  31.  */
  32. public class DiffTools {

  33.     private final FS fs;

  34.     private final File gitDir;

  35.     private final File workTree;

  36.     private final DiffToolConfig config;

  37.     private final Repository repo;

  38.     private final Map<String, ExternalDiffTool> predefinedTools;

  39.     private final Map<String, ExternalDiffTool> userDefinedTools;

  40.     /**
  41.      * Creates the external diff-tools manager for given repository.
  42.      *
  43.      * @param repo
  44.      *            the repository
  45.      */
  46.     public DiffTools(Repository repo) {
  47.         this(repo, repo.getConfig());
  48.     }

  49.     /**
  50.      * Creates the external merge-tools manager for given configuration.
  51.      *
  52.      * @param config
  53.      *            the git configuration
  54.      */
  55.     public DiffTools(StoredConfig config) {
  56.         this(null, config);
  57.     }

  58.     private DiffTools(Repository repo, StoredConfig config) {
  59.         this.repo = repo;
  60.         this.config = config.get(DiffToolConfig.KEY);
  61.         this.gitDir = repo == null ? null : repo.getDirectory();
  62.         this.fs = repo == null ? FS.DETECTED : repo.getFS();
  63.         this.workTree = repo == null ? null : repo.getWorkTree();
  64.         predefinedTools = setupPredefinedTools();
  65.         userDefinedTools = setupUserDefinedTools(predefinedTools);
  66.     }

  67.     /**
  68.      * Compare two versions of a file.
  69.      *
  70.      * @param localFile
  71.      *            The local/left version of the file.
  72.      * @param remoteFile
  73.      *            The remote/right version of the file.
  74.      * @param toolName
  75.      *            Optionally the name of the tool to use. If not given the
  76.      *            default tool will be used.
  77.      * @param prompt
  78.      *            Optionally a flag whether to prompt the user before compare.
  79.      *            If not given the default will be used.
  80.      * @param gui
  81.      *            A flag whether to prefer a gui tool.
  82.      * @param trustExitCode
  83.      *            Optionally a flag whether to trust the exit code of the tool.
  84.      *            If not given the default will be used.
  85.      * @param promptHandler
  86.      *            The handler to use when needing to prompt the user if he wants
  87.      *            to continue.
  88.      * @param noToolHandler
  89.      *            The handler to use when needing to inform the user, that no
  90.      *            tool is configured.
  91.      * @return the optional result of executing the tool if it was executed
  92.      * @throws ToolException
  93.      *             when the tool fails
  94.      */
  95.     public Optional<ExecutionResult> compare(FileElement localFile,
  96.             FileElement remoteFile, Optional<String> toolName,
  97.             BooleanTriState prompt, boolean gui, BooleanTriState trustExitCode,
  98.             PromptContinueHandler promptHandler,
  99.             InformNoToolHandler noToolHandler) throws ToolException {

  100.         String toolNameToUse;

  101.         if (toolName == null) {
  102.             throw new ToolException(JGitText.get().diffToolNullError);
  103.         }

  104.         if (toolName.isPresent()) {
  105.             toolNameToUse = toolName.get();
  106.         } else {
  107.             toolNameToUse = getDefaultToolName(gui);
  108.         }

  109.         if (StringUtils.isEmptyOrNull(toolNameToUse)) {
  110.             throw new ToolException(JGitText.get().diffToolNotGivenError);
  111.         }

  112.         boolean doPrompt;
  113.         if (prompt != BooleanTriState.UNSET) {
  114.             doPrompt = prompt == BooleanTriState.TRUE;
  115.         } else {
  116.             doPrompt = isInteractive();
  117.         }

  118.         if (doPrompt) {
  119.             if (!promptHandler.prompt(toolNameToUse)) {
  120.                 return Optional.empty();
  121.             }
  122.         }

  123.         boolean trust;
  124.         if (trustExitCode != BooleanTriState.UNSET) {
  125.             trust = trustExitCode == BooleanTriState.TRUE;
  126.         } else {
  127.             trust = config.isTrustExitCode();
  128.         }

  129.         ExternalDiffTool tool = getTool(toolNameToUse);
  130.         if (tool == null) {
  131.             throw new ToolException(
  132.                     "External diff tool is not defined: " + toolNameToUse); //$NON-NLS-1$
  133.         }

  134.         return Optional.of(
  135.                 compare(localFile, remoteFile, tool, trust));
  136.     }

  137.     /**
  138.      * Compare two versions of a file.
  139.      *
  140.      * @param localFile
  141.      *            the local file element
  142.      * @param remoteFile
  143.      *            the remote file element
  144.      * @param tool
  145.      *            the selected tool
  146.      * @param trustExitCode
  147.      *            the "trust exit code" option
  148.      * @return the execution result from tool
  149.      * @throws ToolException
  150.      */
  151.     public ExecutionResult compare(FileElement localFile,
  152.             FileElement remoteFile, ExternalDiffTool tool,
  153.             boolean trustExitCode) throws ToolException {
  154.         try {
  155.             if (tool == null) {
  156.                 throw new ToolException(JGitText
  157.                         .get().diffToolNotSpecifiedInGitAttributesError);
  158.             }
  159.             // prepare the command (replace the file paths)
  160.             String command = ExternalToolUtils.prepareCommand(tool.getCommand(),
  161.                     localFile, remoteFile, null, null);
  162.             // prepare the environment
  163.             Map<String, String> env = ExternalToolUtils.prepareEnvironment(
  164.                     gitDir, localFile, remoteFile, null, null);
  165.             // execute the tool
  166.             CommandExecutor cmdExec = new CommandExecutor(fs, trustExitCode);
  167.             return cmdExec.run(command, workTree, env);
  168.         } catch (IOException | InterruptedException e) {
  169.             throw new ToolException(e);
  170.         } finally {
  171.             localFile.cleanTemporaries();
  172.             remoteFile.cleanTemporaries();
  173.         }
  174.     }

  175.     /**
  176.      * Get user defined tool names.
  177.      *
  178.      * @return the user defined tool names
  179.      */
  180.     public Set<String> getUserDefinedToolNames() {
  181.         return userDefinedTools.keySet();
  182.     }

  183.     /**
  184.      * Get predefined tool names.
  185.      *
  186.      * @return the predefined tool names
  187.      */
  188.     public Set<String> getPredefinedToolNames() {
  189.         return predefinedTools.keySet();
  190.     }

  191.     /**
  192.      * Get all tool names.
  193.      *
  194.      * @return the all tool names (default or available tool name is the first
  195.      *         in the set)
  196.      */
  197.     public Set<String> getAllToolNames() {
  198.         String defaultName = getDefaultToolName(false);
  199.         if (defaultName == null) {
  200.             defaultName = getFirstAvailableTool();
  201.         }
  202.         return ExternalToolUtils.createSortedToolSet(defaultName,
  203.                 getUserDefinedToolNames(), getPredefinedToolNames());
  204.     }

  205.     /**
  206.      * Provides {@link Optional} with the name of an external diff tool if
  207.      * specified in git configuration for a path.
  208.      *
  209.      * The formed git configuration results from global rules as well as merged
  210.      * rules from info and worktree attributes.
  211.      *
  212.      * Triggers {@link TreeWalk} until specified path found in the tree.
  213.      *
  214.      * @param path
  215.      *            path to the node in repository to parse git attributes for
  216.      * @return name of the difftool if set
  217.      * @throws ToolException
  218.      */
  219.     public Optional<String> getExternalToolFromAttributes(final String path)
  220.             throws ToolException {
  221.         return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
  222.                 ExternalToolUtils.KEY_DIFF_TOOL);
  223.     }

  224.     /**
  225.      * Checks the availability of the predefined tools in the system.
  226.      *
  227.      * @return set of predefined available tools
  228.      */
  229.     public Set<String> getPredefinedAvailableTools() {
  230.         Map<String, ExternalDiffTool> defTools = getPredefinedTools(true);
  231.         Set<String> availableTools = new LinkedHashSet<>();
  232.         for (Entry<String, ExternalDiffTool> elem : defTools.entrySet()) {
  233.             if (elem.getValue().isAvailable()) {
  234.                 availableTools.add(elem.getKey());
  235.             }
  236.         }
  237.         return availableTools;
  238.     }

  239.     /**
  240.      * Get user defined tools map.
  241.      *
  242.      * @return the user defined tools
  243.      */
  244.     public Map<String, ExternalDiffTool> getUserDefinedTools() {
  245.         return Collections.unmodifiableMap(userDefinedTools);
  246.     }

  247.     /**
  248.      * Get predefined tools map.
  249.      *
  250.      * @param checkAvailability
  251.      *            true: for checking if tools can be executed; ATTENTION: this
  252.      *            check took some time, do not execute often (store the map for
  253.      *            other actions); false: availability is NOT checked:
  254.      *            isAvailable() returns default false is this case!
  255.      * @return the predefined tools with optionally checked availability (long
  256.      *         running operation)
  257.      */
  258.     public Map<String, ExternalDiffTool> getPredefinedTools(
  259.             boolean checkAvailability) {
  260.         if (checkAvailability) {
  261.             for (ExternalDiffTool tool : predefinedTools.values()) {
  262.                 PreDefinedDiffTool predefTool = (PreDefinedDiffTool) tool;
  263.                 predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
  264.                         gitDir, workTree, predefTool.getPath()));
  265.             }
  266.         }
  267.         return Collections.unmodifiableMap(predefinedTools);
  268.     }

  269.     /**
  270.      * Get first available tool name.
  271.      *
  272.      * @return the name of first available predefined tool or null
  273.      */
  274.     public String getFirstAvailableTool() {
  275.         for (ExternalDiffTool tool : predefinedTools.values()) {
  276.             if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
  277.                     tool.getPath())) {
  278.                 return tool.getName();
  279.             }
  280.         }
  281.         return null;
  282.     }

  283.     /**
  284.      * Get default (gui-)tool name.
  285.      *
  286.      * @param gui
  287.      *            use the diff.guitool setting ?
  288.      * @return the default tool name
  289.      */
  290.     public String getDefaultToolName(boolean gui) {
  291.         String guiToolName;
  292.         if (gui) {
  293.             guiToolName = config.getDefaultGuiToolName();
  294.             if (guiToolName != null) {
  295.                 return guiToolName;
  296.             }
  297.         }
  298.         return config.getDefaultToolName();
  299.     }

  300.     /**
  301.      * Is interactive diff (prompt enabled) ?
  302.      *
  303.      * @return is interactive (config prompt enabled) ?
  304.      */
  305.     public boolean isInteractive() {
  306.         return config.isPrompt();
  307.     }

  308.     private ExternalDiffTool getTool(final String name) {
  309.         ExternalDiffTool tool = userDefinedTools.get(name);
  310.         if (tool == null) {
  311.             tool = predefinedTools.get(name);
  312.         }
  313.         return tool;
  314.     }

  315.     private static Map<String, ExternalDiffTool> setupPredefinedTools() {
  316.         Map<String, ExternalDiffTool> tools = new TreeMap<>();
  317.         for (CommandLineDiffTool tool : CommandLineDiffTool.values()) {
  318.             tools.put(tool.name(), new PreDefinedDiffTool(tool));
  319.         }
  320.         return tools;
  321.     }

  322.     private Map<String, ExternalDiffTool> setupUserDefinedTools(
  323.             Map<String, ExternalDiffTool> predefTools) {
  324.         Map<String, ExternalDiffTool> tools = new TreeMap<>();
  325.         Map<String, ExternalDiffTool> userTools = config.getTools();
  326.         for (String name : userTools.keySet()) {
  327.             ExternalDiffTool userTool = userTools.get(name);
  328.             // if difftool.<name>.cmd is defined we have user defined tool
  329.             if (userTool.getCommand() != null) {
  330.                 tools.put(name, userTool);
  331.             } else if (userTool.getPath() != null) {
  332.                 // if difftool.<name>.path is defined we just overload the path
  333.                 // of predefined tool
  334.                 PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefTools
  335.                         .get(name);
  336.                 if (predefTool != null) {
  337.                     predefTool.setPath(userTool.getPath());
  338.                 }
  339.             }
  340.         }
  341.         return tools;
  342.     }

  343. }