Merge branch 'master' into stable-6.2

* master:
  Fixed since tags added for new merge constants
  Add filtering with help of DirCacheCheckout.getContent()
  Avoid warning "no explicit project encoding"
  Add mergetool merge feature (execute external tool)
  Add command line support for "git mergetool"
  Add config reader for user-defined mergetools
  Add difftool compare feature (execute external tool)

Change-Id: Ie215b8205b85cfd9efddb622c90dfad7a5ae0caa
This commit is contained in:
Matthias Sohn 2022-06-01 18:14:28 +02:00
commit 9fccad2e46
30 changed files with 3480 additions and 210 deletions

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

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
@ -9,72 +9,87 @@
*/
package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertEquals;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.junit.Assert.fail;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
import org.eclipse.jgit.pgm.opt.CmdLineParser;
import org.eclipse.jgit.pgm.opt.SubcommandHandler;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.lib.StoredConfig;
import org.junit.Before;
import org.junit.Test;
import org.kohsuke.args4j.Argument;
/**
* Testing the {@code difftool} command.
*/
public class DiffToolTest extends CLIRepositoryTestCase {
public static class GitCliJGitWrapperParser {
@Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
TextBuiltin subcommand;
public class DiffToolTest extends ToolTestCase {
@Argument(index = 1, metaVar = "metaVar_arg")
List<String> arguments = new ArrayList<>();
}
private String[] runAndCaptureUsingInitRaw(String... args)
throws Exception {
CLIGitCommand.Result result = new CLIGitCommand.Result();
GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
CmdLineParser clp = new CmdLineParser(bean);
clp.parseArgument(args);
TextBuiltin cmd = bean.subcommand;
cmd.initRaw(db, null, null, result.out, result.err);
cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()]));
if (cmd.getOutputWriter() != null) {
cmd.getOutputWriter().flush();
}
if (cmd.getErrorWriter() != null) {
cmd.getErrorWriter().flush();
}
return result.outLines().toArray(new String[0]);
}
private Git git;
private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
git = new Git(db);
git.commit().setMessage("initial commit").call();
configureEchoTool(TOOL_NAME);
}
@Test
public void testToolWithPrompt() throws Exception {
String[] inputLines = {
"y", // accept launching diff tool
"y", // accept launching diff tool
};
String[] conflictingFilenames = createUnstagedChanges();
String[] expectedOutput = getExpectedCompareOutput(conflictingFilenames);
String option = "--tool";
InputStream inputStream = createInputStream(inputLines);
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw(inputStream,
DIFF_TOOL, "--prompt", option, TOOL_NAME));
}
@Test
public void testToolAbortLaunch() throws Exception {
String[] inputLines = {
"y", // accept launching diff tool
"n", // don't launch diff tool
};
String[] conflictingFilenames = createUnstagedChanges();
int abortIndex = 1;
String[] expectedOutput = getExpectedAbortOutput(conflictingFilenames, abortIndex);
String option = "--tool";
InputStream inputStream = createInputStream(inputLines);
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput,
runAndCaptureUsingInitRaw(inputStream, DIFF_TOOL, "--prompt", option,
TOOL_NAME));
}
@Test(expected = Die.class)
public void testNotDefinedTool() throws Exception {
createUnstagedChanges();
runAndCaptureUsingInitRaw(DIFF_TOOL, "--tool", "undefined");
fail("Expected exception when trying to run undefined tool");
}
@Test
public void testTool() throws Exception {
RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedDiffToolOutput(changes);
String[] conflictFilenames = createUnstagedChanges();
String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictFilenames);
String[] options = {
"--tool",
@ -84,54 +99,51 @@ public void testTool() throws Exception {
for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput,
runAndCaptureUsingInitRaw("difftool", option,
"some_tool"));
runAndCaptureUsingInitRaw(DIFF_TOOL, option,
TOOL_NAME));
}
}
@Test
public void testToolTrustExitCode() throws Exception {
RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedDiffToolOutput(changes);
String[] conflictingFilenames = createUnstagedChanges();
String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
String[] options = { "--tool", "-t", };
for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw("difftool",
"--trust-exit-code", option, "some_tool"));
expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
"--trust-exit-code", option, TOOL_NAME));
}
}
@Test
public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedDiffToolOutput(changes);
String[] conflictingFilenames = createUnstagedChanges();
String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
String[] options = { "--tool", "-t", };
for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw("difftool",
expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
"--no-gui", "--no-prompt", "--no-trust-exit-code",
option, "some_tool"));
option, TOOL_NAME));
}
}
@Test
public void testToolCached() throws Exception {
RevCommit commit = createStagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedDiffToolOutput(changes);
String[] conflictingFilenames = createStagedChanges();
String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
String[] options = { "--cached", "--staged", };
for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw("difftool",
option, "--tool", "some_tool"));
expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
option, "--tool", TOOL_NAME));
}
}
@ -139,13 +151,17 @@ expectedOutput, runAndCaptureUsingInitRaw("difftool",
public void testToolHelp() throws Exception {
CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
List<String> expectedOutput = new ArrayList<>();
expectedOutput.add("git difftool --tool=<tool> may be set to one of the following:");
expectedOutput.add(
"'git difftool --tool=<tool>' may be set to one of the following:");
for (CommandLineDiffTool defaultTool : defaultTools) {
String toolName = defaultTool.name();
expectedOutput.add(toolName);
}
String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
+ getEchoCommand();
expectedOutput.add("user-defined:");
expectedOutput.add(customToolHelpLine);
String[] userDefinedToolsHelp = {
"user-defined:",
"The following tools are valid, but not currently available:",
"Some of the tools listed above only work in a windowed",
"environment. If run in a terminal-only session, they will fail.",
@ -154,52 +170,66 @@ public void testToolHelp() throws Exception {
String option = "--tool-help";
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput.toArray(new String[0]), runAndCaptureUsingInitRaw("difftool", option));
expectedOutput.toArray(new String[0]),
runAndCaptureUsingInitRaw(DIFF_TOOL, option));
}
private RevCommit createUnstagedChanges() throws Exception {
writeTrashFile("a", "Hello world a");
writeTrashFile("b", "Hello world b");
git.add().addFilepattern(".").call();
RevCommit commit = git.commit().setMessage("files a & b").call();
writeTrashFile("a", "New Hello world a");
writeTrashFile("b", "New Hello world b");
return commit;
private void configureEchoTool(String toolName) {
StoredConfig config = db.getConfig();
// the default diff tool is configured without a subsection
String subsection = null;
config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
toolName);
String command = getEchoCommand();
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
/*
* prevent prompts as we are running in tests and there is no user to
* interact with on the command line
*/
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_PROMPT,
String.valueOf(false));
}
private RevCommit createStagedChanges() throws Exception {
RevCommit commit = createUnstagedChanges();
git.add().addFilepattern(".").call();
return commit;
}
private List<DiffEntry> getRepositoryChanges(RevCommit commit)
throws Exception {
TreeWalk tw = new TreeWalk(db);
tw.addTree(commit.getTree());
FileTreeIterator modifiedTree = new FileTreeIterator(db);
tw.addTree(modifiedTree);
List<DiffEntry> changes = DiffEntry.scan(tw);
return changes;
}
private String[] getExpectedDiffToolOutput(List<DiffEntry> changes) {
String[] expectedToolOutput = new String[changes.size()];
for (int i = 0; i < changes.size(); ++i) {
DiffEntry change = changes.get(i);
String newPath = change.getNewPath();
String oldPath = change.getOldPath();
String newIdName = change.getNewId().name();
String oldIdName = change.getOldId().name();
String expectedLine = "M\t" + newPath + " (" + newIdName + ")"
+ "\t" + oldPath + " (" + oldIdName + ")";
private static String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) {
String[] expectedToolOutput = new String[conflictingFilenames.length];
for (int i = 0; i < conflictingFilenames.length; ++i) {
String newPath = conflictingFilenames[i];
String expectedLine = newPath;
expectedToolOutput[i] = expectedLine;
}
return expectedToolOutput;
}
private static void assertArrayOfLinesEquals(String failMessage,
String[] expected, String[] actual) {
assertEquals(failMessage, toString(expected), toString(actual));
private static String[] getExpectedCompareOutput(String[] conflictingFilenames) {
List<String> expected = new ArrayList<>();
int n = conflictingFilenames.length;
for (int i = 0; i < n; ++i) {
String newPath = conflictingFilenames[i];
expected.add(
"Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'");
expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
expected.add(newPath);
}
return expected.toArray(new String[0]);
}
private static String[] getExpectedAbortOutput(String[] conflictingFilenames,
int abortIndex) {
List<String> expected = new ArrayList<>();
int n = conflictingFilenames.length;
for (int i = 0; i < n; ++i) {
String newPath = conflictingFilenames[i];
expected.add(
"Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'");
expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
if (i == abortIndex) {
break;
}
expected.add(newPath);
}
return expected.toArray(new String[0]);
}
}

View File

@ -0,0 +1,310 @@
/*
* Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.pgm;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jgit.internal.diffmergetool.CommandLineMergeTool;
import org.eclipse.jgit.lib.StoredConfig;
import org.junit.Before;
import org.junit.Test;
/**
* Testing the {@code mergetool} command.
*/
public class MergeToolTest extends ToolTestCase {
private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
configureEchoTool(TOOL_NAME);
}
@Test
public void testAbortMerge() throws Exception {
String[] inputLines = {
"y", // start tool for merge resolution
"n", // don't accept merge tool result
"n", // don't continue resolution
};
String[] conflictingFilenames = createMergeConflict();
int abortIndex = 1;
String[] expectedOutput = getExpectedAbortMergeOutput(
conflictingFilenames,
abortIndex);
String option = "--tool";
InputStream inputStream = createInputStream(inputLines);
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw(inputStream,
MERGE_TOOL, "--prompt", option, TOOL_NAME));
}
@Test
public void testAbortLaunch() throws Exception {
String[] inputLines = {
"n", // abort merge tool launch
};
String[] conflictingFilenames = createMergeConflict();
String[] expectedOutput = getExpectedAbortLaunchOutput(
conflictingFilenames);
String option = "--tool";
InputStream inputStream = createInputStream(inputLines);
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw(inputStream,
MERGE_TOOL, "--prompt", option, TOOL_NAME));
}
@Test
public void testMergeConflict() throws Exception {
String[] inputLines = {
"y", // start tool for merge resolution
"y", // accept merge result as successful
"y", // start tool for merge resolution
"y", // accept merge result as successful
};
String[] conflictingFilenames = createMergeConflict();
String[] expectedOutput = getExpectedMergeConflictOutput(
conflictingFilenames);
String option = "--tool";
InputStream inputStream = createInputStream(inputLines);
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw(inputStream,
MERGE_TOOL, "--prompt", option, TOOL_NAME));
}
@Test
public void testDeletedConflict() throws Exception {
String[] inputLines = {
"d", // choose delete option to resolve conflict
"m", // choose merge option to resolve conflict
};
String[] conflictingFilenames = createDeletedConflict();
String[] expectedOutput = getExpectedDeletedConflictOutput(
conflictingFilenames);
String option = "--tool";
InputStream inputStream = createInputStream(inputLines);
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw(inputStream,
MERGE_TOOL, "--prompt", option, TOOL_NAME));
}
@Test
public void testNoConflict() throws Exception {
createStagedChanges();
String[] expectedOutput = { "No files need merging" };
String[] options = { "--tool", "-t", };
for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput,
runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME));
}
}
@Test
public void testMergeConflictNoPrompt() throws Exception {
String[] conflictingFilenames = createMergeConflict();
String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt(
conflictingFilenames);
String option = "--tool";
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput,
runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME));
}
@Test
public void testMergeConflictNoGuiNoPrompt() throws Exception {
String[] conflictingFilenames = createMergeConflict();
String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt(
conflictingFilenames);
String option = "--tool";
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL,
"--no-gui", "--no-prompt", option, TOOL_NAME));
}
@Test
public void testToolHelp() throws Exception {
CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
List<String> expectedOutput = new ArrayList<>();
expectedOutput.add(
"'git mergetool --tool=<tool>' may be set to one of the following:");
for (CommandLineMergeTool defaultTool : defaultTools) {
String toolName = defaultTool.name();
expectedOutput.add(toolName);
}
String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
+ getEchoCommand();
expectedOutput.add("user-defined:");
expectedOutput.add(customToolHelpLine);
String[] userDefinedToolsHelp = {
"The following tools are valid, but not currently available:",
"Some of the tools listed above only work in a windowed",
"environment. If run in a terminal-only session, they will fail.", };
expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));
String option = "--tool-help";
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput.toArray(new String[0]),
runAndCaptureUsingInitRaw(MERGE_TOOL, option));
}
private void configureEchoTool(String toolName) {
StoredConfig config = db.getConfig();
// the default merge tool is configured without a subsection
String subsection = null;
config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
toolName);
String command = getEchoCommand();
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
/*
* prevent prompts as we are running in tests and there is no user to
* interact with on the command line
*/
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PROMPT,
String.valueOf(false));
}
private static String[] getExpectedMergeConflictOutputNoPrompt(
String[] conflictFilenames) {
List<String> expected = new ArrayList<>();
expected.add("Merging:");
for (String conflictFilename : conflictFilenames) {
expected.add(conflictFilename);
}
for (String conflictFilename : conflictFilenames) {
expected.add("Normal merge conflict for '" + conflictFilename
+ "':");
expected.add("{local}: modified file");
expected.add("{remote}: modified file");
expected.add(conflictFilename);
expected.add(conflictFilename + " seems unchanged.");
}
return expected.toArray(new String[0]);
}
private static String[] getExpectedAbortLaunchOutput(
String[] conflictFilenames) {
List<String> expected = new ArrayList<>();
expected.add("Merging:");
for (String conflictFilename : conflictFilenames) {
expected.add(conflictFilename);
}
if (conflictFilenames.length > 1) {
String conflictFilename = conflictFilenames[0];
expected.add(
"Normal merge conflict for '" + conflictFilename + "':");
expected.add("{local}: modified file");
expected.add("{remote}: modified file");
expected.add("Hit return to start merge resolution tool ("
+ TOOL_NAME + "):");
}
return expected.toArray(new String[0]);
}
private static String[] getExpectedAbortMergeOutput(
String[] conflictFilenames, int abortIndex) {
List<String> expected = new ArrayList<>();
expected.add("Merging:");
for (String conflictFilename : conflictFilenames) {
expected.add(conflictFilename);
}
for (int i = 0; i < conflictFilenames.length; ++i) {
if (i == abortIndex) {
break;
}
String conflictFilename = conflictFilenames[i];
expected.add(
"Normal merge conflict for '" + conflictFilename + "':");
expected.add("{local}: modified file");
expected.add("{remote}: modified file");
expected.add("Hit return to start merge resolution tool ("
+ TOOL_NAME + "): " + conflictFilename);
expected.add(conflictFilename + " seems unchanged.");
expected.add("Was the merge successful [y/n]?");
if (i < conflictFilenames.length - 1) {
expected.add(
"\tContinue merging other unresolved paths [y/n]?");
}
}
return expected.toArray(new String[0]);
}
private static String[] getExpectedMergeConflictOutput(
String[] conflictFilenames) {
List<String> expected = new ArrayList<>();
expected.add("Merging:");
for (String conflictFilename : conflictFilenames) {
expected.add(conflictFilename);
}
for (int i = 0; i < conflictFilenames.length; ++i) {
String conflictFilename = conflictFilenames[i];
expected.add("Normal merge conflict for '" + conflictFilename
+ "':");
expected.add("{local}: modified file");
expected.add("{remote}: modified file");
expected.add("Hit return to start merge resolution tool ("
+ TOOL_NAME + "): " + conflictFilename);
expected.add(conflictFilename + " seems unchanged.");
expected.add("Was the merge successful [y/n]?");
if (i < conflictFilenames.length - 1) {
// expected.add(
// "\tContinue merging other unresolved paths [y/n]?");
}
}
return expected.toArray(new String[0]);
}
private static String[] getExpectedDeletedConflictOutput(
String[] conflictFilenames) {
List<String> expected = new ArrayList<>();
expected.add("Merging:");
for (String mergeConflictFilename : conflictFilenames) {
expected.add(mergeConflictFilename);
}
for (int i = 0; i < conflictFilenames.length; ++i) {
String conflictFilename = conflictFilenames[i];
expected.add(conflictFilename + " seems unchanged.");
expected.add("{local}: deleted");
expected.add("{remote}: modified file");
expected.add("Use (m)odified or (d)eleted file, or (a)bort?");
}
return expected.toArray(new String[0]);
}
}

