diff options
author | Andre Bossert <andre.bossert@siemens.com> | 2019-03-08 22:31:34 +0100 |
---|---|---|
committer | Andrey Loskutov <loskutov@gmx.de> | 2022-05-25 13:52:04 +0200 |
commit | eaf4d500b886a7e776f50bf53497fe463e714b25 (patch) | |
tree | 740ac70c01372243cdbc8c902113ff144c8f3c2b | |
parent | 85734356351ec2df4067b2472a37f6d9bcbb7350 (diff) | |
download | jgit-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>
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() { |