Add mergetool merge feature (execute external tool)

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

* implement mergetool merge function (execute external tool)
* add ExecutionResult and commandExecutionError to ToolException
* handle "base not present" case (empty or null base file path)
* handle deleted (rm) and modified (add) conflicts
* handle settings
 * keepBackup
 * keepTemporaries
 * writeToTemp

Bug: 356832
Change-Id: Id323c2fcb1c24d12ceb299801df8bac51a6d463f
Signed-off-by: Andre Bossert <andre.bossert@siemens.com>
This commit is contained in:
Andre Bossert 2019-03-08 22:31:34 +01:00 committed by Andrey Loskutov
parent 8573435635
commit eaf4d500b8
11 changed files with 853 additions and 191 deletions

View File

@ -16,6 +16,7 @@
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -30,7 +31,7 @@
/** /**
* Testing the {@code difftool} command. * Testing the {@code difftool} command.
*/ */
public class DiffToolTest extends ExternalToolTestCase { public class DiffToolTest extends ToolTestCase {
private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION; private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION;
@ -41,6 +42,46 @@ public void setUp() throws Exception {
configureEchoTool(TOOL_NAME); configureEchoTool(TOOL_NAME);
} }
@Test
public void testToolWithPrompt() throws Exception {
String[] inputLines = {
"y", // accept launching diff tool
"y", // accept launching diff tool
};
RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedCompareOutput(changes);
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
};
RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit);
int abortIndex = 1;
String[] expectedOutput = getExpectedAbortOutput(changes, 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) @Test(expected = Die.class)
public void testNotDefinedTool() throws Exception { public void testNotDefinedTool() throws Exception {
createUnstagedChanges(); createUnstagedChanges();
@ -53,7 +94,7 @@ public void testNotDefinedTool() throws Exception {
public void testTool() throws Exception { public void testTool() throws Exception {
RevCommit commit = createUnstagedChanges(); RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit); List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedToolOutput(changes); String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
String[] options = { String[] options = {
"--tool", "--tool",
@ -72,7 +113,7 @@ public void testTool() throws Exception {
public void testToolTrustExitCode() throws Exception { public void testToolTrustExitCode() throws Exception {
RevCommit commit = createUnstagedChanges(); RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit); List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedToolOutput(changes); String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
String[] options = { "--tool", "-t", }; String[] options = { "--tool", "-t", };
@ -87,7 +128,7 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception { public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
RevCommit commit = createUnstagedChanges(); RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit); List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedToolOutput(changes); String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
String[] options = { "--tool", "-t", }; String[] options = { "--tool", "-t", };
@ -103,7 +144,7 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
public void testToolCached() throws Exception { public void testToolCached() throws Exception {
RevCommit commit = createStagedChanges(); RevCommit commit = createStagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit); List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedToolOutput(changes); String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
String[] options = { "--cached", "--staged", }; String[] options = { "--cached", "--staged", };
@ -118,7 +159,8 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
public void testToolHelp() throws Exception { public void testToolHelp() throws Exception {
CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
List<String> expectedOutput = new ArrayList<>(); 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) { for (CommandLineDiffTool defaultTool : defaultTools) {
String toolName = defaultTool.name(); String toolName = defaultTool.name();
expectedOutput.add(toolName); expectedOutput.add(toolName);
@ -159,7 +201,7 @@ private void configureEchoTool(String toolName) {
String.valueOf(false)); String.valueOf(false));
} }
private String[] getExpectedToolOutput(List<DiffEntry> changes) { private static String[] getExpectedToolOutputNoPrompt(List<DiffEntry> changes) {
String[] expectedToolOutput = new String[changes.size()]; String[] expectedToolOutput = new String[changes.size()];
for (int i = 0; i < changes.size(); ++i) { for (int i = 0; i < changes.size(); ++i) {
DiffEntry change = changes.get(i); DiffEntry change = changes.get(i);
@ -169,4 +211,36 @@ private String[] getExpectedToolOutput(List<DiffEntry> changes) {
} }
return expectedToolOutput; return expectedToolOutput;
} }
private static String[] getExpectedCompareOutput(List<DiffEntry> changes) {
List<String> expected = new ArrayList<>();
int n = changes.size();
for (int i = 0; i < n; ++i) {
DiffEntry change = changes.get(i);
String newPath = change.getNewPath();
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(List<DiffEntry> changes,
int abortIndex) {
List<String> expected = new ArrayList<>();
int n = changes.size();
for (int i = 0; i < n; ++i) {
DiffEntry change = changes.get(i);
String newPath = change.getNewPath();
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

@ -15,6 +15,7 @@
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION; 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_MERGE_SECTION;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -27,7 +28,7 @@
/** /**
* Testing the {@code mergetool} command. * Testing the {@code mergetool} command.
*/ */
public class MergeToolTest extends ExternalToolTestCase { public class MergeToolTest extends ToolTestCase {
private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION; private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION;
@ -39,37 +40,121 @@ public void setUp() throws Exception {
} }
@Test @Test
public void testTool() throws Exception { public void testAbortMerge() throws Exception {
createMergeConflict(); String[] inputLines = {
String[] expectedOutput = getExpectedToolOutput(); "y", // start tool for merge resolution
"n", // don't accept merge tool result
String[] options = { "n", // don't continue resolution
"--tool",
"-t",
}; };
String[] conflictingFilenames = createMergeConflict();
int abortIndex = 1;
String[] expectedOutput = getExpectedAbortMergeOutput(
conflictingFilenames,
abortIndex);
for (String option : options) { String option = "--tool";
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, InputStream inputStream = createInputStream(inputLines);
runAndCaptureUsingInitRaw(MERGE_TOOL, option, assertArrayOfLinesEquals("Incorrect output for option: " + option,
TOOL_NAME)); expectedOutput, runAndCaptureUsingInitRaw(inputStream,
} MERGE_TOOL, "--prompt", option, TOOL_NAME));
} }
@Test @Test
public void testToolNoGuiNoPrompt() throws Exception { public void testAbortLaunch() throws Exception {
createMergeConflict(); String[] inputLines = {
String[] expectedOutput = getExpectedToolOutput(); "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", }; String[] options = { "--tool", "-t", };
for (String option : options) { for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option, assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL, expectedOutput,
"--no-gui", "--no-prompt", option, TOOL_NAME)); 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 @Test
public void testToolHelp() throws Exception { public void testToolHelp() throws Exception {
CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
@ -87,8 +172,7 @@ public void testToolHelp() throws Exception {
String[] userDefinedToolsHelp = { String[] userDefinedToolsHelp = {
"The following tools are valid, but not currently available:", "The following tools are valid, but not currently available:",
"Some of the tools listed above only work in a windowed", "Some of the tools listed above only work in a windowed",
"environment. If run in a terminal-only session, they will fail.", "environment. If run in a terminal-only session, they will fail.", };
};
expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp)); expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));
String option = "--tool-help"; String option = "--tool-help";
@ -116,21 +200,111 @@ private void configureEchoTool(String toolName) {
String.valueOf(false)); String.valueOf(false));
} }
private String[] getExpectedToolOutput() { private static String[] getExpectedMergeConflictOutputNoPrompt(
String[] mergeConflictFilenames = { "a", "b", }; String[] conflictFilenames) {
List<String> expectedOutput = new ArrayList<>(); List<String> expected = new ArrayList<>();
expectedOutput.add("Merging:"); expected.add("Merging:");
for (String mergeConflictFilename : mergeConflictFilenames) { for (String conflictFilename : conflictFilenames) {
expectedOutput.add(mergeConflictFilename); expected.add(conflictFilename);
} }
for (String mergeConflictFilename : mergeConflictFilenames) { for (String conflictFilename : conflictFilenames) {
expectedOutput.add("Normal merge conflict for '" expected.add("Normal merge conflict for '" + conflictFilename
+ mergeConflictFilename + "':"); + "':");
expectedOutput.add("{local}: modified file"); expected.add("{local}: modified file");
expectedOutput.add("{remote}: modified file"); expected.add("{remote}: modified file");
expectedOutput.add("TODO: Launch mergetool '" + TOOL_NAME expected.add(conflictFilename);
+ "' for path '" + mergeConflictFilename + "'..."); expected.add(conflictFilename + " seems unchanged.");
} }
return expectedOutput.toArray(new String[0]); 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

@ -11,10 +11,14 @@
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jgit.api.CherryPickResult;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.CLIRepositoryTestCase; import org.eclipse.jgit.lib.CLIRepositoryTestCase;
@ -29,7 +33,7 @@
/** /**
* Base test case for the {@code difftool} and {@code mergetool} commands. * Base test case for the {@code difftool} and {@code mergetool} commands.
*/ */
public abstract class ExternalToolTestCase extends CLIRepositoryTestCase { public abstract class ToolTestCase extends CLIRepositoryTestCase {
public static class GitCliJGitWrapperParser { public static class GitCliJGitWrapperParser {
@Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class) @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
@ -56,6 +60,12 @@ public void setUp() throws Exception {
protected String[] runAndCaptureUsingInitRaw(String... args) protected String[] runAndCaptureUsingInitRaw(String... args)
throws Exception { 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(); CLIGitCommand.Result result = new CLIGitCommand.Result();
GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
@ -63,7 +73,7 @@ protected String[] runAndCaptureUsingInitRaw(String... args)
clp.parseArgument(args); clp.parseArgument(args);
TextBuiltin cmd = bean.subcommand; TextBuiltin cmd = bean.subcommand;
cmd.initRaw(db, null, null, result.out, result.err); cmd.initRaw(db, null, inputStream, result.out, result.err);
cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()])); cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()]));
if (cmd.getOutputWriter() != null) { if (cmd.getOutputWriter() != null) {
cmd.getOutputWriter().flush(); cmd.getOutputWriter().flush();
@ -71,28 +81,73 @@ protected String[] runAndCaptureUsingInitRaw(String... args)
if (cmd.getErrorWriter() != null) { if (cmd.getErrorWriter() != null) {
cmd.getErrorWriter().flush(); 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]); return result.outLines().toArray(new String[0]);
} }
protected CherryPickResult createMergeConflict() throws Exception { protected String[] createMergeConflict() throws Exception {
// create files on initial branch
git.checkout().setName(TEST_BRANCH_NAME).call();
writeTrashFile("a", "Hello world a"); writeTrashFile("a", "Hello world a");
writeTrashFile("b", "Hello world b"); writeTrashFile("b", "Hello world b");
git.add().addFilepattern(".").call(); git.add().addFilepattern(".").call();
git.commit().setMessage("files a & b added").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("a", "Hello world a 1"); writeTrashFile("a", "Hello world a 1");
writeTrashFile("b", "Hello world b 1"); writeTrashFile("b", "Hello world b 1");
git.add().addFilepattern(".").call(); git.add().addFilepattern(".").call();
RevCommit commit1 = git.commit().setMessage("files a & b commit 1") RevCommit commit1 = git.commit()
.call(); .setMessage("files a & b modified commit 1").call();
git.branchCreate().setName("branch_1").call(); // checkout initial branch
git.checkout().setName(TEST_BRANCH_NAME).call(); 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("a", "Hello world a 2"); writeTrashFile("a", "Hello world a 2");
writeTrashFile("b", "Hello world b 2"); writeTrashFile("b", "Hello world b 2");
git.add().addFilepattern(".").call(); git.add().addFilepattern(".").call();
git.commit().setMessage("files a & b commit 2").call(); git.commit().setMessage("files a & b modified commit 2").call();
// cherry-pick conflicting changes
git.cherryPick().include(commit1).call();
String[] conflictingFilenames = { "a", "b" };
return conflictingFilenames;
}
protected String[] createDeletedConflict() throws Exception {
// create files on initial branch
git.checkout().setName(TEST_BRANCH_NAME).call();
writeTrashFile("a", "Hello world a");
writeTrashFile("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("a", "Hello world a 1");
writeTrashFile("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.branchCreate().setName("branch_2").call();
CherryPickResult result = git.cherryPick().include(commit1).call(); git.checkout().setName("branch_2").call();
return result; git.rm().addFilepattern("a").call();
git.rm().addFilepattern("b").call();
git.commit().setMessage("files a & b deleted commit 2").call();
// cherry-pick conflicting changes
git.cherryPick().include(commit1).call();
String[] conflictingFilenames = { "a", "b" };
return conflictingFilenames;
} }
protected RevCommit createUnstagedChanges() throws Exception { protected RevCommit createUnstagedChanges() throws Exception {
@ -121,6 +176,16 @@ protected List<DiffEntry> getRepositoryChanges(RevCommit commit)
return changes; 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, protected static void assertArrayOfLinesEquals(String failMessage,
String[] expected, String[] actual) { String[] expected, String[] actual) {
assertEquals(failMessage, toString(expected), toString(actual)); assertEquals(failMessage, toString(expected), toString(actual));

View File

@ -58,8 +58,8 @@ couldNotCreateBranch=Could not create branch {0}: {1}
dateInfo=Date: {0} dateInfo=Date: {0}
deletedBranch=Deleted branch {0} deletedBranch=Deleted branch {0}
deletedRemoteBranch=Deleted remote 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. 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]? diffToolLaunch=Viewing ({0}/{1}): ''{2}''\nLaunch ''{3}'' [Y/n]?
diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1} diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1}
doesNotExist={0} does not exist doesNotExist={0} does not exist
dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge: dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge:
@ -91,6 +91,22 @@ listeningOn=Listening on {0}
logNoSignatureVerifier="No signature verifier available" logNoSignatureVerifier="No signature verifier available"
mergeConflict=CONFLICT(content): Merge conflict in {0} mergeConflict=CONFLICT(content): Merge conflict in {0}
mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge: 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 mergeFailed=Automatic merge failed; fix conflicts and then commit the result
mergeCheckoutFailed=Please, commit your changes or stash them before you can merge. mergeCheckoutFailed=Please, commit your changes or stash them before you can merge.
mergeMadeBy=Merge made by the ''{0}'' strategy. mergeMadeBy=Merge made by the ''{0}'' strategy.

View File

@ -113,11 +113,14 @@ void noTrustExitCode(@SuppressWarnings("unused") boolean on) {
@Option(name = "--", metaVar = "metaVar_paths", handler = PathTreeFilterHandler.class) @Option(name = "--", metaVar = "metaVar_paths", handler = PathTreeFilterHandler.class)
private TreeFilter pathFilter = TreeFilter.ALL; private TreeFilter pathFilter = TreeFilter.ALL;
private BufferedReader inputReader;
@Override @Override
protected void init(Repository repository, String gitDir) { protected void init(Repository repository, String gitDir) {
super.init(repository, gitDir); super.init(repository, gitDir);
diffFmt = new DiffFormatter(new BufferedOutputStream(outs)); diffFmt = new DiffFormatter(new BufferedOutputStream(outs));
diffTools = new DiffTools(repository); diffTools = new DiffTools(repository);
inputReader = new BufferedReader(new InputStreamReader(ins, StandardCharsets.UTF_8));
} }
@Override @Override
@ -208,10 +211,9 @@ private boolean isLaunchCompare(int fileIndex, int fileCount,
String fileName, String toolNamePrompt) throws IOException { String fileName, String toolNamePrompt) throws IOException {
boolean launchCompare = true; boolean launchCompare = true;
outw.println(MessageFormat.format(CLIText.get().diffToolLaunch, outw.println(MessageFormat.format(CLIText.get().diffToolLaunch,
fileIndex, fileCount, fileName, toolNamePrompt)); fileIndex, fileCount, fileName, toolNamePrompt) + " "); //$NON-NLS-1$
outw.flush(); outw.flush();
BufferedReader br = new BufferedReader( BufferedReader br = inputReader;
new InputStreamReader(ins, StandardCharsets.UTF_8));
String line = null; String line = null;
if ((line = br.readLine()) != null) { if ((line = br.readLine()) != null) {
if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$ if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$
@ -224,17 +226,18 @@ private boolean isLaunchCompare(int fileIndex, int fileCount,
private void showToolHelp() throws IOException { private void showToolHelp() throws IOException {
StringBuilder availableToolNames = new StringBuilder(); StringBuilder availableToolNames = new StringBuilder();
for (String name : diffTools.getAvailableTools().keySet()) { 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(); StringBuilder notAvailableToolNames = new StringBuilder();
for (String name : diffTools.getNotAvailableTools().keySet()) { 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(); StringBuilder userToolNames = new StringBuilder();
Map<String, ExternalDiffTool> userTools = diffTools Map<String, ExternalDiffTool> userTools = diffTools
.getUserDefinedTools(); .getUserDefinedTools();
for (String name : userTools.keySet()) { 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())); name, userTools.get(name).getCommand()));
} }
outw.println(MessageFormat.format( outw.println(MessageFormat.format(

View File

@ -11,26 +11,35 @@
package org.eclipse.jgit.pgm; package org.eclipse.jgit.pgm;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.TreeMap; import java.util.TreeMap;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status; import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.StatusCommand; import org.eclipse.jgit.api.StatusCommand;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool; import org.eclipse.jgit.diff.ContentSource;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException; 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.MergeTools;
import org.eclipse.jgit.internal.diffmergetool.ToolException;
import org.eclipse.jgit.lib.IndexDiff.StageState; import org.eclipse.jgit.lib.IndexDiff.StageState;
import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option; import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.spi.RestOfArgumentsHandler; import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
@ -43,16 +52,16 @@ class MergeTool extends TextBuiltin {
"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge") "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge")
private String toolName; private String toolName;
private Optional<Boolean> prompt = Optional.empty(); private BooleanTriState prompt = BooleanTriState.UNSET;
@Option(name = "--prompt", usage = "usage_prompt") @Option(name = "--prompt", usage = "usage_prompt")
void setPrompt(@SuppressWarnings("unused") boolean on) { void setPrompt(@SuppressWarnings("unused") boolean on) {
prompt = Optional.of(Boolean.TRUE); prompt = BooleanTriState.TRUE;
} }
@Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt") @Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt")
void noPrompt(@SuppressWarnings("unused") boolean on) { void noPrompt(@SuppressWarnings("unused") boolean on) {
prompt = Optional.of(Boolean.FALSE); prompt = BooleanTriState.FALSE;
} }
@Option(name = "--tool-help", usage = "usage_toolHelp") @Option(name = "--tool-help", usage = "usage_toolHelp")
@ -74,10 +83,17 @@ void noGui(@SuppressWarnings("unused") boolean on) {
@Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class) @Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class)
protected List<String> filterPaths; protected List<String> filterPaths;
private BufferedReader inputReader;
@Override @Override
protected void init(Repository repository, String gitDir) { protected void init(Repository repository, String gitDir) {
super.init(repository, gitDir); super.init(repository, gitDir);
mergeTools = new MergeTools(repository); mergeTools = new MergeTools(repository);
inputReader = new BufferedReader(new InputStreamReader(ins));
}
enum MergeResult {
SUCCESSFUL, FAILED, ABORTED
} }
@Override @Override
@ -88,8 +104,8 @@ protected void run() {
} else { } else {
// get prompt // get prompt
boolean showPrompt = mergeTools.isInteractive(); boolean showPrompt = mergeTools.isInteractive();
if (prompt.isPresent()) { if (prompt != BooleanTriState.UNSET) {
showPrompt = prompt.get().booleanValue(); showPrompt = prompt == BooleanTriState.TRUE;
} }
// get passed or default tool name // get passed or default tool name
String toolNameSelected = toolName; String toolNameSelected = toolName;
@ -101,7 +117,7 @@ protected void run() {
if (files.size() > 0) { if (files.size() > 0) {
merge(files, showPrompt, toolNameSelected); merge(files, showPrompt, toolNameSelected);
} else { } else {
outw.println("No files need merging"); //$NON-NLS-1$ outw.println(CLIText.get().mergeToolNoFiles);
} }
} }
outw.flush(); outw.flush();
@ -113,88 +129,273 @@ protected void run() {
private void merge(Map<String, StageState> files, boolean showPrompt, private void merge(Map<String, StageState> files, boolean showPrompt,
String toolNamePrompt) throws Exception { String toolNamePrompt) throws Exception {
// sort file names // sort file names
List<String> fileNames = new ArrayList<>(files.keySet()); List<String> mergedFilePaths = new ArrayList<>(files.keySet());
Collections.sort(fileNames); Collections.sort(mergedFilePaths);
// show the files // show the files
outw.println("Merging:"); //$NON-NLS-1$ StringBuilder mergedFiles = new StringBuilder();
for (String fileName : fileNames) { for (String mergedFilePath : mergedFilePaths) {
outw.println(fileName); mergedFiles.append(MessageFormat.format("{0}\n", mergedFilePath)); //$NON-NLS-1$
} }
outw.println(MessageFormat.format(CLIText.get().mergeToolMerging,
mergedFiles));
outw.flush(); outw.flush();
for (String fileName : fileNames) { // merge the files
StageState fileState = files.get(fileName); MergeResult mergeResult = MergeResult.SUCCESSFUL;
// only both-modified is valid for mergetool for (String mergedFilePath : mergedFilePaths) {
if (fileState == StageState.BOTH_MODIFIED) { // if last merge failed...
outw.println("\nNormal merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ if (mergeResult == MergeResult.FAILED) {
outw.println(" {local}: modified file"); //$NON-NLS-1$ // check if user wants to continue
outw.println(" {remote}: modified file"); //$NON-NLS-1$ if (showPrompt && !isContinueUnresolvedPaths()) {
// check if user wants to launch merge resolution tool mergeResult = MergeResult.ABORTED;
boolean launch = true;
if (showPrompt) {
launch = isLaunch(toolNamePrompt);
} }
if (launch) { }
outw.println("TODO: Launch mergetool '" + toolNamePrompt //$NON-NLS-1$ // aborted ?
+ "' for path '" + fileName + "'..."); //$NON-NLS-1$ //$NON-NLS-2$ if (mergeResult == MergeResult.ABORTED) {
} else {
break;
}
} else if ((fileState == StageState.DELETED_BY_US) || (fileState == StageState.DELETED_BY_THEM)) {
outw.println("\nDeleted merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$
} else {
outw.println(
"\nUnknown merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$
break; 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 boolean isLaunch(String toolNamePrompt) private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
throws IOException { String toolNamePrompt) throws Exception {
boolean launch = true; outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict,
outw.println("Hit return to start merge resolution tool (" //$NON-NLS-1$ mergedFilePath));
+ toolNamePrompt + "): "); //$NON-NLS-1$
outw.flush(); outw.flush();
BufferedReader br = new BufferedReader(new InputStreamReader(ins)); // 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());
try {
FileElement base = null;
FileElement local = null;
FileElement remote = null;
DirCache cache = db.readDirCache();
int firstIndex = cache.findEntry(mergedFilePath);
if (firstIndex >= 0) {
int nextIndex = cache.nextEntry(firstIndex);
for (; firstIndex < nextIndex; firstIndex++) {
DirCacheEntry entry = cache.getEntry(firstIndex);
ObjectId id = entry.getObjectId();
switch (entry.getStage()) {
case DirCacheEntry.STAGE_1:
base = new FileElement(mergedFilePath, id.name(),
baseSource.open(mergedFilePath, id)
.openStream());
break;
case DirCacheEntry.STAGE_2:
local = new FileElement(mergedFilePath, id.name(),
localSource.open(mergedFilePath, id)
.openStream());
break;
case DirCacheEntry.STAGE_3:
remote = new FileElement(mergedFilePath, id.name(),
remoteSource.open(mergedFilePath, id)
.openStream());
break;
}
}
}
if ((local == null) || (remote == null)) {
throw die(MessageFormat.format(CLIText.get().mergeToolDied,
mergedFilePath));
}
File merged = new File(mergedFilePath);
long modifiedBefore = merged.lastModified();
try {
// TODO: check how to return the exit-code of the
// tool to jgit / java runtime ?
// int rc =...
ExecutionResult executionResult = mergeTools.merge(db, local,
remote, base, mergedFilePath, 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.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; String line = null;
if ((line = br.readLine()) != null) { if ((line = br.readLine()) != null) {
if (!line.equalsIgnoreCase("Y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$ if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$
launch = false; launch = false;
} }
} }
return launch; return launch;
} }
private void showToolHelp() throws IOException { private int getDeletedMergeDecision() throws IOException {
outw.println( int ret = 0; // abort
"'git mergetool --tool=<tool>' may be set to one of the following:"); //$NON-NLS-1$ final String message = CLIText.get().mergeToolDeletedMergeDecision
for (String name : mergeTools.getAvailableTools().keySet()) { + " "; //$NON-NLS-1$
outw.println("\t\t" + name); //$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();
} }
outw.println(""); //$NON-NLS-1$ return ret;
outw.println("\tuser-defined:"); //$NON-NLS-1$ }
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 Map<String, ExternalMergeTool> userTools = mergeTools
.getUserDefinedTools(); .getUserDefinedTools();
for (String name : userTools.keySet()) { for (String name : userTools.keySet()) {
outw.println("\t\t" + name + ".cmd " //$NON-NLS-1$ //$NON-NLS-2$ userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$
+ userTools.get(name).getCommand()); name, userTools.get(name).getCommand()));
} }
outw.println(""); //$NON-NLS-1$ outw.println(MessageFormat.format(
outw.println( CLIText.get().mergeToolHelpSetToFollowing, availableToolNames,
"The following tools are valid, but not currently available:"); //$NON-NLS-1$ userToolNames, notAvailableToolNames));
for (String name : mergeTools.getNotAvailableTools().keySet()) {
outw.println("\t\t" + name); //$NON-NLS-1$
}
outw.println(""); //$NON-NLS-1$
outw.println("Some of the tools listed above only work in a windowed"); //$NON-NLS-1$
outw.println(
"environment. If run in a terminal-only session, they will fail."); //$NON-NLS-1$
return;
} }
private Map<String, StageState> getFiles() private Map<String, StageState> getFiles() throws RevisionSyntaxException,
throws RevisionSyntaxException, NoWorkTreeException, NoWorkTreeException, GitAPIException {
GitAPIException {
Map<String, StageState> files = new TreeMap<>(); Map<String, StageState> files = new TreeMap<>();
try (Git git = new Git(db)) { try (Git git = new Git(db)) {
StatusCommand statusCommand = git.status(); StatusCommand statusCommand = git.status();

View File

@ -169,6 +169,22 @@ public static String fatalError(String message) {
/***/ public String logNoSignatureVerifier; /***/ public String logNoSignatureVerifier;
/***/ public String mergeCheckoutConflict; /***/ public String mergeCheckoutConflict;
/***/ public String mergeConflict; /***/ 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 mergeFailed;
/***/ public String mergeCheckoutFailed; /***/ public String mergeCheckoutFailed;
/***/ public String mergeMadeBy; /***/ public String mergeMadeBy;

View File

@ -72,10 +72,18 @@ public ExecutionResult run(String command, File workingDir,
} }
ExecutionResult result = fs.execute(pb, null); ExecutionResult result = fs.execute(pb, null);
int rc = result.getRc(); int rc = result.getRc();
if ((rc != 0) && (checkExitCode if (rc != 0) {
|| isCommandExecutionError(rc))) { boolean execError = isCommandExecutionError(rc);
throw new ToolException( if (checkExitCode || execError) {
new String(result.getStderr().toByteArray()), result); 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; return result;
} finally { } finally {

View File

@ -11,6 +11,7 @@
package org.eclipse.jgit.internal.diffmergetool; package org.eclipse.jgit.internal.diffmergetool;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@ -80,35 +81,27 @@ public void setStream(ObjectStream stream) {
} }
/** /**
* @param workingDir the working directory used if file cannot be found (e.g. /dev/null) * Returns a temporary file with in passed working directory and fills it
* with stream if valid.
*
* @param directory
* the working directory where the temporary file is created
* @param midName
* name added in the middle of generated temporary file name
* @return the object stream * @return the object stream
* @throws IOException * @throws IOException
*/ */
public File getFile(File workingDir) throws IOException { public File getFile(File directory, String midName) throws IOException {
if (tempFile != null) { if (tempFile != null) {
return tempFile; return tempFile;
} }
File file = new File(path); String[] fileNameAndExtension = splitBaseFileNameAndExtension(
String name = file.getName(); new File(path));
if (path.equals(DiffEntry.DEV_NULL)) { tempFile = File.createTempFile(
file = new File(workingDir, "nul"); //$NON-NLS-1$ fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
} fileNameAndExtension[1], directory);
else if (stream != null) { copyFromStream();
tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$ return tempFile;
try (OutputStream outStream = new FileOutputStream(tempFile)) {
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();
stream = null;
}
return tempFile;
}
return file;
} }
/** /**
@ -130,19 +123,7 @@ public File getFile() throws IOException {
// TODO: avoid long random file name (number generated by // TODO: avoid long random file name (number generated by
// createTempFile) // createTempFile)
tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$ tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$
if (stream != null) { copyFromStream();
try (OutputStream outStream = new FileOutputStream(tempFile)) {
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();
stream = null;
}
}
return tempFile; return tempFile;
} }
return file; return file;
@ -157,4 +138,34 @@ public void cleanTemporaries() {
tempFile = null; tempFile = null;
} }
private void copyFromStream() throws IOException, FileNotFoundException {
if (stream != null) {
try (OutputStream outStream = new FileOutputStream(tempFile)) {
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();
stream = null;
}
}
}
private static String[] splitBaseFileNameAndExtension(File file) {
String[] result = new String[2];
result[0] = file.getName();
result[1] = ""; //$NON-NLS-1$
if (!result[0].startsWith(".")) { //$NON-NLS-1$
int idx = result[0].lastIndexOf("."); //$NON-NLS-1$
if (idx != -1) {
result[1] = result[0].substring(idx, result[0].length());
result[0] = result[0].substring(0, idx);
}
}
return result;
}
} }

View File

@ -10,6 +10,11 @@
package org.eclipse.jgit.internal.diffmergetool; package org.eclipse.jgit.internal.diffmergetool;
import java.io.File; 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.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
@ -48,7 +53,7 @@ public MergeTools(Repository repo) {
* @param remoteFile * @param remoteFile
* the remote file element * the remote file element
* @param baseFile * @param baseFile
* the base file element * the base file element (can be null)
* @param mergedFilePath * @param mergedFilePath
* the path of 'merged' file * the path of 'merged' file
* @param toolName * @param toolName
@ -65,35 +70,79 @@ public ExecutionResult merge(Repository repo, FileElement localFile,
String toolName, BooleanTriState prompt, BooleanTriState gui) String toolName, BooleanTriState prompt, BooleanTriState gui)
throws ToolException { throws ToolException {
ExternalMergeTool tool = guessTool(toolName, gui); ExternalMergeTool tool = guessTool(toolName, gui);
FileElement backup = null;
File tempDir = null;
ExecutionResult result = null;
try { try {
File workingDir = repo.getWorkTree(); File workingDir = repo.getWorkTree();
String localFilePath = localFile.getFile().getPath(); // crate temp-directory or use working directory
String remoteFilePath = remoteFile.getFile().getPath(); tempDir = config.isWriteToTemp()
String baseFilePath = baseFile.getFile().getPath(); ? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$
String command = tool.getCommand(); : workingDir;
command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$ // create additional backup file (copy worktree file)
command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$ backup = createBackupFile(mergedFilePath, tempDir);
command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$ // get local, remote and base file paths
command = command.replace("$BASE", baseFilePath); //$NON-NLS-1$ String localFilePath = localFile.getFile(tempDir, "LOCAL") //$NON-NLS-1$
Map<String, String> env = new TreeMap<>(); .getPath();
env.put(Constants.GIT_DIR_KEY, String remoteFilePath = remoteFile.getFile(tempDir, "REMOTE") //$NON-NLS-1$
repo.getDirectory().getAbsolutePath()); .getPath();
env.put("LOCAL", localFilePath); //$NON-NLS-1$ String baseFilePath = ""; //$NON-NLS-1$
env.put("REMOTE", remoteFilePath); //$NON-NLS-1$ if (baseFile != null) {
env.put("MERGED", mergedFilePath); //$NON-NLS-1$ baseFilePath = baseFile.getFile(tempDir, "BASE").getPath(); //$NON-NLS-1$
env.put("BASE", baseFilePath); //$NON-NLS-1$ }
// prepare the command (replace the file paths)
boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
String command = prepareCommand(mergedFilePath, localFilePath,
remoteFilePath, baseFilePath,
tool.getCommand(baseFile != null));
// prepare the environment
Map<String, String> env = prepareEnvironment(repo, mergedFilePath,
localFilePath, remoteFilePath, baseFilePath);
CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
return cmdExec.run(command, workingDir, env); result = cmdExec.run(command, workingDir, env);
// keep backup as .orig file
if (backup != null) {
keepBackupFile(mergedFilePath, backup);
}
return result;
} catch (Exception e) { } catch (Exception e) {
throw new ToolException(e); throw new ToolException(e);
} finally { } finally {
localFile.cleanTemporaries(); // always delete backup file (ignore that it was may be already
remoteFile.cleanTemporaries(); // moved to keep-backup file)
baseFile.cleanTemporaries(); 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 static FileElement createBackupFile(String mergedFilePath,
File tempDir) throws IOException {
FileElement backup = null;
Path path = Paths.get(tempDir.getPath(), mergedFilePath);
if (Files.exists(path)) {
backup = new FileElement(mergedFilePath, "NOID", null); //$NON-NLS-1$
Files.copy(path, backup.getFile(tempDir, "BACKUP").toPath(), //$NON-NLS-1$
StandardCopyOption.REPLACE_EXISTING);
}
return backup;
}
/** /**
* @return the tool names * @return the tool names
*/ */
@ -159,6 +208,38 @@ private ExternalMergeTool getTool(final String name) {
return tool; return tool;
} }
private String prepareCommand(String mergedFilePath, String localFilePath,
String remoteFilePath, String baseFilePath, String command) {
command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$
command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$
command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$
command = command.replace("$BASE", baseFilePath); //$NON-NLS-1$
return command;
}
private Map<String, String> prepareEnvironment(Repository repo,
String mergedFilePath, String localFilePath, String remoteFilePath,
String baseFilePath) {
Map<String, String> env = new TreeMap<>();
env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath());
env.put("LOCAL", localFilePath); //$NON-NLS-1$
env.put("REMOTE", remoteFilePath); //$NON-NLS-1$
env.put("MERGED", mergedFilePath); //$NON-NLS-1$
env.put("BASE", baseFilePath); //$NON-NLS-1$
return env;
}
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() { private Map<String, ExternalMergeTool> setupPredefinedTools() {
Map<String, ExternalMergeTool> tools = new TreeMap<>(); Map<String, ExternalMergeTool> tools = new TreeMap<>();
for (CommandLineMergeTool tool : CommandLineMergeTool.values()) { for (CommandLineMergeTool tool : CommandLineMergeTool.values()) {

View File

@ -26,6 +26,8 @@ public class ToolException extends Exception {
private final ExecutionResult result; private final ExecutionResult result;
private final boolean commandExecutionError;
/** /**
* the serial version UID * the serial version UID
*/ */
@ -35,8 +37,7 @@ public class ToolException extends Exception {
* *
*/ */
public ToolException() { public ToolException() {
super(); this(null, null, false);
result = null;
} }
/** /**
@ -44,8 +45,7 @@ public ToolException() {
* the exception message * the exception message
*/ */
public ToolException(String message) { public ToolException(String message) {
super(message); this(message, null, false);
result = null;
} }
/** /**
@ -53,10 +53,14 @@ public ToolException(String message) {
* the exception message * the exception message
* @param result * @param result
* the execution result * the execution result
* @param commandExecutionError
* is command execution error happened ?
*/ */
public ToolException(String message, ExecutionResult result) { public ToolException(String message, ExecutionResult result,
boolean commandExecutionError) {
super(message); super(message);
this.result = result; this.result = result;
this.commandExecutionError = commandExecutionError;
} }
/** /**
@ -68,6 +72,7 @@ public ToolException(String message, ExecutionResult result) {
public ToolException(String message, Throwable cause) { public ToolException(String message, Throwable cause) {
super(message, cause); super(message, cause);
result = null; result = null;
commandExecutionError = false;
} }
/** /**
@ -77,6 +82,7 @@ public ToolException(String message, Throwable cause) {
public ToolException(Throwable cause) { public ToolException(Throwable cause) {
super(cause); super(cause);
result = null; result = null;
commandExecutionError = false;
} }
/** /**
@ -93,6 +99,13 @@ public ExecutionResult getResult() {
return result; return result;
} }
/**
* @return true if command execution error appears, false otherwise
*/
public boolean isCommandExecutionError() {
return commandExecutionError;
}
/** /**
* @return the result Stderr * @return the result Stderr
*/ */