View File

@ -0,0 +1,202 @@
/*
* Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
import org.eclipse.jgit.pgm.opt.CmdLineParser;
import org.eclipse.jgit.pgm.opt.SubcommandHandler;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.Before;
import org.kohsuke.args4j.Argument;
/**
* Base test case for the {@code difftool} and {@code mergetool} commands.
*/
public abstract class ToolTestCase extends CLIRepositoryTestCase {
public static class GitCliJGitWrapperParser {
@Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
TextBuiltin subcommand;
@Argument(index = 1, metaVar = "metaVar_arg")
List<String> arguments = new ArrayList<>();
}
protected static final String TOOL_NAME = "some_tool";
private static final String TEST_BRANCH_NAME = "test_branch";
private Git git;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
git = new Git(db);
git.commit().setMessage("initial commit").call();
git.branchCreate().setName(TEST_BRANCH_NAME).call();
}
protected String[] runAndCaptureUsingInitRaw(String... args)
throws Exception {
InputStream inputStream = null; // no input stream
return runAndCaptureUsingInitRaw(inputStream, args);
}
protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
String... args) throws Exception {
CLIGitCommand.Result result = new CLIGitCommand.Result();
GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
CmdLineParser clp = new CmdLineParser(bean);
clp.parseArgument(args);
TextBuiltin cmd = bean.subcommand;
cmd.initRaw(db, null, inputStream, result.out, result.err);
cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()]));
if (cmd.getOutputWriter() != null) {
cmd.getOutputWriter().flush();
}
if (cmd.getErrorWriter() != null) {
cmd.getErrorWriter().flush();
}
List<String> errLines = result.errLines().stream()
.filter(l -> !l.isBlank()) // we care only about error messages
.collect(Collectors.toList());
assertEquals("Expected no standard error output from tool",
Collections.EMPTY_LIST.toString(), errLines.toString());
return result.outLines().toArray(new String[0]);
}
protected String[] createMergeConflict() throws Exception {
// create files on initial branch
git.checkout().setName(TEST_BRANCH_NAME).call();
writeTrashFile("dir1/a", "Hello world a");
writeTrashFile("dir2/b", "Hello world b");
git.add().addFilepattern(".").call();
git.commit().setMessage("files a & b added").call();
// create another branch and change files
git.branchCreate().setName("branch_1").call();
git.checkout().setName("branch_1").call();
writeTrashFile("dir1/a", "Hello world a 1");
writeTrashFile("dir2/b", "Hello world b 1");
git.add().addFilepattern(".").call();
RevCommit commit1 = git.commit()
.setMessage("files a & b modified commit 1").call();
// checkout initial branch
git.checkout().setName(TEST_BRANCH_NAME).call();
// create another branch and change files
git.branchCreate().setName("branch_2").call();
git.checkout().setName("branch_2").call();
writeTrashFile("dir1/a", "Hello world a 2");
writeTrashFile("dir2/b", "Hello world b 2");
git.add().addFilepattern(".").call();
git.commit().setMessage("files a & b modified commit 2").call();
// cherry-pick conflicting changes
git.cherryPick().include(commit1).call();
String[] conflictingFilenames = { "dir1/a", "dir2/b" };
return conflictingFilenames;
}
protected String[] createDeletedConflict() throws Exception {
// create files on initial branch
git.checkout().setName(TEST_BRANCH_NAME).call();
writeTrashFile("dir1/a", "Hello world a");
writeTrashFile("dir2/b", "Hello world b");
git.add().addFilepattern(".").call();
git.commit().setMessage("files a & b added").call();
// create another branch and change files
git.branchCreate().setName("branch_1").call();
git.checkout().setName("branch_1").call();
writeTrashFile("dir1/a", "Hello world a 1");
writeTrashFile("dir2/b", "Hello world b 1");
git.add().addFilepattern(".").call();
RevCommit commit1 = git.commit()
.setMessage("files a & b modified commit 1").call();
// checkout initial branch
git.checkout().setName(TEST_BRANCH_NAME).call();
// create another branch and change files
git.branchCreate().setName("branch_2").call();
git.checkout().setName("branch_2").call();
git.rm().addFilepattern("dir1/a").call();
git.rm().addFilepattern("dir2/b").call();
git.commit().setMessage("files a & b deleted commit 2").call();
// cherry-pick conflicting changes
git.cherryPick().include(commit1).call();
String[] conflictingFilenames = { "dir1/a", "dir2/b" };
return conflictingFilenames;
}
protected String[] createUnstagedChanges() throws Exception {
writeTrashFile("dir1/a", "Hello world a");
writeTrashFile("dir2/b", "Hello world b");
git.add().addFilepattern(".").call();
git.commit().setMessage("files a & b").call();
writeTrashFile("dir1/a", "New Hello world a");
writeTrashFile("dir2/b", "New Hello world b");
String[] conflictingFilenames = { "dir1/a", "dir2/b" };
return conflictingFilenames;
}
protected String[] createStagedChanges() throws Exception {
String[] conflictingFilenames = createUnstagedChanges();
git.add().addFilepattern(".").call();
return conflictingFilenames;
}
protected List<DiffEntry> getRepositoryChanges(RevCommit commit)
throws Exception {
TreeWalk tw = new TreeWalk(db);
tw.addTree(commit.getTree());
FileTreeIterator modifiedTree = new FileTreeIterator(db);
tw.addTree(modifiedTree);
List<DiffEntry> changes = DiffEntry.scan(tw);
return changes;
}
protected static InputStream createInputStream(String[] inputLines) {
return createInputStream(Arrays.asList(inputLines));
}
protected static InputStream createInputStream(List<String> inputLines) {
String input = String.join(System.lineSeparator(), inputLines);
InputStream inputStream = new ByteArrayInputStream(input.getBytes());
return inputStream;
}
protected static void assertArrayOfLinesEquals(String failMessage,
String[] expected, String[] actual) {
assertEquals(failMessage, toString(expected), toString(actual));
}
protected static String getEchoCommand() {
/*
* use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be
* replaced with full paths to a temporary file during some of the tests
*/
return "(echo \"$MERGED\")";
}
}

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

