diff --git a/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..99f26c020 --- /dev/null +++ b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..99f26c020 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..99f26c020 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..99f26c020 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java index e7bf48417..8daaa6ad9 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021, Simeon Andreev and others. + * Copyright (C) 2021-2022, 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 @@ -9,72 +9,87 @@ */ package org.eclipse.jgit.pgm; -import static org.junit.Assert.assertEquals; +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_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; +import static org.junit.Assert.fail; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool; -import org.eclipse.jgit.lib.CLIRepositoryTestCase; -import org.eclipse.jgit.pgm.opt.CmdLineParser; -import org.eclipse.jgit.pgm.opt.SubcommandHandler; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.treewalk.FileTreeIterator; -import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.lib.StoredConfig; import org.junit.Before; import org.junit.Test; -import org.kohsuke.args4j.Argument; /** * Testing the {@code difftool} command. */ -public class DiffToolTest extends CLIRepositoryTestCase { - public static class GitCliJGitWrapperParser { - @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class) - TextBuiltin subcommand; +public class DiffToolTest extends ToolTestCase { - @Argument(index = 1, metaVar = "metaVar_arg") - List arguments = new ArrayList<>(); - } - - private String[] runAndCaptureUsingInitRaw(String... args) - throws Exception { - CLIGitCommand.Result result = new CLIGitCommand.Result(); - - GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); - CmdLineParser clp = new CmdLineParser(bean); - clp.parseArgument(args); - - TextBuiltin cmd = bean.subcommand; - cmd.initRaw(db, null, null, result.out, result.err); - cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()])); - if (cmd.getOutputWriter() != null) { - cmd.getOutputWriter().flush(); - } - if (cmd.getErrorWriter() != null) { - cmd.getErrorWriter().flush(); - } - return result.outLines().toArray(new String[0]); - } - - private Git git; + private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION; @Override @Before public void setUp() throws Exception { super.setUp(); - git = new Git(db); - git.commit().setMessage("initial commit").call(); + configureEchoTool(TOOL_NAME); + } + + @Test + public void testToolWithPrompt() throws Exception { + String[] inputLines = { + "y", // accept launching diff tool + "y", // accept launching diff tool + }; + + String[] conflictingFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedCompareOutput(conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + DIFF_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testToolAbortLaunch() throws Exception { + String[] inputLines = { + "y", // accept launching diff tool + "n", // don't launch diff tool + }; + + String[] conflictingFilenames = createUnstagedChanges(); + int abortIndex = 1; + String[] expectedOutput = getExpectedAbortOutput(conflictingFilenames, abortIndex); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, + runAndCaptureUsingInitRaw(inputStream, DIFF_TOOL, "--prompt", option, + TOOL_NAME)); + } + + @Test(expected = Die.class) + public void testNotDefinedTool() throws Exception { + createUnstagedChanges(); + + runAndCaptureUsingInitRaw(DIFF_TOOL, "--tool", "undefined"); + fail("Expected exception when trying to run undefined tool"); } @Test public void testTool() throws Exception { - RevCommit commit = createUnstagedChanges(); - List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] conflictFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictFilenames); String[] options = { "--tool", @@ -84,54 +99,51 @@ public void testTool() throws Exception { for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, expectedOutput, - runAndCaptureUsingInitRaw("difftool", option, - "some_tool")); + runAndCaptureUsingInitRaw(DIFF_TOOL, option, + TOOL_NAME)); } } @Test public void testToolTrustExitCode() throws Exception { - RevCommit commit = createUnstagedChanges(); - List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] conflictingFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames); String[] options = { "--tool", "-t", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", - "--trust-exit-code", option, "some_tool")); + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, + "--trust-exit-code", option, TOOL_NAME)); } } @Test public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception { - RevCommit commit = createUnstagedChanges(); - List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] conflictingFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames); String[] options = { "--tool", "-t", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, "--no-gui", "--no-prompt", "--no-trust-exit-code", - option, "some_tool")); + option, TOOL_NAME)); } } @Test public void testToolCached() throws Exception { - RevCommit commit = createStagedChanges(); - List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] conflictingFilenames = createStagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames); String[] options = { "--cached", "--staged", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", - option, "--tool", "some_tool")); + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, + option, "--tool", TOOL_NAME)); } } @@ -139,13 +151,17 @@ expectedOutput, runAndCaptureUsingInitRaw("difftool", public void testToolHelp() throws Exception { CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); List expectedOutput = new ArrayList<>(); - expectedOutput.add("git difftool --tool= may be set to one of the following:"); + expectedOutput.add( + "'git difftool --tool=' may be set to one of the following:"); for (CommandLineDiffTool defaultTool : defaultTools) { String toolName = defaultTool.name(); expectedOutput.add(toolName); } + String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " " + + getEchoCommand(); + expectedOutput.add("user-defined:"); + expectedOutput.add(customToolHelpLine); String[] userDefinedToolsHelp = { - "user-defined:", "The following tools are valid, but not currently available:", "Some of the tools listed above only work in a windowed", "environment. If run in a terminal-only session, they will fail.", @@ -154,52 +170,66 @@ public void testToolHelp() throws Exception { String option = "--tool-help"; assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput.toArray(new String[0]), runAndCaptureUsingInitRaw("difftool", option)); + expectedOutput.toArray(new String[0]), + runAndCaptureUsingInitRaw(DIFF_TOOL, option)); } - private RevCommit createUnstagedChanges() throws Exception { - writeTrashFile("a", "Hello world a"); - writeTrashFile("b", "Hello world b"); - git.add().addFilepattern(".").call(); - RevCommit commit = git.commit().setMessage("files a & b").call(); - writeTrashFile("a", "New Hello world a"); - writeTrashFile("b", "New Hello world b"); - return commit; + private void configureEchoTool(String toolName) { + StoredConfig 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); + /* + * prevent prompts as we are running in tests and there is no user to + * interact with on the command line + */ + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_PROMPT, + String.valueOf(false)); } - private RevCommit createStagedChanges() throws Exception { - RevCommit commit = createUnstagedChanges(); - git.add().addFilepattern(".").call(); - return commit; - } - - private List getRepositoryChanges(RevCommit commit) - throws Exception { - TreeWalk tw = new TreeWalk(db); - tw.addTree(commit.getTree()); - FileTreeIterator modifiedTree = new FileTreeIterator(db); - tw.addTree(modifiedTree); - List changes = DiffEntry.scan(tw); - return changes; - } - - private String[] getExpectedDiffToolOutput(List changes) { - String[] expectedToolOutput = new String[changes.size()]; - for (int i = 0; i < changes.size(); ++i) { - DiffEntry change = changes.get(i); - String newPath = change.getNewPath(); - String oldPath = change.getOldPath(); - String newIdName = change.getNewId().name(); - String oldIdName = change.getOldId().name(); - String expectedLine = "M\t" + newPath + " (" + newIdName + ")" - + "\t" + oldPath + " (" + oldIdName + ")"; + private static String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) { + String[] expectedToolOutput = new String[conflictingFilenames.length]; + for (int i = 0; i < conflictingFilenames.length; ++i) { + String newPath = conflictingFilenames[i]; + String expectedLine = newPath; expectedToolOutput[i] = expectedLine; } return expectedToolOutput; } - private static void assertArrayOfLinesEquals(String failMessage, - String[] expected, String[] actual) { - assertEquals(failMessage, toString(expected), toString(actual)); + private static String[] getExpectedCompareOutput(String[] conflictingFilenames) { + List expected = new ArrayList<>(); + int n = conflictingFilenames.length; + for (int i = 0; i < n; ++i) { + String newPath = conflictingFilenames[i]; + expected.add( + "Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'"); + expected.add("Launch '" + TOOL_NAME + "' [Y/n]?"); + expected.add(newPath); + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedAbortOutput(String[] conflictingFilenames, + int abortIndex) { + List expected = new ArrayList<>(); + int n = conflictingFilenames.length; + for (int i = 0; i < n; ++i) { + String newPath = conflictingFilenames[i]; + expected.add( + "Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'"); + expected.add("Launch '" + TOOL_NAME + "' [Y/n]?"); + if (i == abortIndex) { + break; + } + expected.add(newPath); + } + return expected.toArray(new String[0]); } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java new file mode 100644 index 000000000..2e50f0908 --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2022, 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.pgm; + +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +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_MERGETOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jgit.internal.diffmergetool.CommandLineMergeTool; +import org.eclipse.jgit.lib.StoredConfig; +import org.junit.Before; +import org.junit.Test; + +/** + * Testing the {@code mergetool} command. + */ +public class MergeToolTest extends ToolTestCase { + + private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + configureEchoTool(TOOL_NAME); + } + + @Test + public void testAbortMerge() throws Exception { + String[] inputLines = { + "y", // start tool for merge resolution + "n", // don't accept merge tool result + "n", // don't continue resolution + }; + String[] conflictingFilenames = createMergeConflict(); + int abortIndex = 1; + String[] expectedOutput = getExpectedAbortMergeOutput( + conflictingFilenames, + abortIndex); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testAbortLaunch() throws Exception { + String[] inputLines = { + "n", // abort merge tool launch + }; + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedAbortLaunchOutput( + conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testMergeConflict() throws Exception { + String[] inputLines = { + "y", // start tool for merge resolution + "y", // accept merge result as successful + "y", // start tool for merge resolution + "y", // accept merge result as successful + }; + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedMergeConflictOutput( + conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testDeletedConflict() throws Exception { + String[] inputLines = { + "d", // choose delete option to resolve conflict + "m", // choose merge option to resolve conflict + }; + String[] conflictingFilenames = createDeletedConflict(); + String[] expectedOutput = getExpectedDeletedConflictOutput( + conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testNoConflict() throws Exception { + createStagedChanges(); + String[] expectedOutput = { "No files need merging" }; + + String[] options = { "--tool", "-t", }; + + for (String option : options) { + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, + runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME)); + } + } + + @Test + public void testMergeConflictNoPrompt() throws Exception { + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt( + conflictingFilenames); + + String option = "--tool"; + + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, + runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME)); + } + + @Test + public void testMergeConflictNoGuiNoPrompt() throws Exception { + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt( + conflictingFilenames); + + String option = "--tool"; + + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL, + "--no-gui", "--no-prompt", option, TOOL_NAME)); + } + + @Test + public void testToolHelp() throws Exception { + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + List expectedOutput = new ArrayList<>(); + expectedOutput.add( + "'git mergetool --tool=' may be set to one of the following:"); + for (CommandLineMergeTool defaultTool : defaultTools) { + String toolName = defaultTool.name(); + expectedOutput.add(toolName); + } + String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " " + + getEchoCommand(); + expectedOutput.add("user-defined:"); + expectedOutput.add(customToolHelpLine); + String[] userDefinedToolsHelp = { + "The following tools are valid, but not currently available:", + "Some of the tools listed above only work in a windowed", + "environment. If run in a terminal-only session, they will fail.", }; + expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp)); + + String option = "--tool-help"; + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput.toArray(new String[0]), + runAndCaptureUsingInitRaw(MERGE_TOOL, option)); + } + + private void configureEchoTool(String toolName) { + StoredConfig config = db.getConfig(); + // the default merge tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + String command = getEchoCommand(); + + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + /* + * prevent prompts as we are running in tests and there is no user to + * interact with on the command line + */ + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PROMPT, + String.valueOf(false)); + } + + private static String[] getExpectedMergeConflictOutputNoPrompt( + String[] conflictFilenames) { + List expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + for (String conflictFilename : conflictFilenames) { + expected.add("Normal merge conflict for '" + conflictFilename + + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + expected.add(conflictFilename); + expected.add(conflictFilename + " seems unchanged."); + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedAbortLaunchOutput( + String[] conflictFilenames) { + List expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + if (conflictFilenames.length > 1) { + String conflictFilename = conflictFilenames[0]; + expected.add( + "Normal merge conflict for '" + conflictFilename + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + expected.add("Hit return to start merge resolution tool (" + + TOOL_NAME + "):"); + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedAbortMergeOutput( + String[] conflictFilenames, int abortIndex) { + List expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + for (int i = 0; i < conflictFilenames.length; ++i) { + if (i == abortIndex) { + break; + } + + String conflictFilename = conflictFilenames[i]; + expected.add( + "Normal merge conflict for '" + conflictFilename + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + expected.add("Hit return to start merge resolution tool (" + + TOOL_NAME + "): " + conflictFilename); + expected.add(conflictFilename + " seems unchanged."); + expected.add("Was the merge successful [y/n]?"); + if (i < conflictFilenames.length - 1) { + expected.add( + "\tContinue merging other unresolved paths [y/n]?"); + } + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedMergeConflictOutput( + String[] conflictFilenames) { + List expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + for (int i = 0; i < conflictFilenames.length; ++i) { + String conflictFilename = conflictFilenames[i]; + expected.add("Normal merge conflict for '" + conflictFilename + + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + expected.add("Hit return to start merge resolution tool (" + + TOOL_NAME + "): " + conflictFilename); + expected.add(conflictFilename + " seems unchanged."); + expected.add("Was the merge successful [y/n]?"); + if (i < conflictFilenames.length - 1) { + // expected.add( + // "\tContinue merging other unresolved paths [y/n]?"); + } + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedDeletedConflictOutput( + String[] conflictFilenames) { + List expected = new ArrayList<>(); + expected.add("Merging:"); + for (String mergeConflictFilename : conflictFilenames) { + expected.add(mergeConflictFilename); + } + for (int i = 0; i < conflictFilenames.length; ++i) { + String conflictFilename = conflictFilenames[i]; + expected.add(conflictFilename + " seems unchanged."); + expected.add("{local}: deleted"); + expected.add("{remote}: modified file"); + expected.add("Use (m)odified or (d)eleted file, or (a)bort?"); + } + return expected.toArray(new String[0]); + } +} diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java new file mode 100644 index 000000000..933f19bcc --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2022, 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.pgm; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.CLIRepositoryTestCase; +import org.eclipse.jgit.pgm.opt.CmdLineParser; +import org.eclipse.jgit.pgm.opt.SubcommandHandler; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.junit.Before; +import org.kohsuke.args4j.Argument; + +/** + * Base test case for the {@code difftool} and {@code mergetool} commands. + */ +public abstract class ToolTestCase extends CLIRepositoryTestCase { + + public static class GitCliJGitWrapperParser { + @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class) + TextBuiltin subcommand; + + @Argument(index = 1, metaVar = "metaVar_arg") + List arguments = new ArrayList<>(); + } + + protected static final String TOOL_NAME = "some_tool"; + + private static final String TEST_BRANCH_NAME = "test_branch"; + + private Git git; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + git = new Git(db); + git.commit().setMessage("initial commit").call(); + git.branchCreate().setName(TEST_BRANCH_NAME).call(); + } + + protected String[] runAndCaptureUsingInitRaw(String... args) + throws Exception { + InputStream inputStream = null; // no input stream + return runAndCaptureUsingInitRaw(inputStream, args); + } + + protected String[] runAndCaptureUsingInitRaw(InputStream inputStream, + String... args) throws Exception { + CLIGitCommand.Result result = new CLIGitCommand.Result(); + + GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); + CmdLineParser clp = new CmdLineParser(bean); + clp.parseArgument(args); + + TextBuiltin cmd = bean.subcommand; + cmd.initRaw(db, null, inputStream, result.out, result.err); + cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()])); + if (cmd.getOutputWriter() != null) { + cmd.getOutputWriter().flush(); + } + if (cmd.getErrorWriter() != null) { + cmd.getErrorWriter().flush(); + } + + List errLines = result.errLines().stream() + .filter(l -> !l.isBlank()) // we care only about error messages + .collect(Collectors.toList()); + assertEquals("Expected no standard error output from tool", + Collections.EMPTY_LIST.toString(), errLines.toString()); + + return result.outLines().toArray(new String[0]); + } + + protected String[] createMergeConflict() throws Exception { + // create files on initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); + writeTrashFile("dir1/a", "Hello world a"); + writeTrashFile("dir2/b", "Hello world b"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b added").call(); + // create another branch and change files + git.branchCreate().setName("branch_1").call(); + git.checkout().setName("branch_1").call(); + writeTrashFile("dir1/a", "Hello world a 1"); + writeTrashFile("dir2/b", "Hello world b 1"); + git.add().addFilepattern(".").call(); + RevCommit commit1 = git.commit() + .setMessage("files a & b modified commit 1").call(); + // checkout initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); + // create another branch and change files + git.branchCreate().setName("branch_2").call(); + git.checkout().setName("branch_2").call(); + writeTrashFile("dir1/a", "Hello world a 2"); + writeTrashFile("dir2/b", "Hello world b 2"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b modified commit 2").call(); + // cherry-pick conflicting changes + git.cherryPick().include(commit1).call(); + String[] conflictingFilenames = { "dir1/a", "dir2/b" }; + return conflictingFilenames; + } + + protected String[] createDeletedConflict() throws Exception { + // create files on initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); + writeTrashFile("dir1/a", "Hello world a"); + writeTrashFile("dir2/b", "Hello world b"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b added").call(); + // create another branch and change files + git.branchCreate().setName("branch_1").call(); + git.checkout().setName("branch_1").call(); + writeTrashFile("dir1/a", "Hello world a 1"); + writeTrashFile("dir2/b", "Hello world b 1"); + git.add().addFilepattern(".").call(); + RevCommit commit1 = git.commit() + .setMessage("files a & b modified commit 1").call(); + // checkout initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); + // create another branch and change files + git.branchCreate().setName("branch_2").call(); + git.checkout().setName("branch_2").call(); + git.rm().addFilepattern("dir1/a").call(); + git.rm().addFilepattern("dir2/b").call(); + git.commit().setMessage("files a & b deleted commit 2").call(); + // cherry-pick conflicting changes + git.cherryPick().include(commit1).call(); + String[] conflictingFilenames = { "dir1/a", "dir2/b" }; + return conflictingFilenames; + } + + protected String[] createUnstagedChanges() throws Exception { + writeTrashFile("dir1/a", "Hello world a"); + writeTrashFile("dir2/b", "Hello world b"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b").call(); + writeTrashFile("dir1/a", "New Hello world a"); + writeTrashFile("dir2/b", "New Hello world b"); + String[] conflictingFilenames = { "dir1/a", "dir2/b" }; + return conflictingFilenames; + } + + protected String[] createStagedChanges() throws Exception { + String[] conflictingFilenames = createUnstagedChanges(); + git.add().addFilepattern(".").call(); + return conflictingFilenames; + } + + protected List getRepositoryChanges(RevCommit commit) + throws Exception { + TreeWalk tw = new TreeWalk(db); + tw.addTree(commit.getTree()); + FileTreeIterator modifiedTree = new FileTreeIterator(db); + tw.addTree(modifiedTree); + List changes = DiffEntry.scan(tw); + return changes; + } + + protected static InputStream createInputStream(String[] inputLines) { + return createInputStream(Arrays.asList(inputLines)); + } + + protected static InputStream createInputStream(List inputLines) { + String input = String.join(System.lineSeparator(), inputLines); + InputStream inputStream = new ByteArrayInputStream(input.getBytes()); + return inputStream; + } + + protected static void assertArrayOfLinesEquals(String failMessage, + String[] expected, String[] actual) { + assertEquals(failMessage, toString(expected), toString(actual)); + } + + protected static String getEchoCommand() { + /* + * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be + * replaced with full paths to a temporary file during some of the tests + */ + return "(echo \"$MERGED\")"; + } +} diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin index 8c44764c6..ea1d1e3fa 100644 --- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin +++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin @@ -25,6 +25,7 @@ org.eclipse.jgit.pgm.LsRemote org.eclipse.jgit.pgm.LsTree org.eclipse.jgit.pgm.Merge org.eclipse.jgit.pgm.MergeBase +org.eclipse.jgit.pgm.MergeTool org.eclipse.jgit.pgm.Push org.eclipse.jgit.pgm.ReceivePack org.eclipse.jgit.pgm.Reflog diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties index fda0bf6ff..674185df2 100644 --- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties +++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties @@ -58,9 +58,9 @@ couldNotCreateBranch=Could not create branch {0}: {1} dateInfo=Date: {0} deletedBranch=Deleted branch {0} deletedRemoteBranch=Deleted remote branch {0} -diffToolHelpSetToFollowing='git difftool --tool=' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail. -diffToolLaunch=Viewing ({0}/{1}): '{2}'\nLaunch '{3}' [Y/n]? -diffToolDied=external diff died, stopping at {0} +diffToolHelpSetToFollowing=''git difftool --tool='' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail. +diffToolLaunch=Viewing ({0}/{1}): ''{2}''\nLaunch ''{3}'' [Y/n]? +diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1} doesNotExist={0} does not exist dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge: everythingUpToDate=Everything up-to-date @@ -91,6 +91,22 @@ listeningOn=Listening on {0} logNoSignatureVerifier="No signature verifier available" mergeConflict=CONFLICT(content): Merge conflict in {0} mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge: +mergeToolHelpSetToFollowing=''git mergetool --tool='' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail. +mergeToolLaunch=Hit return to start merge resolution tool ({0}): +mergeToolDied=local or remote cannot be found in cache, stopping at {0} +mergeToolNoFiles=No files need merging +mergeToolMerging=Merging:\n{0} +mergeToolUnknownConflict=\nUnknown merge conflict for ''{0}'': +mergeToolNormalConflict=\nNormal merge conflict for ''{0}'':\n '{'local'}': modified file\n '{'remote'}': modified file +mergeToolMergeFailed=merge of {0} failed +mergeToolExecutionError=excution error +mergeToolFileUnchanged=\n{0} seems unchanged. +mergeToolDeletedConflict=\nDeleted merge conflict for ''{0}'': +mergeToolDeletedConflictByUs= {local}: deleted\n {remote}: modified file +mergeToolDeletedConflictByThem= {local}: modified file\n {remote}: deleted +mergeToolContinueUnresolvedPaths=\nContinue merging other unresolved paths [y/n]? +mergeToolWasMergeSuccessfull=Was the merge successful [y/n]? +mergeToolDeletedMergeDecision=Use (m)odified or (d)eleted file, or (a)bort? mergeFailed=Automatic merge failed; fix conflicts and then commit the result mergeCheckoutFailed=Please, commit your changes or stash them before you can merge. mergeMadeBy=Merge made by the ''{0}'' strategy. @@ -255,6 +271,7 @@ usage_DisplayTheVersionOfJgit=Display the version of jgit usage_Gc=Cleanup unnecessary files and optimize the local repository usage_Glog=View commit history as a graph usage_DiffGuiTool=When git-difftool is invoked with the -g or --gui option the default diff tool will be read from the configured diff.guitool variable instead of diff.tool. +usage_MergeGuiTool=When git-mergetool is invoked with the -g or --gui option the default merge tool will be read from the configured merge.guitool variable instead of merge.tool. usage_noGui=The --no-gui option can be used to override -g or --gui setting. usage_IndexPack=Build pack index file for an existing packed archive usage_LFSDirectory=Directory to store large objects @@ -303,6 +320,7 @@ usage_Status=Show the working tree status usage_StopTrackingAFile=Stop tracking a file usage_TextHashFunctions=Scan repository to compute maximum number of collisions for hash functions usage_ToolForDiff=Use the diff tool specified by . Run git difftool --tool-help for the list of valid settings.\nIf a diff tool is not specified, git difftool will use the configuration variable diff.tool. +usage_ToolForMerge=Use the merge resolution program specified by . Run git mergetool --tool-help for the list of valid settings.\nIf a merge resolution program is not specified, git mergetool will use the configuration variable merge.tool. usage_UpdateRemoteRepositoryFromLocalRefs=Update remote repository from local refs usage_UseAll=Use all refs found in refs/ usage_UseTags=Use any tag including lightweight tags @@ -350,6 +368,7 @@ usage_date=date format, one of default, rfc, local, iso, short, raw (as defined usage_detectRenames=detect renamed files usage_diffAlgorithm=the diff algorithm to use. Currently supported are: 'myers', 'histogram' usage_DiffTool=git difftool is a Git command that allows you to compare and edit files between revisions using common diff tools.\ngit difftool is a frontend to git diff and accepts the same options and arguments. +usage_MergeTool=git-mergetool - Run merge conflict resolution tools to resolve merge conflicts.\nUse git mergetool to run one of several merge utilities to resolve merge conflicts. It is typically run after git merge. usage_directoriesToExport=directories to export usage_disableTheServiceInAllRepositories=disable the service in all repositories usage_displayAListOfAllRegisteredJgitCommands=Display a list of all registered jgit commands diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java index 128881779..74d91cd3d 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2021, Andre Bossert + * Copyright (C) 2018-2022, Andre Bossert * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -11,9 +11,11 @@ package org.eclipse.jgit.pgm; import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP; import java.io.BufferedOutputStream; import java.io.BufferedReader; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; @@ -21,27 +23,43 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; - +import org.eclipse.jgit.diff.ContentSource; +import org.eclipse.jgit.diff.ContentSource.Pair; import org.eclipse.jgit.diff.DiffEntry; -import org.eclipse.jgit.diff.DiffFormatter; -import org.eclipse.jgit.dircache.DirCacheIterator; -import org.eclipse.jgit.errors.AmbiguousObjectException; -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.diff.DiffEntry.Side; +import org.eclipse.jgit.internal.diffmergetool.ToolException; import org.eclipse.jgit.internal.diffmergetool.DiffTools; +import org.eclipse.jgit.internal.diffmergetool.FileElement; import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.TextProgressMonitor; +import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeIterator; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.FS.ExecutionResult; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; @@ -106,11 +124,14 @@ void noTrustExitCode(@SuppressWarnings("unused") boolean on) { @Option(name = "--", metaVar = "metaVar_paths", handler = PathTreeFilterHandler.class) private TreeFilter pathFilter = TreeFilter.ALL; + private BufferedReader inputReader; + @Override protected void init(Repository repository, String gitDir) { super.init(repository, gitDir); diffFmt = new DiffFormatter(new BufferedOutputStream(outs)); diffTools = new DiffTools(repository); + inputReader = new BufferedReader(new InputStreamReader(ins, StandardCharsets.UTF_8)); } @Override @@ -145,40 +166,53 @@ protected void run() { private void compare(List files, boolean showPrompt, String toolNamePrompt) throws IOException { - for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) { - DiffEntry ent = files.get(fileIndex); - String mergedFilePath = ent.getNewPath(); - if (mergedFilePath.equals(DiffEntry.DEV_NULL)) { - mergedFilePath = ent.getOldPath(); - } - // check if user wants to launch compare - boolean launchCompare = true; - if (showPrompt) { - launchCompare = isLaunchCompare(fileIndex + 1, files.size(), - mergedFilePath, toolNamePrompt); - } - if (launchCompare) { - switch (ent.getChangeType()) { - case MODIFY: - outw.println("M\t" + ent.getNewPath() //$NON-NLS-1$ - + " (" + ent.getNewId().name() + ")" //$NON-NLS-1$ //$NON-NLS-2$ - + "\t" + ent.getOldPath() //$NON-NLS-1$ - + " (" + ent.getOldId().name() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ - int ret = diffTools.compare(ent.getNewPath(), - ent.getOldPath(), ent.getNewId().name(), - ent.getOldId().name(), toolName, prompt, gui, - trustExitCode); - if (ret != 0) { + ContentSource.Pair sourcePair = new ContentSource.Pair(source(oldTree), + source(newTree)); + try { + for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) { + DiffEntry ent = files.get(fileIndex); + String mergedFilePath = ent.getNewPath(); + if (mergedFilePath.equals(DiffEntry.DEV_NULL)) { + mergedFilePath = ent.getOldPath(); + } + // check if user wants to launch compare + boolean launchCompare = true; + if (showPrompt) { + launchCompare = isLaunchCompare(fileIndex + 1, files.size(), + mergedFilePath, toolNamePrompt); + } + if (launchCompare) { + try { + FileElement local = createFileElement( + FileElement.Type.LOCAL, sourcePair, Side.OLD, + ent); + FileElement remote = createFileElement( + FileElement.Type.REMOTE, sourcePair, Side.NEW, + ent); + FileElement merged = new FileElement(mergedFilePath, + FileElement.Type.MERGED); + // TODO: check how to return the exit-code of the tool + // to jgit / java runtime ? + // int rc =... + ExecutionResult result = diffTools.compare(local, + remote, merged, toolName, prompt, gui, + trustExitCode); + outw.println(new String(result.getStdout().toByteArray())); + errw.println( + new String(result.getStderr().toByteArray())); + } catch (ToolException e) { + outw.println(e.getResultStdout()); + outw.flush(); + errw.println(e.getMessage()); throw die(MessageFormat.format( - CLIText.get().diffToolDied, mergedFilePath)); + CLIText.get().diffToolDied, mergedFilePath), e); } - break; - default: + } else { break; } - } else { - break; } + } finally { + sourcePair.close(); } } @@ -187,10 +221,9 @@ private boolean isLaunchCompare(int fileIndex, int fileCount, String fileName, String toolNamePrompt) throws IOException { boolean launchCompare = true; outw.println(MessageFormat.format(CLIText.get().diffToolLaunch, - fileIndex, fileCount, fileName, toolNamePrompt)); + fileIndex, fileCount, fileName, toolNamePrompt) + " "); //$NON-NLS-1$ outw.flush(); - BufferedReader br = new BufferedReader( - new InputStreamReader(ins, StandardCharsets.UTF_8)); + BufferedReader br = inputReader; String line = null; if ((line = br.readLine()) != null) { if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$ @@ -203,17 +236,18 @@ private boolean isLaunchCompare(int fileIndex, int fileCount, private void showToolHelp() throws IOException { StringBuilder availableToolNames = new StringBuilder(); for (String name : diffTools.getAvailableTools().keySet()) { - availableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$ + availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ } StringBuilder notAvailableToolNames = new StringBuilder(); for (String name : diffTools.getNotAvailableTools().keySet()) { - notAvailableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$ + notAvailableToolNames + .append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ } StringBuilder userToolNames = new StringBuilder(); Map userTools = diffTools .getUserDefinedTools(); for (String name : userTools.keySet()) { - userToolNames.append(String.format("\t\t%s.cmd %s\n", //$NON-NLS-1$ + userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$ name, userTools.get(name).getCommand())); } outw.println(MessageFormat.format( @@ -254,4 +288,53 @@ private List getFiles() return files; } + private FileElement createFileElement(FileElement.Type elementType, + Pair pair, Side side, DiffEntry entry) + throws NoWorkTreeException, CorruptObjectException, IOException, + ToolException { + String entryPath = side == Side.NEW ? entry.getNewPath() + : entry.getOldPath(); + FileElement fileElement = new FileElement(entryPath, elementType); + if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) { + try (RevWalk revWalk = new RevWalk(db); + TreeWalk treeWalk = new TreeWalk(db, + revWalk.getObjectReader())) { + treeWalk.setFilter( + PathFilterGroup.createFromStrings(entryPath)); + if (side == Side.NEW) { + newTree.reset(); + treeWalk.addTree(newTree); + } else { + oldTree.reset(); + treeWalk.addTree(oldTree); + } + if (treeWalk.next()) { + final EolStreamType eolStreamType = treeWalk + .getEolStreamType(CHECKOUT_OP); + final String filterCommand = treeWalk.getFilterCommand( + Constants.ATTR_FILTER_TYPE_SMUDGE); + WorkingTreeOptions opt = db.getConfig() + .get(WorkingTreeOptions.KEY); + CheckoutMetadata checkoutMetadata = new CheckoutMetadata( + eolStreamType, filterCommand); + DirCacheCheckout.getContent(db, entryPath, + checkoutMetadata, pair.open(side, entry), opt, + new FileOutputStream( + fileElement.createTempFile(null))); + } else { + throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$ + + "' in staging area!", null); //$NON-NLS-1$ + } + } + } + return fileElement; + } + + private ContentSource source(AbstractTreeIterator iterator) { + if (iterator instanceof WorkingTreeIterator) { + return ContentSource.create((WorkingTreeIterator) iterator); + } + return ContentSource.create(db.newObjectReader()); + } + } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java new file mode 100644 index 000000000..971277075 --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert + * + * 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.pgm; + +import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.Status; +import org.eclipse.jgit.api.StatusCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.diff.ContentSource; +import org.eclipse.jgit.internal.diffmergetool.FileElement.Type; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool; +import org.eclipse.jgit.internal.diffmergetool.FileElement; +import org.eclipse.jgit.internal.diffmergetool.MergeTools; +import org.eclipse.jgit.internal.diffmergetool.ToolException; +import org.eclipse.jgit.lib.IndexDiff.StageState; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.lib.CoreConfig.EolStreamType; +import org.eclipse.jgit.pgm.internal.CLIText; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.spi.RestOfArgumentsHandler; + +@Command(name = "mergetool", common = true, usage = "usage_MergeTool") +class MergeTool extends TextBuiltin { + private MergeTools mergeTools; + + @Option(name = "--tool", aliases = { + "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge") + private String toolName; + + private BooleanTriState prompt = BooleanTriState.UNSET; + + @Option(name = "--prompt", usage = "usage_prompt") + void setPrompt(@SuppressWarnings("unused") boolean on) { + prompt = BooleanTriState.TRUE; + } + + @Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt") + void noPrompt(@SuppressWarnings("unused") boolean on) { + prompt = BooleanTriState.FALSE; + } + + @Option(name = "--tool-help", usage = "usage_toolHelp") + private boolean toolHelp; + + private BooleanTriState gui = BooleanTriState.UNSET; + + @Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool") + void setGui(@SuppressWarnings("unused") boolean on) { + gui = BooleanTriState.TRUE; + } + + @Option(name = "--no-gui", usage = "usage_noGui") + void noGui(@SuppressWarnings("unused") boolean on) { + gui = BooleanTriState.FALSE; + } + + @Argument(required = false, index = 0, metaVar = "metaVar_paths") + @Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class) + protected List filterPaths; + + private BufferedReader inputReader; + + @Override + protected void init(Repository repository, String gitDir) { + super.init(repository, gitDir); + mergeTools = new MergeTools(repository); + inputReader = new BufferedReader(new InputStreamReader(ins)); + } + + enum MergeResult { + SUCCESSFUL, FAILED, ABORTED + } + + @Override + protected void run() { + try { + if (toolHelp) { + showToolHelp(); + } else { + // get prompt + boolean showPrompt = mergeTools.isInteractive(); + if (prompt != BooleanTriState.UNSET) { + showPrompt = prompt == BooleanTriState.TRUE; + } + // get passed or default tool name + String toolNameSelected = toolName; + if ((toolNameSelected == null) || toolNameSelected.isEmpty()) { + toolNameSelected = mergeTools.getDefaultToolName(gui); + } + // get the changed files + Map files = getFiles(); + if (files.size() > 0) { + merge(files, showPrompt, toolNameSelected); + } else { + outw.println(CLIText.get().mergeToolNoFiles); + } + } + outw.flush(); + } catch (Exception e) { + throw die(e.getMessage(), e); + } + } + + private void merge(Map files, boolean showPrompt, + String toolNamePrompt) throws Exception { + // sort file names + List mergedFilePaths = new ArrayList<>(files.keySet()); + Collections.sort(mergedFilePaths); + // show the files + StringBuilder mergedFiles = new StringBuilder(); + for (String mergedFilePath : mergedFilePaths) { + mergedFiles.append(MessageFormat.format("{0}\n", mergedFilePath)); //$NON-NLS-1$ + } + outw.println(MessageFormat.format(CLIText.get().mergeToolMerging, + mergedFiles)); + outw.flush(); + // merge the files + MergeResult mergeResult = MergeResult.SUCCESSFUL; + for (String mergedFilePath : mergedFilePaths) { + // if last merge failed... + if (mergeResult == MergeResult.FAILED) { + // check if user wants to continue + if (showPrompt && !isContinueUnresolvedPaths()) { + mergeResult = MergeResult.ABORTED; + } + } + // aborted ? + if (mergeResult == MergeResult.ABORTED) { + break; + } + // get file stage state and merge + StageState fileState = files.get(mergedFilePath); + if (fileState == StageState.BOTH_MODIFIED) { + mergeResult = mergeModified(mergedFilePath, showPrompt, + toolNamePrompt); + } else if ((fileState == StageState.DELETED_BY_US) + || (fileState == StageState.DELETED_BY_THEM)) { + mergeResult = mergeDeleted(mergedFilePath, + fileState == StageState.DELETED_BY_US); + } else { + outw.println(MessageFormat.format( + CLIText.get().mergeToolUnknownConflict, + mergedFilePath)); + mergeResult = MergeResult.ABORTED; + } + } + } + + private MergeResult mergeModified(String mergedFilePath, boolean showPrompt, + String toolNamePrompt) throws Exception { + outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict, + mergedFilePath)); + outw.flush(); + // check if user wants to launch merge resolution tool + boolean launch = true; + if (showPrompt) { + launch = isLaunch(toolNamePrompt); + } + if (!launch) { + return MergeResult.ABORTED; // abort + } + boolean isMergeSuccessful = true; + ContentSource baseSource = ContentSource.create(db.newObjectReader()); + ContentSource localSource = ContentSource.create(db.newObjectReader()); + ContentSource remoteSource = ContentSource.create(db.newObjectReader()); + // temporary directory if mergetool.writeToTemp == true + File tempDir = mergeTools.createTempDirectory(); + // the parent directory for temp files (can be same as tempDir or just + // the worktree dir) + File tempFilesParent = tempDir != null ? tempDir : db.getWorkTree(); + try { + FileElement base = null; + FileElement local = null; + FileElement remote = null; + FileElement merged = new FileElement(mergedFilePath, + Type.MERGED); + DirCache cache = db.readDirCache(); + try (RevWalk revWalk = new RevWalk(db); + TreeWalk treeWalk = new TreeWalk(db, + revWalk.getObjectReader())) { + treeWalk.setFilter( + PathFilterGroup.createFromStrings(mergedFilePath)); + DirCacheIterator cacheIter = new DirCacheIterator(cache); + treeWalk.addTree(cacheIter); + while (treeWalk.next()) { + if (treeWalk.isSubtree()) { + treeWalk.enterSubtree(); + continue; + } + final EolStreamType eolStreamType = treeWalk + .getEolStreamType(CHECKOUT_OP); + final String filterCommand = treeWalk.getFilterCommand( + Constants.ATTR_FILTER_TYPE_SMUDGE); + WorkingTreeOptions opt = db.getConfig() + .get(WorkingTreeOptions.KEY); + CheckoutMetadata checkoutMetadata = new CheckoutMetadata( + eolStreamType, filterCommand); + DirCacheEntry entry = treeWalk.getTree(DirCacheIterator.class).getDirCacheEntry(); + if (entry == null) { + continue; + } + ObjectId id = entry.getObjectId(); + switch (entry.getStage()) { + case DirCacheEntry.STAGE_1: + base = new FileElement(mergedFilePath, Type.BASE); + DirCacheCheckout.getContent(db, mergedFilePath, + checkoutMetadata, + baseSource.open(mergedFilePath, id), opt, + new FileOutputStream( + base.createTempFile(tempFilesParent))); + break; + case DirCacheEntry.STAGE_2: + local = new FileElement(mergedFilePath, Type.LOCAL); + DirCacheCheckout.getContent(db, mergedFilePath, + checkoutMetadata, + localSource.open(mergedFilePath, id), opt, + new FileOutputStream( + local.createTempFile(tempFilesParent))); + break; + case DirCacheEntry.STAGE_3: + remote = new FileElement(mergedFilePath, Type.REMOTE); + DirCacheCheckout.getContent(db, mergedFilePath, + checkoutMetadata, + remoteSource.open(mergedFilePath, id), opt, + new FileOutputStream(remote + .createTempFile(tempFilesParent))); + break; + } + } + } + if ((local == null) || (remote == null)) { + throw die(MessageFormat.format(CLIText.get().mergeToolDied, + mergedFilePath)); + } + long modifiedBefore = merged.getFile().lastModified(); + try { + // TODO: check how to return the exit-code of the + // tool to jgit / java runtime ? + // int rc =... + ExecutionResult executionResult = mergeTools.merge(local, + remote, merged, base, tempDir, toolName, prompt, gui); + outw.println( + new String(executionResult.getStdout().toByteArray())); + outw.flush(); + errw.println( + new String(executionResult.getStderr().toByteArray())); + errw.flush(); + } catch (ToolException e) { + isMergeSuccessful = false; + outw.println(e.getResultStdout()); + outw.flush(); + errw.println(MessageFormat.format( + CLIText.get().mergeToolMergeFailed, mergedFilePath)); + errw.flush(); + if (e.isCommandExecutionError()) { + errw.println(e.getMessage()); + throw die(CLIText.get().mergeToolExecutionError, e); + } + } + // if merge was successful check file modified + if (isMergeSuccessful) { + long modifiedAfter = merged.getFile().lastModified(); + if (modifiedBefore == modifiedAfter) { + outw.println(MessageFormat.format( + CLIText.get().mergeToolFileUnchanged, + mergedFilePath)); + isMergeSuccessful = !showPrompt || isMergeSuccessful(); + } + } + // if automatically or manually successful + // -> add the file to the index + if (isMergeSuccessful) { + addFile(mergedFilePath); + } + } finally { + baseSource.close(); + localSource.close(); + remoteSource.close(); + } + return isMergeSuccessful ? MergeResult.SUCCESSFUL : MergeResult.FAILED; + } + + private MergeResult mergeDeleted(String mergedFilePath, boolean deletedByUs) + throws Exception { + outw.println(MessageFormat.format(CLIText.get().mergeToolFileUnchanged, + mergedFilePath)); + if (deletedByUs) { + outw.println(CLIText.get().mergeToolDeletedConflictByUs); + } else { + outw.println(CLIText.get().mergeToolDeletedConflictByThem); + } + int mergeDecision = getDeletedMergeDecision(); + if (mergeDecision == 1) { + // add modified file + addFile(mergedFilePath); + } else if (mergeDecision == -1) { + // remove deleted file + rmFile(mergedFilePath); + } else { + return MergeResult.ABORTED; + } + return MergeResult.SUCCESSFUL; + } + + private void addFile(String fileName) throws Exception { + try (Git git = new Git(db)) { + git.add().addFilepattern(fileName).call(); + } + } + + private void rmFile(String fileName) throws Exception { + try (Git git = new Git(db)) { + git.rm().addFilepattern(fileName).call(); + } + } + + private boolean hasUserAccepted(String message) throws IOException { + boolean yes = true; + outw.print(message + " "); //$NON-NLS-1$ + outw.flush(); + BufferedReader br = inputReader; + String line = null; + while ((line = br.readLine()) != null) { + if (line.equalsIgnoreCase("y")) { //$NON-NLS-1$ + yes = true; + break; + } else if (line.equalsIgnoreCase("n")) { //$NON-NLS-1$ + yes = false; + break; + } + outw.print(message); + outw.flush(); + } + return yes; + } + + private boolean isContinueUnresolvedPaths() throws IOException { + return hasUserAccepted(CLIText.get().mergeToolContinueUnresolvedPaths); + } + + private boolean isMergeSuccessful() throws IOException { + return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull); + } + + private boolean isLaunch(String toolNamePrompt) throws IOException { + boolean launch = true; + outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch, + toolNamePrompt) + " "); //$NON-NLS-1$ + outw.flush(); + BufferedReader br = inputReader; + String line = null; + if ((line = br.readLine()) != null) { + if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$ + launch = false; + } + } + return launch; + } + + private int getDeletedMergeDecision() throws IOException { + int ret = 0; // abort + final String message = CLIText.get().mergeToolDeletedMergeDecision + + " "; //$NON-NLS-1$ + outw.print(message); + outw.flush(); + BufferedReader br = inputReader; + String line = null; + while ((line = br.readLine()) != null) { + if (line.equalsIgnoreCase("m")) { //$NON-NLS-1$ + ret = 1; // modified + break; + } else if (line.equalsIgnoreCase("d")) { //$NON-NLS-1$ + ret = -1; // deleted + break; + } else if (line.equalsIgnoreCase("a")) { //$NON-NLS-1$ + break; + } + outw.print(message); + outw.flush(); + } + return ret; + } + + private void showToolHelp() throws IOException { + StringBuilder availableToolNames = new StringBuilder(); + for (String name : mergeTools.getAvailableTools().keySet()) { + availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ + } + StringBuilder notAvailableToolNames = new StringBuilder(); + for (String name : mergeTools.getNotAvailableTools().keySet()) { + notAvailableToolNames + .append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ + } + StringBuilder userToolNames = new StringBuilder(); + Map userTools = mergeTools + .getUserDefinedTools(); + for (String name : userTools.keySet()) { + userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$ + name, userTools.get(name).getCommand())); + } + outw.println(MessageFormat.format( + CLIText.get().mergeToolHelpSetToFollowing, availableToolNames, + userToolNames, notAvailableToolNames)); + } + + private Map getFiles() throws RevisionSyntaxException, + NoWorkTreeException, GitAPIException { + Map files = new TreeMap<>(); + try (Git git = new Git(db)) { + StatusCommand statusCommand = git.status(); + if (filterPaths != null && filterPaths.size() > 0) { + for (String path : filterPaths) { + statusCommand.addPath(path); + } + } + Status status = statusCommand.call(); + files = status.getConflictingStageState(); + } + return files; + } + +} diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java index 7fe5b0fa4..989e649b7 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java @@ -169,6 +169,22 @@ public static String fatalError(String message) { /***/ public String logNoSignatureVerifier; /***/ public String mergeCheckoutConflict; /***/ public String mergeConflict; + /***/ public String mergeToolHelpSetToFollowing; + /***/ public String mergeToolLaunch; + /***/ public String mergeToolDied; + /***/ public String mergeToolNoFiles; + /***/ public String mergeToolMerging; + /***/ public String mergeToolUnknownConflict; + /***/ public String mergeToolNormalConflict; + /***/ public String mergeToolMergeFailed; + /***/ public String mergeToolExecutionError; + /***/ public String mergeToolFileUnchanged; + /***/ public String mergeToolDeletedConflict; + /***/ public String mergeToolDeletedConflictByUs; + /***/ public String mergeToolDeletedConflictByThem; + /***/ public String mergeToolContinueUnresolvedPaths; + /***/ public String mergeToolWasMergeSuccessfull; + /***/ public String mergeToolDeletedMergeDecision; /***/ public String mergeFailed; /***/ public String mergeCheckoutFailed; /***/ public String mergeMadeBy; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java index c9ebec763..4fd55c6ca 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java @@ -10,13 +10,17 @@ 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.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.util.Collections; import java.util.LinkedHashSet; @@ -25,6 +29,7 @@ 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; /** @@ -32,6 +37,54 @@ */ 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); + + DiffTools manager = new DiffTools(db); + + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + BooleanTriState trustExitCode = BooleanTriState.TRUE; + + manager.compare(local, remote, merged, toolName, prompt, gui, + trustExitCode); + + 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); + + DiffTools manager = new DiffTools(db); + + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + BooleanTriState trustExitCode = BooleanTriState.FALSE; + + manager.compare(local, remote, merged, toolName, prompt, gui, + trustExitCode); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + @Test public void testToolNames() { DiffTools manager = new DiffTools(db); @@ -86,11 +139,11 @@ public void testUserDefinedTools() { config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, CONFIG_KEY_PATH, "/usr/bin/echo"); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, - CONFIG_KEY_PROMPT, "--no-prompt"); + CONFIG_KEY_PROMPT, String.valueOf(false)); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, - CONFIG_KEY_GUITOOL, "--no-gui"); + CONFIG_KEY_GUITOOL, String.valueOf(false)); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, - CONFIG_KEY_TRUST_EXIT_CODE, "--no-trust-exit-code"); + CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false)); DiffTools manager = new DiffTools(db); Set actualToolNames = manager.getUserDefinedTools().keySet(); Set expectedToolNames = new LinkedHashSet<>(); @@ -109,38 +162,50 @@ public void testNotAvailableTools() { } @Test - public void testCompare() { - DiffTools manager = new DiffTools(db); + 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); - String newPath = ""; - String oldPath = ""; - String newId = ""; - String oldId = ""; - String toolName = ""; BooleanTriState prompt = BooleanTriState.UNSET; BooleanTriState gui = BooleanTriState.UNSET; BooleanTriState trustExitCode = BooleanTriState.UNSET; + DiffTools manager = new DiffTools(db); + int expectedCompareResult = 0; - int compareResult = manager.compare(newPath, oldPath, newId, oldId, + ExecutionResult compareResult = manager.compare(local, remote, merged, toolName, prompt, gui, trustExitCode); assertEquals("Incorrect compare result for external diff tool", - expectedCompareResult, compareResult); + expectedCompareResult, compareResult.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("diff", subsection, "tool", "customTool"); + config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); DiffTools manager = new DiffTools(db); BooleanTriState gui = BooleanTriState.UNSET; String defaultToolName = manager.getDefaultToolName(gui); assertEquals( "Expected configured difftool to be the default external diff tool", - "my_default_toolname", defaultToolName); + toolName, defaultToolName); gui = BooleanTriState.TRUE; String defaultGuiToolName = manager.getDefaultToolName(gui); @@ -148,11 +213,63 @@ public void testDefaultTool() throws Exception { "Expected configured difftool to be the default external diff tool", "my_gui_tool", defaultGuiToolName); - config.setString("diff", subsection, "guitool", "customGuiTool"); + 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", "my_gui_tool", 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 + .getAvailableTools(); + 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 { + DiffTools manager = new DiffTools(db); + + String toolName = "undefined"; + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + BooleanTriState trustExitCode = BooleanTriState.UNSET; + + manager.compare(local, remote, merged, toolName, prompt, gui, + trustExitCode); + fail("Expected exception to be thrown due to not defined external diff tool"); + } + + private String getEchoCommand() { + return "(echo \"$LOCAL\" \"$REMOTE\") > " + + commandResult.getAbsolutePath(); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java new file mode 100644 index 000000000..50576682e --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2020-2022, 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_MERGETOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_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.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +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 merge tools. + */ +public class ExternalMergeToolTest 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_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, + CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(Boolean.TRUE)); + + MergeTools manager = new MergeTools(db); + + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + + manager.merge(local, remote, merged, base, null, toolName, prompt, gui); + + 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_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + MergeTools manager = new MergeTools(db); + + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + + manager.merge(local, remote, merged, base, null, toolName, prompt, gui); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + + @Test + public void testToolNames() { + MergeTools manager = new MergeTools(db); + Set actualToolNames = manager.getToolNames(); + Set expectedToolNames = Collections.emptySet(); + assertEquals("Incorrect set of external merge tool names", + expectedToolNames, actualToolNames); + } + + @Test + public void testAllTools() { + MergeTools manager = new MergeTools(db); + Set actualToolNames = manager.getAvailableTools().keySet(); + Set expectedToolNames = new LinkedHashSet<>(); + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + for (CommandLineMergeTool defaultTool : defaultTools) { + String toolName = defaultTool.name(); + expectedToolNames.add(toolName); + } + assertEquals("Incorrect set of external merge tools", expectedToolNames, + actualToolNames); + } + + @Test + public void testOverridePredefinedToolPath() { + String toolName = CommandLineMergeTool.guiffy.name(); + String customToolPath = "/usr/bin/echo"; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + "echo"); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PATH, + customToolPath); + + MergeTools manager = new MergeTools(db); + Map tools = manager.getUserDefinedTools(); + ExternalMergeTool mergeTool = tools.get(toolName); + assertNotNull("Expected tool \"" + toolName + "\" to be user defined", + mergeTool); + + String toolPath = mergeTool.getPath(); + assertEquals("Expected external merge tool to have an overriden path", + customToolPath, toolPath); + } + + @Test + public void testUserDefinedTools() { + FileBasedConfig config = db.getConfig(); + String customToolname = "customTool"; + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_CMD, "echo"); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_PATH, "/usr/bin/echo"); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_PROMPT, String.valueOf(false)); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_GUITOOL, String.valueOf(false)); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false)); + MergeTools manager = new MergeTools(db); + Set actualToolNames = manager.getUserDefinedTools().keySet(); + Set expectedToolNames = new LinkedHashSet<>(); + expectedToolNames.add(customToolname); + assertEquals("Incorrect set of external merge tools", expectedToolNames, + actualToolNames); + } + + @Test + public void testNotAvailableTools() { + MergeTools manager = new MergeTools(db); + Set actualToolNames = manager.getNotAvailableTools().keySet(); + Set expectedToolNames = Collections.emptySet(); + assertEquals("Incorrect set of not available external merge tools", + expectedToolNames, actualToolNames); + } + + @Test + public void testCompare() throws ToolException { + String toolName = "customTool"; + + FileBasedConfig config = db.getConfig(); + // the default merge tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + String command = getEchoCommand(); + + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + + MergeTools manager = new MergeTools(db); + + int expectedCompareResult = 0; + ExecutionResult compareResult = manager.merge(local, remote, merged, + base, null, toolName, prompt, gui); + assertEquals("Incorrect compare result for external merge tool", + expectedCompareResult, compareResult.getRc()); + } + + @Test + public void testDefaultTool() throws Exception { + String toolName = "customTool"; + String guiToolName = "customGuiTool"; + + FileBasedConfig config = db.getConfig(); + // the default merge tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + MergeTools manager = new MergeTools(db); + BooleanTriState gui = BooleanTriState.UNSET; + String defaultToolName = manager.getDefaultToolName(gui); + assertEquals( + "Expected configured mergetool to be the default external merge tool", + toolName, defaultToolName); + + gui = BooleanTriState.TRUE; + String defaultGuiToolName = manager.getDefaultToolName(gui); + assertEquals( + "Expected configured mergetool to be the default external merge tool", + "my_gui_tool", defaultGuiToolName); + + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL, + guiToolName); + manager = new MergeTools(db); + defaultGuiToolName = manager.getDefaultToolName(gui); + assertEquals( + "Expected configured mergetool to be the default external merge guitool", + "my_gui_tool", defaultGuiToolName); + } + + @Test + public void testOverridePreDefinedToolPath() { + String newToolPath = "/tmp/path/"; + + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + assertTrue("Expected to find pre-defined external merge tools", + defaultTools.length > 0); + + CommandLineMergeTool overridenTool = defaultTools[0]; + String overridenToolName = overridenTool.name(); + String overridenToolPath = newToolPath + overridenToolName; + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, overridenToolName, + CONFIG_KEY_PATH, overridenToolPath); + + MergeTools manager = new MergeTools(db); + Map availableTools = manager + .getAvailableTools(); + ExternalMergeTool externalMergeTool = availableTools + .get(overridenToolName); + String actualMergeToolPath = externalMergeTool.getPath(); + assertEquals( + "Expected pre-defined external merge tool to have overriden path", + overridenToolPath, actualMergeToolPath); + boolean withBase = true; + String expectedMergeToolCommand = overridenToolPath + " " + + overridenTool.getParameters(withBase); + String actualMergeToolCommand = externalMergeTool.getCommand(); + assertEquals( + "Expected pre-defined external merge tool to have overriden command", + expectedMergeToolCommand, actualMergeToolCommand); + } + + @Test(expected = ToolException.class) + public void testUndefinedTool() throws Exception { + MergeTools manager = new MergeTools(db); + + String toolName = "undefined"; + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + + manager.merge(local, remote, merged, base, null, toolName, prompt, gui); + fail("Expected exception to be thrown due to not defined external merge tool"); + } + + private String getEchoCommand() { + return "(echo \"$LOCAL\" \"$REMOTE\") > " + + commandResult.getAbsolutePath(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java index 0cc12978a..0fd85cb45 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java @@ -36,6 +36,14 @@ public abstract class ExternalToolTestCase extends RepositoryTestCase { protected File commandResult; + protected FileElement local; + + protected FileElement remote; + + protected FileElement merged; + + protected FileElement base; + @Before @Override public void setUp() throws Exception { @@ -51,6 +59,15 @@ public void setUp() throws Exception { baseFile.deleteOnExit(); commandResult = writeTrashFile("commandResult.txt", ""); commandResult.deleteOnExit(); + + local = new FileElement(localFile.getAbsolutePath(), + FileElement.Type.LOCAL); + remote = new FileElement(remoteFile.getAbsolutePath(), + FileElement.Type.REMOTE); + merged = new FileElement(mergedFile.getAbsolutePath(), + FileElement.Type.MERGED); + base = new FileElement(baseFile.getAbsolutePath(), + FileElement.Type.BASE); } @After diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java index 1a41df3d0..64ff19c9c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, 2020 Google Inc. and others + * Copyright (C) 2010, 2021 Google Inc. 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 @@ -91,6 +91,29 @@ public static ContentSource create(WorkingTreeIterator iterator) { public abstract ObjectLoader open(String path, ObjectId id) throws IOException; + /** + * Closes the used resources like ObjectReader, TreeWalk etc. Default + * implementation does nothing. + * + * @since 6.2 + */ + public void close() { + // Do nothing + } + + /** + * Checks if the source is from "working tree", so it can be accessed as a + * file directly. + * + * @since 6.2 + * + * @return true if working tree source and false otherwise (loader must be + * used) + */ + public boolean isWorkingTreeSource() { + return false; + } + private static class ObjectReaderSource extends ContentSource { private final ObjectReader reader; @@ -111,6 +134,16 @@ public long size(String path, ObjectId id) throws IOException { public ObjectLoader open(String path, ObjectId id) throws IOException { return reader.open(id, Constants.OBJ_BLOB); } + + @Override + public void close() { + reader.close(); + } + + @Override + public boolean isWorkingTreeSource() { + return false; + } } private static class WorkingTreeSource extends ContentSource { @@ -194,6 +227,16 @@ private void seek(String path) throws IOException { throw new FileNotFoundException(path); } } + + @Override + public void close() { + tw.close(); + } + + @Override + public boolean isWorkingTreeSource() { + return true; + } } /** A pair of sources to access the old and new sides of a DiffEntry. */ @@ -261,5 +304,37 @@ public ObjectLoader open(DiffEntry.Side side, DiffEntry ent) throw new IllegalArgumentException(); } } + + /** + * Closes used resources. + * + * @since 6.2 + */ + public void close() { + oldSource.close(); + newSource.close(); + } + + /** + * Checks if source (side) is a "working tree". + * + * @since 6.2 + * + * @param side + * which side of the entry to read (OLD or NEW). + * @return is the source a "working tree" + * + */ + public boolean isWorkingTreeSource(DiffEntry.Side side) { + switch (side) { + case OLD: + return oldSource.isWorkingTreeSource(); + case NEW: + return newSource.isWorkingTreeSource(); + default: + throw new IllegalArgumentException(); + } + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java new file mode 100644 index 000000000..ad79fe8fc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert + * + * 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 java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Map; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.FS_POSIX; +import org.eclipse.jgit.util.FS_Win32; +import org.eclipse.jgit.util.FS_Win32_Cygwin; + +/** + * Runs a command with help of FS. + */ +public class CommandExecutor { + + private FS fs; + + private boolean checkExitCode; + + private File commandFile; + + private boolean useMsys2; + + /** + * @param fs + * the file system + * @param checkExitCode + * should the exit code be checked for errors ? + */ + public CommandExecutor(FS fs, boolean checkExitCode) { + this.fs = fs; + this.checkExitCode = checkExitCode; + } + + /** + * @param command + * the command string + * @param workingDir + * the working directory + * @param env + * the environment + * @return the execution result + * @throws ToolException + * @throws InterruptedException + * @throws IOException + */ + public ExecutionResult run(String command, File workingDir, + Map env) + throws ToolException, IOException, InterruptedException { + String[] commandArray = createCommandArray(command); + try { + ProcessBuilder pb = fs.runInShell(commandArray[0], + Arrays.copyOfRange(commandArray, 1, commandArray.length)); + pb.directory(workingDir); + Map envp = pb.environment(); + if (env != null) { + envp.putAll(env); + } + ExecutionResult result = fs.execute(pb, null); + int rc = result.getRc(); + if (rc != 0) { + boolean execError = isCommandExecutionError(rc); + if (checkExitCode || execError) { + throw new ToolException( + "JGit: tool execution return code: " + rc + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + + "checkExitCode: " + checkExitCode + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + + "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + + "stderr: \n" //$NON-NLS-1$ + + new String( + result.getStderr().toByteArray()), + result, execError); + } + } + return result; + } finally { + deleteCommandArray(); + } + } + + private void deleteCommandArray() { + deleteCommandFile(); + } + + private String[] createCommandArray(String command) + throws ToolException, IOException { + String[] commandArray = null; + checkUseMsys2(command); + createCommandFile(command); + if (fs instanceof FS_POSIX) { + commandArray = new String[1]; + commandArray[0] = commandFile.getCanonicalPath(); + } else if (fs instanceof FS_Win32) { + if (useMsys2) { + commandArray = new String[3]; + commandArray[0] = "bash.exe"; //$NON-NLS-1$ + commandArray[1] = "-c"; //$NON-NLS-1$ + commandArray[2] = commandFile.getCanonicalPath().replace("\\", //$NON-NLS-1$ + "/"); //$NON-NLS-1$ + } else { + commandArray = new String[1]; + commandArray[0] = commandFile.getCanonicalPath(); + } + } else if (fs instanceof FS_Win32_Cygwin) { + commandArray = new String[1]; + commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + throw new ToolException( + "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$ + } + return commandArray; + } + + private void checkUseMsys2(String command) { + useMsys2 = false; + String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$ + if (useMsys2Str != null && !useMsys2Str.isEmpty()) { + if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$ + useMsys2 = command.contains(".sh"); //$NON-NLS-1$ + } else { + useMsys2 = Boolean.parseBoolean(useMsys2Str); + } + } + } + + private void createCommandFile(String command) + throws ToolException, IOException { + String fileExtension = null; + if (useMsys2 || fs instanceof FS_POSIX + || fs instanceof FS_Win32_Cygwin) { + fileExtension = ".sh"; //$NON-NLS-1$ + } else if (fs instanceof FS_Win32) { + fileExtension = ".cmd"; //$NON-NLS-1$ + command = "@echo off" + System.lineSeparator() + command //$NON-NLS-1$ + + System.lineSeparator() + "exit /B %ERRORLEVEL%"; //$NON-NLS-1$ + } else { + throw new ToolException( + "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$ + } + commandFile = File.createTempFile(".__", //$NON-NLS-1$ + "__jgit_tool" + fileExtension); //$NON-NLS-1$ + try (OutputStream outStream = new FileOutputStream(commandFile)) { + byte[] strToBytes = command.getBytes(); + outStream.write(strToBytes); + outStream.close(); + } + commandFile.setExecutable(true); + } + + private void deleteCommandFile() { + if (commandFile != null && commandFile.exists()) { + commandFile.delete(); + } + } + + private boolean isCommandExecutionError(int rc) { + if (useMsys2 || fs instanceof FS_POSIX + || fs instanceof FS_Win32_Cygwin) { + // 126: permission for executing command denied + // 127: command not found + if ((rc == 126) || (rc == 127)) { + return true; + } + } + else if (fs instanceof FS_Win32) { + // 9009, 0x2331: Program is not recognized as an internal or + // external command, operable program or batch file. Indicates that + // command, application name or path has been misspelled when + // configuring the Action. + if (rc == 9009) { + return true; + } + } + return false; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java new file mode 100644 index 000000000..3a2212432 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert + * + * 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; + +/** + * Pre-defined merge tools. + * + * Adds same merge tools as also pre-defined in C-Git see "git-core\mergetools\" + * see links to command line parameter description for the tools + * + *
+ * araxis
+ * bc
+ * bc3
+ * codecompare
+ * deltawalker
+ * diffmerge
+ * diffuse
+ * ecmerge
+ * emerge
+ * examdiff
+ * guiffy
+ * gvimdiff
+ * gvimdiff2
+ * gvimdiff3
+ * kdiff3
+ * kompare
+ * meld
+ * opendiff
+ * p4merge
+ * tkdiff
+ * tortoisemerge
+ * vimdiff
+ * vimdiff2
+ * vimdiff3
+ * winmerge
+ * xxdiff
+ * 
+ * + */ +@SuppressWarnings("nls") +public enum CommandLineMergeTool { + /** + * See: https://www.araxis.com/merge/documentation-windows/command-line.en + */ + araxis("compare", + "-wait -merge -3 -a1 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + "-wait -2 \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + false), + /** + * See: https://www.scootersoftware.com/v4help/index.html?command_line_reference.html + */ + bc("bcomp", "\"$LOCAL\" \"$REMOTE\" \"$BASE\" --mergeoutput=\"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" --mergeoutput=\"$MERGED\"", + false), + /** + * See: https://www.scootersoftware.com/v4help/index.html?command_line_reference.html + */ + bc3("bcompare", bc), + /** + * See: https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm + */ + codecompare("CodeMerge", + "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -BF=\"$BASE\" -RF=\"$MERGED\"", + "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -RF=\"$MERGED\"", + false), + /** + * See: https://www.deltawalker.com/integrate/command-line + *

+ * Hint: $(pwd) command must be defined + *

+ */ + deltawalker("DeltaWalker", + "\"$LOCAL\" \"$REMOTE\" \"$BASE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"", + true), + /** + * See: https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html + */ + diffmerge("diffmerge", //$NON-NLS-1$ + "--merge --result=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "--merge --result=\"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: http://diffuse.sourceforge.net/manual.html#introduction-usage + *

+ * Hint: check the ' | cat' for the call + *

+ */ + diffuse("diffuse", "\"$LOCAL\" \"$MERGED\" \"$REMOTE\" \"$BASE\"", + "\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", false), + /** + * See: http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp + */ + ecmerge("ecmerge", + "--default --mode=merge3 \"$BASE\" \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"", + "--default --mode=merge2 \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"", + false), + /** + * See: https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html + *

+ * Hint: $(basename) command must be defined + *

+ */ + emerge("emacs", + "-f emerge-files-with-ancestor-command \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$(basename \"$MERGED\")\"", + "-f emerge-files-command \"$LOCAL\" \"$REMOTE\" \"$(basename \"$MERGED\")\"", + true), + /** + * See: https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options + */ + examdiff("ExamDiff", + "-merge \"$LOCAL\" \"$BASE\" \"$REMOTE\" -o:\"$MERGED\" -nh", + "-merge \"$LOCAL\" \"$REMOTE\" -o:\"$MERGED\" -nh", + false), + /** + * See: https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html + */ + guiffy("guiffy", "-s \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"", + "-m \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true), + /** + * See: http://vimdoc.sourceforge.net/htmldoc/diff.html + */ + gvimdiff("gvim", + "-f -d -c '4wincmd w | wincmd J' \"$LOCAL\" \"$BASE\" \"$REMOTE\" \"$MERGED\"", + "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + true), + /** + * See: http://vimdoc.sourceforge.net/htmldoc/diff.html + */ + gvimdiff2("gvim", "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", true), + /** + * See: + */ + gvimdiff3("gvim", + "-f -d -c 'hid | hid | hid' \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"", + "-f -d -c 'hid | hid' \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true), + /** + * See: http://kdiff3.sourceforge.net/doc/documentation.html + */ + kdiff3("kdiff3", + "--auto --L1 \"$MERGED (Base)\" --L2 \"$MERGED (Local)\" --L3 \"$MERGED (Remote)\" -o \"$MERGED\" \"$BASE\" \"$LOCAL\" \"$REMOTE\"", + "--auto --L1 \"$MERGED (Local)\" --L2 \"$MERGED (Remote)\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: http://meldmerge.org/help/file-mode.html + *

+ * Hint: use meld with output option only (new versions) + *

+ */ + meld("meld", "--output=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + false), + /** + * See: http://www.manpagez.com/man/1/opendiff/ + *

+ * Hint: check the ' | cat' for the call + *

+ */ + opendiff("opendiff", + "\"$LOCAL\" \"$REMOTE\" -ancestor \"$BASE\" -merge \"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" -merge \"$MERGED\"", + false), + /** + * See: https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html + *

+ * Hint: check how to fix "no base present" / create_virtual_base problem + *

+ */ + p4merge("p4merge", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", + "\"$REMOTE\" \"$LOCAL\" \"$MERGED\"", false), + /** + * See: http://linux.math.tifr.res.in/manuals/man/tkdiff.html + */ + tkdiff("tkdiff", "-a \"$BASE\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + "-o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics + *

+ * Hint: merge without base is not supported + *

+ *

+ * Hint: cannot diff + *

+ */ + tortoisegitmerge("tortoisegitmerge", + "-base \"$BASE\" -mine \"$LOCAL\" -theirs \"$REMOTE\" -merged \"$MERGED\"", + null, false), + /** + * See: https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics + *

+ * Hint: merge without base is not supported + *

+ *

+ * Hint: cannot diff + *

+ */ + tortoisemerge("tortoisemerge", + "-base:\"$BASE\" -mine:\"$LOCAL\" -theirs:\"$REMOTE\" -merged:\"$MERGED\"", + null, false), + /** + * See: http://vimdoc.sourceforge.net/htmldoc/diff.html + */ + vimdiff("vim", gvimdiff), + /** + * See: http://vimdoc.sourceforge.net/htmldoc/diff.html + */ + vimdiff2("vim", gvimdiff2), + /** + * See: http://vimdoc.sourceforge.net/htmldoc/diff.html + */ + vimdiff3("vim", gvimdiff3), + /** + * See: http://manual.winmerge.org/Command_line.html + *

+ * Hint: check how 'mergetool_find_win32_cmd "WinMergeU.exe" "WinMerge"' + * works + *

+ */ + winmerge("WinMergeU", + "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + false), + /** + * See: http://furius.ca/xxdiff/doc/xxdiff-doc.html + */ + xxdiff("xxdiff", + "-X --show-merged-pane -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "-X -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + false); + + CommandLineMergeTool(String path, String parametersWithBase, + String parametersWithoutBase, + boolean exitCodeTrustable) { + this.path = path; + this.parametersWithBase = parametersWithBase; + this.parametersWithoutBase = parametersWithoutBase; + this.exitCodeTrustable = exitCodeTrustable; + } + + CommandLineMergeTool(CommandLineMergeTool from) { + this(from.getPath(), from.getParameters(true), + from.getParameters(false), from.isExitCodeTrustable()); + } + + CommandLineMergeTool(String path, CommandLineMergeTool from) { + this(path, from.getParameters(true), from.getParameters(false), + from.isExitCodeTrustable()); + } + + private final String path; + + private final String parametersWithBase; + + private final String parametersWithoutBase; + + private final boolean exitCodeTrustable; + + /** + * @return path + */ + public String getPath() { + return path; + } + + /** + * @param withBase + * return parameters with base present? + * @return parameters with or without base present + */ + public String getParameters(boolean withBase) { + if (withBase) { + return parametersWithBase; + } + return parametersWithoutBase; + } + + /** + * @return parameters + */ + public boolean isExitCodeTrustable() { + return exitCodeTrustable; + } + + /** + * @return true if command with base present is valid, false otherwise + */ + public boolean canMergeWithoutBasePresent() { + return parametersWithoutBase != null; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java index 551f634f2..c8b04f90f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java @@ -49,9 +49,10 @@ private DiffToolConfig(Config rc) { toolName = rc.getString(CONFIG_DIFF_SECTION, null, CONFIG_KEY_TOOL); guiToolName = rc.getString(CONFIG_DIFF_SECTION, null, CONFIG_KEY_GUITOOL); - prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, CONFIG_KEY_PROMPT, + prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, toolName, + CONFIG_KEY_PROMPT, true); - String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, null, + String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_TRUST_EXIT_CODE); if (trustStr != null) { trustExitCode = Boolean.parseBoolean(trustStr) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index 39729a4ee..1dcc523bf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2021, Andre Bossert + * Copyright (C) 2018-2022, Andre Bossert * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -12,22 +12,27 @@ import java.util.TreeMap; import java.util.Collections; +import java.io.IOException; import java.util.Map; import java.util.Set; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.StringUtils; /** * Manages diff tools. */ public class DiffTools { + private final Repository repo; + private final DiffToolConfig config; - private Map predefinedTools; + private final Map predefinedTools; - private Map userDefinedTools; + private final Map userDefinedTools; /** * Creates the external diff-tools manager for given repository. @@ -36,22 +41,22 @@ public class DiffTools { * the repository */ public DiffTools(Repository repo) { + this.repo = repo; config = repo.getConfig().get(DiffToolConfig.KEY); - setupPredefinedTools(); - setupUserDefinedTools(); + predefinedTools = setupPredefinedTools(); + userDefinedTools = setupUserDefinedTools(config, predefinedTools); } /** * Compare two versions of a file. * - * @param newPath - * the new file path - * @param oldPath - * the old file path - * @param newId - * the new object ID - * @param oldId - * the old object ID + * @param localFile + * the local file element + * @param remoteFile + * the remote file element + * @param mergedFile + * the merged file element, it's path equals local or remote + * element path * @param toolName * the selected tool name (can be null) * @param prompt @@ -60,12 +65,35 @@ public DiffTools(Repository repo) { * the GUI option * @param trustExitCode * the "trust exit code" option - * @return the return code from executed tool + * @return the execution result from tool + * @throws ToolException */ - public int compare(String newPath, String oldPath, String newId, - String oldId, String toolName, BooleanTriState prompt, - BooleanTriState gui, BooleanTriState trustExitCode) { - return 0; + public ExecutionResult compare(FileElement localFile, + FileElement remoteFile, FileElement mergedFile, String toolName, + BooleanTriState prompt, BooleanTriState gui, + BooleanTriState trustExitCode) throws ToolException { + try { + // prepare the command (replace the file paths) + String command = ExternalToolUtils.prepareCommand( + guessTool(toolName, gui).getCommand(), localFile, + remoteFile, mergedFile, null); + // prepare the environment + Map env = ExternalToolUtils.prepareEnvironment(repo, + localFile, remoteFile, mergedFile, null); + boolean trust = config.isTrustExitCode(); + if (trustExitCode != BooleanTriState.UNSET) { + trust = trustExitCode == BooleanTriState.TRUE; + } + // execute the tool + CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); + return cmdExec.run(command, repo.getWorkTree(), env); + } catch (IOException | InterruptedException e) { + throw new ToolException(e); + } finally { + localFile.cleanTemporaries(); + remoteFile.cleanTemporaries(); + mergedFile.cleanTemporaries(); + } } /** @@ -103,41 +131,64 @@ public Map getNotAvailableTools() { */ public String getDefaultToolName(BooleanTriState gui) { return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ - : "my_default_toolname"; //$NON-NLS-1$ + : config.getDefaultToolName(); } /** * @return is interactive (config prompt enabled) ? */ public boolean isInteractive() { - return false; + return config.isPrompt(); } - private void setupPredefinedTools() { - predefinedTools = new TreeMap<>(); - for (CommandLineDiffTool tool : CommandLineDiffTool.values()) { - predefinedTools.put(tool.name(), new PreDefinedDiffTool(tool)); + private ExternalDiffTool guessTool(String toolName, BooleanTriState gui) + throws ToolException { + if (StringUtils.isEmptyOrNull(toolName)) { + toolName = getDefaultToolName(gui); } + ExternalDiffTool tool = getTool(toolName); + if (tool == null) { + throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$ + } + return tool; } - private void setupUserDefinedTools() { - userDefinedTools = new TreeMap<>(); - Map userTools = config.getTools(); + private ExternalDiffTool getTool(final String name) { + ExternalDiffTool tool = userDefinedTools.get(name); + if (tool == null) { + tool = predefinedTools.get(name); + } + return tool; + } + + private static Map setupPredefinedTools() { + Map tools = new TreeMap<>(); + for (CommandLineDiffTool tool : CommandLineDiffTool.values()) { + tools.put(tool.name(), new PreDefinedDiffTool(tool)); + } + return tools; + } + + private static Map setupUserDefinedTools( + DiffToolConfig cfg, Map predefTools) { + Map tools = new TreeMap<>(); + Map userTools = cfg.getTools(); for (String name : userTools.keySet()) { ExternalDiffTool userTool = userTools.get(name); // if difftool..cmd is defined we have user defined tool if (userTool.getCommand() != null) { - userDefinedTools.put(name, userTool); + tools.put(name, userTool); } else if (userTool.getPath() != null) { // if difftool..path is defined we just overload the path // of predefined tool - PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefinedTools + PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefTools .get(name); if (predefTool != null) { predefTool.setPath(userTool.getPath()); } } } + return tools; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java new file mode 100644 index 000000000..0c3ddf9af --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert + * + * 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 org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * The merge tool interface. + */ +public interface ExternalMergeTool extends ExternalDiffTool { + + /** + * @return the tool "trust exit code" option + */ + BooleanTriState getTrustExitCode(); + + /** + * @param withBase + * get command with base present (true) or without base present + * (false) + * @return the tool command + */ + String getCommand(boolean withBase); + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java new file mode 100644 index 000000000..3efb90c49 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert + * + * 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 java.util.TreeMap; +import java.io.IOException; +import java.util.Map; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Repository; + +/** + * Utilities for diff- and merge-tools. + */ +public class ExternalToolUtils { + + /** + * Prepare command for execution. + * + * @param command + * the input "command" string + * @param localFile + * the local file (ours) + * @param remoteFile + * the remote file (theirs) + * @param mergedFile + * the merged file (worktree) + * @param baseFile + * the base file (can be null) + * @return the prepared (with replaced variables) command string + * @throws IOException + */ + public static String prepareCommand(String command, FileElement localFile, + FileElement remoteFile, FileElement mergedFile, + FileElement baseFile) throws IOException { + command = localFile.replaceVariable(command); + command = remoteFile.replaceVariable(command); + command = mergedFile.replaceVariable(command); + if (baseFile != null) { + command = baseFile.replaceVariable(command); + } + return command; + } + + /** + * Prepare environment needed for execution. + * + * @param repo + * the repository + * @param localFile + * the local file (ours) + * @param remoteFile + * the remote file (theirs) + * @param mergedFile + * the merged file (worktree) + * @param baseFile + * the base file (can be null) + * @return the environment map with variables and values (file paths) + * @throws IOException + */ + public static Map prepareEnvironment(Repository repo, + FileElement localFile, FileElement remoteFile, + FileElement mergedFile, FileElement baseFile) throws IOException { + Map env = new TreeMap<>(); + env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath()); + localFile.addToEnv(env); + remoteFile.addToEnv(env); + mergedFile.addToEnv(env); + if (baseFile != null) { + baseFile.addToEnv(env); + } + return env; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java new file mode 100644 index 000000000..5902c1e1b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert + * + * 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 java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; + +import org.eclipse.jgit.diff.DiffEntry; + +/** + * The element used as left or right file for compare. + * + */ +public class FileElement { + + /** + * The file element type. + * + */ + public enum Type { + /** + * The local file element (ours). + */ + LOCAL, + /** + * The remote file element (theirs). + */ + REMOTE, + /** + * The merged file element (path in worktree). + */ + MERGED, + /** + * The base file element (of ours and theirs). + */ + BASE, + /** + * The backup file element (copy of merged / conflicted). + */ + BACKUP + } + + private final String path; + + private final Type type; + + private InputStream stream; + + private File tempFile; + + /** + * Creates file element for path. + * + * @param path + * the file path + * @param type + * the element type + */ + public FileElement(String path, Type type) { + this(path, type, null, null); + } + + /** + * Creates file element for path. + * + * @param path + * the file path + * @param type + * the element type + * @param tempFile + * the temporary file to be used (can be null and will be created + * then) + * @param stream + * the object stream to load instead of file + */ + public FileElement(String path, Type type, File tempFile, + InputStream stream) { + this.path = path; + this.type = type; + this.tempFile = tempFile; + this.stream = stream; + } + + /** + * @return the file path + */ + public String getPath() { + return path; + } + + /** + * @return the element type + */ + public Type getType() { + return type; + } + + /** + * Return a temporary file within passed directory and fills it with stream + * if valid. + * + * @param directory + * the directory where the temporary file is created + * @param midName + * name added in the middle of generated temporary file name + * @return the object stream + * @throws IOException + */ + public File getFile(File directory, String midName) throws IOException { + if ((tempFile != null) && (stream == null)) { + return tempFile; + } + tempFile = getTempFile(path, directory, midName); + return copyFromStream(tempFile, stream); + } + + /** + * Return a real file from work tree or a temporary file with content if + * stream is valid or if path is "/dev/null" + * + * @return the object stream + * @throws IOException + */ + public File getFile() throws IOException { + if ((tempFile != null) && (stream == null)) { + return tempFile; + } + File file = new File(path); + // if we have a stream or file is missing ("/dev/null") then create + // temporary file + if ((stream != null) || isNullPath()) { + tempFile = getTempFile(file); + return copyFromStream(tempFile, stream); + } + return file; + } + + /** + * Check if path id "/dev/null" + * + * @return true if path is "/dev/null" + */ + public boolean isNullPath() { + return path.equals(DiffEntry.DEV_NULL); + } + + /** + * Create temporary file in given or system temporary directory + * + * @param directory + * the directory for the file (can be null); if null system + * temporary directory is used + * @return temporary file in directory or in the system temporary directory + * @throws IOException + */ + public File createTempFile(File directory) throws IOException { + if (tempFile == null) { + File file = new File(path); + if (directory != null) { + tempFile = getTempFile(file, directory, type.name()); + } else { + tempFile = getTempFile(file); + } + } + return tempFile; + } + + private static File getTempFile(File file) throws IOException { + return File.createTempFile(".__", "__" + file.getName()); //$NON-NLS-1$ //$NON-NLS-2$ + } + + private static File getTempFile(File file, File directory, String midName) + throws IOException { + String[] fileNameAndExtension = splitBaseFileNameAndExtension(file); + return File.createTempFile( + fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$ + fileNameAndExtension[1], directory); + } + + private static File getTempFile(String path, File directory, String midName) + throws IOException { + return getTempFile(new File(path), directory, midName); + } + + /** + * Delete and invalidate temporary file if necessary. + */ + public void cleanTemporaries() { + if (tempFile != null && tempFile.exists()) + tempFile.delete(); + tempFile = null; + } + + private static File copyFromStream(File file, final InputStream stream) + throws IOException, FileNotFoundException { + if (stream != null) { + try (OutputStream outStream = new FileOutputStream(file)) { + int read = 0; + byte[] bytes = new byte[8 * 1024]; + while ((read = stream.read(bytes)) != -1) { + outStream.write(bytes, 0, read); + } + } finally { + // stream can only be consumed once --> close it + stream.close(); + } + } + return file; + } + + private static String[] splitBaseFileNameAndExtension(File file) { + String[] result = new String[2]; + result[0] = file.getName(); + result[1] = ""; //$NON-NLS-1$ + int idx = result[0].lastIndexOf("."); //$NON-NLS-1$ + // if "." was found (>-1) and last-index is not first char (>0), then + // split (same behavior like cgit) + if (idx > 0) { + result[1] = result[0].substring(idx, result[0].length()); + result[0] = result[0].substring(0, idx); + } + return result; + } + + /** + * Replace variable in input + * + * @param input + * the input string + * @return the replaced input string + * @throws IOException + */ + public String replaceVariable(String input) throws IOException { + return input.replace("$" + type.name(), getFile().getPath()); //$NON-NLS-1$ + } + + /** + * Add variable to environment map. + * + * @param env + * the environment where this element should be added + * @throws IOException + */ + public void addToEnv(Map env) throws IOException { + env.put(type.name(), getFile().getPath()); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java new file mode 100644 index 000000000..9be20b75a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert + * + * 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_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_BACKUP; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_TEMPORARIES; +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.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WRITE_TO_TEMP; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Config.SectionParser; +import org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * Keeps track of difftool related configuration options. + */ +public class MergeToolConfig { + + /** Key for {@link Config#get(SectionParser)}. */ + public static final Config.SectionParser KEY = MergeToolConfig::new; + + private final String toolName; + + private final String guiToolName; + + private final boolean prompt; + + private final boolean keepBackup; + + private final boolean keepTemporaries; + + private final boolean writeToTemp; + + private final Map tools; + + private MergeToolConfig(Config rc) { + toolName = rc.getString(CONFIG_MERGE_SECTION, null, CONFIG_KEY_TOOL); + guiToolName = rc.getString(CONFIG_MERGE_SECTION, null, + CONFIG_KEY_GUITOOL); + prompt = rc.getBoolean(CONFIG_MERGETOOL_SECTION, toolName, + CONFIG_KEY_PROMPT, true); + keepBackup = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_KEEP_BACKUP, true); + keepTemporaries = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_KEEP_TEMPORARIES, false); + writeToTemp = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_WRITE_TO_TEMP, false); + tools = new HashMap<>(); + Set subsections = rc.getSubsections(CONFIG_MERGETOOL_SECTION); + for (String name : subsections) { + String cmd = rc.getString(CONFIG_MERGETOOL_SECTION, name, + CONFIG_KEY_CMD); + String path = rc.getString(CONFIG_MERGETOOL_SECTION, name, + CONFIG_KEY_PATH); + BooleanTriState trustExitCode = BooleanTriState.FALSE; + String trustStr = rc.getString(CONFIG_MERGETOOL_SECTION, name, + CONFIG_KEY_TRUST_EXIT_CODE); + if (trustStr != null) { + trustExitCode = Boolean.valueOf(trustStr).booleanValue() + ? BooleanTriState.TRUE + : BooleanTriState.FALSE; + } else { + trustExitCode = BooleanTriState.UNSET; + } + if ((cmd != null) || (path != null)) { + tools.put(name, new UserDefinedMergeTool(name, path, cmd, + trustExitCode)); + } + } + } + + /** + * @return the default merge tool name (merge.tool) + */ + public String getDefaultToolName() { + return toolName; + } + + /** + * @return the default GUI merge tool name (merge.guitool) + */ + public String getDefaultGuiToolName() { + return guiToolName; + } + + /** + * @return the merge tool "prompt" option (mergetool.prompt) + */ + public boolean isPrompt() { + return prompt; + } + + /** + * @return the tool "keep backup" option + */ + public boolean isKeepBackup() { + return keepBackup; + } + + /** + * @return the tool "keepTemporaries" option + */ + public boolean isKeepTemporaries() { + return keepTemporaries; + } + + /** + * @return the tool "write to temp" option + */ + public boolean isWriteToTemp() { + return writeToTemp; + } + + /** + * @return the tools map + */ + public Map getTools() { + return tools; + } + + /** + * @return the tool names + */ + public Set getToolNames() { + return tools.keySet(); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java new file mode 100644 index 000000000..9a2a8304e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert + * + * 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 java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.eclipse.jgit.internal.diffmergetool.FileElement.Type; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.util.FS.ExecutionResult; + +/** + * Manages merge tools. + */ +public class MergeTools { + + Repository repo; + + private final MergeToolConfig config; + + private final Map predefinedTools; + + private final Map userDefinedTools; + + /** + * @param repo + * the repository + */ + public MergeTools(Repository repo) { + this.repo = repo; + config = repo.getConfig().get(MergeToolConfig.KEY); + predefinedTools = setupPredefinedTools(); + userDefinedTools = setupUserDefinedTools(config, predefinedTools); + } + + /** + * @param localFile + * the local file element + * @param remoteFile + * the remote file element + * @param mergedFile + * the merged file element + * @param baseFile + * the base file element (can be null) + * @param tempDir + * the temporary directory (needed for backup and auto-remove, + * can be null) + * @param toolName + * the selected tool name (can be null) + * @param prompt + * the prompt option + * @param gui + * the GUI option + * @return the execution result from tool + * @throws ToolException + */ + public ExecutionResult merge(FileElement localFile, FileElement remoteFile, + FileElement mergedFile, FileElement baseFile, File tempDir, + String toolName, BooleanTriState prompt, BooleanTriState gui) + throws ToolException { + ExternalMergeTool tool = guessTool(toolName, gui); + FileElement backup = null; + ExecutionResult result = null; + try { + File workingDir = repo.getWorkTree(); + // create additional backup file (copy worktree file) + backup = createBackupFile(mergedFile.getPath(), + tempDir != null ? tempDir : workingDir); + // prepare the command (replace the file paths) + boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; + String command = ExternalToolUtils.prepareCommand( + tool.getCommand(baseFile != null), localFile, remoteFile, + mergedFile, baseFile); + // prepare the environment + Map env = ExternalToolUtils.prepareEnvironment(repo, + localFile, remoteFile, mergedFile, baseFile); + // execute the tool + CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); + result = cmdExec.run(command, workingDir, env); + // keep backup as .orig file + if (backup != null) { + keepBackupFile(mergedFile.getPath(), backup); + } + return result; + } catch (IOException | InterruptedException e) { + throw new ToolException(e); + } finally { + // always delete backup file (ignore that it was may be already + // moved to keep-backup file) + if (backup != null) { + backup.cleanTemporaries(); + } + // if the tool returns an error and keepTemporaries is set to true, + // then these temporary files will be preserved + if (!((result == null) && config.isKeepTemporaries())) { + // delete the files + localFile.cleanTemporaries(); + remoteFile.cleanTemporaries(); + if (baseFile != null) { + baseFile.cleanTemporaries(); + } + // delete temporary directory if needed + if (config.isWriteToTemp() && (tempDir != null) + && tempDir.exists()) { + tempDir.delete(); + } + } + } + } + + private FileElement createBackupFile(String filePath, File parentDir) + throws IOException { + FileElement backup = null; + Path path = Paths.get(filePath); + if (Files.exists(path)) { + backup = new FileElement(filePath, Type.BACKUP); + Files.copy(path, backup.createTempFile(parentDir).toPath(), + StandardCopyOption.REPLACE_EXISTING); + } + return backup; + } + + /** + * @return the created temporary directory if (mergetol.writeToTemp == true) + * or null if not configured or false. + * @throws IOException + */ + public File createTempDirectory() throws IOException { + return config.isWriteToTemp() + ? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$ + : null; + } + + /** + * @return the tool names + */ + public Set getToolNames() { + return config.getToolNames(); + } + + /** + * @return the user defined tools + */ + public Map getUserDefinedTools() { + return userDefinedTools; + } + + /** + * @return the available predefined tools + */ + public Map getAvailableTools() { + return predefinedTools; + } + + /** + * @return the NOT available predefined tools + */ + public Map getNotAvailableTools() { + return new TreeMap<>(); + } + + /** + * @param gui + * use the diff.guitool setting ? + * @return the default tool name + */ + public String getDefaultToolName(BooleanTriState gui) { + return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ + : config.getDefaultToolName(); + } + + /** + * @return is interactive (config prompt enabled) ? + */ + public boolean isInteractive() { + return config.isPrompt(); + } + + private ExternalMergeTool guessTool(String toolName, BooleanTriState gui) + throws ToolException { + if ((toolName == null) || toolName.isEmpty()) { + toolName = getDefaultToolName(gui); + } + ExternalMergeTool tool = getTool(toolName); + if (tool == null) { + throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$ + } + return tool; + } + + private ExternalMergeTool getTool(final String name) { + ExternalMergeTool tool = userDefinedTools.get(name); + if (tool == null) { + tool = predefinedTools.get(name); + } + return tool; + } + + private void keepBackupFile(String mergedFilePath, FileElement backup) + throws IOException { + if (config.isKeepBackup()) { + Path backupPath = backup.getFile().toPath(); + Files.move(backupPath, + backupPath.resolveSibling( + Paths.get(mergedFilePath).getFileName() + ".orig"), //$NON-NLS-1$ + StandardCopyOption.REPLACE_EXISTING); + } + } + + private Map setupPredefinedTools() { + Map tools = new TreeMap<>(); + for (CommandLineMergeTool tool : CommandLineMergeTool.values()) { + tools.put(tool.name(), new PreDefinedMergeTool(tool)); + } + return tools; + } + + private Map setupUserDefinedTools( + MergeToolConfig cfg, Map predefTools) { + Map tools = new TreeMap<>(); + Map userTools = cfg.getTools(); + for (String name : userTools.keySet()) { + ExternalMergeTool userTool = userTools.get(name); + // if mergetool..cmd is defined we have user defined tool + if (userTool.getCommand() != null) { + tools.put(name, userTool); + } else if (userTool.getPath() != null) { + // if mergetool..path is defined we just overload the path + // of predefined tool + PreDefinedMergeTool predefTool = (PreDefinedMergeTool) predefTools + .get(name); + if (predefTool != null) { + predefTool.setPath(userTool.getPath()); + if (userTool.getTrustExitCode() != BooleanTriState.UNSET) { + predefTool + .setTrustExitCode(userTool.getTrustExitCode()); + } + } + } + } + return tools; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java index 1c69fb491..092cb605b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java @@ -46,17 +46,6 @@ public PreDefinedDiffTool(CommandLineDiffTool tool) { */ @Override public void setPath(String path) { - // handling of spaces in path - if (path.contains(" ")) { //$NON-NLS-1$ - // add quotes before if needed - if (!path.startsWith("\"")) { //$NON-NLS-1$ - path = "\"" + path; //$NON-NLS-1$ - } - // add quotes after if needed - if (!path.endsWith("\"")) { //$NON-NLS-1$ - path = path + "\""; //$NON-NLS-1$ - } - } super.setPath(path); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java new file mode 100644 index 000000000..2c64c1666 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert + * + * 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 org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * The pre-defined merge tool. + */ +public class PreDefinedMergeTool extends UserDefinedMergeTool { + + /** + * the tool parameters without base + */ + private final String parametersWithoutBase; + + /** + * Creates the pre-defined merge tool + * + * @param name + * the name + * @param path + * the path + * @param parametersWithBase + * the tool parameters that are used together with path as + * command and "base is present" ($BASE) + * @param parametersWithoutBase + * the tool parameters that are used together with path as + * command and "base is present" ($BASE) + * @param trustExitCode + * the "trust exit code" option + */ + public PreDefinedMergeTool(String name, String path, + String parametersWithBase, String parametersWithoutBase, + BooleanTriState trustExitCode) { + super(name, path, parametersWithBase, trustExitCode); + this.parametersWithoutBase = parametersWithoutBase; + } + + /** + * Creates the pre-defined merge tool + * + * @param tool + * the command line merge tool + * + */ + public PreDefinedMergeTool(CommandLineMergeTool tool) { + this(tool.name(), tool.getPath(), tool.getParameters(true), + tool.getParameters(false), + tool.isExitCodeTrustable() ? BooleanTriState.TRUE + : BooleanTriState.FALSE); + } + + /** + * @param trustExitCode + * the "trust exit code" option + */ + @Override + public void setTrustExitCode(BooleanTriState trustExitCode) { + super.setTrustExitCode(trustExitCode); + } + + /** + * @return the tool command (with base present) + */ + @Override + public String getCommand() { + return getCommand(true); + } + + /** + * @param withBase + * get command with base present (true) or without base present + * (false) + * @return the tool command + */ + @Override + public String getCommand(boolean withBase) { + return getPath() + " " //$NON-NLS-1$ + + (withBase ? super.getCommand() : parametersWithoutBase); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java new file mode 100644 index 000000000..27f7d12e6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert + * + * 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 org.eclipse.jgit.util.FS.ExecutionResult; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tool exception for differentiation. + * + */ +public class ToolException extends Exception { + + private final static Logger LOG = LoggerFactory + .getLogger(ToolException.class); + + private final ExecutionResult result; + + private final boolean commandExecutionError; + + /** + * the serial version UID + */ + private static final long serialVersionUID = 1L; + + /** + * + */ + public ToolException() { + this(null, null, false); + } + + /** + * @param message + * the exception message + */ + public ToolException(String message) { + this(message, null, false); + } + + /** + * @param message + * the exception message + * @param result + * the execution result + * @param commandExecutionError + * is command execution error happened ? + */ + public ToolException(String message, ExecutionResult result, + boolean commandExecutionError) { + super(message); + this.result = result; + this.commandExecutionError = commandExecutionError; + } + + /** + * @param message + * the exception message + * @param cause + * the cause for throw + */ + public ToolException(String message, Throwable cause) { + super(message, cause); + result = null; + commandExecutionError = false; + } + + /** + * @param cause + * the cause for throw + */ + public ToolException(Throwable cause) { + super(cause); + result = null; + commandExecutionError = false; + } + + /** + * @return true if result is valid, false else + */ + public boolean isResult() { + return result != null; + } + + /** + * @return the execution result + */ + public ExecutionResult getResult() { + return result; + } + + /** + * @return true if command execution error appears, false otherwise + */ + public boolean isCommandExecutionError() { + return commandExecutionError; + } + + /** + * @return the result Stderr + */ + public String getResultStderr() { + try { + return new String(result.getStderr().toByteArray()); + } catch (Exception e) { + LOG.warn("Failed to retrieve standard error output", e); //$NON-NLS-1$ + } + return ""; //$NON-NLS-1$ + } + + /** + * @return the result Stdout + */ + public String getResultStdout() { + try { + return new String(result.getStdout().toByteArray()); + } catch (Exception e) { + LOG.warn("Failed to retrieve standard output", e); //$NON-NLS-1$ + } + return ""; //$NON-NLS-1$ + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java new file mode 100644 index 000000000..1dd2f0d79 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert + * + * 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 org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * The user-defined merge tool. + */ +public class UserDefinedMergeTool extends UserDefinedDiffTool + implements ExternalMergeTool { + + /** + * the merge tool "trust exit code" option + */ + private BooleanTriState trustExitCode; + + /** + * Creates the merge tool + * + * @param name + * the name + * @param path + * the path + * @param cmd + * the command + * @param trustExitCode + * the "trust exit code" option + */ + public UserDefinedMergeTool(String name, String path, String cmd, + BooleanTriState trustExitCode) { + super(name, path, cmd); + this.trustExitCode = trustExitCode; + } + /** + * @return the "trust exit code" flag + */ + @Override + public BooleanTriState getTrustExitCode() { + return trustExitCode; + } + + /** + * @param trustExitCode + * the new "trust exit code" flag + */ + protected void setTrustExitCode(BooleanTriState trustExitCode) { + this.trustExitCode = trustExitCode; + } + + /** + * @param withBase + * not used, because user-defined merge tool can only define one + * cmd -> it must handle with and without base present (empty) + * @return the tool command + */ + @Override + public String getCommand(boolean withBase) { + return getCommand(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 8ad32d41c..2342cad0d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -10,6 +10,7 @@ * * SPDX-License-Identifier: BSD-3-Clause */ + package org.eclipse.jgit.lib; /** @@ -31,14 +32,14 @@ public final class ConfigConstants { public static final String CONFIG_DIFF_SECTION = "diff"; /** - * The "tool" key within "diff" section + * The "tool" key within "diff" or "merge" section * * @since 6.1 */ public static final String CONFIG_KEY_TOOL = "tool"; /** - * The "guitool" key within "diff" section + * The "guitool" key within "diff" or "merge" section * * @since 6.1 */ @@ -52,21 +53,21 @@ public final class ConfigConstants { public static final String CONFIG_DIFFTOOL_SECTION = "difftool"; /** - * The "prompt" key within "difftool" section + * The "prompt" key within "difftool" or "mergetool" section * * @since 6.1 */ public static final String CONFIG_KEY_PROMPT = "prompt"; /** - * The "trustExitCode" key within "difftool" section + * The "trustExitCode" key within "difftool" or "mergetool.." section * * @since 6.1 */ public static final String CONFIG_KEY_TRUST_EXIT_CODE = "trustExitCode"; /** - * The "cmd" key within "difftool.*." section + * The "cmd" key within "difftool.*." or "mergetool.*." section * * @since 6.1 */ @@ -123,6 +124,34 @@ public final class ConfigConstants { */ public static final String CONFIG_MERGE_SECTION = "merge"; + /** + * The "mergetool" section + * + * @since 6.2 + */ + public static final String CONFIG_MERGETOOL_SECTION = "mergetool"; + + /** + * The "keepBackup" key within "mergetool" section + * + * @since 6.2 + */ + public static final String CONFIG_KEY_KEEP_BACKUP = "keepBackup"; + + /** + * The "keepTemporaries" key within "mergetool" section + * + * @since 6.2 + */ + public static final String CONFIG_KEY_KEEP_TEMPORARIES = "keepTemporaries"; + + /** + * The "writeToTemp" key within "mergetool" section + * + * @since 6.2 + */ + public static final String CONFIG_KEY_WRITE_TO_TEMP = "writeToTemp"; + /** * The "filter" section * @since 4.6