summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndre Bossert <andre.bossert@siemens.com>2019-03-08 22:31:34 +0100
committerAndrey Loskutov <loskutov@gmx.de>2022-05-25 13:52:04 +0200
commiteaf4d500b886a7e776f50bf53497fe463e714b25 (patch)
tree740ac70c01372243cdbc8c902113ff144c8f3c2b
parent85734356351ec2df4067b2472a37f6d9bcbb7350 (diff)
downloadjgit-eaf4d500b886a7e776f50bf53497fe463e714b25.tar.gz
jgit-eaf4d500b886a7e776f50bf53497fe463e714b25.zip
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>
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java88
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java242
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java (renamed from org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java)85
-rw-r--r--org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties20
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java15
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java323
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java16
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java16
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java83
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java121
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java23
11 files changed, 847 insertions, 185 deletions
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
index 017a5d994f..dc34c0d67b 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
@@ -16,6 +16,7 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.junit.Assert.fail;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -30,7 +31,7 @@ import org.junit.Test;
/**
* 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 class DiffToolTest extends ExternalToolTestCase {
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 class DiffToolTest extends ExternalToolTestCase {
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 class DiffToolTest extends ExternalToolTestCase {
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 @@ public class DiffToolTest extends ExternalToolTestCase {
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 @@ public class DiffToolTest extends ExternalToolTestCase {
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 @@ public class DiffToolTest extends ExternalToolTestCase {
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 @@ public class DiffToolTest extends ExternalToolTestCase {
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 @@ public class DiffToolTest extends ExternalToolTestCase {
}
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]);
+ }
}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
index 32cd60415e..2e50f09081 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
@@ -15,6 +15,7 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -27,7 +28,7 @@ import org.junit.Test;
/**
* 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,38 +40,122 @@ public class MergeToolTest extends ExternalToolTestCase {
}
@Test
- public void testTool() throws Exception {
- createMergeConflict();
- String[] expectedOutput = getExpectedToolOutput();
+ public void testAbortMerge() throws Exception {
+ String[] inputLines = {
+ "y", // start tool for merge resolution
+ "n", // don't accept merge tool result
+ "n", // don't continue resolution
+ };
+ String[] conflictingFilenames = createMergeConflict();
+ int abortIndex = 1;
+ String[] expectedOutput = getExpectedAbortMergeOutput(
+ conflictingFilenames,
+ abortIndex);
+
+ String option = "--tool";
+
+ InputStream inputStream = createInputStream(inputLines);
+ assertArrayOfLinesEquals("Incorrect output for option: " + option,
+ expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+ MERGE_TOOL, "--prompt", option, TOOL_NAME));
+ }
- String[] options = {
- "--tool",
- "-t",
+ @Test
+ public void testAbortLaunch() throws Exception {
+ String[] inputLines = {
+ "n", // abort merge tool launch
};
+ String[] conflictingFilenames = createMergeConflict();
+ String[] expectedOutput = getExpectedAbortLaunchOutput(
+ conflictingFilenames);
- 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 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();
List<String> expectedOutput = new ArrayList<>();
@@ -87,8 +172,7 @@ public class MergeToolTest extends ExternalToolTestCase {
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 @@ public class MergeToolTest extends ExternalToolTestCase {
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 conflictFilename : conflictFilenames) {
+ expected.add("Normal merge conflict for '" + conflictFilename
+ + "':");
+ expected.add("{local}: modified file");
+ expected.add("{remote}: modified file");
+ expected.add(conflictFilename);
+ expected.add(conflictFilename + " seems unchanged.");
+ }
+ return expected.toArray(new String[0]);
+ }
+
+ private static String[] getExpectedAbortLaunchOutput(
+ String[] conflictFilenames) {
+ List<String> expected = new ArrayList<>();
+ expected.add("Merging:");
+ for (String conflictFilename : conflictFilenames) {
+ expected.add(conflictFilename);
+ }
+ if (conflictFilenames.length > 1) {
+ String conflictFilename = conflictFilenames[0];
+ expected.add(
+ "Normal merge conflict for '" + conflictFilename + "':");
+ expected.add("{local}: modified file");
+ expected.add("{remote}: modified file");
+ expected.add("Hit return to start merge resolution tool ("
+ + TOOL_NAME + "):");
+ }
+ return expected.toArray(new String[0]);
+ }
+
+ private static String[] getExpectedAbortMergeOutput(
+ String[] conflictFilenames, int abortIndex) {
+ List<String> expected = new ArrayList<>();
+ expected.add("Merging:");
+ for (String conflictFilename : conflictFilenames) {
+ expected.add(conflictFilename);
+ }
+ for (int i = 0; i < conflictFilenames.length; ++i) {
+ if (i == abortIndex) {
+ break;
+ }
+
+ String conflictFilename = conflictFilenames[i];
+ expected.add(
+ "Normal merge conflict for '" + conflictFilename + "':");
+ expected.add("{local}: modified file");
+ expected.add("{remote}: modified file");
+ expected.add("Hit return to start merge resolution tool ("
+ + TOOL_NAME + "): " + conflictFilename);
+ expected.add(conflictFilename + " seems unchanged.");
+ expected.add("Was the merge successful [y/n]?");
+ if (i < conflictFilenames.length - 1) {
+ expected.add(
+ "\tContinue merging other unresolved paths [y/n]?");
+ }
+ }
+ return expected.toArray(new String[0]);
+ }
+
+ private static String[] getExpectedMergeConflictOutput(
+ String[] conflictFilenames) {
+ List<String> expected = new ArrayList<>();
+ expected.add("Merging:");
+ for (String conflictFilename : conflictFilenames) {
+ expected.add(conflictFilename);
+ }
+ for (int i = 0; i < conflictFilenames.length; ++i) {
+ String conflictFilename = conflictFilenames[i];
+ expected.add("Normal merge conflict for '" + conflictFilename
+ + "':");
+ expected.add("{local}: modified file");
+ expected.add("{remote}: modified file");
+ expected.add("Hit return to start merge resolution tool ("
+ + TOOL_NAME + "): " + conflictFilename);
+ expected.add(conflictFilename + " seems unchanged.");
+ expected.add("Was the merge successful [y/n]?");
+ if (i < conflictFilenames.length - 1) {
+ // expected.add(
+ // "\tContinue merging other unresolved paths [y/n]?");
+ }
+ }
+ return expected.toArray(new String[0]);
+ }
+
+ private static String[] getExpectedDeletedConflictOutput(
+ String[] conflictFilenames) {
+ List<String> expected = new ArrayList<>();
+ expected.add("Merging:");
+ for (String mergeConflictFilename : conflictFilenames) {
+ expected.add(mergeConflictFilename);
}
- for (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 (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 expectedOutput.toArray(new String[0]);
+ return expected.toArray(new String[0]);
}
}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java
index e10b13efb1..d13eeb7e4d 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java
@@ -11,10 +11,14 @@ package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertEquals;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.stream.Collectors;
-import org.eclipse.jgit.api.CherryPickResult;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
@@ -29,7 +33,7 @@ import org.kohsuke.args4j.Argument;
/**
* 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 abstract class ExternalToolTestCase extends CLIRepositoryTestCase {
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 @@ public abstract class ExternalToolTestCase extends CLIRepositoryTestCase {
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 @@ public abstract class ExternalToolTestCase extends CLIRepositoryTestCase {
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 @@ public abstract class ExternalToolTestCase extends CLIRepositoryTestCase {
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));
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index 8e2eef7eb2..674185df2b 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -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.
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
index 2e90d52cba..ffba36fe20 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
@@ -113,11 +113,14 @@ class DiffTool extends TextBuiltin {
@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 @@ class DiffTool extends TextBuiltin {
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 @@ class DiffTool extends TextBuiltin {
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(
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
index 37afa54c78..dce5a7996d 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
@@ -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 @@ class MergeTool extends TextBuiltin {
@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 @@ class MergeTool extends TextBuiltin {
} 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 @@ class MergeTool extends TextBuiltin {
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 @@ class MergeTool extends TextBuiltin {
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);
- }
- if (launch) {
- outw.println("TODO: Launch mergetool '" + toolNamePrompt //$NON-NLS-1$
- + "' for path '" + fileName + "'..."); //$NON-NLS-1$ //$NON-NLS-2$
- } else {
- break;
+ // 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;
}
- } 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$
+ }
+ // aborted ?
+ if (mergeResult == MergeResult.ABORTED) {
+ break;
+ }
+ // get file stage state and merge
+ StageState fileState = files.get(mergedFilePath);
+ if (fileState == StageState.BOTH_MODIFIED) {
+ mergeResult = mergeModified(mergedFilePath, showPrompt,
+ toolNamePrompt);
+ } else if ((fileState == StageState.DELETED_BY_US)
+ || (fileState == StageState.DELETED_BY_THEM)) {
+ mergeResult = mergeDeleted(mergedFilePath,
+ fileState == StageState.DELETED_BY_US);
} else {
+ outw.println(MessageFormat.format(
+ CLIText.get().mergeToolUnknownConflict,
+ mergedFilePath));
+ mergeResult = MergeResult.ABORTED;
+ }
+ }
+ }
+
+ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
+ String toolNamePrompt) throws Exception {
+ outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict,
+ mergedFilePath));
+ outw.flush();
+ // check if user wants to launch merge resolution tool
+ boolean launch = true;
+ if (showPrompt) {
+ launch = isLaunch(toolNamePrompt);
+ }
+ if (!launch) {
+ return MergeResult.ABORTED; // abort
+ }
+ boolean isMergeSuccessful = true;
+ ContentSource baseSource = ContentSource.create(db.newObjectReader());
+ ContentSource localSource = ContentSource.create(db.newObjectReader());
+ ContentSource remoteSource = ContentSource.create(db.newObjectReader());
+ 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(
- "\nUnknown merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$
+ 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 {
+ private boolean isLaunch(String toolNamePrompt) throws IOException {
boolean launch = true;
- outw.println("Hit return to start merge resolution tool (" //$NON-NLS-1$
- + toolNamePrompt + "): "); //$NON-NLS-1$
+ outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch,
+ toolNamePrompt) + " "); //$NON-NLS-1$
outw.flush();
- BufferedReader br = new BufferedReader(new InputStreamReader(ins));
+ 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 int getDeletedMergeDecision() throws IOException {
+ int ret = 0; // abort
+ final String message = CLIText.get().mergeToolDeletedMergeDecision
+ + " "; //$NON-NLS-1$
+ outw.print(message);
+ outw.flush();
+ BufferedReader br = inputReader;
+ String line = null;
+ while ((line = br.readLine()) != null) {
+ if (line.equalsIgnoreCase("m")) { //$NON-NLS-1$
+ ret = 1; // modified
+ break;
+ } else if (line.equalsIgnoreCase("d")) { //$NON-NLS-1$
+ ret = -1; // deleted
+ break;
+ } else if (line.equalsIgnoreCase("a")) { //$NON-NLS-1$
+ break;
+ }
+ outw.print(message);
+ outw.flush();
+ }
+ return ret;
+ }
+
private void showToolHelp() throws IOException {
- outw.println(
- "'git mergetool --tool=<tool>' may be set to one of the following:"); //$NON-NLS-1$
+ StringBuilder availableToolNames = new StringBuilder();
for (String name : mergeTools.getAvailableTools().keySet()) {
- outw.println("\t\t" + name); //$NON-NLS-1$
+ availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
}
- outw.println(""); //$NON-NLS-1$
- outw.println("\tuser-defined:"); //$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());
- }
- 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$
+ 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("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();
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
index 7fe5b0fa45..989e649b72 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
@@ -169,6 +169,22 @@ public class CLIText extends TranslationBundle {
/***/ 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;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
index 0dde9b5f39..ad79fe8fc6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
@@ -72,10 +72,18 @@ public class CommandExecutor {
}
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 {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
index cdc8f015f6..1ae87aaa62 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
@@ -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 class FileElement {
}
/**
- * @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 class FileElement {
// 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 class FileElement {
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;
+ }
+
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
index cefefb8e75..c4c2ceccff 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
@@ -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 class MergeTools {
* @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,33 +70,77 @@ public class MergeTools {
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;
}
/**
@@ -159,6 +208,38 @@ public class MergeTools {
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()) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
index 7862cf5967..1ae0780ac8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
@@ -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 class ToolException extends Exception {
* the exception message
*/
public ToolException(String message) {
- super(message);
- result = null;
+ this(message, null, false);
}
/**
@@ -53,10 +53,14 @@ public class ToolException extends Exception {
* 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 class ToolException extends Exception {
public ToolException(String message, Throwable cause) {
super(message, cause);
result = null;
+ commandExecutionError = false;
}
/**
@@ -77,6 +82,7 @@ public class ToolException extends Exception {
public ToolException(Throwable cause) {
super(cause);
result = null;
+ commandExecutionError = false;
}
/**
@@ -94,6 +100,13 @@ public class ToolException extends Exception {
}
/**
+ * @return true if command execution error appears, false otherwise
+ */
+ public boolean isCommandExecutionError() {
+ return commandExecutionError;
+ }
+
+ /**
* @return the result Stderr
*/
public String getResultStderr() {