diff options
author | Matthias Sohn <matthias.sohn@sap.com> | 2022-06-01 18:14:28 +0200 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2022-06-01 18:14:28 +0200 |
commit | 9fccad2e46d09052f4433e6764448c3518ae37f0 (patch) | |
tree | 6bb2c4188f1fac42120407a425774ad713e42780 | |
parent | 4bb024192d838938d1673bfb9652dea237f1317f (diff) | |
parent | bb30be6b3335c67ee59d3705d5e04e2dbb688128 (diff) | |
download | jgit-9fccad2e46d09052f4433e6764448c3518ae37f0.tar.gz jgit-9fccad2e46d09052f4433e6764448c3518ae37f0.zip |
Merge branch 'master' into stable-6.2
* master:
Fixed since tags added for new merge constants
Add filtering with help of DirCacheCheckout.getContent()
Avoid warning "no explicit project encoding"
Add mergetool merge feature (execute external tool)
Add command line support for "git mergetool"
Add config reader for user-defined mergetools
Add difftool compare feature (execute external tool)
Change-Id: Ie215b8205b85cfd9efddb622c90dfad7a5ae0caa
30 files changed, 3474 insertions, 204 deletions
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 0000000000..99f26c0203 --- /dev/null +++ b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/<project>=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 0000000000..99f26c0203 --- /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/<project>=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 0000000000..99f26c0203 --- /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/<project>=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 0000000000..99f26c0203 --- /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/<project>=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 e7bf48417d..8daaa6ad9e 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 <simeon.danailov.andreev@gmail.com> and others. + * Copyright (C) 2021-2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> 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<String> arguments = new ArrayList<>(); + private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + configureEchoTool(TOOL_NAME); } - private String[] runAndCaptureUsingInitRaw(String... args) - throws Exception { - CLIGitCommand.Result result = new CLIGitCommand.Result(); + @Test + public void testToolWithPrompt() throws Exception { + String[] inputLines = { + "y", // accept launching diff tool + "y", // accept launching diff tool + }; - GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); - CmdLineParser clp = new CmdLineParser(bean); - clp.parseArgument(args); + String[] conflictingFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedCompareOutput(conflictingFilenames); - 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]); + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + DIFF_TOOL, "--prompt", option, TOOL_NAME)); } - private Git git; + @Test + public void testToolAbortLaunch() throws Exception { + String[] inputLines = { + "y", // accept launching diff tool + "n", // don't launch diff tool + }; - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - git = new Git(db); - git.commit().setMessage("initial commit").call(); + 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<DiffEntry> changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] conflictFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictFilenames); String[] options = { "--tool", @@ -84,54 +99,51 @@ public class DiffToolTest extends CLIRepositoryTestCase { 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<DiffEntry> 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<DiffEntry> 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<DiffEntry> 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 @@ public class DiffToolTest extends CLIRepositoryTestCase { public void testToolHelp() throws Exception { CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); List<String> expectedOutput = new ArrayList<>(); - expectedOutput.add("git difftool --tool=<tool> may be set to one of the following:"); + expectedOutput.add( + "'git difftool --tool=<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 class DiffToolTest extends CLIRepositoryTestCase { 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<DiffEntry> getRepositoryChanges(RevCommit commit) - throws Exception { - TreeWalk tw = new TreeWalk(db); - tw.addTree(commit.getTree()); - FileTreeIterator modifiedTree = new FileTreeIterator(db); - tw.addTree(modifiedTree); - List<DiffEntry> changes = DiffEntry.scan(tw); - return changes; - } - - private String[] getExpectedDiffToolOutput(List<DiffEntry> 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<String> 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<String> 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 0000000000..2e50f09081 --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> 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<String> expectedOutput = new ArrayList<>(); + expectedOutput.add( + "'git mergetool --tool=<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<String> 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<String> 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<String> 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<String> 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<String> 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 0000000000..933f19bcc4 --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> 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<String> 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<String> 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<DiffEntry> getRepositoryChanges(RevCommit commit) + throws Exception { + TreeWalk tw = new TreeWalk(db); + tw.addTree(commit.getTree()); + FileTreeIterator modifiedTree = new FileTreeIterator(db); + tw.addTree(modifiedTree); + List<DiffEntry> changes = DiffEntry.scan(tw); + return changes; + } + + protected static InputStream createInputStream(String[] inputLines) { + return createInputStream(Arrays.asList(inputLines)); + } + + protected static InputStream createInputStream(List<String> 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 8c44764c63..ea1d1e3faa 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 fda0bf6ff4..674185df2b 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=<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=<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=<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 <tool>. Run git difftool --tool-help for the list of valid <tool> 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 <tool>. Run git mergetool --tool-help for the list of valid <tool> 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 128881779b..74d91cd3d7 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 <andre.bossert@siemens.com> + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> * * 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.text.MessageFormat; 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.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.internal.diffmergetool.DiffTools; -import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool; +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 @@ class DiffTool extends TextBuiltin { @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 @@ class DiffTool extends TextBuiltin { private void compare(List<DiffEntry> 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 @@ class DiffTool extends TextBuiltin { 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 @@ class DiffTool extends TextBuiltin { 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<String, ExternalDiffTool> 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 @@ class DiffTool extends TextBuiltin { 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 0000000000..9712770758 --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * 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<String> 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<String, StageState> 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<String, StageState> files, boolean showPrompt, + String toolNamePrompt) throws Exception { + // sort file names + List<String> 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<String, ExternalMergeTool> 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<String, StageState> getFiles() throws RevisionSyntaxException, + NoWorkTreeException, GitAPIException { + Map<String, StageState> 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 7fe5b0fa45..989e649b72 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 class CLIText extends TranslationBundle { /***/ 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 c9ebec7638..4fd55c6cad 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 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; /** @@ -32,6 +37,54 @@ import org.junit.Test; */ 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 class ExternalDiffToolTest extends ExternalToolTestCase { 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<String> actualToolNames = manager.getUserDefinedTools().keySet(); Set<String> expectedToolNames = new LinkedHashSet<>(); @@ -109,38 +162,50 @@ public class ExternalDiffToolTest extends ExternalToolTestCase { } @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 class ExternalDiffToolTest extends ExternalToolTestCase { "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<String, ExternalDiffTool> 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 0000000000..50576682eb --- /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 <simeon.danailov.andreev@gmail.com> 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<String> actualToolNames = manager.getToolNames(); + Set<String> expectedToolNames = Collections.emptySet(); + assertEquals("Incorrect set of external merge tool names", + expectedToolNames, actualToolNames); + } + + @Test + public void testAllTools() { + MergeTools manager = new MergeTools(db); + Set<String> actualToolNames = manager.getAvailableTools().keySet(); + Set<String> 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<String, ExternalMergeTool> 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<String> actualToolNames = manager.getUserDefinedTools().keySet(); + Set<String> 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<String> actualToolNames = manager.getNotAvailableTools().keySet(); + Set<String> 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<String, ExternalMergeTool> 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 0cc12978a8..0fd85cb456 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 abstract class ExternalToolTestCase extends RepositoryTestCase { 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 1a41df3d0a..64ff19c9c3 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 abstract class ContentSource { 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 abstract class ContentSource { 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 @@ public abstract class ContentSource { 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 abstract class ContentSource { 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 0000000000..ad79fe8fc6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * 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<String, String> 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<String, String> 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 0000000000..3a22124328 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * 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 + * + * <pre> + * 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 + * </pre> + * + */ +@SuppressWarnings("nls") +public enum CommandLineMergeTool { + /** + * See: <a href= + * "https://www.araxis.com/merge/documentation-windows/command-line.en">https://www.araxis.com/merge/documentation-windows/command-line.en</a> + */ + araxis("compare", + "-wait -merge -3 -a1 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + "-wait -2 \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + false), + /** + * See: <a href= + * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a> + */ + bc("bcomp", "\"$LOCAL\" \"$REMOTE\" \"$BASE\" --mergeoutput=\"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" --mergeoutput=\"$MERGED\"", + false), + /** + * See: <a href= + * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a> + */ + bc3("bcompare", bc), + /** + * See: <a href= + * "https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm">https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm</a> + */ + codecompare("CodeMerge", + "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -BF=\"$BASE\" -RF=\"$MERGED\"", + "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -RF=\"$MERGED\"", + false), + /** + * See: <a href= + * "https://www.deltawalker.com/integrate/command-line">https://www.deltawalker.com/integrate/command-line</a> + * <p> + * Hint: $(pwd) command must be defined + * </p> + */ + deltawalker("DeltaWalker", + "\"$LOCAL\" \"$REMOTE\" \"$BASE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"", + true), + /** + * See: <a href= + * "https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html">https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html</a> + */ + diffmerge("diffmerge", //$NON-NLS-1$ + "--merge --result=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "--merge --result=\"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: <a href= + * "http://diffuse.sourceforge.net/manual.html#introduction-usage">http://diffuse.sourceforge.net/manual.html#introduction-usage</a> + * <p> + * Hint: check the ' | cat' for the call + * </p> + */ + diffuse("diffuse", "\"$LOCAL\" \"$MERGED\" \"$REMOTE\" \"$BASE\"", + "\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", false), + /** + * See: <a href= + * "http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp">http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp</a> + */ + ecmerge("ecmerge", + "--default --mode=merge3 \"$BASE\" \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"", + "--default --mode=merge2 \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"", + false), + /** + * See: <a href= + * "https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html">https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html</a> + * <p> + * Hint: $(basename) command must be defined + * </p> + */ + emerge("emacs", + "-f emerge-files-with-ancestor-command \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$(basename \"$MERGED\")\"", + "-f emerge-files-command \"$LOCAL\" \"$REMOTE\" \"$(basename \"$MERGED\")\"", + true), + /** + * See: <a href= + * "https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options">https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options</a> + */ + examdiff("ExamDiff", + "-merge \"$LOCAL\" \"$BASE\" \"$REMOTE\" -o:\"$MERGED\" -nh", + "-merge \"$LOCAL\" \"$REMOTE\" -o:\"$MERGED\" -nh", + false), + /** + * See: <a href= + * "https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html">https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html</a> + */ + guiffy("guiffy", "-s \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"", + "-m \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + gvimdiff("gvim", + "-f -d -c '4wincmd w | wincmd J' \"$LOCAL\" \"$BASE\" \"$REMOTE\" \"$MERGED\"", + "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + true), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + gvimdiff2("gvim", "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", true), + /** + * See: <a href= "http://vimdoc.sourceforge.net/htmldoc/diff.html"></a> + */ + gvimdiff3("gvim", + "-f -d -c 'hid | hid | hid' \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"", + "-f -d -c 'hid | hid' \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true), + /** + * See: <a href= + * "http://kdiff3.sourceforge.net/doc/documentation.html">http://kdiff3.sourceforge.net/doc/documentation.html</a> + */ + 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: <a href= + * "http://meldmerge.org/help/file-mode.html">http://meldmerge.org/help/file-mode.html</a> + * <p> + * Hint: use meld with output option only (new versions) + * </p> + */ + meld("meld", "--output=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + false), + /** + * See: <a href= + * "http://www.manpagez.com/man/1/opendiff/">http://www.manpagez.com/man/1/opendiff/</a> + * <p> + * Hint: check the ' | cat' for the call + * </p> + */ + opendiff("opendiff", + "\"$LOCAL\" \"$REMOTE\" -ancestor \"$BASE\" -merge \"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" -merge \"$MERGED\"", + false), + /** + * See: <a href= + * "https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html">https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html</a> + * <p> + * Hint: check how to fix "no base present" / create_virtual_base problem + * </p> + */ + p4merge("p4merge", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", + "\"$REMOTE\" \"$LOCAL\" \"$MERGED\"", false), + /** + * See: <a href= + * "http://linux.math.tifr.res.in/manuals/man/tkdiff.html">http://linux.math.tifr.res.in/manuals/man/tkdiff.html</a> + */ + tkdiff("tkdiff", "-a \"$BASE\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + "-o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: <a href= + * "https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics">https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics</a> + * <p> + * Hint: merge without base is not supported + * </p> + * <p> + * Hint: cannot diff + * </p> + */ + tortoisegitmerge("tortoisegitmerge", + "-base \"$BASE\" -mine \"$LOCAL\" -theirs \"$REMOTE\" -merged \"$MERGED\"", + null, false), + /** + * See: <a href= + * "https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics">https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics</a> + * <p> + * Hint: merge without base is not supported + * </p> + * <p> + * Hint: cannot diff + * </p> + */ + tortoisemerge("tortoisemerge", + "-base:\"$BASE\" -mine:\"$LOCAL\" -theirs:\"$REMOTE\" -merged:\"$MERGED\"", + null, false), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + vimdiff("vim", gvimdiff), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + vimdiff2("vim", gvimdiff2), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + vimdiff3("vim", gvimdiff3), + /** + * See: <a href= + * "http://manual.winmerge.org/Command_line.html">http://manual.winmerge.org/Command_line.html</a> + * <p> + * Hint: check how 'mergetool_find_win32_cmd "WinMergeU.exe" "WinMerge"' + * works + * </p> + */ + winmerge("WinMergeU", + "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + false), + /** + * See: <a href= + * "http://furius.ca/xxdiff/doc/xxdiff-doc.html">http://furius.ca/xxdiff/doc/xxdiff-doc.html</a> + */ + 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 551f634f2d..c8b04f90f2 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 @@ public class DiffToolConfig { 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 39729a4eec..1dcc523bf8 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 <andre.bossert@siemens.com> + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> * * 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 @@ package org.eclipse.jgit.internal.diffmergetool; 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<String, ExternalDiffTool> predefinedTools; + private final Map<String, ExternalDiffTool> predefinedTools; - private Map<String, ExternalDiffTool> userDefinedTools; + private final Map<String, ExternalDiffTool> 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 class DiffTools { * 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<String, String> 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 class DiffTools { */ 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 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 ExternalDiffTool getTool(final String name) { + ExternalDiffTool tool = userDefinedTools.get(name); + if (tool == null) { + tool = predefinedTools.get(name); + } + return tool; } - private void setupPredefinedTools() { - predefinedTools = new TreeMap<>(); + private static Map<String, ExternalDiffTool> setupPredefinedTools() { + Map<String, ExternalDiffTool> tools = new TreeMap<>(); for (CommandLineDiffTool tool : CommandLineDiffTool.values()) { - predefinedTools.put(tool.name(), new PreDefinedDiffTool(tool)); + tools.put(tool.name(), new PreDefinedDiffTool(tool)); } + return tools; } - private void setupUserDefinedTools() { - userDefinedTools = new TreeMap<>(); - Map<String, ExternalDiffTool> userTools = config.getTools(); + private static Map<String, ExternalDiffTool> setupUserDefinedTools( + DiffToolConfig cfg, Map<String, ExternalDiffTool> predefTools) { + Map<String, ExternalDiffTool> tools = new TreeMap<>(); + Map<String, ExternalDiffTool> userTools = cfg.getTools(); for (String name : userTools.keySet()) { ExternalDiffTool userTool = userTools.get(name); // if difftool.<name>.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.<name>.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 0000000000..0c3ddf9afe --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * 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 0000000000..3efb90c490 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * 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<String, String> prepareEnvironment(Repository repo, + FileElement localFile, FileElement remoteFile, + FileElement mergedFile, FileElement baseFile) throws IOException { + Map<String, String> 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 0000000000..5902c1e1b8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * 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<String, String> 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 0000000000..9be20b75ad --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * 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<MergeToolConfig> 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<String, ExternalMergeTool> 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<String> 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<String, ExternalMergeTool> getTools() { + return tools; + } + + /** + * @return the tool names + */ + public Set<String> 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 0000000000..9a2a8304eb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * 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<String, ExternalMergeTool> predefinedTools; + + private final Map<String, ExternalMergeTool> 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<String, String> 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<String> getToolNames() { + return config.getToolNames(); + } + + /** + * @return the user defined tools + */ + public Map<String, ExternalMergeTool> getUserDefinedTools() { + return userDefinedTools; + } + + /** + * @return the available predefined tools + */ + public Map<String, ExternalMergeTool> getAvailableTools() { + return predefinedTools; + } + + /** + * @return the NOT available predefined tools + */ + public Map<String, ExternalMergeTool> 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<String, ExternalMergeTool> setupPredefinedTools() { + Map<String, ExternalMergeTool> tools = new TreeMap<>(); + for (CommandLineMergeTool tool : CommandLineMergeTool.values()) { + tools.put(tool.name(), new PreDefinedMergeTool(tool)); + } + return tools; + } + + private Map<String, ExternalMergeTool> setupUserDefinedTools( + MergeToolConfig cfg, Map<String, ExternalMergeTool> predefTools) { + Map<String, ExternalMergeTool> tools = new TreeMap<>(); + Map<String, ExternalMergeTool> userTools = cfg.getTools(); + for (String name : userTools.keySet()) { + ExternalMergeTool userTool = userTools.get(name); + // if mergetool.<name>.cmd is defined we have user defined tool + if (userTool.getCommand() != null) { + tools.put(name, userTool); + } else if (userTool.getPath() != null) { + // if mergetool.<name>.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 1c69fb4911..092cb605be 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 class PreDefinedDiffTool extends UserDefinedDiffTool { */ @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 0000000000..2c64c16667 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * 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 0000000000..27f7d12e66 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * 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 0000000000..1dd2f0d793 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * 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 8ad32d41c7..2342cad0d7 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.<name>." 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 */ @@ -124,6 +125,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 */ |