/* * Copyright (C) 2020-2021, Simeon Andreev and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.diffmergetool; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.util.FS.ExecutionResult; import org.junit.Test; /** * Testing external diff tools. */ public class ExternalDiffToolTest extends ExternalToolTestCase { @Test(expected = ToolException.class) public void testUserToolWithError() throws Exception { String toolName = "customTool"; int errorReturnCode = 1; String command = "exit " + errorReturnCode; FileBasedConfig config = db.getConfig(); config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, command); invokeCompare(toolName); fail("Expected exception to be thrown due to external tool exiting with error code: " + errorReturnCode); } @Test(expected = ToolException.class) public void testUserToolWithCommandNotFoundError() throws Exception { String toolName = "customTool"; int errorReturnCode = 127; // command not found String command = "exit " + errorReturnCode; FileBasedConfig config = db.getConfig(); config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, command); invokeCompare(toolName); fail("Expected exception to be thrown due to external tool exiting with error code: " + errorReturnCode); } @Test public void testUserDefinedTool() throws Exception { String command = getEchoCommand(); FileBasedConfig config = db.getConfig(); String customToolName = "customTool"; config.setString(CONFIG_DIFFTOOL_SECTION, customToolName, CONFIG_KEY_CMD, command); DiffTools manager = new DiffTools(db); Map tools = manager.getUserDefinedTools(); ExternalDiffTool externalTool = tools.get(customToolName); boolean trustExitCode = true; manager.compare(local, remote, externalTool, trustExitCode); assertEchoCommandHasCorrectOutput(); } @Test public void testUserDefinedToolWithPrompt() throws Exception { String command = getEchoCommand(); FileBasedConfig config = db.getConfig(); String customToolName = "customTool"; config.setString(CONFIG_DIFFTOOL_SECTION, customToolName, CONFIG_KEY_CMD, command); DiffTools manager = new DiffTools(db); PromptHandler promptHandler = PromptHandler.acceptPrompt(); MissingToolHandler noToolHandler = new MissingToolHandler(); manager.compare(local, remote, Optional.of(customToolName), BooleanTriState.TRUE, false, BooleanTriState.TRUE, promptHandler, noToolHandler); assertEchoCommandHasCorrectOutput(); List actualToolPrompts = promptHandler.toolPrompts; List expectedToolPrompts = Arrays.asList("customTool"); assertEquals("Expected a user prompt for custom tool call", expectedToolPrompts, actualToolPrompts); assertEquals("Expected to no informing about missing tools", Collections.EMPTY_LIST, noToolHandler.missingTools); } @Test public void testUserDefinedToolWithCancelledPrompt() throws Exception { String command = getEchoCommand(); FileBasedConfig config = db.getConfig(); String customToolName = "customTool"; config.setString(CONFIG_DIFFTOOL_SECTION, customToolName, CONFIG_KEY_CMD, command); DiffTools manager = new DiffTools(db); PromptHandler promptHandler = PromptHandler.cancelPrompt(); MissingToolHandler noToolHandler = new MissingToolHandler(); Optional result = manager.compare(local, remote, Optional.of(customToolName), BooleanTriState.TRUE, false, BooleanTriState.TRUE, promptHandler, noToolHandler); assertFalse("Expected no result if user cancels the operation", result.isPresent()); } @Test public void testAllTools() { FileBasedConfig config = db.getConfig(); String customToolName = "customTool"; config.setString(CONFIG_DIFFTOOL_SECTION, customToolName, CONFIG_KEY_CMD, "echo"); DiffTools manager = new DiffTools(db); Set actualToolNames = manager.getAllToolNames(); Set expectedToolNames = new LinkedHashSet<>(); expectedToolNames.add(customToolName); CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); for (CommandLineDiffTool defaultTool : defaultTools) { String toolName = defaultTool.name(); expectedToolNames.add(toolName); } assertEquals("Incorrect set of external diff tools", expectedToolNames, actualToolNames); } @Test public void testOverridePredefinedToolPath() { String toolName = CommandLineDiffTool.guiffy.name(); String customToolPath = "/usr/bin/echo"; FileBasedConfig config = db.getConfig(); config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, "echo"); config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_PATH, customToolPath); DiffTools manager = new DiffTools(db); Map tools = manager.getUserDefinedTools(); ExternalDiffTool diffTool = tools.get(toolName); assertNotNull("Expected tool \"" + toolName + "\" to be user defined", diffTool); String toolPath = diffTool.getPath(); assertEquals("Expected external diff tool to have an overriden path", customToolPath, toolPath); } @Test public void testUserDefinedTools() { FileBasedConfig config = db.getConfig(); String customToolname = "customTool"; config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, CONFIG_KEY_CMD, "echo"); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, CONFIG_KEY_PATH, "/usr/bin/echo"); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, CONFIG_KEY_PROMPT, String.valueOf(false)); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, CONFIG_KEY_GUITOOL, String.valueOf(false)); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false)); DiffTools manager = new DiffTools(db); Set actualToolNames = manager.getUserDefinedTools().keySet(); Set expectedToolNames = new LinkedHashSet<>(); expectedToolNames.add(customToolname); assertEquals("Incorrect set of external diff tools", expectedToolNames, actualToolNames); } @Test public void testCompare() throws ToolException { String toolName = "customTool"; FileBasedConfig config = db.getConfig(); // the default diff tool is configured without a subsection String subsection = null; config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL, toolName); String command = getEchoCommand(); config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, command); Optional result = invokeCompare(toolName); assertTrue("Expected external diff tool result to be available", result.isPresent()); int expectedCompareResult = 0; assertEquals("Incorrect compare result for external diff tool", expectedCompareResult, result.get().getRc()); } @Test public void testDefaultTool() throws Exception { String toolName = "customTool"; String guiToolName = "customGuiTool"; FileBasedConfig config = db.getConfig(); // the default diff tool is configured without a subsection String subsection = null; config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL, toolName); DiffTools manager = new DiffTools(db); boolean gui = false; String defaultToolName = manager.getDefaultToolName(gui); assertEquals( "Expected configured difftool to be the default external diff tool", toolName, defaultToolName); gui = true; String defaultGuiToolName = manager.getDefaultToolName(gui); assertEquals( "Expected default gui difftool to be the default tool if no gui tool is set", toolName, defaultGuiToolName); config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL, guiToolName); manager = new DiffTools(db); defaultGuiToolName = manager.getDefaultToolName(gui); assertEquals( "Expected configured difftool to be the default external diff guitool", guiToolName, defaultGuiToolName); } @Test public void testOverridePreDefinedToolPath() { String newToolPath = "/tmp/path/"; CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); assertTrue("Expected to find pre-defined external diff tools", defaultTools.length > 0); CommandLineDiffTool overridenTool = defaultTools[0]; String overridenToolName = overridenTool.name(); String overridenToolPath = newToolPath + overridenToolName; FileBasedConfig config = db.getConfig(); config.setString(CONFIG_DIFFTOOL_SECTION, overridenToolName, CONFIG_KEY_PATH, overridenToolPath); DiffTools manager = new DiffTools(db); Map availableTools = manager .getPredefinedTools(true); ExternalDiffTool externalDiffTool = availableTools .get(overridenToolName); String actualDiffToolPath = externalDiffTool.getPath(); assertEquals( "Expected pre-defined external diff tool to have overriden path", overridenToolPath, actualDiffToolPath); String expectedDiffToolCommand = overridenToolPath + " " + overridenTool.getParameters(); String actualDiffToolCommand = externalDiffTool.getCommand(); assertEquals( "Expected pre-defined external diff tool to have overriden command", expectedDiffToolCommand, actualDiffToolCommand); } @Test(expected = ToolException.class) public void testUndefinedTool() throws Exception { String toolName = "undefined"; invokeCompare(toolName); fail("Expected exception to be thrown due to not defined external diff tool"); } @Test public void testDefaultToolExecutionWithPrompt() throws Exception { FileBasedConfig config = db.getConfig(); // the default diff tool is configured without a subsection String subsection = null; config.setString("diff", subsection, "tool", "customTool"); String command = getEchoCommand(); config.setString("difftool", "customTool", "cmd", command); DiffTools manager = new DiffTools(db); PromptHandler promptHandler = PromptHandler.acceptPrompt(); MissingToolHandler noToolHandler = new MissingToolHandler(); manager.compare(local, remote, Optional.empty(), BooleanTriState.TRUE, false, BooleanTriState.TRUE, promptHandler, noToolHandler); assertEchoCommandHasCorrectOutput(); } @Test public void testNoDefaultToolName() { DiffTools manager = new DiffTools(db); boolean gui = false; String defaultToolName = manager.getDefaultToolName(gui); assertNull("Expected no default tool when none is configured", defaultToolName); gui = true; defaultToolName = manager.getDefaultToolName(gui); assertNull("Expected no default tool when none is configured", defaultToolName); } @Test public void testExternalToolInGitAttributes() throws Exception { String content = "attributes:\n*.txt difftool=customTool"; File gitattributes = writeTrashFile(".gitattributes", content); gitattributes.deleteOnExit(); try (TestRepository testRepository = new TestRepository<>( db)) { FileBasedConfig config = db.getConfig(); config.setString("difftool", "customTool", "cmd", "echo"); testRepository.git().add().addFilepattern(localFile.getName()) .call(); testRepository.git().add().addFilepattern(".gitattributes").call(); testRepository.branch("master").commit().message("first commit") .create(); DiffTools manager = new DiffTools(db); Optional tool = manager .getExternalToolFromAttributes(localFile.getName()); assertTrue("Failed to find user defined tool", tool.isPresent()); assertEquals("Failed to find user defined tool", "customTool", tool.get()); } finally { Files.delete(gitattributes.toPath()); } } @Test public void testNotExternalToolInGitAttributes() throws Exception { String content = ""; File gitattributes = writeTrashFile(".gitattributes", content); gitattributes.deleteOnExit(); try (TestRepository testRepository = new TestRepository<>( db)) { FileBasedConfig config = db.getConfig(); config.setString("difftool", "customTool", "cmd", "echo"); testRepository.git().add().addFilepattern(localFile.getName()) .call(); testRepository.git().add().addFilepattern(".gitattributes").call(); testRepository.branch("master").commit().message("first commit") .create(); DiffTools manager = new DiffTools(db); Optional tool = manager .getExternalToolFromAttributes(localFile.getName()); assertFalse( "Expected no external tool if no default tool is specified in .gitattributes", tool.isPresent()); } finally { Files.delete(gitattributes.toPath()); } } @Test(expected = ToolException.class) public void testNullTool() throws Exception { DiffTools manager = new DiffTools(db); boolean trustExitCode = true; ExternalDiffTool tool = null; manager.compare(local, remote, tool, trustExitCode); } @Test(expected = ToolException.class) public void testNullToolWithPrompt() throws Exception { DiffTools manager = new DiffTools(db); PromptHandler promptHandler = PromptHandler.cancelPrompt(); MissingToolHandler noToolHandler = new MissingToolHandler(); Optional tool = null; manager.compare(local, remote, tool, BooleanTriState.TRUE, false, BooleanTriState.TRUE, promptHandler, noToolHandler); } private Optional invokeCompare(String toolName) throws ToolException { DiffTools manager = new DiffTools(db); BooleanTriState prompt = BooleanTriState.UNSET; boolean gui = false; BooleanTriState trustExitCode = BooleanTriState.TRUE; PromptHandler promptHandler = PromptHandler.acceptPrompt(); MissingToolHandler noToolHandler = new MissingToolHandler(); Optional result = manager.compare(local, remote, Optional.of(toolName), prompt, gui, trustExitCode, promptHandler, noToolHandler); return result; } private String getEchoCommand() { return "(echo \"$LOCAL\" \"$REMOTE\") > " + commandResult.getAbsolutePath(); } private void assertEchoCommandHasCorrectOutput() throws IOException { List actualLines = Files.readAllLines(commandResult.toPath()); String actualContent = String.join(System.lineSeparator(), actualLines); actualLines = Arrays.asList(actualContent.split(" ")); List expectedLines = Arrays.asList(localFile.getAbsolutePath(), remoteFile.getAbsolutePath()); assertEquals("Dummy test tool called with unexpected arguments", expectedLines, actualLines); } }