View Javadoc
1   /*
2    * Copyright (C) 2020-2022, 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_KEY_CMD;
13  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
14  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
15  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
16  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
17  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
18  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
19  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
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  import static org.junit.Assume.assumeTrue;
27  
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.lib.internal.BooleanTriState;
39  import org.eclipse.jgit.storage.file.FileBasedConfig;
40  import org.eclipse.jgit.util.FS.ExecutionResult;
41  import org.junit.Test;
42  
43  /**
44   * Testing external merge tools.
45   */
46  public class ExternalMergeToolTest extends ExternalToolTestCase {
47  
48  	@Test(expected = ToolException.class)
49  	public void testUserToolWithError() throws Exception {
50  		String toolName = "customTool";
51  
52  		int errorReturnCode = 1;
53  		String command = "exit " + errorReturnCode;
54  
55  		FileBasedConfig config = db.getConfig();
56  		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
57  				command);
58  		config.setString(CONFIG_MERGETOOL_SECTION, toolName,
59  				CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(Boolean.TRUE));
60  
61  		invokeMerge(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_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
76  				command);
77  
78  		invokeMerge(toolName);
79  
80  		fail("Expected exception to be thrown due to external tool exiting with error code: "
81  				+ errorReturnCode);
82  	}
83  
84  	@Test
85  	public void testKdiff3() throws Exception {
86  		assumePosixPlatform();
87  
88  		CommandLineMergeTool autoMergingTool = CommandLineMergeTool.kdiff3;
89  		assumeMergeToolIsAvailable(autoMergingTool);
90  
91  		CommandLineMergeTool tool = autoMergingTool;
92  		PreDefinedMergeTool externalTool = new PreDefinedMergeTool(tool.name(),
93  				tool.getPath(), tool.getParameters(true),
94  				tool.getParameters(false),
95  				tool.isExitCodeTrustable() ? BooleanTriState.TRUE
96  						: BooleanTriState.FALSE);
97  
98  		MergeTools manager = new MergeTools(db);
99  		ExecutionResult result = manager.merge(local, remote, merged, null,
100 				null, externalTool);
101 		assertEquals("Expected merge tool to succeed", 0, result.getRc());
102 
103 		List<String> actualLines = Files.readAllLines(mergedFile.toPath());
104 		String actualMergeResult = String.join(System.lineSeparator(),
105 				actualLines);
106 		String expectedMergeResult = DEFAULT_CONTENT;
107 		assertEquals(
108 				"Failed to merge equal local and remote versions with pre-defined tool: "
109 						+ tool.getPath(),
110 				expectedMergeResult, actualMergeResult);
111 	}
112 
113 	@Test
114 	public void testUserDefinedTool() throws Exception {
115 		String customToolName = "customTool";
116 		String command = getEchoCommand();
117 
118 		FileBasedConfig config = db.getConfig();
119 		config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
120 				CONFIG_KEY_CMD, command);
121 
122 		MergeTools manager = new MergeTools(db);
123 		Map<String, ExternalMergeTool> tools = manager.getUserDefinedTools();
124 		ExternalMergeTool externalTool = tools.get(customToolName);
125 		manager.merge(local, remote, merged, base, null, externalTool);
126 
127 		assertEchoCommandHasCorrectOutput();
128 	}
129 
130 	@Test
131 	public void testUserDefinedToolWithPrompt() throws Exception {
132 		String customToolName = "customTool";
133 		String command = getEchoCommand();
134 
135 		FileBasedConfig config = db.getConfig();
136 		config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
137 				CONFIG_KEY_CMD, command);
138 
139 		MergeTools manager = new MergeTools(db);
140 
141 		PromptHandler promptHandler = PromptHandler.acceptPrompt();
142 		MissingToolHandler noToolHandler = new MissingToolHandler();
143 
144 		manager.merge(local, remote, merged, base, null,
145 				Optional.of(customToolName), BooleanTriState.TRUE, false,
146 				promptHandler, noToolHandler);
147 
148 		assertEchoCommandHasCorrectOutput();
149 
150 		List<String> actualToolPrompts = promptHandler.toolPrompts;
151 		List<String> expectedToolPrompts = Arrays.asList("customTool");
152 		assertEquals("Expected a user prompt for custom tool call",
153 				expectedToolPrompts, actualToolPrompts);
154 
155 		assertEquals("Expected to no informing about missing tools",
156 				Collections.EMPTY_LIST, noToolHandler.missingTools);
157 	}
158 
159 	@Test
160 	public void testUserDefinedToolWithCancelledPrompt() throws Exception {
161 		MergeTools manager = new MergeTools(db);
162 
163 		PromptHandler promptHandler = PromptHandler.cancelPrompt();
164 		MissingToolHandler noToolHandler = new MissingToolHandler();
165 
166 		Optional<ExecutionResult> result = manager.merge(local, remote, merged,
167 				base, null, Optional.empty(), BooleanTriState.TRUE, false,
168 				promptHandler, noToolHandler);
169 		assertFalse("Expected no result if user cancels the operation",
170 				result.isPresent());
171 	}
172 
173 	@Test
174 	public void testAllTools() {
175 		FileBasedConfig config = db.getConfig();
176 		String customToolName = "customTool";
177 		config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
178 				CONFIG_KEY_CMD, "echo");
179 
180 		MergeTools manager = new MergeTools(db);
181 		Set<String> actualToolNames = manager.getAllToolNames();
182 		Set<String> expectedToolNames = new LinkedHashSet<>();
183 		expectedToolNames.add(customToolName);
184 		CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
185 		for (CommandLineMergeTool defaultTool : defaultTools) {
186 			String toolName = defaultTool.name();
187 			expectedToolNames.add(toolName);
188 		}
189 		assertEquals("Incorrect set of external merge tools", expectedToolNames,
190 				actualToolNames);
191 	}
192 
193 	@Test
194 	public void testOverridePredefinedToolPath() {
195 		String toolName = CommandLineMergeTool.guiffy.name();
196 		String customToolPath = "/usr/bin/echo";
197 
198 		FileBasedConfig config = db.getConfig();
199 		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
200 				"echo");
201 		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PATH,
202 				customToolPath);
203 
204 		MergeTools manager = new MergeTools(db);
205 		Map<String, ExternalMergeTool> tools = manager.getUserDefinedTools();
206 		ExternalMergeTool mergeTool = tools.get(toolName);
207 		assertNotNull("Expected tool \"" + toolName + "\" to be user defined",
208 				mergeTool);
209 
210 		String toolPath = mergeTool.getPath();
211 		assertEquals("Expected external merge tool to have an overriden path",
212 				customToolPath, toolPath);
213 	}
214 
215 	@Test
216 	public void testUserDefinedTools() {
217 		FileBasedConfig config = db.getConfig();
218 		String customToolname = "customTool";
219 		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
220 				CONFIG_KEY_CMD, "echo");
221 		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
222 				CONFIG_KEY_PATH, "/usr/bin/echo");
223 		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
224 				CONFIG_KEY_PROMPT, String.valueOf(false));
225 		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
226 				CONFIG_KEY_GUITOOL, String.valueOf(false));
227 		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
228 				CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false));
229 		MergeTools manager = new MergeTools(db);
230 		Set<String> actualToolNames = manager.getUserDefinedTools().keySet();
231 		Set<String> expectedToolNames = new LinkedHashSet<>();
232 		expectedToolNames.add(customToolname);
233 		assertEquals("Incorrect set of external merge tools", expectedToolNames,
234 				actualToolNames);
235 	}
236 
237 	@Test
238 	public void testCompare() throws ToolException {
239 		String toolName = "customTool";
240 
241 		FileBasedConfig config = db.getConfig();
242 		// the default merge tool is configured without a subsection
243 		String subsection = null;
244 		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
245 				toolName);
246 
247 		String command = getEchoCommand();
248 
249 		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
250 				command);
251 
252 		Optional<ExecutionResult> result = invokeMerge(toolName);
253 		assertTrue("Expected external merge tool result to be available",
254 				result.isPresent());
255 		int expectedCompareResult = 0;
256 		assertEquals("Incorrect compare result for external merge tool",
257 				expectedCompareResult, result.get().getRc());
258 	}
259 
260 	@Test
261 	public void testDefaultTool() throws Exception {
262 		String toolName = "customTool";
263 		String guiToolName = "customGuiTool";
264 
265 		FileBasedConfig config = db.getConfig();
266 		// the default merge tool is configured without a subsection
267 		String subsection = null;
268 		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
269 				toolName);
270 
271 		MergeTools manager = new MergeTools(db);
272 		boolean gui = false;
273 		String defaultToolName = manager.getDefaultToolName(gui);
274 		assertEquals(
275 				"Expected configured mergetool to be the default external merge tool",
276 				toolName, defaultToolName);
277 
278 		gui = true;
279 		String defaultGuiToolName = manager.getDefaultToolName(gui);
280 		assertNull("Expected default mergetool to not be set",
281 				defaultGuiToolName);
282 
283 		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL,
284 				guiToolName);
285 		manager = new MergeTools(db);
286 		defaultGuiToolName = manager.getDefaultToolName(gui);
287 		assertEquals(
288 				"Expected configured mergetool to be the default external merge guitool",
289 				guiToolName, defaultGuiToolName);
290 	}
291 
292 	@Test
293 	public void testOverridePreDefinedToolPath() {
294 		String newToolPath = "/tmp/path/";
295 
296 		CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
297 		assertTrue("Expected to find pre-defined external merge tools",
298 				defaultTools.length > 0);
299 
300 		CommandLineMergeTool overridenTool = defaultTools[0];
301 		String overridenToolName = overridenTool.name();
302 		String overridenToolPath = newToolPath + overridenToolName;
303 		FileBasedConfig config = db.getConfig();
304 		config.setString(CONFIG_MERGETOOL_SECTION, overridenToolName,
305 				CONFIG_KEY_PATH, overridenToolPath);
306 
307 		MergeTools manager = new MergeTools(db);
308 		Map<String, ExternalMergeTool> availableTools = manager
309 				.getPredefinedTools(true);
310 		ExternalMergeTool externalMergeTool = availableTools
311 				.get(overridenToolName);
312 		String actualMergeToolPath = externalMergeTool.getPath();
313 		assertEquals(
314 				"Expected pre-defined external merge tool to have overriden path",
315 				overridenToolPath, actualMergeToolPath);
316 		boolean withBase = true;
317 		String expectedMergeToolCommand = overridenToolPath + " "
318 				+ overridenTool.getParameters(withBase);
319 		String actualMergeToolCommand = externalMergeTool.getCommand();
320 		assertEquals(
321 				"Expected pre-defined external merge tool to have overriden command",
322 				expectedMergeToolCommand, actualMergeToolCommand);
323 	}
324 
325 	@Test(expected = ToolException.class)
326 	public void testUndefinedTool() throws Exception {
327 		String toolName = "undefined";
328 		invokeMerge(toolName);
329 		fail("Expected exception to be thrown due to not defined external merge tool");
330 	}
331 
332 	@Test
333 	public void testDefaultToolExecutionWithPrompt() throws Exception {
334 		FileBasedConfig config = db.getConfig();
335 		// the default diff tool is configured without a subsection
336 		String subsection = null;
337 		config.setString("merge", subsection, "tool", "customTool");
338 
339 		String command = getEchoCommand();
340 
341 		config.setString("mergetool", "customTool", "cmd", command);
342 
343 		MergeTools manager = new MergeTools(db);
344 
345 		PromptHandler promptHandler = PromptHandler.acceptPrompt();
346 		MissingToolHandler noToolHandler = new MissingToolHandler();
347 
348 		manager.merge(local, remote, merged, base, null, Optional.empty(),
349 				BooleanTriState.TRUE, false, promptHandler, noToolHandler);
350 
351 		assertEchoCommandHasCorrectOutput();
352 	}
353 
354 	@Test
355 	public void testNoDefaultToolName() {
356 		MergeTools manager = new MergeTools(db);
357 		boolean gui = false;
358 		String defaultToolName = manager.getDefaultToolName(gui);
359 		assertNull("Expected no default tool when none is configured",
360 				defaultToolName);
361 
362 		gui = true;
363 		defaultToolName = manager.getDefaultToolName(gui);
364 		assertNull("Expected no default tool when none is configured",
365 				defaultToolName);
366 	}
367 
368 	@Test(expected = ToolException.class)
369 	public void testNullTool() throws Exception {
370 		MergeTools manager = new MergeTools(db);
371 
372 		PromptHandler promptHandler = null;
373 		MissingToolHandler noToolHandler = null;
374 
375 		Optional<String> tool = null;
376 
377 		manager.merge(local, remote, merged, base, null, tool,
378 				BooleanTriState.TRUE, false, promptHandler, noToolHandler);
379 	}
380 
381 	@Test(expected = ToolException.class)
382 	public void testNullToolWithPrompt() throws Exception {
383 		MergeTools manager = new MergeTools(db);
384 
385 		PromptHandler promptHandler = PromptHandler.cancelPrompt();
386 		MissingToolHandler noToolHandler = new MissingToolHandler();
387 
388 		Optional<String> tool = null;
389 
390 		manager.merge(local, remote, merged, base, null, tool,
391 				BooleanTriState.TRUE, false, promptHandler, noToolHandler);
392 	}
393 
394 	private Optional<ExecutionResult> invokeMerge(String toolName)
395 			throws ToolException {
396 		BooleanTriState prompt = BooleanTriState.UNSET;
397 		boolean gui = false;
398 
399 		MergeTools manager = new MergeTools(db);
400 
401 		PromptHandler promptHandler = PromptHandler.acceptPrompt();
402 		MissingToolHandler noToolHandler = new MissingToolHandler();
403 
404 		Optional<ExecutionResult> result = manager.merge(local, remote, merged,
405 				base, null, Optional.of(toolName), prompt, gui, promptHandler,
406 				noToolHandler);
407 		return result;
408 	}
409 
410 	private void assumeMergeToolIsAvailable(
411 			CommandLineMergeTool autoMergingTool) {
412 		boolean isAvailable = ExternalToolUtils.isToolAvailable(db.getFS(),
413 				db.getDirectory(), db.getWorkTree(), autoMergingTool.getPath());
414 		assumeTrue("Assuming external tool is available: "
415 				+ autoMergingTool.name(), isAvailable);
416 	}
417 
418 	private String getEchoCommand() {
419 		return "(echo $LOCAL $REMOTE $MERGED $BASE) > "
420 				+ commandResult.getAbsolutePath();
421 	}
422 
423 	private void assertEchoCommandHasCorrectOutput() throws IOException {
424 		List<String> actualLines = Files.readAllLines(commandResult.toPath());
425 		String actualContent = String.join(System.lineSeparator(), actualLines);
426 		actualLines = Arrays.asList(actualContent.split(" "));
427 		List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(),
428 				remoteFile.getAbsolutePath(), mergedFile.getAbsolutePath(),
429 				baseFile.getAbsolutePath());
430 		assertEquals("Dummy test tool called with unexpected arguments",
431 				expectedLines, actualLines);
432 	}
433 }