Add command line support for "git mergetool"

see: https://git-scm.com/docs/git-mergetool
see: https://git-scm.com/docs/git-config

* add command line support for "git mergetool"
  * add option handling for "--tool-help", "--tool=<mytool>",
"--[no-]prompt",  "--[no-]gui"
  * handle prompt
  * add MergeTools
  * add pre-defined mergetools
  * print merge actions --> no execute, will be done later

Bug: 356832
Change-Id: I6e505ffc3d03f75ecf4bba452a25d25dfcf5793f
Signed-off-by: Andre Bossert <andre.bossert@siemens.com>
This commit is contained in:
Andre Bossert 2020-01-19 20:52:56 +01:00 committed by Andrey Loskutov
parent 24171b05f0
commit 8573435635
16 changed files with 1272 additions and 174 deletions

View File

@ -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
@ -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<String> 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<DiffEntry> 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<DiffEntry> 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<DiffEntry> 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<DiffEntry> 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<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) {
private String[] getExpectedToolOutput(List<DiffEntry> 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<DiffEntry> 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\")";
}
}

View File

@ -0,0 +1,136 @@
/*
* 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.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<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 {
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<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 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\")";
}
}

View File

@ -0,0 +1,136 @@
/*
* 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.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<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 String[] getExpectedToolOutput() {
String[] mergeConflictFilenames = { "a", "b", };
List<String> 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]);
}
}

View File

@ -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

View File

@ -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 <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 +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

View File

@ -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
@ -192,7 +192,7 @@ private void compare(List<DiffEntry> 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;

View File

@ -0,0 +1,212 @@
/*
* 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 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<Boolean> 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<String> 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<String, StageState> 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<String, StageState> files, boolean showPrompt,
String toolNamePrompt) throws Exception {
// sort file names
List<String> 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=<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<String, ExternalMergeTool> 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<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;
}
}

View File

@ -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<String> actualToolNames = manager.getToolNames();
Set<String> 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<String> actualToolNames = manager.getAvailableTools().keySet();
Set<String> expectedToolNames = Collections.emptySet();
assertEquals("Incorrect set of available external diff tools",
expectedToolNames, actualToolNames);
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 = Collections.emptySet();
assertEquals("Incorrect set of user defined external diff tools",
expectedToolNames, actualToolNames);
Set<String> 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<String> actualToolNames = manager.getNotAvailableTools().keySet();
Set<String> 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<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(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();
}
}

View File

@ -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;
}
}

View File

@ -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
@ -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,

View File

@ -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);
}

View File

@ -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<String, ExternalMergeTool> 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<String> subsections = rc
.getSubsections(ConfigConstants.CONFIG_MERGETOOL_SECTION);
Set<String> 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));
}
}
}

View File

@ -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<String, ExternalMergeTool> 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<String, String> 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<String, ExternalMergeTool> 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<String, ExternalMergeTool> 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<String, ExternalMergeTool> setupUserDefinedTools() {
return new TreeMap<>();
private ExternalMergeTool getTool(final String name) {
ExternalMergeTool tool = userDefinedTools.get(name);
if (tool == null) {
tool = predefinedTools.get(name);
}
return tool;
}
}
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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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
*/