View Javadoc
1   /*
2    * Copyright (C) 2020-2021, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.internal.diffmergetool;
11  
12  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION;
13  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION;
14  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
15  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
16  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
17  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
18  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
19  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
20  import static org.junit.Assert.assertEquals;
21  import static org.junit.Assert.assertFalse;
22  import static org.junit.Assert.assertNotNull;
23  import static org.junit.Assert.assertNull;
24  import static org.junit.Assert.assertTrue;
25  import static org.junit.Assert.fail;
26  
27  import java.io.File;
28  import java.io.IOException;
29  import java.nio.file.Files;
30  import java.util.Arrays;
31  import java.util.Collections;
32  import java.util.LinkedHashSet;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Optional;
36  import java.util.Set;
37  
38  import org.eclipse.jgit.junit.TestRepository;
39  import org.eclipse.jgit.lib.Repository;
40  import org.eclipse.jgit.lib.internal.BooleanTriState;
41  import org.eclipse.jgit.storage.file.FileBasedConfig;
42  import org.eclipse.jgit.util.FS.ExecutionResult;
43  import org.junit.Test;
44  
45  /**
46   * Testing external diff tools.
47   */
48  public class ExternalDiffToolTest extends ExternalToolTestCase {
49  
50  	@Test(expected = ToolException.class)
51  	public void testUserToolWithError() throws Exception {
52  		String toolName = "customTool";
53  
54  		int errorReturnCode = 1;
55  		String command = "exit " + errorReturnCode;
56  
57  		FileBasedConfig config = db.getConfig();
58  		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
59  				command);
60  
61  		invokeCompare(toolName);
62  
63  		fail("Expected exception to be thrown due to external tool exiting with error code: "
64  				+ errorReturnCode);
65  	}
66  
67  	@Test(expected = ToolException.class)
68  	public void testUserToolWithCommandNotFoundError() throws Exception {
69  		String toolName = "customTool";
70  
71  		int errorReturnCode = 127; // command not found
72  		String command = "exit " + errorReturnCode;
73  
74  		FileBasedConfig config = db.getConfig();
75  		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
76  				command);
77  
78  		invokeCompare(toolName);
79  		fail("Expected exception to be thrown due to external tool exiting with error code: "
80  				+ errorReturnCode);
81  	}
82  
83  	@Test
84  	public void testUserDefinedTool() throws Exception {
85  		String command = getEchoCommand();
86  
87  		FileBasedConfig config = db.getConfig();
88  		String customToolName = "customTool";
89  		config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
90  				CONFIG_KEY_CMD, command);
91  
92  		DiffTools manager = new DiffTools(db);
93  
94  		Map<String, ExternalDiffTool> tools = manager.getUserDefinedTools();
95  		ExternalDiffTool externalTool = tools.get(customToolName);
96  		boolean trustExitCode = true;
97  		manager.compare(local, remote, externalTool, trustExitCode);
98  
99  		assertEchoCommandHasCorrectOutput();
100 	}
101 
102 	@Test
103 	public void testUserDefinedToolWithPrompt() throws Exception {
104 		String command = getEchoCommand();
105 
106 		FileBasedConfig config = db.getConfig();
107 		String customToolName = "customTool";
108 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
109 				CONFIG_KEY_CMD, command);
110 
111 		DiffTools manager = new DiffTools(db);
112 
113 		PromptHandler promptHandler = PromptHandler.acceptPrompt();
114 		MissingToolHandler noToolHandler = new MissingToolHandler();
115 
116 		manager.compare(local, remote, Optional.of(customToolName),
117 				BooleanTriState.TRUE, false, BooleanTriState.TRUE,
118 				promptHandler, noToolHandler);
119 
120 		assertEchoCommandHasCorrectOutput();
121 
122 		List<String> actualToolPrompts = promptHandler.toolPrompts;
123 		List<String> expectedToolPrompts = Arrays.asList("customTool");
124 		assertEquals("Expected a user prompt for custom tool call",
125 				expectedToolPrompts, actualToolPrompts);
126 
127 		assertEquals("Expected to no informing about missing tools",
128 				Collections.EMPTY_LIST, noToolHandler.missingTools);
129 	}
130 
131 	@Test
132 	public void testUserDefinedToolWithCancelledPrompt() throws Exception {
133 		String command = getEchoCommand();
134 
135 		FileBasedConfig config = db.getConfig();
136 		String customToolName = "customTool";
137 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
138 				CONFIG_KEY_CMD, command);
139 
140 		DiffTools manager = new DiffTools(db);
141 
142 		PromptHandler promptHandler = PromptHandler.cancelPrompt();
143 		MissingToolHandler noToolHandler = new MissingToolHandler();
144 
145 		Optional<ExecutionResult> result = manager.compare(local, remote,
146 				Optional.of(customToolName), BooleanTriState.TRUE, false,
147 				BooleanTriState.TRUE, promptHandler, noToolHandler);
148 		assertFalse("Expected no result if user cancels the operation",
149 				result.isPresent());
150 	}
151 
152 	@Test
153 	public void testAllTools() {
154 		FileBasedConfig config = db.getConfig();
155 		String customToolName = "customTool";
156 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
157 				CONFIG_KEY_CMD, "echo");
158 
159 		DiffTools manager = new DiffTools(db);
160 		Set<String> actualToolNames = manager.getAllToolNames();
161 		Set<String> expectedToolNames = new LinkedHashSet<>();
162 		expectedToolNames.add(customToolName);
163 		CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
164 		for (CommandLineDiffTool defaultTool : defaultTools) {
165 			String toolName = defaultTool.name();
166 			expectedToolNames.add(toolName);
167 		}
168 		assertEquals("Incorrect set of external diff tools", expectedToolNames,
169 				actualToolNames);
170 	}
171 
172 	@Test
173 	public void testOverridePredefinedToolPath() {
174 		String toolName = CommandLineDiffTool.guiffy.name();
175 		String customToolPath = "/usr/bin/echo";
176 
177 		FileBasedConfig config = db.getConfig();
178 		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
179 				"echo");
180 		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_PATH,
181 				customToolPath);
182 
183 		DiffTools manager = new DiffTools(db);
184 		Map<String, ExternalDiffTool> tools = manager.getUserDefinedTools();
185 		ExternalDiffTool diffTool = tools.get(toolName);
186 		assertNotNull("Expected tool \"" + toolName + "\" to be user defined",
187 				diffTool);
188 
189 		String toolPath = diffTool.getPath();
190 		assertEquals("Expected external diff tool to have an overriden path",
191 				customToolPath, toolPath);
192 	}
193 
194 	@Test
195 	public void testUserDefinedTools() {
196 		FileBasedConfig config = db.getConfig();
197 		String customToolname = "customTool";
198 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
199 				CONFIG_KEY_CMD, "echo");
200 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
201 				CONFIG_KEY_PATH, "/usr/bin/echo");
202 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
203 				CONFIG_KEY_PROMPT, String.valueOf(false));
204 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
205 				CONFIG_KEY_GUITOOL, String.valueOf(false));
206 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
207 				CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false));
208 		DiffTools manager = new DiffTools(db);
209 		Set<String> actualToolNames = manager.getUserDefinedTools().keySet();
210 		Set<String> expectedToolNames = new LinkedHashSet<>();
211 		expectedToolNames.add(customToolname);
212 		assertEquals("Incorrect set of external diff tools", expectedToolNames,
213 				actualToolNames);
214 	}
215 
216 	@Test
217 	public void testCompare() throws ToolException {
218 		String toolName = "customTool";
219 
220 		FileBasedConfig config = db.getConfig();
221 		// the default diff tool is configured without a subsection
222 		String subsection = null;
223 		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
224 				toolName);
225 
226 		String command = getEchoCommand();
227 
228 		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
229 				command);
230 		Optional<ExecutionResult> result = invokeCompare(toolName);
231 		assertTrue("Expected external diff tool result to be available",
232 				result.isPresent());
233 		int expectedCompareResult = 0;
234 		assertEquals("Incorrect compare result for external diff tool",
235 				expectedCompareResult, result.get().getRc());
236 	}
237 
238 	@Test
239 	public void testDefaultTool() throws Exception {
240 		String toolName = "customTool";
241 		String guiToolName = "customGuiTool";
242 
243 		FileBasedConfig config = db.getConfig();
244 		// the default diff tool is configured without a subsection
245 		String subsection = null;
246 		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
247 				toolName);
248 
249 		DiffTools manager = new DiffTools(db);
250 		boolean gui = false;
251 		String defaultToolName = manager.getDefaultToolName(gui);
252 		assertEquals(
253 				"Expected configured difftool to be the default external diff tool",
254 				toolName, defaultToolName);
255 
256 		gui = true;
257 		String defaultGuiToolName = manager.getDefaultToolName(gui);
258 		assertEquals(
259 				"Expected default gui difftool to be the default tool if no gui tool is set",
260 				toolName, defaultGuiToolName);
261 
262 		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL,
263 				guiToolName);
264 		manager = new DiffTools(db);
265 		defaultGuiToolName = manager.getDefaultToolName(gui);
266 		assertEquals(
267 				"Expected configured difftool to be the default external diff guitool",
268 				guiToolName, defaultGuiToolName);
269 	}
270 
271 	@Test
272 	public void testOverridePreDefinedToolPath() {
273 		String newToolPath = "/tmp/path/";
274 
275 		CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
276 		assertTrue("Expected to find pre-defined external diff tools",
277 				defaultTools.length > 0);
278 
279 		CommandLineDiffTool overridenTool = defaultTools[0];
280 		String overridenToolName = overridenTool.name();
281 		String overridenToolPath = newToolPath + overridenToolName;
282 		FileBasedConfig config = db.getConfig();
283 		config.setString(CONFIG_DIFFTOOL_SECTION, overridenToolName,
284 				CONFIG_KEY_PATH, overridenToolPath);
285 
286 		DiffTools manager = new DiffTools(db);
287 		Map<String, ExternalDiffTool> availableTools = manager
288 				.getPredefinedTools(true);
289 		ExternalDiffTool externalDiffTool = availableTools
290 				.get(overridenToolName);
291 		String actualDiffToolPath = externalDiffTool.getPath();
292 		assertEquals(
293 				"Expected pre-defined external diff tool to have overriden path",
294 				overridenToolPath, actualDiffToolPath);
295 		String expectedDiffToolCommand = overridenToolPath + " "
296 				+ overridenTool.getParameters();
297 		String actualDiffToolCommand = externalDiffTool.getCommand();
298 		assertEquals(
299 				"Expected pre-defined external diff tool to have overriden command",
300 				expectedDiffToolCommand, actualDiffToolCommand);
301 	}
302 
303 	@Test(expected = ToolException.class)
304 	public void testUndefinedTool() throws Exception {
305 		String toolName = "undefined";
306 		invokeCompare(toolName);
307 		fail("Expected exception to be thrown due to not defined external diff tool");
308 	}
309 
310 	@Test
311 	public void testDefaultToolExecutionWithPrompt() throws Exception {
312 		FileBasedConfig config = db.getConfig();
313 		// the default diff tool is configured without a subsection
314 		String subsection = null;
315 		config.setString("diff", subsection, "tool", "customTool");
316 
317 		String command = getEchoCommand();
318 
319 		config.setString("difftool", "customTool", "cmd", command);
320 
321 		DiffTools manager = new DiffTools(db);
322 
323 		PromptHandler promptHandler = PromptHandler.acceptPrompt();
324 		MissingToolHandler noToolHandler = new MissingToolHandler();
325 
326 		manager.compare(local, remote, Optional.empty(), BooleanTriState.TRUE,
327 				false, BooleanTriState.TRUE, promptHandler, noToolHandler);
328 
329 		assertEchoCommandHasCorrectOutput();
330 	}
331 
332 	@Test
333 	public void testNoDefaultToolName() {
334 		DiffTools manager = new DiffTools(db);
335 		boolean gui = false;
336 		String defaultToolName = manager.getDefaultToolName(gui);
337 		assertNull("Expected no default tool when none is configured",
338 				defaultToolName);
339 
340 		gui = true;
341 		defaultToolName = manager.getDefaultToolName(gui);
342 		assertNull("Expected no default tool when none is configured",
343 				defaultToolName);
344 	}
345 
346 	@Test
347 	public void testExternalToolInGitAttributes() throws Exception {
348 		String content = "attributes:\n*.txt 		difftool=customTool";
349 		File gitattributes = writeTrashFile(".gitattributes", content);
350 		gitattributes.deleteOnExit();
351 		try (TestRepository<Repository> testRepository = new TestRepository<>(
352 				db)) {
353 			FileBasedConfig config = db.getConfig();
354 			config.setString("difftool", "customTool", "cmd", "echo");
355 			testRepository.git().add().addFilepattern(localFile.getName())
356 					.call();
357 
358 			testRepository.git().add().addFilepattern(".gitattributes").call();
359 
360 			testRepository.branch("master").commit().message("first commit")
361 					.create();
362 
363 			DiffTools manager = new DiffTools(db);
364 			Optional<String> tool = manager
365 					.getExternalToolFromAttributes(localFile.getName());
366 			assertTrue("Failed to find user defined tool", tool.isPresent());
367 			assertEquals("Failed to find user defined tool", "customTool",
368 					tool.get());
369 		} finally {
370 			Files.delete(gitattributes.toPath());
371 		}
372 	}
373 
374 	@Test
375 	public void testNotExternalToolInGitAttributes() throws Exception {
376 		String content = "";
377 		File gitattributes = writeTrashFile(".gitattributes", content);
378 		gitattributes.deleteOnExit();
379 		try (TestRepository<Repository> testRepository = new TestRepository<>(
380 				db)) {
381 			FileBasedConfig config = db.getConfig();
382 			config.setString("difftool", "customTool", "cmd", "echo");
383 			testRepository.git().add().addFilepattern(localFile.getName())
384 					.call();
385 
386 			testRepository.git().add().addFilepattern(".gitattributes").call();
387 
388 			testRepository.branch("master").commit().message("first commit")
389 					.create();
390 
391 			DiffTools manager = new DiffTools(db);
392 			Optional<String> tool = manager
393 					.getExternalToolFromAttributes(localFile.getName());
394 			assertFalse(
395 					"Expected no external tool if no default tool is specified in .gitattributes",
396 					tool.isPresent());
397 		} finally {
398 			Files.delete(gitattributes.toPath());
399 		}
400 	}
401 
402 	@Test(expected = ToolException.class)
403 	public void testNullTool() throws Exception {
404 		DiffTools manager = new DiffTools(db);
405 
406 		boolean trustExitCode = true;
407 		ExternalDiffTool tool = null;
408 		manager.compare(local, remote, tool, trustExitCode);
409 	}
410 
411 	@Test(expected = ToolException.class)
412 	public void testNullToolWithPrompt() throws Exception {
413 		DiffTools manager = new DiffTools(db);
414 
415 		PromptHandler promptHandler = PromptHandler.cancelPrompt();
416 		MissingToolHandler noToolHandler = new MissingToolHandler();
417 
418 		Optional<String> tool = null;
419 		manager.compare(local, remote, tool, BooleanTriState.TRUE, false,
420 				BooleanTriState.TRUE, promptHandler, noToolHandler);
421 	}
422 
423 	private Optional<ExecutionResult> invokeCompare(String toolName)
424 			throws ToolException {
425 		DiffTools manager = new DiffTools(db);
426 
427 		BooleanTriState prompt = BooleanTriState.UNSET;
428 		boolean gui = false;
429 		BooleanTriState trustExitCode = BooleanTriState.TRUE;
430 		PromptHandler promptHandler = PromptHandler.acceptPrompt();
431 		MissingToolHandler noToolHandler = new MissingToolHandler();
432 
433 		Optional<ExecutionResult> result = manager.compare(local, remote,
434 				Optional.of(toolName), prompt, gui, trustExitCode,
435 				promptHandler, noToolHandler);
436 		return result;
437 	}
438 
439 	private String getEchoCommand() {
440 		return "(echo \"$LOCAL\" \"$REMOTE\") > "
441 				+ commandResult.getAbsolutePath();
442 	}
443 
444 	private void assertEchoCommandHasCorrectOutput() throws IOException {
445 		List<String> actualLines = Files.readAllLines(commandResult.toPath());
446 		String actualContent = String.join(System.lineSeparator(), actualLines);
447 		actualLines = Arrays.asList(actualContent.split(" "));
448 		List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(),
449 				remoteFile.getAbsolutePath());
450 		assertEquals("Dummy test tool called with unexpected arguments",
451 				expectedLines, actualLines);
452 	}
453 }