@ -58,9 +58,9 @@ couldNotCreateBranch=Could not create branch {0}: {1}
dateInfo=Date: {0}
deletedBranch=Deleted branch {0}
deletedRemoteBranch=Deleted remote branch {0}
diffToolHelpSetToFollowing='git difftool --tool=<tool>' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
diffToolLaunch=Viewing ({0}/{1}): '{2}'\nLaunch '{3}' [Y/n]?
diffToolDied=external diff died, stopping at {0}
diffToolHelpSetToFollowing=''git difftool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
diffToolLaunch=Viewing ({0}/{1}): ''{2}''\nLaunch ''{3}'' [Y/n]?
diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1}
doesNotExist={0} does not exist
dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge:
everythingUpToDate=Everything up-to-date
@ -91,6 +91,22 @@ listeningOn=Listening on {0}
logNoSignatureVerifier="No signature verifier available"
mergeConflict=CONFLICT(content): Merge conflict in {0}
mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge:
mergeToolHelpSetToFollowing=''git mergetool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
mergeToolLaunch=Hit return to start merge resolution tool ({0}):
mergeToolDied=local or remote cannot be found in cache, stopping at {0}
mergeToolNoFiles=No files need merging
mergeToolMerging=Merging:\n{0}
mergeToolUnknownConflict=\nUnknown merge conflict for ''{0}'':
mergeToolNormalConflict=\nNormal merge conflict for ''{0}'':\n '{'local'}': modified file\n '{'remote'}': modified file
mergeToolMergeFailed=merge of {0} failed
mergeToolExecutionError=excution error
mergeToolFileUnchanged=\n{0} seems unchanged.
mergeToolDeletedConflict=\nDeleted merge conflict for ''{0}'':
mergeToolDeletedConflictByUs= {local}: deleted\n {remote}: modified file
mergeToolDeletedConflictByThem= {local}: modified file\n {remote}: deleted
mergeToolContinueUnresolvedPaths=\nContinue merging other unresolved paths [y/n]?
mergeToolWasMergeSuccessfull=Was the merge successful [y/n]?
mergeToolDeletedMergeDecision=Use (m)odified or (d)eleted file, or (a)bort?
mergeFailed=Automatic merge failed; fix conflicts and then commit the result
mergeCheckoutFailed=Please, commit your changes or stash them before you can merge.
mergeMadeBy=Merge made by the ''{0}'' strategy.
@ -255,6 +271,7 @@ usage_DisplayTheVersionOfJgit=Display the version of jgit
usage_Gc=Cleanup unnecessary files and optimize the local repository
usage_Glog=View commit history as a graph
usage_DiffGuiTool=When git-difftool is invoked with the -g or --gui option the default diff tool will be read from the configured diff.guitool variable instead of diff.tool.
usage_MergeGuiTool=When git-mergetool is invoked with the -g or --gui option the default merge tool will be read from the configured merge.guitool variable instead of merge.tool.
usage_noGui=The --no-gui option can be used to override -g or --gui setting.
usage_IndexPack=Build pack index file for an existing packed archive
usage_LFSDirectory=Directory to store large objects
@ -303,6 +320,7 @@ usage_Status=Show the working tree status
usage_StopTrackingAFile=Stop tracking a file
usage_TextHashFunctions=Scan repository to compute maximum number of collisions for hash functions
usage_ToolForDiff=Use the diff tool specified by <tool>. Run git difftool --tool-help for the list of valid <tool> settings.\nIf a diff tool is not specified, git difftool will use the configuration variable diff.tool.
usage_ToolForMerge=Use the merge resolution program specified by <tool>. Run git mergetool --tool-help for the list of valid <tool> settings.\nIf a merge resolution program is not specified, git mergetool will use the configuration variable merge.tool.
usage_UpdateRemoteRepositoryFromLocalRefs=Update remote repository from local refs
usage_UseAll=Use all refs found in refs/
usage_UseTags=Use any tag including lightweight tags
@ -350,6 +368,7 @@ usage_date=date format, one of default, rfc, local, iso, short, raw (as defined
usage_detectRenames=detect renamed files
usage_diffAlgorithm=the diff algorithm to use. Currently supported are: 'myers', 'histogram'
usage_DiffTool=git difftool is a Git command that allows you to compare and edit files between revisions using common diff tools.\ngit difftool is a frontend to git diff and accepts the same options and arguments.
usage_MergeTool=git-mergetool - Run merge conflict resolution tools to resolve merge conflicts.\nUse git mergetool to run one of several merge utilities to resolve merge conflicts. It is typically run after git merge.
usage_directoriesToExport=directories to export
usage_disableTheServiceInAllRepositories=disable the service in all repositories
usage_displayAListOfAllRegisteredJgitCommands=Display a list of all registered jgit commands

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
@ -11,9 +11,11 @@
package org.eclipse.jgit.pgm;
import static org.eclipse.jgit.lib.Constants.HEAD;
import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
@ -21,27 +23,43 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.diff.ContentSource;
import org.eclipse.jgit.diff.ContentSource.Pair;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.diff.DiffEntry.Side;
import org.eclipse.jgit.internal.diffmergetool.ToolException;
import org.eclipse.jgit.internal.diffmergetool.DiffTools;
import org.eclipse.jgit.internal.diffmergetool.FileElement;
import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
import org.eclipse.jgit.treewalk.WorkingTreeOptions;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.StringUtils;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@ -106,11 +124,14 @@ void noTrustExitCode(@SuppressWarnings("unused") boolean on) {
@Option(name = "--", metaVar = "metaVar_paths", handler = PathTreeFilterHandler.class)
private TreeFilter pathFilter = TreeFilter.ALL;
private BufferedReader inputReader;
@Override
protected void init(Repository repository, String gitDir) {
super.init(repository, gitDir);
diffFmt = new DiffFormatter(new BufferedOutputStream(outs));
diffTools = new DiffTools(repository);
inputReader = new BufferedReader(new InputStreamReader(ins, StandardCharsets.UTF_8));
}
@Override
@ -145,40 +166,53 @@ protected void run() {
private void compare(List<DiffEntry> files, boolean showPrompt,
String toolNamePrompt) throws IOException {
for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
DiffEntry ent = files.get(fileIndex);
String mergedFilePath = ent.getNewPath();
if (mergedFilePath.equals(DiffEntry.DEV_NULL)) {
mergedFilePath = ent.getOldPath();
}
// check if user wants to launch compare
boolean launchCompare = true;
if (showPrompt) {
launchCompare = isLaunchCompare(fileIndex + 1, files.size(),
mergedFilePath, toolNamePrompt);
}
if (launchCompare) {
switch (ent.getChangeType()) {
case MODIFY:
outw.println("M\t" + ent.getNewPath() //$NON-NLS-1$
+ " (" + ent.getNewId().name() + ")" //$NON-NLS-1$ //$NON-NLS-2$
+ "\t" + ent.getOldPath() //$NON-NLS-1$
+ " (" + ent.getOldId().name() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
int ret = diffTools.compare(ent.getNewPath(),
ent.getOldPath(), ent.getNewId().name(),
ent.getOldId().name(), toolName, prompt, gui,
trustExitCode);
if (ret != 0) {
ContentSource.Pair sourcePair = new ContentSource.Pair(source(oldTree),
source(newTree));
try {
for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
DiffEntry ent = files.get(fileIndex);
String mergedFilePath = ent.getNewPath();
if (mergedFilePath.equals(DiffEntry.DEV_NULL)) {
mergedFilePath = ent.getOldPath();
}
// check if user wants to launch compare
boolean launchCompare = true;
if (showPrompt) {
launchCompare = isLaunchCompare(fileIndex + 1, files.size(),
mergedFilePath, toolNamePrompt);
}
if (launchCompare) {
try {
FileElement local = createFileElement(
FileElement.Type.LOCAL, sourcePair, Side.OLD,
ent);
FileElement remote = createFileElement(
FileElement.Type.REMOTE, sourcePair, Side.NEW,
ent);
FileElement merged = new FileElement(mergedFilePath,
FileElement.Type.MERGED);
// TODO: check how to return the exit-code of the tool
// to jgit / java runtime ?
// int rc =...
ExecutionResult result = diffTools.compare(local,
remote, merged, toolName, prompt, gui,
trustExitCode);
outw.println(new String(result.getStdout().toByteArray()));
errw.println(
new String(result.getStderr().toByteArray()));
} catch (ToolException e) {
outw.println(e.getResultStdout());
outw.flush();
errw.println(e.getMessage());
throw die(MessageFormat.format(
CLIText.get().diffToolDied, mergedFilePath));
CLIText.get().diffToolDied, mergedFilePath), e);
}
break;
default:
} else {
break;
}
} else {
break;
}
} finally {
sourcePair.close();
}
}
@ -187,10 +221,9 @@ private boolean isLaunchCompare(int fileIndex, int fileCount,
String fileName, String toolNamePrompt) throws IOException {
boolean launchCompare = true;
outw.println(MessageFormat.format(CLIText.get().diffToolLaunch,
fileIndex, fileCount, fileName, toolNamePrompt));
fileIndex, fileCount, fileName, toolNamePrompt) + " "); //$NON-NLS-1$
outw.flush();
BufferedReader br = new BufferedReader(
new InputStreamReader(ins, StandardCharsets.UTF_8));
BufferedReader br = inputReader;
String line = null;
if ((line = br.readLine()) != null) {
if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$
@ -203,17 +236,18 @@ private boolean isLaunchCompare(int fileIndex, int fileCount,
private void showToolHelp() throws IOException {
StringBuilder availableToolNames = new StringBuilder();
for (String name : diffTools.getAvailableTools().keySet()) {
availableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$
availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
}
StringBuilder notAvailableToolNames = new StringBuilder();
for (String name : diffTools.getNotAvailableTools().keySet()) {
notAvailableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$
notAvailableToolNames
.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
}
StringBuilder userToolNames = new StringBuilder();
Map<String, ExternalDiffTool> userTools = diffTools
.getUserDefinedTools();
for (String name : userTools.keySet()) {
userToolNames.append(String.format("\t\t%s.cmd %s\n", //$NON-NLS-1$
userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$
name, userTools.get(name).getCommand()));
}
outw.println(MessageFormat.format(
@ -254,4 +288,53 @@ private List<DiffEntry> getFiles()
return files;
}
private FileElement createFileElement(FileElement.Type elementType,
Pair pair, Side side, DiffEntry entry)
throws NoWorkTreeException, CorruptObjectException, IOException,
ToolException {
String entryPath = side == Side.NEW ? entry.getNewPath()
: entry.getOldPath();
FileElement fileElement = new FileElement(entryPath, elementType);
if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) {
try (RevWalk revWalk = new RevWalk(db);
TreeWalk treeWalk = new TreeWalk(db,
revWalk.getObjectReader())) {
treeWalk.setFilter(
PathFilterGroup.createFromStrings(entryPath));
if (side == Side.NEW) {
newTree.reset();
treeWalk.addTree(newTree);
} else {
oldTree.reset();
treeWalk.addTree(oldTree);
}
if (treeWalk.next()) {
final EolStreamType eolStreamType = treeWalk
.getEolStreamType(CHECKOUT_OP);
final String filterCommand = treeWalk.getFilterCommand(
Constants.ATTR_FILTER_TYPE_SMUDGE);
WorkingTreeOptions opt = db.getConfig()
.get(WorkingTreeOptions.KEY);
CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
eolStreamType, filterCommand);
DirCacheCheckout.getContent(db, entryPath,
checkoutMetadata, pair.open(side, entry), opt,
new FileOutputStream(
fileElement.createTempFile(null)));
} else {
throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$
+ "' in staging area!", null); //$NON-NLS-1$
}
}
}
return fileElement;
}
private ContentSource source(AbstractTreeIterator iterator) {
if (iterator instanceof WorkingTreeIterator) {
return ContentSource.create((WorkingTreeIterator) iterator);
}
return ContentSource.create(db.newObjectReader());
}
}

View File

@ -0,0 +1,460 @@
/*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.pgm;
import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.StatusCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.ContentSource;
import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
import org.eclipse.jgit.internal.diffmergetool.FileElement;
import org.eclipse.jgit.internal.diffmergetool.MergeTools;
import org.eclipse.jgit.internal.diffmergetool.ToolException;
import org.eclipse.jgit.lib.IndexDiff.StageState;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeOptions;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
@Command(name = "mergetool", common = true, usage = "usage_MergeTool")
class MergeTool extends TextBuiltin {
private MergeTools mergeTools;
@Option(name = "--tool", aliases = {
"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge")
private String toolName;
private BooleanTriState prompt = BooleanTriState.UNSET;
@Option(name = "--prompt", usage = "usage_prompt")
void setPrompt(@SuppressWarnings("unused") boolean on) {
prompt = BooleanTriState.TRUE;
}
@Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt")
void noPrompt(@SuppressWarnings("unused") boolean on) {
prompt = BooleanTriState.FALSE;
}
@Option(name = "--tool-help", usage = "usage_toolHelp")
private boolean toolHelp;
private BooleanTriState gui = BooleanTriState.UNSET;
@Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool")
void setGui(@SuppressWarnings("unused") boolean on) {
gui = BooleanTriState.TRUE;
}
@Option(name = "--no-gui", usage = "usage_noGui")
void noGui(@SuppressWarnings("unused") boolean on) {
gui = BooleanTriState.FALSE;
}
@Argument(required = false, index = 0, metaVar = "metaVar_paths")
@Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class)
protected List<String> filterPaths;
private BufferedReader inputReader;
@Override
protected void init(Repository repository, String gitDir) {
super.init(repository, gitDir);
mergeTools = new MergeTools(repository);
inputReader = new BufferedReader(new InputStreamReader(ins));
}
enum MergeResult {
SUCCESSFUL, FAILED, ABORTED
}
@Override
protected void run() {
try {
if (toolHelp) {
showToolHelp();
} else {
// get prompt
boolean showPrompt = mergeTools.isInteractive();
if (prompt != BooleanTriState.UNSET) {
showPrompt = prompt == BooleanTriState.TRUE;
}
// get passed or default tool name
String toolNameSelected = toolName;
if ((toolNameSelected == null) || toolNameSelected.isEmpty()) {
toolNameSelected = mergeTools.getDefaultToolName(gui);
}
// get the changed files
Map<String, StageState> files = getFiles();
if (files.size() > 0) {
merge(files, showPrompt, toolNameSelected);
} else {
outw.println(CLIText.get().mergeToolNoFiles);
}
}
outw.flush();
} catch (Exception e) {
throw die(e.getMessage(), e);
}
}
private void merge(Map<String, StageState> files, boolean showPrompt,
String toolNamePrompt) throws Exception {
// sort file names
List<String> mergedFilePaths = new ArrayList<>(files.keySet());
Collections.sort(mergedFilePaths);
// show the files
StringBuilder mergedFiles = new StringBuilder();
for (String mergedFilePath : mergedFilePaths) {
mergedFiles.append(MessageFormat.format("{0}\n", mergedFilePath)); //$NON-NLS-1$
}
outw.println(MessageFormat.format(CLIText.get().mergeToolMerging,
mergedFiles));
outw.flush();
// merge the files
MergeResult mergeResult = MergeResult.SUCCESSFUL;
for (String mergedFilePath : mergedFilePaths) {
// if last merge failed...
if (mergeResult == MergeResult.FAILED) {
// check if user wants to continue
if (showPrompt && !isContinueUnresolvedPaths()) {
mergeResult = MergeResult.ABORTED;
}
}
// aborted ?
if (mergeResult == MergeResult.ABORTED) {
break;
}
// get file stage state and merge
StageState fileState = files.get(mergedFilePath);
if (fileState == StageState.BOTH_MODIFIED) {
mergeResult = mergeModified(mergedFilePath, showPrompt,
toolNamePrompt);
} else if ((fileState == StageState.DELETED_BY_US)
|| (fileState == StageState.DELETED_BY_THEM)) {
mergeResult = mergeDeleted(mergedFilePath,
fileState == StageState.DELETED_BY_US);
} else {
outw.println(MessageFormat.format(
CLIText.get().mergeToolUnknownConflict,
mergedFilePath));
mergeResult = MergeResult.ABORTED;
}
}
}
private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
String toolNamePrompt) throws Exception {
outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict,
mergedFilePath));
outw.flush();
// check if user wants to launch merge resolution tool
boolean launch = true;
if (showPrompt) {
launch = isLaunch(toolNamePrompt);
}
if (!launch) {
return MergeResult.ABORTED; // abort
}
boolean isMergeSuccessful = true;
ContentSource baseSource = ContentSource.create(db.newObjectReader());
ContentSource localSource = ContentSource.create(db.newObjectReader());
ContentSource remoteSource = ContentSource.create(db.newObjectReader());
// temporary directory if mergetool.writeToTemp == true
File tempDir = mergeTools.createTempDirectory();
// the parent directory for temp files (can be same as tempDir or just
// the worktree dir)
File tempFilesParent = tempDir != null ? tempDir : db.getWorkTree();
try {
FileElement base = null;
FileElement local = null;
FileElement remote = null;
FileElement merged = new FileElement(mergedFilePath,
Type.MERGED);
DirCache cache = db.readDirCache();
try (RevWalk revWalk = new RevWalk(db);
TreeWalk treeWalk = new TreeWalk(db,
revWalk.getObjectReader())) {
treeWalk.setFilter(
PathFilterGroup.createFromStrings(mergedFilePath));
DirCacheIterator cacheIter = new DirCacheIterator(cache);
treeWalk.addTree(cacheIter);
while (treeWalk.next()) {
if (treeWalk.isSubtree()) {
treeWalk.enterSubtree();
continue;
}
final EolStreamType eolStreamType = treeWalk
.getEolStreamType(CHECKOUT_OP);
final String filterCommand = treeWalk.getFilterCommand(
Constants.ATTR_FILTER_TYPE_SMUDGE);
WorkingTreeOptions opt = db.getConfig()
.get(WorkingTreeOptions.KEY);
CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
eolStreamType, filterCommand);
DirCacheEntry entry = treeWalk.getTree(DirCacheIterator.class).getDirCacheEntry();
if (entry == null) {
continue;
}
ObjectId id = entry.getObjectId();
switch (entry.getStage()) {
case DirCacheEntry.STAGE_1:
base = new FileElement(mergedFilePath, Type.BASE);
DirCacheCheckout.getContent(db, mergedFilePath,
checkoutMetadata,
baseSource.open(mergedFilePath, id), opt,
new FileOutputStream(
base.createTempFile(tempFilesParent)));
break;
case DirCacheEntry.STAGE_2:
local = new FileElement(mergedFilePath, Type.LOCAL);
DirCacheCheckout.getContent(db, mergedFilePath,
checkoutMetadata,
localSource.open(mergedFilePath, id), opt,
new FileOutputStream(
local.createTempFile(tempFilesParent)));
break;
case DirCacheEntry.STAGE_3:
remote = new FileElement(mergedFilePath, Type.REMOTE);
DirCacheCheckout.getContent(db, mergedFilePath,
checkoutMetadata,
remoteSource.open(mergedFilePath, id), opt,
new FileOutputStream(remote
.createTempFile(tempFilesParent)));
break;
}
}
}
if ((local == null) || (remote == null)) {
throw die(MessageFormat.format(CLIText.get().mergeToolDied,
mergedFilePath));
}
long modifiedBefore = merged.getFile().lastModified();
try {
// TODO: check how to return the exit-code of the
// tool to jgit / java runtime ?
// int rc =...
ExecutionResult executionResult = mergeTools.merge(local,
remote, merged, base, tempDir, toolName, prompt, gui);
outw.println(
new String(executionResult.getStdout().toByteArray()));
outw.flush();
errw.println(
new String(executionResult.getStderr().toByteArray()));
errw.flush();
} catch (ToolException e) {
isMergeSuccessful = false;
outw.println(e.getResultStdout());
outw.flush();
errw.println(MessageFormat.format(
CLIText.get().mergeToolMergeFailed, mergedFilePath));
errw.flush();
if (e.isCommandExecutionError()) {
errw.println(e.getMessage());
throw die(CLIText.get().mergeToolExecutionError, e);
}
}
// if merge was successful check file modified
if (isMergeSuccessful) {
long modifiedAfter = merged.getFile().lastModified();
if (modifiedBefore == modifiedAfter) {
outw.println(MessageFormat.format(
CLIText.get().mergeToolFileUnchanged,
mergedFilePath));
isMergeSuccessful = !showPrompt || isMergeSuccessful();
}
}
// if automatically or manually successful
// -> add the file to the index
if (isMergeSuccessful) {
addFile(mergedFilePath);
}
} finally {
baseSource.close();
localSource.close();
remoteSource.close();
}
return isMergeSuccessful ? MergeResult.SUCCESSFUL : MergeResult.FAILED;
}
private MergeResult mergeDeleted(String mergedFilePath, boolean deletedByUs)
throws Exception {
outw.println(MessageFormat.format(CLIText.get().mergeToolFileUnchanged,
mergedFilePath));
if (deletedByUs) {
outw.println(CLIText.get().mergeToolDeletedConflictByUs);
} else {
outw.println(CLIText.get().mergeToolDeletedConflictByThem);
}
int mergeDecision = getDeletedMergeDecision();
if (mergeDecision == 1) {
// add modified file
addFile(mergedFilePath);
} else if (mergeDecision == -1) {
// remove deleted file
rmFile(mergedFilePath);
} else {
return MergeResult.ABORTED;
}
return MergeResult.SUCCESSFUL;
}
private void addFile(String fileName) throws Exception {
try (Git git = new Git(db)) {
git.add().addFilepattern(fileName).call();
}
}
private void rmFile(String fileName) throws Exception {
try (Git git = new Git(db)) {
git.rm().addFilepattern(fileName).call();
}
}
private boolean hasUserAccepted(String message) throws IOException {
boolean yes = true;
outw.print(message + " "); //$NON-NLS-1$
outw.flush();
BufferedReader br = inputReader;
String line = null;
while ((line = br.readLine()) != null) {
if (line.equalsIgnoreCase("y")) { //$NON-NLS-1$
yes = true;
break;
} else if (line.equalsIgnoreCase("n")) { //$NON-NLS-1$
yes = false;
break;
}
outw.print(message);
outw.flush();
}
return yes;
}
private boolean isContinueUnresolvedPaths() throws IOException {
return hasUserAccepted(CLIText.get().mergeToolContinueUnresolvedPaths);
}
private boolean isMergeSuccessful() throws IOException {
return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull);
}
private boolean isLaunch(String toolNamePrompt) throws IOException {
boolean launch = true;
outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch,
toolNamePrompt) + " "); //$NON-NLS-1$
outw.flush();
BufferedReader br = inputReader;
String line = null;
if ((line = br.readLine()) != null) {
if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$
launch = false;
}
}
return launch;
}
private int getDeletedMergeDecision() throws IOException {
int ret = 0; // abort
final String message = CLIText.get().mergeToolDeletedMergeDecision
+ " "; //$NON-NLS-1$
outw.print(message);
outw.flush();
BufferedReader br = inputReader;
String line = null;
while ((line = br.readLine()) != null) {
if (line.equalsIgnoreCase("m")) { //$NON-NLS-1$
ret = 1; // modified
break;
} else if (line.equalsIgnoreCase("d")) { //$NON-NLS-1$
ret = -1; // deleted
break;
} else if (line.equalsIgnoreCase("a")) { //$NON-NLS-1$
break;
}
outw.print(message);
outw.flush();
}
return ret;
}
private void showToolHelp() throws IOException {
StringBuilder availableToolNames = new StringBuilder();
for (String name : mergeTools.getAvailableTools().keySet()) {
availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
}
StringBuilder notAvailableToolNames = new StringBuilder();
for (String name : mergeTools.getNotAvailableTools().keySet()) {
notAvailableToolNames
.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
}
StringBuilder userToolNames = new StringBuilder();
Map<String, ExternalMergeTool> userTools = mergeTools
.getUserDefinedTools();
for (String name : userTools.keySet()) {
userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$
name, userTools.get(name).getCommand()));
}
outw.println(MessageFormat.format(
CLIText.get().mergeToolHelpSetToFollowing, availableToolNames,
userToolNames, notAvailableToolNames));
}
private Map<String, StageState> getFiles() throws RevisionSyntaxException,
NoWorkTreeException, GitAPIException {
Map<String, StageState> files = new TreeMap<>();
try (Git git = new Git(db)) {
StatusCommand statusCommand = git.status();
if (filterPaths != null && filterPaths.size() > 0) {
for (String path : filterPaths) {
statusCommand.addPath(path);
}
}
Status status = statusCommand.call();
files = status.getConflictingStageState();
}
return files;
}
}

View File

@ -169,6 +169,22 @@ public static String fatalError(String message) {
/***/ public String logNoSignatureVerifier;
/***/ public String mergeCheckoutConflict;
/***/ public String mergeConflict;
/***/ public String mergeToolHelpSetToFollowing;
/***/ public String mergeToolLaunch;
/***/ public String mergeToolDied;
/***/ public String mergeToolNoFiles;
/***/ public String mergeToolMerging;
/***/ public String mergeToolUnknownConflict;
/***/ public String mergeToolNormalConflict;
/***/ public String mergeToolMergeFailed;
/***/ public String mergeToolExecutionError;
/***/ public String mergeToolFileUnchanged;
/***/ public String mergeToolDeletedConflict;
/***/ public String mergeToolDeletedConflictByUs;
/***/ public String mergeToolDeletedConflictByThem;
/***/ public String mergeToolContinueUnresolvedPaths;
/***/ public String mergeToolWasMergeSuccessfull;
/***/ public String mergeToolDeletedMergeDecision;
/***/ public String mergeFailed;
/***/ public String mergeCheckoutFailed;
/***/ public String mergeMadeBy;

View File

@ -10,13 +10,17 @@
package org.eclipse.jgit.internal.diffmergetool;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Collections;
import java.util.LinkedHashSet;
@ -25,6 +29,7 @@
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.junit.Test;
/**
@ -32,6 +37,54 @@
*/
public class ExternalDiffToolTest extends ExternalToolTestCase {
@Test(expected = ToolException.class)
public void testUserToolWithError() throws Exception {
String toolName = "customTool";
int errorReturnCode = 1;
String command = "exit " + errorReturnCode;
FileBasedConfig config = db.getConfig();
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
DiffTools manager = new DiffTools(db);
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
BooleanTriState trustExitCode = BooleanTriState.TRUE;
manager.compare(local, remote, merged, toolName, prompt, gui,
trustExitCode);
fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode);
}
@Test(expected = ToolException.class)
public void testUserToolWithCommandNotFoundError() throws Exception {
String toolName = "customTool";
int errorReturnCode = 127; // command not found
String command = "exit " + errorReturnCode;
FileBasedConfig config = db.getConfig();
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
DiffTools manager = new DiffTools(db);
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
BooleanTriState trustExitCode = BooleanTriState.FALSE;
manager.compare(local, remote, merged, toolName, prompt, gui,
trustExitCode);
fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode);
}
@Test
public void testToolNames() {
DiffTools manager = new DiffTools(db);
@ -86,11 +139,11 @@ public void testUserDefinedTools() {
config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
CONFIG_KEY_PATH, "/usr/bin/echo");
config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
CONFIG_KEY_PROMPT, "--no-prompt");
CONFIG_KEY_PROMPT, String.valueOf(false));
config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
CONFIG_KEY_GUITOOL, "--no-gui");
CONFIG_KEY_GUITOOL, String.valueOf(false));
config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
CONFIG_KEY_TRUST_EXIT_CODE, "--no-trust-exit-code");
CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false));
DiffTools manager = new DiffTools(db);
Set<String> actualToolNames = manager.getUserDefinedTools().keySet();
Set<String> expectedToolNames = new LinkedHashSet<>();
@ -109,38 +162,50 @@ public void testNotAvailableTools() {
}
@Test
public void testCompare() {
DiffTools manager = new DiffTools(db);
public void testCompare() throws ToolException {
String toolName = "customTool";
FileBasedConfig config = db.getConfig();
// the default diff tool is configured without a subsection
String subsection = null;
config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
toolName);
String command = getEchoCommand();
config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
String newPath = "";
String oldPath = "";
String newId = "";
String oldId = "";
String toolName = "";
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
BooleanTriState trustExitCode = BooleanTriState.UNSET;
DiffTools manager = new DiffTools(db);
int expectedCompareResult = 0;
int compareResult = manager.compare(newPath, oldPath, newId, oldId,
ExecutionResult compareResult = manager.compare(local, remote, merged,
toolName, prompt, gui, trustExitCode);
assertEquals("Incorrect compare result for external diff tool",
expectedCompareResult, compareResult);
expectedCompareResult, compareResult.getRc());
}
@Test
public void testDefaultTool() throws Exception {
String toolName = "customTool";
String guiToolName = "customGuiTool";
FileBasedConfig config = db.getConfig();
// the default diff tool is configured without a subsection
String subsection = null;
config.setString("diff", subsection, "tool", "customTool");
config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
toolName);
DiffTools manager = new DiffTools(db);
BooleanTriState gui = BooleanTriState.UNSET;
String defaultToolName = manager.getDefaultToolName(gui);
assertEquals(
"Expected configured difftool to be the default external diff tool",
"my_default_toolname", defaultToolName);
toolName, defaultToolName);
gui = BooleanTriState.TRUE;
String defaultGuiToolName = manager.getDefaultToolName(gui);
@ -148,11 +213,63 @@ public void testDefaultTool() throws Exception {
"Expected configured difftool to be the default external diff tool",
"my_gui_tool", defaultGuiToolName);
config.setString("diff", subsection, "guitool", "customGuiTool");
config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL,
guiToolName);
manager = new DiffTools(db);
defaultGuiToolName = manager.getDefaultToolName(gui);
assertEquals(
"Expected configured difftool to be the default external diff guitool",
"my_gui_tool", defaultGuiToolName);
}
@Test
public void testOverridePreDefinedToolPath() {
String newToolPath = "/tmp/path/";
CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
assertTrue("Expected to find pre-defined external diff tools",
defaultTools.length > 0);
CommandLineDiffTool overridenTool = defaultTools[0];
String overridenToolName = overridenTool.name();
String overridenToolPath = newToolPath + overridenToolName;
FileBasedConfig config = db.getConfig();
config.setString(CONFIG_DIFFTOOL_SECTION, overridenToolName,
CONFIG_KEY_PATH, overridenToolPath);
DiffTools manager = new DiffTools(db);
Map<String, ExternalDiffTool> availableTools = manager
.getAvailableTools();
ExternalDiffTool externalDiffTool = availableTools
.get(overridenToolName);
String actualDiffToolPath = externalDiffTool.getPath();
assertEquals(
"Expected pre-defined external diff tool to have overriden path",
overridenToolPath, actualDiffToolPath);
String expectedDiffToolCommand = overridenToolPath + " "
+ overridenTool.getParameters();
String actualDiffToolCommand = externalDiffTool.getCommand();
assertEquals(
"Expected pre-defined external diff tool to have overriden command",
expectedDiffToolCommand, actualDiffToolCommand);
}
@Test(expected = ToolException.class)
public void testUndefinedTool() throws Exception {
DiffTools manager = new DiffTools(db);
String toolName = "undefined";
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
BooleanTriState trustExitCode = BooleanTriState.UNSET;
manager.compare(local, remote, merged, toolName, prompt, gui,
trustExitCode);
fail("Expected exception to be thrown due to not defined external diff tool");
}
private String getEchoCommand() {
return "(echo \"$LOCAL\" \"$REMOTE\") > "
+ commandResult.getAbsolutePath();
}
}

