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 e2ff18927..017a5d994 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021, Simeon Andreev and others. + * Copyright (C) 2021-2022, Simeon Andreev and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -14,68 +14,30 @@ 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.assertEquals; import static org.junit.Assert.fail; 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.lib.StoredConfig; -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.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 ExternalToolTestCase { - @Argument(index = 1, metaVar = "metaVar_arg") - List arguments = new ArrayList<>(); - } - - private String[] runAndCaptureUsingInitRaw(String... args) - throws Exception { - CLIGitCommand.Result result = new CLIGitCommand.Result(); - - GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); - CmdLineParser clp = new CmdLineParser(bean); - clp.parseArgument(args); - - TextBuiltin cmd = bean.subcommand; - cmd.initRaw(db, null, null, result.out, result.err); - cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()])); - if (cmd.getOutputWriter() != null) { - cmd.getOutputWriter().flush(); - } - if (cmd.getErrorWriter() != null) { - cmd.getErrorWriter().flush(); - } - return result.outLines().toArray(new String[0]); - } - - private static final String TOOL_NAME = "some_tool"; - private Git git; + private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION; @Override @Before public void setUp() throws Exception { super.setUp(); - git = new Git(db); - git.commit().setMessage("initial commit").call(); configureEchoTool(TOOL_NAME); } @@ -83,7 +45,7 @@ public void setUp() throws Exception { public void testNotDefinedTool() throws Exception { createUnstagedChanges(); - runAndCaptureUsingInitRaw("difftool", "--tool", "undefined"); + runAndCaptureUsingInitRaw(DIFF_TOOL, "--tool", "undefined"); fail("Expected exception when trying to run undefined tool"); } @@ -91,7 +53,7 @@ public void testNotDefinedTool() throws Exception { public void testTool() throws Exception { RevCommit commit = createUnstagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] expectedOutput = getExpectedToolOutput(changes); String[] options = { "--tool", @@ -101,7 +63,7 @@ public void testTool() throws Exception { for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, expectedOutput, - runAndCaptureUsingInitRaw("difftool", option, + runAndCaptureUsingInitRaw(DIFF_TOOL, option, TOOL_NAME)); } } @@ -110,13 +72,13 @@ public void testTool() throws Exception { public void testToolTrustExitCode() throws Exception { RevCommit commit = createUnstagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] expectedOutput = getExpectedToolOutput(changes); String[] options = { "--tool", "-t", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, "--trust-exit-code", option, TOOL_NAME)); } } @@ -125,13 +87,13 @@ expectedOutput, runAndCaptureUsingInitRaw("difftool", public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception { RevCommit commit = createUnstagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] expectedOutput = getExpectedToolOutput(changes); 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, TOOL_NAME)); } @@ -141,13 +103,13 @@ expectedOutput, runAndCaptureUsingInitRaw("difftool", public void testToolCached() throws Exception { RevCommit commit = createStagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] expectedOutput = getExpectedToolOutput(changes); String[] options = { "--cached", "--staged", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, option, "--tool", TOOL_NAME)); } } @@ -174,7 +136,8 @@ public void testToolHelp() throws Exception { String option = "--tool-help"; assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput.toArray(new String[0]), runAndCaptureUsingInitRaw("difftool", option)); + expectedOutput.toArray(new String[0]), + runAndCaptureUsingInitRaw(DIFF_TOOL, option)); } private void configureEchoTool(String toolName) { @@ -196,33 +159,7 @@ private void configureEchoTool(String toolName) { String.valueOf(false)); } - 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 RevCommit createStagedChanges() throws Exception { - RevCommit commit = createUnstagedChanges(); - git.add().addFilepattern(".").call(); - return commit; - } - - private List getRepositoryChanges(RevCommit commit) - throws Exception { - TreeWalk tw = new TreeWalk(db); - tw.addTree(commit.getTree()); - FileTreeIterator modifiedTree = new FileTreeIterator(db); - tw.addTree(modifiedTree); - List changes = DiffEntry.scan(tw); - return changes; - } - - private String[] getExpectedDiffToolOutput(List changes) { + private String[] getExpectedToolOutput(List changes) { String[] expectedToolOutput = new String[changes.size()]; for (int i = 0; i < changes.size(); ++i) { DiffEntry change = changes.get(i); @@ -232,17 +169,4 @@ private String[] getExpectedDiffToolOutput(List changes) { } return expectedToolOutput; } - - private static void assertArrayOfLinesEquals(String failMessage, - String[] expected, String[] actual) { - assertEquals(failMessage, toString(expected), toString(actual)); - } - - private 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.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java new file mode 100644 index 000000000..e10b13efb --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022, Simeon Andreev and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.pgm; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.api.CherryPickResult; +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 ExternalToolTestCase extends CLIRepositoryTestCase { + + public static class GitCliJGitWrapperParser { + @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class) + TextBuiltin subcommand; + + @Argument(index = 1, metaVar = "metaVar_arg") + List arguments = new ArrayList<>(); + } + + protected static final String TOOL_NAME = "some_tool"; + + private static final String TEST_BRANCH_NAME = "test_branch"; + + private Git git; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + git = new Git(db); + git.commit().setMessage("initial commit").call(); + git.branchCreate().setName(TEST_BRANCH_NAME).call(); + } + + protected String[] runAndCaptureUsingInitRaw(String... args) + throws Exception { + CLIGitCommand.Result result = new CLIGitCommand.Result(); + + GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); + CmdLineParser clp = new CmdLineParser(bean); + clp.parseArgument(args); + + TextBuiltin cmd = bean.subcommand; + cmd.initRaw(db, null, null, result.out, result.err); + cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()])); + if (cmd.getOutputWriter() != null) { + cmd.getOutputWriter().flush(); + } + if (cmd.getErrorWriter() != null) { + cmd.getErrorWriter().flush(); + } + return result.outLines().toArray(new String[0]); + } + + protected CherryPickResult createMergeConflict() throws Exception { + writeTrashFile("a", "Hello world a"); + writeTrashFile("b", "Hello world b"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b added").call(); + writeTrashFile("a", "Hello world a 1"); + writeTrashFile("b", "Hello world b 1"); + git.add().addFilepattern(".").call(); + RevCommit commit1 = git.commit().setMessage("files a & b commit 1") + .call(); + git.branchCreate().setName("branch_1").call(); + git.checkout().setName(TEST_BRANCH_NAME).call(); + writeTrashFile("a", "Hello world a 2"); + writeTrashFile("b", "Hello world b 2"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b commit 2").call(); + git.branchCreate().setName("branch_2").call(); + CherryPickResult result = git.cherryPick().include(commit1).call(); + return result; + } + + protected 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; + } + + protected RevCommit createStagedChanges() throws Exception { + RevCommit commit = createUnstagedChanges(); + git.add().addFilepattern(".").call(); + return commit; + } + + protected List getRepositoryChanges(RevCommit commit) + throws Exception { + TreeWalk tw = new TreeWalk(db); + tw.addTree(commit.getTree()); + FileTreeIterator modifiedTree = new FileTreeIterator(db); + tw.addTree(modifiedTree); + List changes = DiffEntry.scan(tw); + return changes; + } + + protected static 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.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java new file mode 100644 index 000000000..32cd60415 --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022, Simeon Andreev and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.pgm; + +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; + +import java.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 ExternalToolTestCase { + + private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + configureEchoTool(TOOL_NAME); + } + + @Test + public void testTool() throws Exception { + createMergeConflict(); + String[] expectedOutput = getExpectedToolOutput(); + + String[] options = { + "--tool", + "-t", + }; + + for (String option : options) { + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, + runAndCaptureUsingInitRaw(MERGE_TOOL, option, + TOOL_NAME)); + } + } + + @Test + public void testToolNoGuiNoPrompt() throws Exception { + createMergeConflict(); + String[] expectedOutput = getExpectedToolOutput(); + + String[] options = { "--tool", "-t", }; + + for (String option : options) { + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL, + "--no-gui", "--no-prompt", option, TOOL_NAME)); + } + } + + @Test + public void testToolHelp() throws Exception { + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + List expectedOutput = new ArrayList<>(); + expectedOutput.add( + "'git mergetool --tool=' may be set to one of the following:"); + for (CommandLineMergeTool defaultTool : defaultTools) { + String toolName = defaultTool.name(); + expectedOutput.add(toolName); + } + String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " " + + getEchoCommand(); + expectedOutput.add("user-defined:"); + expectedOutput.add(customToolHelpLine); + String[] userDefinedToolsHelp = { + "The following tools are valid, but not currently available:", + "Some of the tools listed above only work in a windowed", + "environment. If run in a terminal-only session, they will fail.", + }; + expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp)); + + String option = "--tool-help"; + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput.toArray(new String[0]), + runAndCaptureUsingInitRaw(MERGE_TOOL, option)); + } + + private void configureEchoTool(String toolName) { + StoredConfig config = db.getConfig(); + // the default merge tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + String command = getEchoCommand(); + + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + /* + * prevent prompts as we are running in tests and there is no user to + * interact with on the command line + */ + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PROMPT, + String.valueOf(false)); + } + + private String[] getExpectedToolOutput() { + String[] mergeConflictFilenames = { "a", "b", }; + List expectedOutput = new ArrayList<>(); + expectedOutput.add("Merging:"); + for (String mergeConflictFilename : mergeConflictFilenames) { + expectedOutput.add(mergeConflictFilename); + } + for (String mergeConflictFilename : mergeConflictFilenames) { + expectedOutput.add("Normal merge conflict for '" + + mergeConflictFilename + "':"); + expectedOutput.add("{local}: modified file"); + expectedOutput.add("{remote}: modified file"); + expectedOutput.add("TODO: Launch mergetool '" + TOOL_NAME + + "' for path '" + mergeConflictFilename + "'..."); + } + return expectedOutput.toArray(new String[0]); + } +} diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin index 8c44764c6..ea1d1e3fa 100644 --- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin +++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin @@ -25,6 +25,7 @@ org.eclipse.jgit.pgm.LsRemote org.eclipse.jgit.pgm.LsTree org.eclipse.jgit.pgm.Merge org.eclipse.jgit.pgm.MergeBase +org.eclipse.jgit.pgm.MergeTool org.eclipse.jgit.pgm.Push org.eclipse.jgit.pgm.ReceivePack org.eclipse.jgit.pgm.Reflog diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties index 3653b9d8f..8e2eef7eb 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 @@ -255,6 +255,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 +304,7 @@ usage_Status=Show the working tree status usage_StopTrackingAFile=Stop tracking a file usage_TextHashFunctions=Scan repository to compute maximum number of collisions for hash functions usage_ToolForDiff=Use the diff tool specified by . Run git difftool --tool-help for the list of valid settings.\nIf a diff tool is not specified, git difftool will use the configuration variable diff.tool. +usage_ToolForMerge=Use the merge resolution program specified by . Run git mergetool --tool-help for the list of valid settings.\nIf a merge resolution program is not specified, git mergetool will use the configuration variable merge.tool. usage_UpdateRemoteRepositoryFromLocalRefs=Update remote repository from local refs usage_UseAll=Use all refs found in refs/ usage_UseTags=Use any tag including lightweight tags @@ -350,6 +352,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 2f7417745..2e90d52cb 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2021, Andre Bossert + * Copyright (C) 2018-2022, Andre Bossert * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -192,7 +192,7 @@ private void compare(List files, boolean showPrompt, outw.flush(); errw.println(e.getMessage()); throw die(MessageFormat.format( - CLIText.get().diffToolDied, mergedFilePath, e)); + CLIText.get().diffToolDied, mergedFilePath), e); } } else { break; diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java new file mode 100644 index 000000000..37afa54c7 --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.pgm; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +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.internal.diffmergetool.ExternalMergeTool; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.internal.diffmergetool.MergeTools; +import org.eclipse.jgit.lib.IndexDiff.StageState; +import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.lib.Repository; +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 Optional prompt = Optional.empty(); + + @Option(name = "--prompt", usage = "usage_prompt") + void setPrompt(@SuppressWarnings("unused") boolean on) { + prompt = Optional.of(Boolean.TRUE); + } + + @Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt") + void noPrompt(@SuppressWarnings("unused") boolean on) { + prompt = Optional.of(Boolean.FALSE); + } + + @Option(name = "--tool-help", usage = "usage_toolHelp") + private boolean toolHelp; + + private BooleanTriState gui = BooleanTriState.UNSET; + + @Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool") + void setGui(@SuppressWarnings("unused") boolean on) { + gui = BooleanTriState.TRUE; + } + + @Option(name = "--no-gui", usage = "usage_noGui") + void noGui(@SuppressWarnings("unused") boolean on) { + gui = BooleanTriState.FALSE; + } + + @Argument(required = false, index = 0, metaVar = "metaVar_paths") + @Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class) + protected List filterPaths; + + @Override + protected void init(Repository repository, String gitDir) { + super.init(repository, gitDir); + mergeTools = new MergeTools(repository); + } + + @Override + protected void run() { + try { + if (toolHelp) { + showToolHelp(); + } else { + // get prompt + boolean showPrompt = mergeTools.isInteractive(); + if (prompt.isPresent()) { + showPrompt = prompt.get().booleanValue(); + } + // get passed or default tool name + String toolNameSelected = toolName; + if ((toolNameSelected == null) || toolNameSelected.isEmpty()) { + toolNameSelected = mergeTools.getDefaultToolName(gui); + } + // get the changed files + Map files = getFiles(); + if (files.size() > 0) { + merge(files, showPrompt, toolNameSelected); + } else { + outw.println("No files need merging"); //$NON-NLS-1$ + } + } + outw.flush(); + } catch (Exception e) { + throw die(e.getMessage(), e); + } + } + + private void merge(Map files, boolean showPrompt, + String toolNamePrompt) throws Exception { + // sort file names + List fileNames = new ArrayList<>(files.keySet()); + Collections.sort(fileNames); + // show the files + outw.println("Merging:"); //$NON-NLS-1$ + for (String fileName : fileNames) { + outw.println(fileName); + } + outw.flush(); + for (String fileName : fileNames) { + StageState fileState = files.get(fileName); + // only both-modified is valid for mergetool + if (fileState == StageState.BOTH_MODIFIED) { + outw.println("\nNormal merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ + outw.println(" {local}: modified file"); //$NON-NLS-1$ + outw.println(" {remote}: modified file"); //$NON-NLS-1$ + // check if user wants to launch merge resolution tool + boolean launch = true; + if (showPrompt) { + launch = isLaunch(toolNamePrompt); + } + if (launch) { + outw.println("TODO: Launch mergetool '" + toolNamePrompt //$NON-NLS-1$ + + "' for path '" + fileName + "'..."); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + break; + } + } else if ((fileState == StageState.DELETED_BY_US) || (fileState == StageState.DELETED_BY_THEM)) { + outw.println("\nDeleted merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + outw.println( + "\nUnknown merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ + break; + } + } + } + + private boolean isLaunch(String toolNamePrompt) + throws IOException { + boolean launch = true; + outw.println("Hit return to start merge resolution tool (" //$NON-NLS-1$ + + toolNamePrompt + "): "); //$NON-NLS-1$ + outw.flush(); + BufferedReader br = new BufferedReader(new InputStreamReader(ins)); + 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 void showToolHelp() throws IOException { + outw.println( + "'git mergetool --tool=' may be set to one of the following:"); //$NON-NLS-1$ + for (String name : mergeTools.getAvailableTools().keySet()) { + outw.println("\t\t" + name); //$NON-NLS-1$ + } + outw.println(""); //$NON-NLS-1$ + outw.println("\tuser-defined:"); //$NON-NLS-1$ + Map userTools = mergeTools + .getUserDefinedTools(); + for (String name : userTools.keySet()) { + outw.println("\t\t" + name + ".cmd " //$NON-NLS-1$ //$NON-NLS-2$ + + userTools.get(name).getCommand()); + } + outw.println(""); //$NON-NLS-1$ + outw.println( + "The following tools are valid, but not currently available:"); //$NON-NLS-1$ + for (String name : mergeTools.getNotAvailableTools().keySet()) { + outw.println("\t\t" + name); //$NON-NLS-1$ + } + outw.println(""); //$NON-NLS-1$ + outw.println("Some of the tools listed above only work in a windowed"); //$NON-NLS-1$ + outw.println( + "environment. If run in a terminal-only session, they will fail."); //$NON-NLS-1$ + return; + } + + private Map getFiles() + throws RevisionSyntaxException, NoWorkTreeException, + GitAPIException { + Map files = new TreeMap<>(); + try (Git git = new Git(db)) { + StatusCommand statusCommand = git.status(); + if (filterPaths != null && filterPaths.size() > 0) { + for (String path : filterPaths) { + statusCommand.addPath(path); + } + } + Status status = statusCommand.call(); + files = status.getConflictingStageState(); + } + return files; + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java index 96fd1026c..1dea44eaa 100644 --- 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 @@ -9,13 +9,27 @@ */ 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; /** @@ -23,12 +37,60 @@ */ 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(db, local, remote, base, merged.getPath(), 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(db, local, remote, base, merged.getPath(), toolName, + prompt, gui); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + @Test public void testToolNames() { MergeTools manager = new MergeTools(db); Set actualToolNames = manager.getToolNames(); Set expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of external diff tool names", + assertEquals("Incorrect set of external merge tool names", expectedToolNames, actualToolNames); } @@ -36,18 +98,58 @@ public void testToolNames() { public void testAllTools() { MergeTools manager = new MergeTools(db); Set actualToolNames = manager.getAvailableTools().keySet(); - Set expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of available external diff tools", - expectedToolNames, actualToolNames); + Set expectedToolNames = new LinkedHashSet<>(); + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + for (CommandLineMergeTool defaultTool : defaultTools) { + String toolName = defaultTool.name(); + expectedToolNames.add(toolName); + } + assertEquals("Incorrect set of external merge tools", expectedToolNames, + actualToolNames); + } + + @Test + public void testOverridePredefinedToolPath() { + String toolName = CommandLineMergeTool.guiffy.name(); + String customToolPath = "/usr/bin/echo"; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + "echo"); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PATH, + customToolPath); + + MergeTools manager = new MergeTools(db); + Map tools = manager.getUserDefinedTools(); + ExternalMergeTool mergeTool = tools.get(toolName); + assertNotNull("Expected tool \"" + toolName + "\" to be user defined", + mergeTool); + + String toolPath = mergeTool.getPath(); + assertEquals("Expected external merge tool to have an overriden path", + customToolPath, toolPath); } @Test public void testUserDefinedTools() { + FileBasedConfig config = db.getConfig(); + String customToolname = "customTool"; + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_CMD, "echo"); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_PATH, "/usr/bin/echo"); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_PROMPT, String.valueOf(false)); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_GUITOOL, String.valueOf(false)); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false)); MergeTools manager = new MergeTools(db); Set actualToolNames = manager.getUserDefinedTools().keySet(); - Set expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of user defined external diff tools", - expectedToolNames, actualToolNames); + Set expectedToolNames = new LinkedHashSet<>(); + expectedToolNames.add(customToolname); + assertEquals("Incorrect set of external merge tools", expectedToolNames, + actualToolNames); } @Test @@ -55,55 +157,118 @@ public void testNotAvailableTools() { MergeTools manager = new MergeTools(db); Set actualToolNames = manager.getNotAvailableTools().keySet(); Set expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of not available external diff tools", + assertEquals("Incorrect set of not available external merge tools", expectedToolNames, actualToolNames); } @Test public void testCompare() throws ToolException { - MergeTools manager = new MergeTools(db); + 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); - String newPath = ""; - String oldPath = ""; - String newId = ""; - String oldId = ""; - String toolName = ""; BooleanTriState prompt = BooleanTriState.UNSET; BooleanTriState gui = BooleanTriState.UNSET; - BooleanTriState trustExitCode = BooleanTriState.UNSET; + + MergeTools manager = new MergeTools(db); int expectedCompareResult = 0; - int compareResult = manager.merge(newPath, oldPath, newId, oldId, - toolName, prompt, gui, trustExitCode); - assertEquals("Incorrect compare result for external diff tool", - expectedCompareResult, compareResult); + ExecutionResult compareResult = manager.merge(db, local, remote, base, + merged.getPath(), 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 diff tool is configured without a subsection + // the default merge tool is configured without a subsection String subsection = null; - config.setString("diff", subsection, "tool", "customTool"); + 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 difftool to be the default external diff tool", - "my_default_toolname", defaultToolName); + "Expected configured mergetool to be the default external merge tool", + toolName, defaultToolName); gui = BooleanTriState.TRUE; String defaultGuiToolName = manager.getDefaultToolName(gui); assertEquals( - "Expected configured difftool to be the default external diff tool", + "Expected configured mergetool to be the default external merge tool", "my_gui_tool", defaultGuiToolName); - config.setString("diff", subsection, "guitool", "customGuiTool"); + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL, + guiToolName); manager = new MergeTools(db); defaultGuiToolName = manager.getDefaultToolName(gui); assertEquals( - "Expected configured difftool to be the default external diff guitool", + "Expected configured mergetool to be the default external merge guitool", "my_gui_tool", defaultGuiToolName); } + + @Test + public void testOverridePreDefinedToolPath() { + String newToolPath = "/tmp/path/"; + + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + assertTrue("Expected to find pre-defined external merge tools", + defaultTools.length > 0); + + CommandLineMergeTool overridenTool = defaultTools[0]; + String overridenToolName = overridenTool.name(); + String overridenToolPath = newToolPath + overridenToolName; + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, overridenToolName, + CONFIG_KEY_PATH, overridenToolPath); + + MergeTools manager = new MergeTools(db); + Map availableTools = manager + .getAvailableTools(); + ExternalMergeTool externalMergeTool = availableTools + .get(overridenToolName); + String actualMergeToolPath = externalMergeTool.getPath(); + assertEquals( + "Expected pre-defined external merge tool to have overriden path", + overridenToolPath, actualMergeToolPath); + boolean withBase = true; + String expectedMergeToolCommand = overridenToolPath + " " + + overridenTool.getParameters(withBase); + String actualMergeToolCommand = externalMergeTool.getCommand(); + assertEquals( + "Expected pre-defined external merge tool to have overriden command", + expectedMergeToolCommand, actualMergeToolCommand); + } + + @Test(expected = ToolException.class) + public void testUndefinedTool() throws Exception { + MergeTools manager = new MergeTools(db); + + String toolName = "undefined"; + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + + manager.merge(db, local, remote, base, merged.getPath(), 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/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java new file mode 100644 index 000000000..3a2212432 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +/** + * Pre-defined merge tools. + * + * Adds same merge tools as also pre-defined in C-Git see "git-core\mergetools\" + * see links to command line parameter description for the tools + * + *
+ * araxis
+ * bc
+ * bc3
+ * codecompare
+ * deltawalker
+ * diffmerge
+ * diffuse
+ * ecmerge
+ * emerge
+ * examdiff
+ * guiffy
+ * gvimdiff
+ * gvimdiff2
+ * gvimdiff3
+ * kdiff3
+ * kompare
+ * meld
+ * opendiff
+ * p4merge
+ * tkdiff
+ * tortoisemerge
+ * vimdiff
+ * vimdiff2
+ * vimdiff3
+ * winmerge
+ * xxdiff
+ * 
+ * + */ +@SuppressWarnings("nls") +public enum CommandLineMergeTool { + /** + * See: https://www.araxis.com/merge/documentation-windows/command-line.en + */ + araxis("compare", + "-wait -merge -3 -a1 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + "-wait -2 \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + false), + /** + * See: https://www.scootersoftware.com/v4help/index.html?command_line_reference.html + */ + bc("bcomp", "\"$LOCAL\" \"$REMOTE\" \"$BASE\" --mergeoutput=\"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" --mergeoutput=\"$MERGED\"", + false), + /** + * See: https://www.scootersoftware.com/v4help/index.html?command_line_reference.html + */ + bc3("bcompare", bc), + /** + * See: https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm + */ + codecompare("CodeMerge", + "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -BF=\"$BASE\" -RF=\"$MERGED\"", + "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -RF=\"$MERGED\"", + false), + /** + * See: https://www.deltawalker.com/integrate/command-line + *

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

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

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

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

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

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

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

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

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

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

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

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

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

+ *

+ * Hint: cannot diff + *

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

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

+ *

+ * Hint: cannot diff + *

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

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

+ */ + winmerge("WinMergeU", + "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + false), + /** + * See: http://furius.ca/xxdiff/doc/xxdiff-doc.html + */ + xxdiff("xxdiff", + "-X --show-merged-pane -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "-X -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + false); + + CommandLineMergeTool(String path, String parametersWithBase, + String parametersWithoutBase, + boolean exitCodeTrustable) { + this.path = path; + this.parametersWithBase = parametersWithBase; + this.parametersWithoutBase = parametersWithoutBase; + this.exitCodeTrustable = exitCodeTrustable; + } + + CommandLineMergeTool(CommandLineMergeTool from) { + this(from.getPath(), from.getParameters(true), + from.getParameters(false), from.isExitCodeTrustable()); + } + + CommandLineMergeTool(String path, CommandLineMergeTool from) { + this(path, from.getParameters(true), from.getParameters(false), + from.isExitCodeTrustable()); + } + + private final String path; + + private final String parametersWithBase; + + private final String parametersWithoutBase; + + private final boolean exitCodeTrustable; + + /** + * @return path + */ + public String getPath() { + return path; + } + + /** + * @param withBase + * return parameters with base present? + * @return parameters with or without base present + */ + public String getParameters(boolean withBase) { + if (withBase) { + return parametersWithBase; + } + return parametersWithoutBase; + } + + /** + * @return parameters + */ + public boolean isExitCodeTrustable() { + return exitCodeTrustable; + } + + /** + * @return true if command with base present is valid, false otherwise + */ + public boolean canMergeWithoutBasePresent() { + return parametersWithoutBase != null; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index b15cbdc34..2f2b9de81 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2021, Andre Bossert + * Copyright (C) 2018-2022, Andre Bossert * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -56,8 +56,7 @@ public DiffTools(Repository repo) { * @param remoteFile * the remote file element * @param mergedFilePath - * the path of 'merged' file, it equals local or remote path for - * difftool + * the path of 'merged' file, it equals local or remote path * @param toolName * the selected tool name (can be null) * @param prompt @@ -66,7 +65,7 @@ public DiffTools(Repository repo) { * the GUI option * @param trustExitCode * the "trust exit code" option - * @return the return code from executed tool + * @return the execution result from tool * @throws ToolException */ public ExecutionResult compare(Repository repo, FileElement localFile, 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 index bcc749ada..0c3ddf9af 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.internal.diffmergetool; +import org.eclipse.jgit.lib.internal.BooleanTriState; + /** * The merge tool interface. */ @@ -18,6 +20,14 @@ public interface ExternalMergeTool extends ExternalDiffTool { /** * @return the tool "trust exit code" option */ - boolean isTrustExitCode(); + 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/MergeToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java index e91282261..9be20b75a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java @@ -10,13 +10,24 @@ 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.ConfigConstants; import org.eclipse.jgit.lib.internal.BooleanTriState; /** @@ -42,31 +53,27 @@ public class MergeToolConfig { private final Map tools; private MergeToolConfig(Config rc) { - toolName = rc.getString(ConfigConstants.CONFIG_MERGE_SECTION, null, - ConfigConstants.CONFIG_KEY_TOOL); - guiToolName = rc.getString(ConfigConstants.CONFIG_MERGE_SECTION, null, - ConfigConstants.CONFIG_KEY_GUITOOL); - prompt = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION, - ConfigConstants.CONFIG_KEY_PROMPT, true); - keepBackup = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION, - ConfigConstants.CONFIG_KEY_KEEP_BACKUP, true); - keepTemporaries = rc.getBoolean( - ConfigConstants.CONFIG_MERGETOOL_SECTION, - ConfigConstants.CONFIG_KEY_KEEP_TEMPORARIES, false); - writeToTemp = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION, - ConfigConstants.CONFIG_KEY_WRITE_TO_TEMP, false); + toolName = rc.getString(CONFIG_MERGE_SECTION, null, CONFIG_KEY_TOOL); + guiToolName = rc.getString(CONFIG_MERGE_SECTION, null, + CONFIG_KEY_GUITOOL); + prompt = rc.getBoolean(CONFIG_MERGETOOL_SECTION, toolName, + CONFIG_KEY_PROMPT, true); + keepBackup = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_KEEP_BACKUP, true); + keepTemporaries = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_KEEP_TEMPORARIES, false); + writeToTemp = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_WRITE_TO_TEMP, false); tools = new HashMap<>(); - Set subsections = rc - .getSubsections(ConfigConstants.CONFIG_MERGETOOL_SECTION); + Set subsections = rc.getSubsections(CONFIG_MERGETOOL_SECTION); for (String name : subsections) { - String cmd = rc.getString(ConfigConstants.CONFIG_MERGETOOL_SECTION, - name, ConfigConstants.CONFIG_KEY_CMD); - String path = rc.getString(ConfigConstants.CONFIG_MERGETOOL_SECTION, - name, ConfigConstants.CONFIG_KEY_PATH); + 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( - ConfigConstants.CONFIG_MERGETOOL_SECTION, name, - ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE); + String trustStr = rc.getString(CONFIG_MERGETOOL_SECTION, name, + CONFIG_KEY_TRUST_EXIT_CODE); if (trustStr != null) { trustExitCode = Boolean.valueOf(trustStr).booleanValue() ? BooleanTriState.TRUE @@ -75,9 +82,8 @@ private MergeToolConfig(Config rc) { trustExitCode = BooleanTriState.UNSET; } if ((cmd != null) || (path != null)) { - tools.put(name, - new UserDefinedMergeTool(name, path, cmd, - trustExitCode)); + tools.put(name, new UserDefinedMergeTool(name, path, cmd, + trustExitCode)); } } } 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 index bb5d73eeb..cefefb8e7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -9,17 +9,21 @@ */ package org.eclipse.jgit.internal.diffmergetool; +import java.io.File; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import org.eclipse.jgit.lib.Constants; 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 { + private final MergeToolConfig config; private final Map predefinedTools; @@ -33,10 +37,12 @@ public class MergeTools { public MergeTools(Repository repo) { config = repo.getConfig().get(MergeToolConfig.KEY); predefinedTools = setupPredefinedTools(); - userDefinedTools = setupUserDefinedTools(); + userDefinedTools = setupUserDefinedTools(config, predefinedTools); } /** + * @param repo + * the repository * @param localFile * the local file element * @param remoteFile @@ -49,19 +55,43 @@ public MergeTools(Repository repo) { * the selected tool name (can be null) * @param prompt * the prompt option - * @param trustExitCode - * the "trust exit code" option * @param gui * the GUI option * @return the execution result from tool * @throws ToolException */ - public int merge(String localFile, - String remoteFile, String baseFile, String mergedFilePath, - String toolName, BooleanTriState prompt, BooleanTriState gui, - BooleanTriState trustExitCode) + public ExecutionResult merge(Repository repo, FileElement localFile, + FileElement remoteFile, FileElement baseFile, String mergedFilePath, + String toolName, BooleanTriState prompt, BooleanTriState gui) throws ToolException { - return 0; + ExternalMergeTool tool = guessTool(toolName, gui); + try { + File workingDir = repo.getWorkTree(); + String localFilePath = localFile.getFile().getPath(); + String remoteFilePath = remoteFile.getFile().getPath(); + String baseFilePath = baseFile.getFile().getPath(); + String command = tool.getCommand(); + command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$ + command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$ + command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$ + command = command.replace("$BASE", baseFilePath); //$NON-NLS-1$ + Map env = new TreeMap<>(); + env.put(Constants.GIT_DIR_KEY, + repo.getDirectory().getAbsolutePath()); + env.put("LOCAL", localFilePath); //$NON-NLS-1$ + env.put("REMOTE", remoteFilePath); //$NON-NLS-1$ + env.put("MERGED", mergedFilePath); //$NON-NLS-1$ + env.put("BASE", baseFilePath); //$NON-NLS-1$ + boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; + CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); + return cmdExec.run(command, workingDir, env); + } catch (Exception e) { + throw new ToolException(e); + } finally { + localFile.cleanTemporaries(); + remoteFile.cleanTemporaries(); + baseFile.cleanTemporaries(); + } } /** @@ -99,7 +129,7 @@ public Map getNotAvailableTools() { */ public String getDefaultToolName(BooleanTriState gui) { return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ - : "my_default_toolname"; //$NON-NLS-1$ + : config.getDefaultToolName(); } /** @@ -109,11 +139,58 @@ public boolean isInteractive() { return config.isPrompt(); } - private Map setupPredefinedTools() { - return new TreeMap<>(); + 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 Map setupUserDefinedTools() { - return new TreeMap<>(); + private ExternalMergeTool getTool(final String name) { + ExternalMergeTool tool = userDefinedTools.get(name); + if (tool == null) { + tool = predefinedTools.get(name); + } + return tool; } -} \ No newline at end of file + + private Map setupPredefinedTools() { + Map tools = new TreeMap<>(); + for (CommandLineMergeTool tool : CommandLineMergeTool.values()) { + tools.put(tool.name(), new PreDefinedMergeTool(tool)); + } + return tools; + } + + private Map setupUserDefinedTools( + MergeToolConfig cfg, Map predefTools) { + Map tools = new TreeMap<>(); + Map userTools = cfg.getTools(); + for (String name : userTools.keySet()) { + ExternalMergeTool userTool = userTools.get(name); + // if mergetool..cmd is defined we have user defined tool + if (userTool.getCommand() != null) { + tools.put(name, userTool); + } else if (userTool.getPath() != null) { + // if mergetool..path is defined we just overload the path + // of predefined tool + PreDefinedMergeTool predefTool = (PreDefinedMergeTool) predefTools + .get(name); + if (predefTool != null) { + predefTool.setPath(userTool.getPath()); + if (userTool.getTrustExitCode() != BooleanTriState.UNSET) { + predefTool + .setTrustExitCode(userTool.getTrustExitCode()); + } + } + } + } + return tools; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java new file mode 100644 index 000000000..2c64c1666 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +import org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * The pre-defined merge tool. + */ +public class PreDefinedMergeTool extends UserDefinedMergeTool { + + /** + * the tool parameters without base + */ + private final String parametersWithoutBase; + + /** + * Creates the pre-defined merge tool + * + * @param name + * the name + * @param path + * the path + * @param parametersWithBase + * the tool parameters that are used together with path as + * command and "base is present" ($BASE) + * @param parametersWithoutBase + * the tool parameters that are used together with path as + * command and "base is present" ($BASE) + * @param trustExitCode + * the "trust exit code" option + */ + public PreDefinedMergeTool(String name, String path, + String parametersWithBase, String parametersWithoutBase, + BooleanTriState trustExitCode) { + super(name, path, parametersWithBase, trustExitCode); + this.parametersWithoutBase = parametersWithoutBase; + } + + /** + * Creates the pre-defined merge tool + * + * @param tool + * the command line merge tool + * + */ + public PreDefinedMergeTool(CommandLineMergeTool tool) { + this(tool.name(), tool.getPath(), tool.getParameters(true), + tool.getParameters(false), + tool.isExitCodeTrustable() ? BooleanTriState.TRUE + : BooleanTriState.FALSE); + } + + /** + * @param trustExitCode + * the "trust exit code" option + */ + @Override + public void setTrustExitCode(BooleanTriState trustExitCode) { + super.setTrustExitCode(trustExitCode); + } + + /** + * @return the tool command (with base present) + */ + @Override + public String getCommand() { + return getCommand(true); + } + + /** + * @param withBase + * get command with base present (true) or without base present + * (false) + * @return the tool command + */ + @Override + public String getCommand(boolean withBase) { + return getPath() + " " //$NON-NLS-1$ + + (withBase ? super.getCommand() : parametersWithoutBase); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java index df4d8cb8c..1dd2f0d79 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java @@ -21,7 +21,7 @@ public class UserDefinedMergeTool extends UserDefinedDiffTool /** * the merge tool "trust exit code" option */ - private final BooleanTriState trustExitCode; + private BooleanTriState trustExitCode; /** * Creates the merge tool @@ -40,20 +40,30 @@ public UserDefinedMergeTool(String name, String path, String cmd, super(name, path, cmd); this.trustExitCode = trustExitCode; } - /** * @return the "trust exit code" flag */ @Override - public boolean isTrustExitCode() { - return trustExitCode == BooleanTriState.TRUE; - } - - /** - * @return the "trust exit code" option - */ 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 e982a33b2..29c66f516 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; /** @@ -66,7 +67,7 @@ public final class ConfigConstants { 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 */