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