View File

@ -0,0 +1,271 @@
/*
* Copyright (C) 2020-2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.junit.Test;
/**
* Testing external merge tools.
*/
public class ExternalMergeToolTest extends ExternalToolTestCase {
@Test(expected = ToolException.class)
public void testUserToolWithError() throws Exception {
String toolName = "customTool";
int errorReturnCode = 1;
String command = "exit " + errorReturnCode;
FileBasedConfig config = db.getConfig();
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
config.setString(CONFIG_MERGETOOL_SECTION, toolName,
CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(Boolean.TRUE));
MergeTools manager = new MergeTools(db);
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode);
}
@Test(expected = ToolException.class)
public void testUserToolWithCommandNotFoundError() throws Exception {
String toolName = "customTool";
int errorReturnCode = 127; // command not found
String command = "exit " + errorReturnCode;
FileBasedConfig config = db.getConfig();
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
MergeTools manager = new MergeTools(db);
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
fail("Expected exception to be thrown due to external tool exiting with error code: "
+ errorReturnCode);
}
@Test
public void testToolNames() {
MergeTools manager = new MergeTools(db);
Set<String> actualToolNames = manager.getToolNames();
Set<String> expectedToolNames = Collections.emptySet();
assertEquals("Incorrect set of external merge tool names",
expectedToolNames, actualToolNames);
}
@Test
public void testAllTools() {
MergeTools manager = new MergeTools(db);
Set<String> actualToolNames = manager.getAvailableTools().keySet();
Set<String> expectedToolNames = new LinkedHashSet<>();
CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
for (CommandLineMergeTool defaultTool : defaultTools) {
String toolName = defaultTool.name();
expectedToolNames.add(toolName);
}
assertEquals("Incorrect set of external merge tools", expectedToolNames,
actualToolNames);
}
@Test
public void testOverridePredefinedToolPath() {
String toolName = CommandLineMergeTool.guiffy.name();
String customToolPath = "/usr/bin/echo";
FileBasedConfig config = db.getConfig();
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
"echo");
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PATH,
customToolPath);
MergeTools manager = new MergeTools(db);
Map<String, ExternalMergeTool> tools = manager.getUserDefinedTools();
ExternalMergeTool mergeTool = tools.get(toolName);
assertNotNull("Expected tool \"" + toolName + "\" to be user defined",
mergeTool);
String toolPath = mergeTool.getPath();
assertEquals("Expected external merge tool to have an overriden path",
customToolPath, toolPath);
}
@Test
public void testUserDefinedTools() {
FileBasedConfig config = db.getConfig();
String customToolname = "customTool";
config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
CONFIG_KEY_CMD, "echo");
config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
CONFIG_KEY_PATH, "/usr/bin/echo");
config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
CONFIG_KEY_PROMPT, String.valueOf(false));
config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
CONFIG_KEY_GUITOOL, String.valueOf(false));
config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false));
MergeTools manager = new MergeTools(db);
Set<String> actualToolNames = manager.getUserDefinedTools().keySet();
Set<String> expectedToolNames = new LinkedHashSet<>();
expectedToolNames.add(customToolname);
assertEquals("Incorrect set of external merge tools", expectedToolNames,
actualToolNames);
}
@Test
public void testNotAvailableTools() {
MergeTools manager = new MergeTools(db);
Set<String> actualToolNames = manager.getNotAvailableTools().keySet();
Set<String> expectedToolNames = Collections.emptySet();
assertEquals("Incorrect set of not available external merge tools",
expectedToolNames, actualToolNames);
}
@Test
public void testCompare() throws ToolException {
String toolName = "customTool";
FileBasedConfig config = db.getConfig();
// the default merge tool is configured without a subsection
String subsection = null;
config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
toolName);
String command = getEchoCommand();
config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
command);
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
MergeTools manager = new MergeTools(db);
int expectedCompareResult = 0;
ExecutionResult compareResult = manager.merge(local, remote, merged,
base, null, toolName, prompt, gui);
assertEquals("Incorrect compare result for external merge tool",
expectedCompareResult, compareResult.getRc());
}
@Test
public void testDefaultTool() throws Exception {
String toolName = "customTool";
String guiToolName = "customGuiTool";
FileBasedConfig config = db.getConfig();
// the default merge tool is configured without a subsection
String subsection = null;
config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
toolName);
MergeTools manager = new MergeTools(db);
BooleanTriState gui = BooleanTriState.UNSET;
String defaultToolName = manager.getDefaultToolName(gui);
assertEquals(
"Expected configured mergetool to be the default external merge tool",
toolName, defaultToolName);
gui = BooleanTriState.TRUE;
String defaultGuiToolName = manager.getDefaultToolName(gui);
assertEquals(
"Expected configured mergetool to be the default external merge tool",
"my_gui_tool", defaultGuiToolName);
config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL,
guiToolName);
manager = new MergeTools(db);
defaultGuiToolName = manager.getDefaultToolName(gui);
assertEquals(
"Expected configured mergetool to be the default external merge guitool",
"my_gui_tool", defaultGuiToolName);
}
@Test
public void testOverridePreDefinedToolPath() {
String newToolPath = "/tmp/path/";
CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
assertTrue("Expected to find pre-defined external merge tools",
defaultTools.length > 0);
CommandLineMergeTool overridenTool = defaultTools[0];
String overridenToolName = overridenTool.name();
String overridenToolPath = newToolPath + overridenToolName;
FileBasedConfig config = db.getConfig();
config.setString(CONFIG_MERGETOOL_SECTION, overridenToolName,
CONFIG_KEY_PATH, overridenToolPath);
MergeTools manager = new MergeTools(db);
Map<String, ExternalMergeTool> availableTools = manager
.getAvailableTools();
ExternalMergeTool externalMergeTool = availableTools
.get(overridenToolName);
String actualMergeToolPath = externalMergeTool.getPath();
assertEquals(
"Expected pre-defined external merge tool to have overriden path",
overridenToolPath, actualMergeToolPath);
boolean withBase = true;
String expectedMergeToolCommand = overridenToolPath + " "
+ overridenTool.getParameters(withBase);
String actualMergeToolCommand = externalMergeTool.getCommand();
assertEquals(
"Expected pre-defined external merge tool to have overriden command",
expectedMergeToolCommand, actualMergeToolCommand);
}
@Test(expected = ToolException.class)
public void testUndefinedTool() throws Exception {
MergeTools manager = new MergeTools(db);
String toolName = "undefined";
BooleanTriState prompt = BooleanTriState.UNSET;
BooleanTriState gui = BooleanTriState.UNSET;
manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
fail("Expected exception to be thrown due to not defined external merge tool");
}
private String getEchoCommand() {
return "(echo \"$LOCAL\" \"$REMOTE\") > "
+ commandResult.getAbsolutePath();
}
}

