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.junit.Assert.fail;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -30,7 +31,7 @@
/**
* Testing the {@code difftool} command.
*/
public class DiffToolTest extends ExternalToolTestCase {
public class DiffToolTest extends ToolTestCase {
private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION;
@ -41,6 +42,46 @@ public void setUp() throws Exception {
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)
public void testNotDefinedTool() throws Exception {
createUnstagedChanges();
@ -53,7 +94,7 @@ public void testNotDefinedTool() throws Exception {
public void testTool() throws Exception {
RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedToolOutput(changes);
String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
String[] options = {
"--tool",
@ -72,7 +113,7 @@ public void testTool() throws Exception {
public void testToolTrustExitCode() throws Exception {
RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedToolOutput(changes);
String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
String[] options = { "--tool", "-t", };
@ -87,7 +128,7 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
RevCommit commit = createUnstagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedToolOutput(changes);
String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
String[] options = { "--tool", "-t", };
@ -103,7 +144,7 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
public void testToolCached() throws Exception {
RevCommit commit = createStagedChanges();
List<DiffEntry> changes = getRepositoryChanges(commit);
String[] expectedOutput = getExpectedToolOutput(changes);
String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
String[] options = { "--cached", "--staged", };
@ -118,7 +159,8 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
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);
@ -159,7 +201,7 @@ private void configureEchoTool(String toolName) {
String.valueOf(false));
}
private String[] getExpectedToolOutput(List<DiffEntry> changes) {
private static String[] getExpectedToolOutputNoPrompt(List<DiffEntry> changes) {
String[] expectedToolOutput = new String[changes.size()];
for (int i = 0; i < changes.size(); ++i) {
DiffEntry change = changes.get(i);
@ -169,4 +211,36 @@ private String[] getExpectedToolOutput(List<DiffEntry> changes) {
}
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_MERGE_SECTION;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -27,7 +28,7 @@
/**
* Testing the {@code mergetool} command.
*/
public class MergeToolTest extends ExternalToolTestCase {
public class MergeToolTest extends ToolTestCase {
private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION;
@ -39,37 +40,121 @@ public void setUp() throws Exception {
}
@Test
public void testTool() throws Exception {
createMergeConflict();
String[] expectedOutput = getExpectedToolOutput();
String[] options = {
"--tool",
"-t",
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);
for (String option : options) {
assertArrayOfLinesEquals("Incorrect output for option: " + option,
expectedOutput,
runAndCaptureUsingInitRaw(MERGE_TOOL, option,
TOOL_NAME));
}
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 testToolNoGuiNoPrompt() throws Exception {
createMergeConflict();
String[] expectedOutput = getExpectedToolOutput();
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,
"--no-gui", "--no-prompt", option, TOOL_NAME));
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();
@ -87,8 +172,7 @@ public void testToolHelp() throws Exception {
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.",
};
"environment. If run in a terminal-only session, they will fail.", };
expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));
String option = "--tool-help";
@ -116,21 +200,111 @@ private void configureEchoTool(String toolName) {
String.valueOf(false));
}
private String[] getExpectedToolOutput() {
String[] mergeConflictFilenames = { "a", "b", };
List<String> expectedOutput = new ArrayList<>();
expectedOutput.add("Merging:");
for (String mergeConflictFilename : mergeConflictFilenames) {
expectedOutput.add(mergeConflictFilename);
private static String[] getExpectedMergeConflictOutputNoPrompt(
String[] conflictFilenames) {
List<String> expected = new ArrayList<>();
expected.add("Merging:");
for (String conflictFilename : conflictFilenames) {
expected.add(conflictFilename);
}
for (String mergeConflictFilename : mergeConflictFilenames) {
expectedOutput.add("Normal merge conflict for '"
+ mergeConflictFilename + "':");
expectedOutput.add("{local}: modified file");
expectedOutput.add("{remote}: modified file");
expectedOutput.add("TODO: Launch mergetool '" + TOOL_NAME
+ "' for path '" + mergeConflictFilename + "'...");
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 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 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.CherryPickResult;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
@ -29,7 +33,7 @@
/**
* 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 {
@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)
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();
@ -63,7 +73,7 @@ protected String[] runAndCaptureUsingInitRaw(String... args)
clp.parseArgument(args);
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()]));
if (cmd.getOutputWriter() != null) {
cmd.getOutputWriter().flush();
@ -71,28 +81,73 @@ protected String[] runAndCaptureUsingInitRaw(String... args)
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 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("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 commit 1")
.call();
git.branchCreate().setName("branch_1").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("a", "Hello world a 2");
writeTrashFile("b", "Hello world b 2");
git.add().addFilepattern(".").call();
git.commit().setMessage("files a & b commit 2").call();
git.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();
CherryPickResult result = git.cherryPick().include(commit1).call();
return result;
git.checkout().setName("branch_2").call();
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 {
@ -121,6 +176,16 @@ protected List<DiffEntry> getRepositoryChanges(RevCommit commit)
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));

View File

@ -58,8 +58,8 @@ 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]?
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:
@ -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.

View File

@ -113,11 +113,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
@ -208,10 +211,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$
@ -224,17 +226,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(

View File

@ -11,26 +11,35 @@
package org.eclipse.jgit.pgm;
import java.io.BufferedReader;
import java.io.File;
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.Optional;
import java.util.TreeMap;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.StatusCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
import org.eclipse.jgit.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.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.lib.internal.BooleanTriState;
import org.eclipse.jgit.lib.ObjectId;
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.Option;
import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
@ -43,16 +52,16 @@ class MergeTool extends TextBuiltin {
"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge")
private String toolName;
private Optional<Boolean> prompt = Optional.empty();
private BooleanTriState prompt = BooleanTriState.UNSET;
@Option(name = "--prompt", usage = "usage_prompt")
void setPrompt(@SuppressWarnings("unused") boolean on) {
prompt = Optional.of(Boolean.TRUE);
prompt = BooleanTriState.TRUE;
}
@Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt")
void noPrompt(@SuppressWarnings("unused") boolean on) {
prompt = Optional.of(Boolean.FALSE);
prompt = BooleanTriState.FALSE;
}
@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)
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
@ -88,8 +104,8 @@ protected void run() {
} else {
// get prompt
boolean showPrompt = mergeTools.isInteractive();
if (prompt.isPresent()) {
showPrompt = prompt.get().booleanValue();
if (prompt != BooleanTriState.UNSET) {
showPrompt = prompt == BooleanTriState.TRUE;
}
// get passed or default tool name
String toolNameSelected = toolName;
@ -101,7 +117,7 @@ protected void run() {
if (files.size() > 0) {
merge(files, showPrompt, toolNameSelected);
} else {
outw.println("No files need merging"); //$NON-NLS-1$
outw.println(CLIText.get().mergeToolNoFiles);
}
}
outw.flush();
@ -113,88 +129,273 @@ protected void run() {
private void merge(Map<String, StageState> files, boolean showPrompt,
String toolNamePrompt) throws Exception {
// sort file names
List<String> fileNames = new ArrayList<>(files.keySet());
Collections.sort(fileNames);
List<String> mergedFilePaths = new ArrayList<>(files.keySet());
Collections.sort(mergedFilePaths);
// show the files
outw.println("Merging:"); //$NON-NLS-1$
for (String fileName : fileNames) {
outw.println(fileName);
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();
for (String fileName : fileNames) {
StageState fileState = files.get(fileName);
// only both-modified is valid for mergetool
if (fileState == StageState.BOTH_MODIFIED) {
outw.println("\nNormal merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$
outw.println(" {local}: modified file"); //$NON-NLS-1$
outw.println(" {remote}: modified file"); //$NON-NLS-1$
// check if user wants to launch merge resolution tool
boolean launch = true;
if (showPrompt) {
launch = isLaunch(toolNamePrompt);
// 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;
}
if (launch) {
outw.println("TODO: Launch mergetool '" + toolNamePrompt //$NON-NLS-1$
+ "' for path '" + fileName + "'..."); //$NON-NLS-1$ //$NON-NLS-2$
} else {
break;
}
} else if ((fileState == StageState.DELETED_BY_US) || (fileState == StageState.DELETED_BY_THEM)) {
outw.println("\nDeleted merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$
} else {
outw.println(
"\nUnknown merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$
}
// 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 boolean isLaunch(String toolNamePrompt)
throws IOException {
boolean launch = true;
outw.println("Hit return to start merge resolution tool (" //$NON-NLS-1$
+ toolNamePrompt + "): "); //$NON-NLS-1$
private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
String toolNamePrompt) throws Exception {
outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict,
mergedFilePath));
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;
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;
}
}
return launch;
}
private void showToolHelp() throws IOException {
outw.println(
"'git mergetool --tool=<tool>' may be set to one of the following:"); //$NON-NLS-1$
for (String name : mergeTools.getAvailableTools().keySet()) {
outw.println("\t\t" + name); //$NON-NLS-1$
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();
}
outw.println(""); //$NON-NLS-1$
outw.println("\tuser-defined:"); //$NON-NLS-1$
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()) {
outw.println("\t\t" + name + ".cmd " //$NON-NLS-1$ //$NON-NLS-2$
+ userTools.get(name).getCommand());
userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$
name, userTools.get(name).getCommand()));
}
outw.println(""); //$NON-NLS-1$
outw.println(
"The following tools are valid, but not currently available:"); //$NON-NLS-1$
for (String name : mergeTools.getNotAvailableTools().keySet()) {
outw.println("\t\t" + name); //$NON-NLS-1$
}
outw.println(""); //$NON-NLS-1$
outw.println("Some of the tools listed above only work in a windowed"); //$NON-NLS-1$
outw.println(
"environment. If run in a terminal-only session, they will fail."); //$NON-NLS-1$
return;
outw.println(MessageFormat.format(
CLIText.get().mergeToolHelpSetToFollowing, availableToolNames,
userToolNames, notAvailableToolNames));
}
private Map<String, StageState> getFiles()
throws RevisionSyntaxException, NoWorkTreeException,
GitAPIException {
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();

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

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

View File

@ -11,6 +11,7 @@
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.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
* @throws IOException
*/
public File getFile(File workingDir) throws IOException {
public File getFile(File directory, String midName) throws IOException {
if (tempFile != null) {
return tempFile;
}
File file = new File(path);
String name = file.getName();
if (path.equals(DiffEntry.DEV_NULL)) {
file = new File(workingDir, "nul"); //$NON-NLS-1$
}
else if (stream != null) {
tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$
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;
String[] fileNameAndExtension = splitBaseFileNameAndExtension(
new File(path));
tempFile = File.createTempFile(
fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
fileNameAndExtension[1], directory);
copyFromStream();
return tempFile;
}
/**
@ -130,19 +123,7 @@ public File getFile() throws IOException {
// TODO: avoid long random file name (number generated by
// createTempFile)
tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$
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;
}
}
copyFromStream();
return tempFile;
}
return file;
@ -157,4 +138,34 @@ public void cleanTemporaries() {
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;
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;
@ -48,7 +53,7 @@ public MergeTools(Repository repo) {
* @param remoteFile
* the remote file element
* @param baseFile
* the base file element
* the base file element (can be null)
* @param mergedFilePath
* the path of 'merged' file
* @param toolName
@ -65,35 +70,79 @@ public ExecutionResult merge(Repository repo, FileElement localFile,
String toolName, BooleanTriState prompt, BooleanTriState gui)
throws ToolException {
ExternalMergeTool tool = guessTool(toolName, gui);
FileElement backup = null;
File tempDir = null;
ExecutionResult result = null;
try {
File workingDir = repo.getWorkTree();
String localFilePath = localFile.getFile().getPath();
String remoteFilePath = remoteFile.getFile().getPath();
String baseFilePath = baseFile.getFile().getPath();
String command = tool.getCommand();
command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$
command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$
command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$
command = command.replace("$BASE", baseFilePath); //$NON-NLS-1$
Map<String, String> env = new TreeMap<>();
env.put(Constants.GIT_DIR_KEY,
repo.getDirectory().getAbsolutePath());
env.put("LOCAL", localFilePath); //$NON-NLS-1$
env.put("REMOTE", remoteFilePath); //$NON-NLS-1$
env.put("MERGED", mergedFilePath); //$NON-NLS-1$
env.put("BASE", baseFilePath); //$NON-NLS-1$
// crate temp-directory or use working directory
tempDir = config.isWriteToTemp()
? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$
: workingDir;
// create additional backup file (copy worktree file)
backup = createBackupFile(mergedFilePath, tempDir);
// get local, remote and base file paths
String localFilePath = localFile.getFile(tempDir, "LOCAL") //$NON-NLS-1$
.getPath();
String remoteFilePath = remoteFile.getFile(tempDir, "REMOTE") //$NON-NLS-1$
.getPath();
String baseFilePath = ""; //$NON-NLS-1$
if (baseFile != null) {
baseFilePath = baseFile.getFile(tempDir, "BASE").getPath(); //$NON-NLS-1$
}
// prepare the command (replace the file paths)
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);
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) {
throw new ToolException(e);
} finally {
localFile.cleanTemporaries();
remoteFile.cleanTemporaries();
baseFile.cleanTemporaries();
// 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 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
*/
@ -159,6 +208,38 @@ private ExternalMergeTool getTool(final String name) {
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() {
Map<String, ExternalMergeTool> tools = new TreeMap<>();
for (CommandLineMergeTool tool : CommandLineMergeTool.values()) {

View File

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