View Javadoc
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  
12  package org.eclipse.jgit.internal.diffmergetool;
13  
14  import java.io.File;
15  import java.io.IOException;
16  import java.util.Collections;
17  import java.util.LinkedHashSet;
18  import java.util.Map;
19  import java.util.Map.Entry;
20  import java.util.Optional;
21  import java.util.Set;
22  import java.util.TreeMap;
23  
24  import org.eclipse.jgit.internal.JGitText;
25  import org.eclipse.jgit.lib.Repository;
26  import org.eclipse.jgit.lib.StoredConfig;
27  import org.eclipse.jgit.lib.internal.BooleanTriState;
28  import org.eclipse.jgit.treewalk.TreeWalk;
29  import org.eclipse.jgit.util.FS;
30  import org.eclipse.jgit.util.FS.ExecutionResult;
31  import org.eclipse.jgit.util.StringUtils;
32  
33  /**
34   * Manages diff tools.
35   */
36  public class DiffTools {
37  
38  	private final FS fs;
39  
40  	private final File gitDir;
41  
42  	private final File workTree;
43  
44  	private final DiffToolConfig config;
45  
46  	private final Repository repo;
47  
48  	private final Map<String, ExternalDiffTool> predefinedTools;
49  
50  	private final Map<String, ExternalDiffTool> userDefinedTools;
51  
52  	/**
53  	 * Creates the external diff-tools manager for given repository.
54  	 *
55  	 * @param repo
56  	 *            the repository
57  	 */
58  	public DiffTools(Repository repo) {
59  		this(repo, repo.getConfig());
60  	}
61  
62  	/**
63  	 * Creates the external merge-tools manager for given configuration.
64  	 *
65  	 * @param config
66  	 *            the git configuration
67  	 */
68  	public DiffTools(StoredConfig config) {
69  		this(null, config);
70  	}
71  
72  	private DiffTools(Repository repo, StoredConfig config) {
73  		this.repo = repo;
74  		this.config = config.get(DiffToolConfig.KEY);
75  		this.gitDir = repo == null ? null : repo.getDirectory();
76  		this.fs = repo == null ? FS.DETECTED : repo.getFS();
77  		this.workTree = repo == null ? null : repo.getWorkTree();
78  		predefinedTools = setupPredefinedTools();
79  		userDefinedTools = setupUserDefinedTools(predefinedTools);
80  	}
81  
82  	/**
83  	 * Compare two versions of a file.
84  	 *
85  	 * @param localFile
86  	 *            The local/left version of the file.
87  	 * @param remoteFile
88  	 *            The remote/right version of the file.
89  	 * @param toolName
90  	 *            Optionally the name of the tool to use. If not given the
91  	 *            default tool will be used.
92  	 * @param prompt
93  	 *            Optionally a flag whether to prompt the user before compare.
94  	 *            If not given the default will be used.
95  	 * @param gui
96  	 *            A flag whether to prefer a gui tool.
97  	 * @param trustExitCode
98  	 *            Optionally a flag whether to trust the exit code of the tool.
99  	 *            If not given the default will be used.
100 	 * @param promptHandler
101 	 *            The handler to use when needing to prompt the user if he wants
102 	 *            to continue.
103 	 * @param noToolHandler
104 	 *            The handler to use when needing to inform the user, that no
105 	 *            tool is configured.
106 	 * @return the optional result of executing the tool if it was executed
107 	 * @throws ToolException
108 	 *             when the tool fails
109 	 */
110 	public Optional<ExecutionResult> compare(FileElement localFile,
111 			FileElement remoteFile, Optional<String> toolName,
112 			BooleanTriState prompt, boolean gui, BooleanTriState trustExitCode,
113 			PromptContinueHandler promptHandler,
114 			InformNoToolHandler noToolHandler) throws ToolException {
115 
116 		String toolNameToUse;
117 
118 		if (toolName == null) {
119 			throw new ToolException(JGitText.get().diffToolNullError);
120 		}
121 
122 		if (toolName.isPresent()) {
123 			toolNameToUse = toolName.get();
124 		} else {
125 			toolNameToUse = getDefaultToolName(gui);
126 		}
127 
128 		if (StringUtils.isEmptyOrNull(toolNameToUse)) {
129 			throw new ToolException(JGitText.get().diffToolNotGivenError);
130 		}
131 
132 		boolean doPrompt;
133 		if (prompt != BooleanTriState.UNSET) {
134 			doPrompt = prompt == BooleanTriState.TRUE;
135 		} else {
136 			doPrompt = isInteractive();
137 		}
138 
139 		if (doPrompt) {
140 			if (!promptHandler.prompt(toolNameToUse)) {
141 				return Optional.empty();
142 			}
143 		}
144 
145 		boolean trust;
146 		if (trustExitCode != BooleanTriState.UNSET) {
147 			trust = trustExitCode == BooleanTriState.TRUE;
148 		} else {
149 			trust = config.isTrustExitCode();
150 		}
151 
152 		ExternalDiffTool tool = getTool(toolNameToUse);
153 		if (tool == null) {
154 			throw new ToolException(
155 					"External diff tool is not defined: " + toolNameToUse); //$NON-NLS-1$
156 		}
157 
158 		return Optional.of(
159 				compare(localFile, remoteFile, tool, trust));
160 	}
161 
162 	/**
163 	 * Compare two versions of a file.
164 	 *
165 	 * @param localFile
166 	 *            the local file element
167 	 * @param remoteFile
168 	 *            the remote file element
169 	 * @param tool
170 	 *            the selected tool
171 	 * @param trustExitCode
172 	 *            the "trust exit code" option
173 	 * @return the execution result from tool
174 	 * @throws ToolException
175 	 */
176 	public ExecutionResult compare(FileElement localFile,
177 			FileElement remoteFile, ExternalDiffTool tool,
178 			boolean trustExitCode) throws ToolException {
179 		try {
180 			if (tool == null) {
181 				throw new ToolException(JGitText
182 						.get().diffToolNotSpecifiedInGitAttributesError);
183 			}
184 			// prepare the command (replace the file paths)
185 			String command = ExternalToolUtils.prepareCommand(tool.getCommand(),
186 					localFile, remoteFile, null, null);
187 			// prepare the environment
188 			Map<String, String> env = ExternalToolUtils.prepareEnvironment(
189 					gitDir, localFile, remoteFile, null, null);
190 			// execute the tool
191 			CommandExecutor cmdExec = new CommandExecutor(fs, trustExitCode);
192 			return cmdExec.run(command, workTree, env);
193 		} catch (IOException | InterruptedException e) {
194 			throw new ToolException(e);
195 		} finally {
196 			localFile.cleanTemporaries();
197 			remoteFile.cleanTemporaries();
198 		}
199 	}
200 
201 	/**
202 	 * Get user defined tool names.
203 	 *
204 	 * @return the user defined tool names
205 	 */
206 	public Set<String> getUserDefinedToolNames() {
207 		return userDefinedTools.keySet();
208 	}
209 
210 	/**
211 	 * Get predefined tool names.
212 	 *
213 	 * @return the predefined tool names
214 	 */
215 	public Set<String> getPredefinedToolNames() {
216 		return predefinedTools.keySet();
217 	}
218 
219 	/**
220 	 * Get all tool names.
221 	 *
222 	 * @return the all tool names (default or available tool name is the first
223 	 *         in the set)
224 	 */
225 	public Set<String> getAllToolNames() {
226 		String defaultName = getDefaultToolName(false);
227 		if (defaultName == null) {
228 			defaultName = getFirstAvailableTool();
229 		}
230 		return ExternalToolUtils.createSortedToolSet(defaultName,
231 				getUserDefinedToolNames(), getPredefinedToolNames());
232 	}
233 
234 	/**
235 	 * Provides {@link Optional} with the name of an external diff tool if
236 	 * specified in git configuration for a path.
237 	 *
238 	 * The formed git configuration results from global rules as well as merged
239 	 * rules from info and worktree attributes.
240 	 *
241 	 * Triggers {@link TreeWalk} until specified path found in the tree.
242 	 *
243 	 * @param path
244 	 *            path to the node in repository to parse git attributes for
245 	 * @return name of the difftool if set
246 	 * @throws ToolException
247 	 */
248 	public Optional<String> getExternalToolFromAttributes(final String path)
249 			throws ToolException {
250 		return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
251 				ExternalToolUtils.KEY_DIFF_TOOL);
252 	}
253 
254 	/**
255 	 * Checks the availability of the predefined tools in the system.
256 	 *
257 	 * @return set of predefined available tools
258 	 */
259 	public Set<String> getPredefinedAvailableTools() {
260 		Map<String, ExternalDiffTool> defTools = getPredefinedTools(true);
261 		Set<String> availableTools = new LinkedHashSet<>();
262 		for (Entry<String, ExternalDiffTool> elem : defTools.entrySet()) {
263 			if (elem.getValue().isAvailable()) {
264 				availableTools.add(elem.getKey());
265 			}
266 		}
267 		return availableTools;
268 	}
269 
270 	/**
271 	 * Get user defined tools map.
272 	 *
273 	 * @return the user defined tools
274 	 */
275 	public Map<String, ExternalDiffTool> getUserDefinedTools() {
276 		return Collections.unmodifiableMap(userDefinedTools);
277 	}
278 
279 	/**
280 	 * Get predefined tools map.
281 	 *
282 	 * @param checkAvailability
283 	 *            true: for checking if tools can be executed; ATTENTION: this
284 	 *            check took some time, do not execute often (store the map for
285 	 *            other actions); false: availability is NOT checked:
286 	 *            isAvailable() returns default false is this case!
287 	 * @return the predefined tools with optionally checked availability (long
288 	 *         running operation)
289 	 */
290 	public Map<String, ExternalDiffTool> getPredefinedTools(
291 			boolean checkAvailability) {
292 		if (checkAvailability) {
293 			for (ExternalDiffTool tool : predefinedTools.values()) {
294 				PreDefinedDiffTool predefTool = (PreDefinedDiffTool) tool;
295 				predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
296 						gitDir, workTree, predefTool.getPath()));
297 			}
298 		}
299 		return Collections.unmodifiableMap(predefinedTools);
300 	}
301 
302 	/**
303 	 * Get first available tool name.
304 	 *
305 	 * @return the name of first available predefined tool or null
306 	 */
307 	public String getFirstAvailableTool() {
308 		for (ExternalDiffTool tool : predefinedTools.values()) {
309 			if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
310 					tool.getPath())) {
311 				return tool.getName();
312 			}
313 		}
314 		return null;
315 	}
316 
317 	/**
318 	 * Get default (gui-)tool name.
319 	 *
320 	 * @param gui
321 	 *            use the diff.guitool setting ?
322 	 * @return the default tool name
323 	 */
324 	public String getDefaultToolName(boolean gui) {
325 		String guiToolName;
326 		if (gui) {
327 			guiToolName = config.getDefaultGuiToolName();
328 			if (guiToolName != null) {
329 				return guiToolName;
330 			}
331 		}
332 		return config.getDefaultToolName();
333 	}
334 
335 	/**
336 	 * Is interactive diff (prompt enabled) ?
337 	 *
338 	 * @return is interactive (config prompt enabled) ?
339 	 */
340 	public boolean isInteractive() {
341 		return config.isPrompt();
342 	}
343 
344 	private ExternalDiffTool getTool(final String name) {
345 		ExternalDiffTool tool = userDefinedTools.get(name);
346 		if (tool == null) {
347 			tool = predefinedTools.get(name);
348 		}
349 		return tool;
350 	}
351 
352 	private static Map<String, ExternalDiffTool> setupPredefinedTools() {
353 		Map<String, ExternalDiffTool> tools = new TreeMap<>();
354 		for (CommandLineDiffTool tool : CommandLineDiffTool.values()) {
355 			tools.put(tool.name(), new PreDefinedDiffTool(tool));
356 		}
357 		return tools;
358 	}
359 
360 	private Map<String, ExternalDiffTool> setupUserDefinedTools(
361 			Map<String, ExternalDiffTool> predefTools) {
362 		Map<String, ExternalDiffTool> tools = new TreeMap<>();
363 		Map<String, ExternalDiffTool> userTools = config.getTools();
364 		for (String name : userTools.keySet()) {
365 			ExternalDiffTool userTool = userTools.get(name);
366 			// if difftool.<name>.cmd is defined we have user defined tool
367 			if (userTool.getCommand() != null) {
368 				tools.put(name, userTool);
369 			} else if (userTool.getPath() != null) {
370 				// if difftool.<name>.path is defined we just overload the path
371 				// of predefined tool
372 				PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefTools
373 						.get(name);
374 				if (predefTool != null) {
375 					predefTool.setPath(userTool.getPath());
376 				}
377 			}
378 		}
379 		return tools;
380 	}
381 
382 }