View File

@ -36,6 +36,14 @@ public abstract class ExternalToolTestCase extends RepositoryTestCase {
protected File commandResult;
protected FileElement local;
protected FileElement remote;
protected FileElement merged;
protected FileElement base;
@Before
@Override
public void setUp() throws Exception {
@ -51,6 +59,15 @@ public void setUp() throws Exception {
baseFile.deleteOnExit();
commandResult = writeTrashFile("commandResult.txt", "");
commandResult.deleteOnExit();
local = new FileElement(localFile.getAbsolutePath(),
FileElement.Type.LOCAL);
remote = new FileElement(remoteFile.getAbsolutePath(),
FileElement.Type.REMOTE);
merged = new FileElement(mergedFile.getAbsolutePath(),
FileElement.Type.MERGED);
base = new FileElement(baseFile.getAbsolutePath(),
FileElement.Type.BASE);
}
@After

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2010, 2020 Google Inc. and others
* Copyright (C) 2010, 2021 Google Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@ -91,6 +91,29 @@ public static ContentSource create(WorkingTreeIterator iterator) {
public abstract ObjectLoader open(String path, ObjectId id)
throws IOException;
/**
* Closes the used resources like ObjectReader, TreeWalk etc. Default
* implementation does nothing.
*
* @since 6.2
*/
public void close() {
// Do nothing
}
/**
* Checks if the source is from "working tree", so it can be accessed as a
* file directly.
*
* @since 6.2
*
* @return true if working tree source and false otherwise (loader must be
* used)
*/
public boolean isWorkingTreeSource() {
return false;
}
private static class ObjectReaderSource extends ContentSource {
private final ObjectReader reader;
@ -111,6 +134,16 @@ public long size(String path, ObjectId id) throws IOException {
public ObjectLoader open(String path, ObjectId id) throws IOException {
return reader.open(id, Constants.OBJ_BLOB);
}
@Override
public void close() {
reader.close();
}
@Override
public boolean isWorkingTreeSource() {
return false;
}
}
private static class WorkingTreeSource extends ContentSource {
@ -194,6 +227,16 @@ private void seek(String path) throws IOException {
throw new FileNotFoundException(path);
}
}
@Override
public void close() {
tw.close();
}
@Override
public boolean isWorkingTreeSource() {
return true;
}
}
/** A pair of sources to access the old and new sides of a DiffEntry. */
@ -261,5 +304,37 @@ public ObjectLoader open(DiffEntry.Side side, DiffEntry ent)
throw new IllegalArgumentException();
}
}
/**
* Closes used resources.
*
* @since 6.2
*/
public void close() {
oldSource.close();
newSource.close();
}
/**
* Checks if source (side) is a "working tree".
*
* @since 6.2
*
* @param side
* which side of the entry to read (OLD or NEW).
* @return is the source a "working tree"
*
*/
public boolean isWorkingTreeSource(DiffEntry.Side side) {
switch (side) {
case OLD:
return oldSource.isWorkingTreeSource();
case NEW:
return newSource.isWorkingTreeSource();
default:
throw new IllegalArgumentException();
}
}
}
}

