View Javadoc
1   /*
2    * Copyright (C) 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.pgm;
11  
12  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
13  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
14  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
15  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
16  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
17  import static org.junit.Assert.fail;
18  
19  import java.io.InputStream;
20  import java.nio.file.Path;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.List;
24  import java.util.Map;
25  
26  import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
27  import org.eclipse.jgit.internal.diffmergetool.MergeTools;
28  import org.eclipse.jgit.lib.StoredConfig;
29  import org.junit.Before;
30  import org.junit.Test;
31  
32  /**
33   * Testing the {@code mergetool} command.
34   */
35  public class MergeToolTest extends ToolTestCase {
36  
37  	private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION;
38  
39  	@Override
40  	@Before
41  	public void setUp() throws Exception {
42  		super.setUp();
43  		configureEchoTool(TOOL_NAME);
44  	}
45  
46  	@Test
47  	public void testUndefinedTool() throws Exception {
48  		String toolName = "undefined";
49  		String[] conflictingFilenames = createMergeConflict();
50  
51  		List<String> expectedErrors = new ArrayList<>();
52  		for (String conflictingFilename : conflictingFilenames) {
53  			expectedErrors.add("External merge tool is not defined: " + toolName);
54  			expectedErrors.add("merge of " + conflictingFilename + " failed");
55  		}
56  
57  		runAndCaptureUsingInitRaw(expectedErrors, MERGE_TOOL,
58  				"--no-prompt", "--tool", toolName);
59  	}
60  
61  	@Test(expected = Die.class)
62  	public void testUserToolWithCommandNotFoundError() throws Exception {
63  		String toolName = "customTool";
64  
65  		int errorReturnCode = 127; // command not found
66  		String command = "exit " + errorReturnCode;
67  
68  		StoredConfig config = db.getConfig();
69  		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
70  				command);
71  
72  		createMergeConflict();
73  		runAndCaptureUsingInitRaw(MERGE_TOOL, "--no-prompt", "--tool",
74  				toolName);
75  
76  		fail("Expected exception to be thrown due to external tool exiting with error code: "
77  				+ errorReturnCode);
78  	}
79  
80  	@Test
81  	public void testEmptyToolName() throws Exception {
82  		assumeLinuxPlatform();
83  
84  		String emptyToolName = "";
85  
86  		StoredConfig config = db.getConfig();
87  		// the default merge tool is configured without a subsection
88  		String subsection = null;
89  		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
90  				emptyToolName);
91  
92  		createMergeConflict();
93  
94  		String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
95  		String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
96  		runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput),
97  				MERGE_TOOL, "--no-prompt");
98  	}
99  
100 	@Test
101 	public void testAbortMerge() throws Exception {
102 		String[] inputLines = {
103 				"y", // start tool for merge resolution
104 				"n", // don't accept merge tool result
105 				"n", // don't continue resolution
106 		};
107 		String[] conflictingFilenames = createMergeConflict();
108 		int abortIndex = 1;
109 		String[] expectedOutput = getExpectedAbortMergeOutput(
110 				conflictingFilenames,
111 				abortIndex);
112 
113 		String option = "--tool";
114 
115 		InputStream inputStream = createInputStream(inputLines);
116 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
117 				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
118 						MERGE_TOOL, "--prompt", option, TOOL_NAME));
119 	}
120 
121 	@Test
122 	public void testAbortLaunch() throws Exception {
123 		String[] inputLines = {
124 				"n", // abort merge tool launch
125 		};
126 		String[] conflictingFilenames = createMergeConflict();
127 		String[] expectedOutput = getExpectedAbortLaunchOutput(
128 				conflictingFilenames);
129 
130 		String option = "--tool";
131 
132 		InputStream inputStream = createInputStream(inputLines);
133 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
134 				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
135 						MERGE_TOOL, "--prompt", option, TOOL_NAME));
136 	}
137 
138 	@Test
139 	public void testMergeConflict() throws Exception {
140 		String[] inputLines = {
141 				"y", // start tool for merge resolution
142 				"y", // accept merge result as successful
143 				"y", // start tool for merge resolution
144 				"y", // accept merge result as successful
145 		};
146 		String[] conflictingFilenames = createMergeConflict();
147 		String[] expectedOutput = getExpectedMergeConflictOutput(
148 				conflictingFilenames);
149 
150 		String option = "--tool";
151 
152 		InputStream inputStream = createInputStream(inputLines);
153 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
154 				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
155 						MERGE_TOOL, "--prompt", option, TOOL_NAME));
156 	}
157 
158 	@Test
159 	public void testDeletedConflict() throws Exception {
160 		String[] inputLines = {
161 				"d", // choose delete option to resolve conflict
162 				"m", // choose merge option to resolve conflict
163 		};
164 		String[] conflictingFilenames = createDeletedConflict();
165 		String[] expectedOutput = getExpectedDeletedConflictOutput(
166 				conflictingFilenames);
167 
168 		String option = "--tool";
169 
170 		InputStream inputStream = createInputStream(inputLines);
171 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
172 				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
173 						MERGE_TOOL, "--prompt", option, TOOL_NAME));
174 	}
175 
176 	@Test
177 	public void testNoConflict() throws Exception {
178 		createStagedChanges();
179 		String[] expectedOutput = { "No files need merging" };
180 
181 		String[] options = { "--tool", "-t", };
182 
183 		for (String option : options) {
184 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
185 					expectedOutput,
186 					runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME));
187 		}
188 	}
189 
190 	@Test
191 	public void testMergeConflictNoPrompt() throws Exception {
192 		String[] conflictingFilenames = createMergeConflict();
193 		String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt(
194 				conflictingFilenames);
195 
196 		String option = "--tool";
197 
198 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
199 				expectedOutput,
200 				runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME));
201 	}
202 
203 	@Test
204 	public void testMergeConflictNoGuiNoPrompt() throws Exception {
205 		String[] conflictingFilenames = createMergeConflict();
206 		String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt(
207 				conflictingFilenames);
208 
209 		String option = "--tool";
210 
211 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
212 				expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL,
213 						"--no-gui", "--no-prompt", option, TOOL_NAME));
214 	}
215 
216 	@Test
217 	public void testToolHelp() throws Exception {
218 		List<String> expectedOutput = new ArrayList<>();
219 
220 		MergeTools diffTools = new MergeTools(db);
221 		Map<String, ExternalMergeTool> predefinedTools = diffTools
222 				.getPredefinedTools(true);
223 		List<ExternalMergeTool> availableTools = new ArrayList<>();
224 		List<ExternalMergeTool> notAvailableTools = new ArrayList<>();
225 		for (ExternalMergeTool tool : predefinedTools.values()) {
226 			if (tool.isAvailable()) {
227 				availableTools.add(tool);
228 			} else {
229 				notAvailableTools.add(tool);
230 			}
231 		}
232 
233 		expectedOutput.add(
234 				"'git mergetool --tool=<tool>' may be set to one of the following:");
235 		for (ExternalMergeTool tool : availableTools) {
236 			String toolName = tool.getName();
237 			expectedOutput.add(toolName);
238 		}
239 		String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
240 				+ getEchoCommand();
241 		expectedOutput.add("user-defined:");
242 		expectedOutput.add(customToolHelpLine);
243 		expectedOutput.add(
244 				"The following tools are valid, but not currently available:");
245 		for (ExternalMergeTool tool : notAvailableTools) {
246 			String toolName = tool.getName();
247 			expectedOutput.add(toolName);
248 		}
249 		String[] userDefinedToolsHelp = {
250 				"Some of the tools listed above only work in a windowed",
251 				"environment. If run in a terminal-only session, they will fail.", };
252 		expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));
253 
254 		String option = "--tool-help";
255 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
256 				expectedOutput.toArray(new String[0]),
257 				runAndCaptureUsingInitRaw(MERGE_TOOL, option));
258 	}
259 
260 	private void configureEchoTool(String toolName) {
261 		StoredConfig config = db.getConfig();
262 		// the default merge tool is configured without a subsection
263 		String subsection = null;
264 		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
265 				toolName);
266 
267 		String command = getEchoCommand();
268 
269 		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
270 				command);
271 		/*
272 		 * prevent prompts as we are running in tests and there is no user to
273 		 * interact with on the command line
274 		 */
275 		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PROMPT,
276 				String.valueOf(false));
277 	}
278 
279 	private String[] getExpectedMergeConflictOutputNoPrompt(
280 			String[] conflictFilenames) {
281 		List<String> expected = new ArrayList<>();
282 		expected.add("Merging:");
283 		for (String conflictFilename : conflictFilenames) {
284 			expected.add(conflictFilename);
285 		}
286 		for (String conflictFilename : conflictFilenames) {
287 			expected.add("Normal merge conflict for '" + conflictFilename
288 					+ "':");
289 			expected.add("{local}: modified file");
290 			expected.add("{remote}: modified file");
291 			Path filePath = getFullPath(conflictFilename);
292 			expected.add(filePath.toString());
293 			expected.add(conflictFilename + " seems unchanged.");
294 		}
295 		return expected.toArray(new String[0]);
296 	}
297 
298 	private static String[] getExpectedAbortLaunchOutput(
299 			String[] conflictFilenames) {
300 		List<String> expected = new ArrayList<>();
301 		expected.add("Merging:");
302 		for (String conflictFilename : conflictFilenames) {
303 			expected.add(conflictFilename);
304 		}
305 		if (conflictFilenames.length > 1) {
306 			String conflictFilename = conflictFilenames[0];
307 			expected.add(
308 					"Normal merge conflict for '" + conflictFilename + "':");
309 			expected.add("{local}: modified file");
310 			expected.add("{remote}: modified file");
311 			expected.add("Hit return to start merge resolution tool ("
312 					+ TOOL_NAME + "):");
313 		}
314 		return expected.toArray(new String[0]);
315 	}
316 
317 	private String[] getExpectedAbortMergeOutput(
318 			String[] conflictFilenames, int abortIndex) {
319 		List<String> expected = new ArrayList<>();
320 		expected.add("Merging:");
321 		for (String conflictFilename : conflictFilenames) {
322 			expected.add(conflictFilename);
323 		}
324 		for (int i = 0; i < conflictFilenames.length; ++i) {
325 			if (i == abortIndex) {
326 				break;
327 			}
328 
329 			String conflictFilename = conflictFilenames[i];
330 			expected.add(
331 					"Normal merge conflict for '" + conflictFilename + "':");
332 			expected.add("{local}: modified file");
333 			expected.add("{remote}: modified file");
334 			Path fullPath = getFullPath(conflictFilename);
335 			expected.add("Hit return to start merge resolution tool ("
336 					+ TOOL_NAME + "): " + fullPath);
337 			expected.add(conflictFilename + " seems unchanged.");
338 			expected.add("Was the merge successful [y/n]?");
339 			if (i < conflictFilenames.length - 1) {
340 				expected.add(
341 						"\tContinue merging other unresolved paths [y/n]?");
342 			}
343 		}
344 		return expected.toArray(new String[0]);
345 	}
346 
347 	private String[] getExpectedMergeConflictOutput(
348 			String[] conflictFilenames) {
349 		List<String> expected = new ArrayList<>();
350 		expected.add("Merging:");
351 		for (String conflictFilename : conflictFilenames) {
352 			expected.add(conflictFilename);
353 		}
354 		for (int i = 0; i < conflictFilenames.length; ++i) {
355 			String conflictFilename = conflictFilenames[i];
356 			expected.add("Normal merge conflict for '" + conflictFilename
357 					+ "':");
358 			expected.add("{local}: modified file");
359 			expected.add("{remote}: modified file");
360 			Path filePath = getFullPath(conflictFilename);
361 			expected.add("Hit return to start merge resolution tool ("
362 					+ TOOL_NAME + "): " + filePath);
363 			expected.add(conflictFilename + " seems unchanged.");
364 			expected.add("Was the merge successful [y/n]?");
365 			if (i < conflictFilenames.length - 1) {
366 				// expected.add(
367 				// "\tContinue merging other unresolved paths [y/n]?");
368 			}
369 		}
370 		return expected.toArray(new String[0]);
371 	}
372 
373 	private static String[] getExpectedDeletedConflictOutput(
374 			String[] conflictFilenames) {
375 		List<String> expected = new ArrayList<>();
376 		expected.add("Merging:");
377 		for (String mergeConflictFilename : conflictFilenames) {
378 			expected.add(mergeConflictFilename);
379 		}
380 		for (int i = 0; i < conflictFilenames.length; ++i) {
381 			String conflictFilename = conflictFilenames[i];
382 			expected.add(conflictFilename + " seems unchanged.");
383 			expected.add("{local}: deleted");
384 			expected.add("{remote}: modified file");
385 			expected.add("Use (m)odified or (d)eleted file, or (a)bort?");
386 		}
387 		return expected.toArray(new String[0]);
388 	}
389 
390 	private static String getEchoCommand() {
391 		/*
392 		 * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be
393 		 * replaced with full paths to a temporary file during some of the tests
394 		 */
395 		return "(echo \"$MERGED\")";
396 	}
397 }