View File

@ -0,0 +1,190 @@
/*
* Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Map;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.eclipse.jgit.util.FS_POSIX;
import org.eclipse.jgit.util.FS_Win32;
import org.eclipse.jgit.util.FS_Win32_Cygwin;
/**
* Runs a command with help of FS.
*/
public class CommandExecutor {
private FS fs;
private boolean checkExitCode;
private File commandFile;
private boolean useMsys2;
/**
* @param fs
* the file system
* @param checkExitCode
* should the exit code be checked for errors ?
*/
public CommandExecutor(FS fs, boolean checkExitCode) {
this.fs = fs;
this.checkExitCode = checkExitCode;
}
/**
* @param command
* the command string
* @param workingDir
* the working directory
* @param env
* the environment
* @return the execution result
* @throws ToolException
* @throws InterruptedException
* @throws IOException
*/
public ExecutionResult run(String command, File workingDir,
Map<String, String> env)
throws ToolException, IOException, InterruptedException {
String[] commandArray = createCommandArray(command);
try {
ProcessBuilder pb = fs.runInShell(commandArray[0],
Arrays.copyOfRange(commandArray, 1, commandArray.length));
pb.directory(workingDir);
Map<String, String> envp = pb.environment();
if (env != null) {
envp.putAll(env);
}
ExecutionResult result = fs.execute(pb, null);
int rc = result.getRc();
if (rc != 0) {
boolean execError = isCommandExecutionError(rc);
if (checkExitCode || execError) {
throw new ToolException(
"JGit: tool execution return code: " + rc + "\n" //$NON-NLS-1$ //$NON-NLS-2$
+ "checkExitCode: " + checkExitCode + "\n" //$NON-NLS-1$ //$NON-NLS-2$
+ "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$
+ "stderr: \n" //$NON-NLS-1$
+ new String(
result.getStderr().toByteArray()),
result, execError);
}
}
return result;
} finally {
deleteCommandArray();
}
}
private void deleteCommandArray() {
deleteCommandFile();
}
private String[] createCommandArray(String command)
throws ToolException, IOException {
String[] commandArray = null;
checkUseMsys2(command);
createCommandFile(command);
if (fs instanceof FS_POSIX) {
commandArray = new String[1];
commandArray[0] = commandFile.getCanonicalPath();
} else if (fs instanceof FS_Win32) {
if (useMsys2) {
commandArray = new String[3];
commandArray[0] = "bash.exe"; //$NON-NLS-1$
commandArray[1] = "-c"; //$NON-NLS-1$
commandArray[2] = commandFile.getCanonicalPath().replace("\\", //$NON-NLS-1$
"/"); //$NON-NLS-1$
} else {
commandArray = new String[1];
commandArray[0] = commandFile.getCanonicalPath();
}
} else if (fs instanceof FS_Win32_Cygwin) {
commandArray = new String[1];
commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
} else {
throw new ToolException(
"JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
}
return commandArray;
}
private void checkUseMsys2(String command) {
useMsys2 = false;
String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$
if (useMsys2Str != null && !useMsys2Str.isEmpty()) {
if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$
useMsys2 = command.contains(".sh"); //$NON-NLS-1$
} else {
useMsys2 = Boolean.parseBoolean(useMsys2Str);
}
}
}
private void createCommandFile(String command)
throws ToolException, IOException {
String fileExtension = null;
if (useMsys2 || fs instanceof FS_POSIX
|| fs instanceof FS_Win32_Cygwin) {
fileExtension = ".sh"; //$NON-NLS-1$
} else if (fs instanceof FS_Win32) {
fileExtension = ".cmd"; //$NON-NLS-1$
command = "@echo off" + System.lineSeparator() + command //$NON-NLS-1$
+ System.lineSeparator() + "exit /B %ERRORLEVEL%"; //$NON-NLS-1$
} else {
throw new ToolException(
"JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
}
commandFile = File.createTempFile(".__", //$NON-NLS-1$
"__jgit_tool" + fileExtension); //$NON-NLS-1$
try (OutputStream outStream = new FileOutputStream(commandFile)) {
byte[] strToBytes = command.getBytes();
outStream.write(strToBytes);
outStream.close();
}
commandFile.setExecutable(true);
}
private void deleteCommandFile() {
if (commandFile != null && commandFile.exists()) {
commandFile.delete();
}
}
private boolean isCommandExecutionError(int rc) {
if (useMsys2 || fs instanceof FS_POSIX
|| fs instanceof FS_Win32_Cygwin) {
// 126: permission for executing command denied
// 127: command not found
if ((rc == 126) || (rc == 127)) {
return true;
}
}
else if (fs instanceof FS_Win32) {
// 9009, 0x2331: Program is not recognized as an internal or
// external command, operable program or batch file. Indicates that
// command, application name or path has been misspelled when
// configuring the Action.
if (rc == 9009) {
return true;
}
}
return false;
}
}

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

@ -49,9 +49,10 @@ private DiffToolConfig(Config rc) {
toolName = rc.getString(CONFIG_DIFF_SECTION, null, CONFIG_KEY_TOOL);
guiToolName = rc.getString(CONFIG_DIFF_SECTION, null,
CONFIG_KEY_GUITOOL);
prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, CONFIG_KEY_PROMPT,
prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, toolName,
CONFIG_KEY_PROMPT,
true);
String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, null,
String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, toolName,
CONFIG_KEY_TRUST_EXIT_CODE);
if (trustStr != null) {
trustExitCode = Boolean.parseBoolean(trustStr)

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
@ -12,22 +12,27 @@
import java.util.TreeMap;
import java.util.Collections;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.eclipse.jgit.util.StringUtils;
/**
* Manages diff tools.
*/
public class DiffTools {
private final Repository repo;
private final DiffToolConfig config;
private Map<String, ExternalDiffTool> predefinedTools;
private final Map<String, ExternalDiffTool> predefinedTools;
private Map<String, ExternalDiffTool> userDefinedTools;
private final Map<String, ExternalDiffTool> userDefinedTools;
/**
* Creates the external diff-tools manager for given repository.
@ -36,22 +41,22 @@ public class DiffTools {
* the repository
*/
public DiffTools(Repository repo) {
this.repo = repo;
config = repo.getConfig().get(DiffToolConfig.KEY);
setupPredefinedTools();
setupUserDefinedTools();
predefinedTools = setupPredefinedTools();
userDefinedTools = setupUserDefinedTools(config, predefinedTools);
}
/**
* Compare two versions of a file.
*
* @param newPath
* the new file path
* @param oldPath
* the old file path
* @param newId
* the new object ID
* @param oldId
* the old object ID
* @param localFile
* the local file element
* @param remoteFile
* the remote file element
* @param mergedFile
* the merged file element, it's path equals local or remote
* element path
* @param toolName
* the selected tool name (can be null)
* @param prompt
@ -60,12 +65,35 @@ public DiffTools(Repository repo) {
* the GUI option
* @param trustExitCode
* the "trust exit code" option
* @return the return code from executed tool
* @return the execution result from tool
* @throws ToolException
*/
public int compare(String newPath, String oldPath, String newId,
String oldId, String toolName, BooleanTriState prompt,
BooleanTriState gui, BooleanTriState trustExitCode) {
return 0;
public ExecutionResult compare(FileElement localFile,
FileElement remoteFile, FileElement mergedFile, String toolName,
BooleanTriState prompt, BooleanTriState gui,
BooleanTriState trustExitCode) throws ToolException {
try {
// prepare the command (replace the file paths)
String command = ExternalToolUtils.prepareCommand(
guessTool(toolName, gui).getCommand(), localFile,
remoteFile, mergedFile, null);
// prepare the environment
Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo,
localFile, remoteFile, mergedFile, null);
boolean trust = config.isTrustExitCode();
if (trustExitCode != BooleanTriState.UNSET) {
trust = trustExitCode == BooleanTriState.TRUE;
}
// execute the tool
CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
return cmdExec.run(command, repo.getWorkTree(), env);
} catch (IOException | InterruptedException e) {
throw new ToolException(e);
} finally {
localFile.cleanTemporaries();
remoteFile.cleanTemporaries();
mergedFile.cleanTemporaries();
}
}
/**
@ -103,41 +131,64 @@ public Map<String, ExternalDiffTool> getNotAvailableTools() {
*/
public String getDefaultToolName(BooleanTriState gui) {
return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$
: "my_default_toolname"; //$NON-NLS-1$
: config.getDefaultToolName();
}
/**
* @return is interactive (config prompt enabled) ?
*/
public boolean isInteractive() {
return false;
return config.isPrompt();
}
private void setupPredefinedTools() {
predefinedTools = new TreeMap<>();
for (CommandLineDiffTool tool : CommandLineDiffTool.values()) {
predefinedTools.put(tool.name(), new PreDefinedDiffTool(tool));
private ExternalDiffTool guessTool(String toolName, BooleanTriState gui)
throws ToolException {
if (StringUtils.isEmptyOrNull(toolName)) {
toolName = getDefaultToolName(gui);
}
ExternalDiffTool tool = getTool(toolName);
if (tool == null) {
throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$
}
return tool;
}
private void setupUserDefinedTools() {
userDefinedTools = new TreeMap<>();
Map<String, ExternalDiffTool> userTools = config.getTools();
private ExternalDiffTool getTool(final String name) {
ExternalDiffTool tool = userDefinedTools.get(name);
if (tool == null) {
tool = predefinedTools.get(name);
}
return tool;
}
private static Map<String, ExternalDiffTool> setupPredefinedTools() {
Map<String, ExternalDiffTool> tools = new TreeMap<>();
for (CommandLineDiffTool tool : CommandLineDiffTool.values()) {
tools.put(tool.name(), new PreDefinedDiffTool(tool));
}
return tools;
}
private static Map<String, ExternalDiffTool> setupUserDefinedTools(
DiffToolConfig cfg, Map<String, ExternalDiffTool> predefTools) {
Map<String, ExternalDiffTool> tools = new TreeMap<>();
Map<String, ExternalDiffTool> userTools = cfg.getTools();
for (String name : userTools.keySet()) {
ExternalDiffTool userTool = userTools.get(name);
// if difftool.<name>.cmd is defined we have user defined tool
if (userTool.getCommand() != null) {
userDefinedTools.put(name, userTool);
tools.put(name, userTool);
} else if (userTool.getPath() != null) {
// if difftool.<name>.path is defined we just overload the path
// of predefined tool
PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefinedTools
PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefTools
.get(name);
if (predefTool != null) {
predefTool.setPath(userTool.getPath());
}
}
}
return tools;
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
import org.eclipse.jgit.lib.internal.BooleanTriState;
/**
* The merge tool interface.
*/
public interface ExternalMergeTool extends ExternalDiffTool {
/**
* @return the tool "trust exit code" option
*/
BooleanTriState getTrustExitCode();
/**
* @param withBase
* get command with base present (true) or without base present
* (false)
* @return the tool command
*/
String getCommand(boolean withBase);
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
import java.util.TreeMap;
import java.io.IOException;
import java.util.Map;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
/**
* Utilities for diff- and merge-tools.
*/
public class ExternalToolUtils {
/**
* Prepare command for execution.
*
* @param command
* the input "command" string
* @param localFile
* the local file (ours)
* @param remoteFile
* the remote file (theirs)
* @param mergedFile
* the merged file (worktree)
* @param baseFile
* the base file (can be null)
* @return the prepared (with replaced variables) command string
* @throws IOException
*/
public static String prepareCommand(String command, FileElement localFile,
FileElement remoteFile, FileElement mergedFile,
FileElement baseFile) throws IOException {
command = localFile.replaceVariable(command);
command = remoteFile.replaceVariable(command);
command = mergedFile.replaceVariable(command);
if (baseFile != null) {
command = baseFile.replaceVariable(command);
}
return command;
}
/**
* Prepare environment needed for execution.
*
* @param repo
* the repository
* @param localFile
* the local file (ours)
* @param remoteFile
* the remote file (theirs)
* @param mergedFile
* the merged file (worktree)
* @param baseFile
* the base file (can be null)
* @return the environment map with variables and values (file paths)
* @throws IOException
*/
public static Map<String, String> prepareEnvironment(Repository repo,
FileElement localFile, FileElement remoteFile,
FileElement mergedFile, FileElement baseFile) throws IOException {
Map<String, String> env = new TreeMap<>();
env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath());
localFile.addToEnv(env);
remoteFile.addToEnv(env);
mergedFile.addToEnv(env);
if (baseFile != null) {
baseFile.addToEnv(env);
}
return env;
}
}

View File

@ -0,0 +1,261 @@
/*
* Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import org.eclipse.jgit.diff.DiffEntry;
/**
* The element used as left or right file for compare.
*
*/
public class FileElement {
/**
* The file element type.
*
*/
public enum Type {
/**
* The local file element (ours).
*/
LOCAL,
/**
* The remote file element (theirs).
*/
REMOTE,
/**
* The merged file element (path in worktree).
*/
MERGED,
/**
* The base file element (of ours and theirs).
*/
BASE,
/**
* The backup file element (copy of merged / conflicted).
*/
BACKUP
}
private final String path;
private final Type type;
private InputStream stream;
private File tempFile;
/**
* Creates file element for path.
*
* @param path
* the file path
* @param type
* the element type
*/
public FileElement(String path, Type type) {
this(path, type, null, null);
}
/**
* Creates file element for path.
*
* @param path
* the file path
* @param type
* the element type
* @param tempFile
* the temporary file to be used (can be null and will be created
* then)
* @param stream
* the object stream to load instead of file
*/
public FileElement(String path, Type type, File tempFile,
InputStream stream) {
this.path = path;
this.type = type;
this.tempFile = tempFile;
this.stream = stream;
}
/**
* @return the file path
*/
public String getPath() {
return path;
}
/**
* @return the element type
*/
public Type getType() {
return type;
}
/**
* Return a temporary file within passed directory and fills it with stream
* if valid.
*
* @param directory
* the directory where the temporary file is created
* @param midName
* name added in the middle of generated temporary file name
* @return the object stream
* @throws IOException
*/
public File getFile(File directory, String midName) throws IOException {
if ((tempFile != null) && (stream == null)) {
return tempFile;
}
tempFile = getTempFile(path, directory, midName);
return copyFromStream(tempFile, stream);
}
/**
* Return a real file from work tree or a temporary file with content if
* stream is valid or if path is "/dev/null"
*
* @return the object stream
* @throws IOException
*/
public File getFile() throws IOException {
if ((tempFile != null) && (stream == null)) {
return tempFile;
}
File file = new File(path);
// if we have a stream or file is missing ("/dev/null") then create
// temporary file
if ((stream != null) || isNullPath()) {
tempFile = getTempFile(file);
return copyFromStream(tempFile, stream);
}
return file;
}
/**
* Check if path id "/dev/null"
*
* @return true if path is "/dev/null"
*/
public boolean isNullPath() {
return path.equals(DiffEntry.DEV_NULL);
}
/**
* Create temporary file in given or system temporary directory
*
* @param directory
* the directory for the file (can be null); if null system
* temporary directory is used
* @return temporary file in directory or in the system temporary directory
* @throws IOException
*/
public File createTempFile(File directory) throws IOException {
if (tempFile == null) {
File file = new File(path);
if (directory != null) {
tempFile = getTempFile(file, directory, type.name());
} else {
tempFile = getTempFile(file);
}
}
return tempFile;
}
private static File getTempFile(File file) throws IOException {
return File.createTempFile(".__", "__" + file.getName()); //$NON-NLS-1$ //$NON-NLS-2$
}
private static File getTempFile(File file, File directory, String midName)
throws IOException {
String[] fileNameAndExtension = splitBaseFileNameAndExtension(file);
return File.createTempFile(
fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
fileNameAndExtension[1], directory);
}
private static File getTempFile(String path, File directory, String midName)
throws IOException {
return getTempFile(new File(path), directory, midName);
}
/**
* Delete and invalidate temporary file if necessary.
*/
public void cleanTemporaries() {
if (tempFile != null && tempFile.exists())
tempFile.delete();
tempFile = null;
}
private static File copyFromStream(File file, final InputStream stream)
throws IOException, FileNotFoundException {
if (stream != null) {
try (OutputStream outStream = new FileOutputStream(file)) {
int read = 0;
byte[] bytes = new byte[8 * 1024];
while ((read = stream.read(bytes)) != -1) {
outStream.write(bytes, 0, read);
}
} finally {
// stream can only be consumed once --> close it
stream.close();
}
}
return file;
}
private static String[] splitBaseFileNameAndExtension(File file) {
String[] result = new String[2];
result[0] = file.getName();
result[1] = ""; //$NON-NLS-1$
int idx = result[0].lastIndexOf("."); //$NON-NLS-1$
// if "." was found (>-1) and last-index is not first char (>0), then
// split (same behavior like cgit)
if (idx > 0) {
result[1] = result[0].substring(idx, result[0].length());
result[0] = result[0].substring(0, idx);
}
return result;
}
/**
* Replace variable in input
*
* @param input
* the input string
* @return the replaced input string
* @throws IOException
*/
public String replaceVariable(String input) throws IOException {
return input.replace("$" + type.name(), getFile().getPath()); //$NON-NLS-1$
}
/**
* Add variable to environment map.
*
* @param env
* the environment where this element should be added
* @throws IOException
*/
public void addToEnv(Map<String, String> env) throws IOException {
env.put(type.name(), getFile().getPath());
}
}

View File

@ -0,0 +1,147 @@
/*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_BACKUP;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_TEMPORARIES;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WRITE_TO_TEMP;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Config.SectionParser;
import org.eclipse.jgit.lib.internal.BooleanTriState;
/**
* Keeps track of difftool related configuration options.
*/
public class MergeToolConfig {
/** Key for {@link Config#get(SectionParser)}. */
public static final Config.SectionParser<MergeToolConfig> KEY = MergeToolConfig::new;
private final String toolName;
private final String guiToolName;
private final boolean prompt;
private final boolean keepBackup;
private final boolean keepTemporaries;
private final boolean writeToTemp;
private final Map<String, ExternalMergeTool> tools;
private MergeToolConfig(Config rc) {
toolName = rc.getString(CONFIG_MERGE_SECTION, null, CONFIG_KEY_TOOL);
guiToolName = rc.getString(CONFIG_MERGE_SECTION, null,
CONFIG_KEY_GUITOOL);
prompt = rc.getBoolean(CONFIG_MERGETOOL_SECTION, toolName,
CONFIG_KEY_PROMPT, true);
keepBackup = rc.getBoolean(CONFIG_MERGETOOL_SECTION,
CONFIG_KEY_KEEP_BACKUP, true);
keepTemporaries = rc.getBoolean(CONFIG_MERGETOOL_SECTION,
CONFIG_KEY_KEEP_TEMPORARIES, false);
writeToTemp = rc.getBoolean(CONFIG_MERGETOOL_SECTION,
CONFIG_KEY_WRITE_TO_TEMP, false);
tools = new HashMap<>();
Set<String> subsections = rc.getSubsections(CONFIG_MERGETOOL_SECTION);
for (String name : subsections) {
String cmd = rc.getString(CONFIG_MERGETOOL_SECTION, name,
CONFIG_KEY_CMD);
String path = rc.getString(CONFIG_MERGETOOL_SECTION, name,
CONFIG_KEY_PATH);
BooleanTriState trustExitCode = BooleanTriState.FALSE;
String trustStr = rc.getString(CONFIG_MERGETOOL_SECTION, name,
CONFIG_KEY_TRUST_EXIT_CODE);
if (trustStr != null) {
trustExitCode = Boolean.valueOf(trustStr).booleanValue()
? BooleanTriState.TRUE
: BooleanTriState.FALSE;
} else {
trustExitCode = BooleanTriState.UNSET;
}
if ((cmd != null) || (path != null)) {
tools.put(name, new UserDefinedMergeTool(name, path, cmd,
trustExitCode));
}
}
}
/**
* @return the default merge tool name (merge.tool)
*/
public String getDefaultToolName() {
return toolName;
}
/**
* @return the default GUI merge tool name (merge.guitool)
*/
public String getDefaultGuiToolName() {
return guiToolName;
}
/**
* @return the merge tool "prompt" option (mergetool.prompt)
*/
public boolean isPrompt() {
return prompt;
}
/**
* @return the tool "keep backup" option
*/
public boolean isKeepBackup() {
return keepBackup;
}
/**
* @return the tool "keepTemporaries" option
*/
public boolean isKeepTemporaries() {
return keepTemporaries;
}
/**
* @return the tool "write to temp" option
*/
public boolean isWriteToTemp() {
return writeToTemp;
}
/**
* @return the tools map
*/
public Map<String, ExternalMergeTool> getTools() {
return tools;
}
/**
* @return the tool names
*/
public Set<String> getToolNames() {
return tools.keySet();
}
}

View File

@ -0,0 +1,259 @@
/*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.util.FS.ExecutionResult;
/**
* Manages merge tools.
*/
public class MergeTools {
Repository repo;
private final MergeToolConfig config;
private final Map<String, ExternalMergeTool> predefinedTools;
private final Map<String, ExternalMergeTool> userDefinedTools;
/**
* @param repo
* the repository
*/
public MergeTools(Repository repo) {
this.repo = repo;
config = repo.getConfig().get(MergeToolConfig.KEY);
predefinedTools = setupPredefinedTools();
userDefinedTools = setupUserDefinedTools(config, predefinedTools);
}
/**
* @param localFile
* the local file element
* @param remoteFile
* the remote file element
* @param mergedFile
* the merged file element
* @param baseFile
* the base file element (can be null)
* @param tempDir
* the temporary directory (needed for backup and auto-remove,
* can be null)
* @param toolName
* the selected tool name (can be null)
* @param prompt
* the prompt option
* @param gui
* the GUI option
* @return the execution result from tool
* @throws ToolException
*/
public ExecutionResult merge(FileElement localFile, FileElement remoteFile,
FileElement mergedFile, FileElement baseFile, File tempDir,
String toolName, BooleanTriState prompt, BooleanTriState gui)
throws ToolException {
ExternalMergeTool tool = guessTool(toolName, gui);
FileElement backup = null;
ExecutionResult result = null;
try {
File workingDir = repo.getWorkTree();
// create additional backup file (copy worktree file)
backup = createBackupFile(mergedFile.getPath(),
tempDir != null ? tempDir : workingDir);
// prepare the command (replace the file paths)
boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
String command = ExternalToolUtils.prepareCommand(
tool.getCommand(baseFile != null), localFile, remoteFile,
mergedFile, baseFile);
// prepare the environment
Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo,
localFile, remoteFile, mergedFile, baseFile);
// execute the tool
CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
result = cmdExec.run(command, workingDir, env);
// keep backup as .orig file
if (backup != null) {
keepBackupFile(mergedFile.getPath(), backup);
}
return result;
} catch (IOException | InterruptedException e) {
throw new ToolException(e);
} finally {
// always delete backup file (ignore that it was may be already
// moved to keep-backup file)
if (backup != null) {
backup.cleanTemporaries();
}
// if the tool returns an error and keepTemporaries is set to true,
// then these temporary files will be preserved
if (!((result == null) && config.isKeepTemporaries())) {
// delete the files
localFile.cleanTemporaries();
remoteFile.cleanTemporaries();
if (baseFile != null) {
baseFile.cleanTemporaries();
}
// delete temporary directory if needed
if (config.isWriteToTemp() && (tempDir != null)
&& tempDir.exists()) {
tempDir.delete();
}
}
}
}
private FileElement createBackupFile(String filePath, File parentDir)
throws IOException {
FileElement backup = null;
Path path = Paths.get(filePath);
if (Files.exists(path)) {
backup = new FileElement(filePath, Type.BACKUP);
Files.copy(path, backup.createTempFile(parentDir).toPath(),
StandardCopyOption.REPLACE_EXISTING);
}
return backup;
}
/**
* @return the created temporary directory if (mergetol.writeToTemp == true)
* or null if not configured or false.
* @throws IOException
*/
public File createTempDirectory() throws IOException {
return config.isWriteToTemp()
? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$
: null;
}
/**
* @return the tool names
*/
public Set<String> getToolNames() {
return config.getToolNames();
}
/**
* @return the user defined tools
*/
public Map<String, ExternalMergeTool> getUserDefinedTools() {
return userDefinedTools;
}
/**
* @return the available predefined tools
*/
public Map<String, ExternalMergeTool> getAvailableTools() {
return predefinedTools;
}
/**
* @return the NOT available predefined tools
*/
public Map<String, ExternalMergeTool> getNotAvailableTools() {
return new TreeMap<>();
}
/**
* @param gui
* use the diff.guitool setting ?
* @return the default tool name
*/
public String getDefaultToolName(BooleanTriState gui) {
return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$
: config.getDefaultToolName();
}
/**
* @return is interactive (config prompt enabled) ?
*/
public boolean isInteractive() {
return config.isPrompt();
}
private ExternalMergeTool guessTool(String toolName, BooleanTriState gui)
throws ToolException {
if ((toolName == null) || toolName.isEmpty()) {
toolName = getDefaultToolName(gui);
}
ExternalMergeTool tool = getTool(toolName);
if (tool == null) {
throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$
}
return tool;
}
private ExternalMergeTool getTool(final String name) {
ExternalMergeTool tool = userDefinedTools.get(name);
if (tool == null) {
tool = predefinedTools.get(name);
}
return tool;
}
private void keepBackupFile(String mergedFilePath, FileElement backup)
throws IOException {
if (config.isKeepBackup()) {
Path backupPath = backup.getFile().toPath();
Files.move(backupPath,
backupPath.resolveSibling(
Paths.get(mergedFilePath).getFileName() + ".orig"), //$NON-NLS-1$
StandardCopyOption.REPLACE_EXISTING);
}
}
private Map<String, ExternalMergeTool> setupPredefinedTools() {
Map<String, ExternalMergeTool> tools = new TreeMap<>();
for (CommandLineMergeTool tool : CommandLineMergeTool.values()) {
tools.put(tool.name(), new PreDefinedMergeTool(tool));
}
return tools;
}
private Map<String, ExternalMergeTool> setupUserDefinedTools(
MergeToolConfig cfg, Map<String, ExternalMergeTool> predefTools) {
Map<String, ExternalMergeTool> tools = new TreeMap<>();
Map<String, ExternalMergeTool> userTools = cfg.getTools();
for (String name : userTools.keySet()) {
ExternalMergeTool userTool = userTools.get(name);
// if mergetool.<name>.cmd is defined we have user defined tool
if (userTool.getCommand() != null) {
tools.put(name, userTool);
} else if (userTool.getPath() != null) {
// if mergetool.<name>.path is defined we just overload the path
// of predefined tool
PreDefinedMergeTool predefTool = (PreDefinedMergeTool) predefTools
.get(name);
if (predefTool != null) {
predefTool.setPath(userTool.getPath());
if (userTool.getTrustExitCode() != BooleanTriState.UNSET) {
predefTool
.setTrustExitCode(userTool.getTrustExitCode());
}
}
}
}
return tools;
}
}

View File

@ -46,17 +46,6 @@ public PreDefinedDiffTool(CommandLineDiffTool tool) {
*/
@Override
public void setPath(String path) {
// handling of spaces in path
if (path.contains(" ")) { //$NON-NLS-1$
// add quotes before if needed
if (!path.startsWith("\"")) { //$NON-NLS-1$
path = "\"" + path; //$NON-NLS-1$
}
// add quotes after if needed
if (!path.endsWith("\"")) { //$NON-NLS-1$
path = path + "\""; //$NON-NLS-1$
}
}
super.setPath(path);
}

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

@ -0,0 +1,133 @@
/*
* Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tool exception for differentiation.
*
*/
public class ToolException extends Exception {
private final static Logger LOG = LoggerFactory
.getLogger(ToolException.class);
private final ExecutionResult result;
private final boolean commandExecutionError;
/**
* the serial version UID
*/
private static final long serialVersionUID = 1L;
/**
*
*/
public ToolException() {
this(null, null, false);
}
/**
* @param message
* the exception message
*/
public ToolException(String message) {
this(message, null, false);
}
/**
* @param message
* the exception message
* @param result
* the execution result
* @param commandExecutionError
* is command execution error happened ?
*/
public ToolException(String message, ExecutionResult result,
boolean commandExecutionError) {
super(message);
this.result = result;
this.commandExecutionError = commandExecutionError;
}
/**
* @param message
* the exception message
* @param cause
* the cause for throw
*/
public ToolException(String message, Throwable cause) {
super(message, cause);
result = null;
commandExecutionError = false;
}
/**
* @param cause
* the cause for throw
*/
public ToolException(Throwable cause) {
super(cause);
result = null;
commandExecutionError = false;
}
/**
* @return true if result is valid, false else
*/
public boolean isResult() {
return result != null;
}
/**
* @return the execution result
*/
public ExecutionResult getResult() {
return result;
}
/**
* @return true if command execution error appears, false otherwise
*/
public boolean isCommandExecutionError() {
return commandExecutionError;
}
/**
* @return the result Stderr
*/
public String getResultStderr() {
try {
return new String(result.getStderr().toByteArray());
} catch (Exception e) {
LOG.warn("Failed to retrieve standard error output", e); //$NON-NLS-1$
}
return ""; //$NON-NLS-1$
}
/**
* @return the result Stdout
*/
public String getResultStdout() {
try {
return new String(result.getStdout().toByteArray());
} catch (Exception e) {
LOG.warn("Failed to retrieve standard output", e); //$NON-NLS-1$
}
return ""; //$NON-NLS-1$
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.diffmergetool;
import org.eclipse.jgit.lib.internal.BooleanTriState;
/**
* The user-defined merge tool.
*/
public class UserDefinedMergeTool extends UserDefinedDiffTool
implements ExternalMergeTool {
/**
* the merge tool "trust exit code" option
*/
private BooleanTriState trustExitCode;
/**
* Creates the merge tool
*
* @param name
* the name
* @param path
* the path
* @param cmd
* the command
* @param trustExitCode
* the "trust exit code" option
*/
public UserDefinedMergeTool(String name, String path, String cmd,
BooleanTriState trustExitCode) {
super(name, path, cmd);
this.trustExitCode = trustExitCode;
}
/**
* @return the "trust exit code" flag
*/
@Override
public BooleanTriState getTrustExitCode() {
return trustExitCode;
}
/**
* @param trustExitCode
* the new "trust exit code" flag
*/
protected void setTrustExitCode(BooleanTriState trustExitCode) {
this.trustExitCode = trustExitCode;
}
/**
* @param withBase
* not used, because user-defined merge tool can only define one
* cmd -> it must handle with and without base present (empty)
* @return the tool command
*/
@Override
public String getCommand(boolean withBase) {
return getCommand();
}
}

View File

@ -10,6 +10,7 @@
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.lib;
/**
@ -31,14 +32,14 @@ public final class ConfigConstants {
public static final String CONFIG_DIFF_SECTION = "diff";
/**
* The "tool" key within "diff" section
* The "tool" key within "diff" or "merge" section
*
* @since 6.1
*/
public static final String CONFIG_KEY_TOOL = "tool";
/**
* The "guitool" key within "diff" section
* The "guitool" key within "diff" or "merge" section
*
* @since 6.1
*/
@ -52,21 +53,21 @@ public final class ConfigConstants {
public static final String CONFIG_DIFFTOOL_SECTION = "difftool";
/**
* The "prompt" key within "difftool" section
* The "prompt" key within "difftool" or "mergetool" section
*
* @since 6.1
*/
public static final String CONFIG_KEY_PROMPT = "prompt";
/**
* The "trustExitCode" key within "difftool" section
* The "trustExitCode" key within "difftool" or "mergetool.<name>." section
*
* @since 6.1
*/
public static final String CONFIG_KEY_TRUST_EXIT_CODE = "trustExitCode";
/**
* The "cmd" key within "difftool.*." section
* The "cmd" key within "difftool.*." or "mergetool.*." section
*
* @since 6.1
*/
@ -123,6 +124,34 @@ public final class ConfigConstants {
*/
public static final String CONFIG_MERGE_SECTION = "merge";
/**
* The "mergetool" section
*
* @since 6.2
*/
public static final String CONFIG_MERGETOOL_SECTION = "mergetool";
/**
* The "keepBackup" key within "mergetool" section
*
* @since 6.2
*/
public static final String CONFIG_KEY_KEEP_BACKUP = "keepBackup";
/**
* The "keepTemporaries" key within "mergetool" section
*
* @since 6.2
*/
public static final String CONFIG_KEY_KEEP_TEMPORARIES = "keepTemporaries";
/**
* The "writeToTemp" key within "mergetool" section
*
* @since 6.2
*/
public static final String CONFIG_KEY_WRITE_TO_TEMP = "writeToTemp";
/**
* The "filter" section
* @since 4.6