]> source.dussan.org Git - jgit.git/commitdiff
Add mergetool merge feature (execute external tool) 10/138410/38
authorAndre Bossert <andre.bossert@siemens.com>
Fri, 8 Mar 2019 21:31:34 +0000 (22:31 +0100)
committerAndrey Loskutov <loskutov@gmx.de>
Wed, 25 May 2022 11:52:04 +0000 (13:52 +0200)
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>
12 files changed:
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java [deleted file]
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java [new file with mode: 0644]
org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java

index 017a5d994f42de9d9df0b38cb293e6163d29a7ed..dc34c0d67bee9f75e78c0c31adbaef0ef771edc0 100644 (file)
@@ -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/ExternalToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java
deleted file mode 100644 (file)
index e10b13e..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-package org.eclipse.jgit.pgm;
-
-import static org.junit.Assert.assertEquals;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jgit.api.CherryPickResult;
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.lib.CLIRepositoryTestCase;
-import org.eclipse.jgit.pgm.opt.CmdLineParser;
-import org.eclipse.jgit.pgm.opt.SubcommandHandler;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.treewalk.FileTreeIterator;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.junit.Before;
-import org.kohsuke.args4j.Argument;
-
-/**
- * Base test case for the {@code difftool} and {@code mergetool} commands.
- */
-public abstract class ExternalToolTestCase extends CLIRepositoryTestCase {
-
-       public static class GitCliJGitWrapperParser {
-               @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
-               TextBuiltin subcommand;
-
-               @Argument(index = 1, metaVar = "metaVar_arg")
-               List<String> arguments = new ArrayList<>();
-       }
-
-       protected static final String TOOL_NAME = "some_tool";
-
-       private static final String TEST_BRANCH_NAME = "test_branch";
-
-       private Git git;
-
-       @Override
-       @Before
-       public void setUp() throws Exception {
-               super.setUp();
-               git = new Git(db);
-               git.commit().setMessage("initial commit").call();
-               git.branchCreate().setName(TEST_BRANCH_NAME).call();
-       }
-
-       protected String[] runAndCaptureUsingInitRaw(String... args)
-                       throws Exception {
-               CLIGitCommand.Result result = new CLIGitCommand.Result();
-
-               GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
-               CmdLineParser clp = new CmdLineParser(bean);
-               clp.parseArgument(args);
-
-               TextBuiltin cmd = bean.subcommand;
-               cmd.initRaw(db, null, null, result.out, result.err);
-               cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()]));
-               if (cmd.getOutputWriter() != null) {
-                       cmd.getOutputWriter().flush();
-               }
-               if (cmd.getErrorWriter() != null) {
-                       cmd.getErrorWriter().flush();
-               }
-               return result.outLines().toArray(new String[0]);
-       }
-
-       protected CherryPickResult createMergeConflict() throws Exception {
-               writeTrashFile("a", "Hello world a");
-               writeTrashFile("b", "Hello world b");
-               git.add().addFilepattern(".").call();
-               git.commit().setMessage("files a & b added").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();
-               git.checkout().setName(TEST_BRANCH_NAME).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.branchCreate().setName("branch_2").call();
-               CherryPickResult result = git.cherryPick().include(commit1).call();
-               return result;
-       }
-
-       protected RevCommit createUnstagedChanges() throws Exception {
-               writeTrashFile("a", "Hello world a");
-               writeTrashFile("b", "Hello world b");
-               git.add().addFilepattern(".").call();
-               RevCommit commit = git.commit().setMessage("files a & b").call();
-               writeTrashFile("a", "New Hello world a");
-               writeTrashFile("b", "New Hello world b");
-               return commit;
-       }
-
-       protected RevCommit createStagedChanges() throws Exception {
-               RevCommit commit = createUnstagedChanges();
-               git.add().addFilepattern(".").call();
-               return commit;
-       }
-
-       protected List<DiffEntry> getRepositoryChanges(RevCommit commit)
-                       throws Exception {
-               TreeWalk tw = new TreeWalk(db);
-               tw.addTree(commit.getTree());
-               FileTreeIterator modifiedTree = new FileTreeIterator(db);
-               tw.addTree(modifiedTree);
-               List<DiffEntry> changes = DiffEntry.scan(tw);
-               return changes;
-       }
-
-       protected static void assertArrayOfLinesEquals(String failMessage,
-                       String[] expected, String[] actual) {
-               assertEquals(failMessage, toString(expected), toString(actual));
-       }
-
-       protected static String getEchoCommand() {
-               /*
-                * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be
-                * replaced with full paths to a temporary file during some of the tests
-                */
-               return "(echo \"$MERGED\")";
-       }
-}
index 32cd60415e9f4414a664cc1d6688a413a99cb433..2e50f090814f4c7daee76a72faeb09eff5389be2 100644 (file)
@@ -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,37 +40,121 @@ 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();
@@ -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/ToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java
new file mode 100644 (file)
index 0000000..d13eeb7
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.pgm;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.eclipse.jgit.pgm.opt.CmdLineParser;
+import org.eclipse.jgit.pgm.opt.SubcommandHandler;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Before;
+import org.kohsuke.args4j.Argument;
+
+/**
+ * Base test case for the {@code difftool} and {@code mergetool} commands.
+ */
+public abstract class ToolTestCase extends CLIRepositoryTestCase {
+
+       public static class GitCliJGitWrapperParser {
+               @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
+               TextBuiltin subcommand;
+
+               @Argument(index = 1, metaVar = "metaVar_arg")
+               List<String> arguments = new ArrayList<>();
+       }
+
+       protected static final String TOOL_NAME = "some_tool";
+
+       private static final String TEST_BRANCH_NAME = "test_branch";
+
+       private Git git;
+
+       @Override
+       @Before
+       public void setUp() throws Exception {
+               super.setUp();
+               git = new Git(db);
+               git.commit().setMessage("initial commit").call();
+               git.branchCreate().setName(TEST_BRANCH_NAME).call();
+       }
+
+       protected String[] runAndCaptureUsingInitRaw(String... args)
+                       throws Exception {
+               InputStream inputStream = null; // no input stream
+               return runAndCaptureUsingInitRaw(inputStream, args);
+       }
+
+       protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
+                       String... args) throws Exception {
+               CLIGitCommand.Result result = new CLIGitCommand.Result();
+
+               GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
+               CmdLineParser clp = new CmdLineParser(bean);
+               clp.parseArgument(args);
+
+               TextBuiltin cmd = bean.subcommand;
+               cmd.initRaw(db, null, inputStream, result.out, result.err);
+               cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()]));
+               if (cmd.getOutputWriter() != null) {
+                       cmd.getOutputWriter().flush();
+               }
+               if (cmd.getErrorWriter() != null) {
+                       cmd.getErrorWriter().flush();
+               }
+
+               List<String> errLines = result.errLines().stream()
+                               .filter(l -> !l.isBlank()) // we care only about error messages
+                               .collect(Collectors.toList());
+               assertEquals("Expected no standard error output from tool",
+                               Collections.EMPTY_LIST.toString(), errLines.toString());
+
+               return result.outLines().toArray(new String[0]);
+       }
+
+       protected String[] createMergeConflict() throws Exception {
+               // create files on initial branch
+               git.checkout().setName(TEST_BRANCH_NAME).call();
+               writeTrashFile("a", "Hello world a");
+               writeTrashFile("b", "Hello world b");
+               git.add().addFilepattern(".").call();
+               git.commit().setMessage("files a & b added").call();
+               // create another branch and change files
+               git.branchCreate().setName("branch_1").call();
+               git.checkout().setName("branch_1").call();
+               writeTrashFile("a", "Hello world a 1");
+               writeTrashFile("b", "Hello world b 1");
+               git.add().addFilepattern(".").call();
+               RevCommit commit1 = git.commit()
+                               .setMessage("files a & b modified commit 1").call();
+               // checkout initial branch
+               git.checkout().setName(TEST_BRANCH_NAME).call();
+               // create another branch and change files
+               git.branchCreate().setName("branch_2").call();
+               git.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 modified commit 2").call();
+               // cherry-pick conflicting changes
+               git.cherryPick().include(commit1).call();
+               String[] conflictingFilenames = { "a", "b" };
+               return conflictingFilenames;
+       }
+
+       protected String[] createDeletedConflict() throws Exception {
+               // create files on initial branch
+               git.checkout().setName(TEST_BRANCH_NAME).call();
+               writeTrashFile("a", "Hello world a");
+               writeTrashFile("b", "Hello world b");
+               git.add().addFilepattern(".").call();
+               git.commit().setMessage("files a & b added").call();
+               // create another branch and change files
+               git.branchCreate().setName("branch_1").call();
+               git.checkout().setName("branch_1").call();
+               writeTrashFile("a", "Hello world a 1");
+               writeTrashFile("b", "Hello world b 1");
+               git.add().addFilepattern(".").call();
+               RevCommit commit1 = git.commit()
+                               .setMessage("files a & b modified commit 1").call();
+               // checkout initial branch
+               git.checkout().setName(TEST_BRANCH_NAME).call();
+               // create another branch and change files
+               git.branchCreate().setName("branch_2").call();
+               git.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 {
+               writeTrashFile("a", "Hello world a");
+               writeTrashFile("b", "Hello world b");
+               git.add().addFilepattern(".").call();
+               RevCommit commit = git.commit().setMessage("files a & b").call();
+               writeTrashFile("a", "New Hello world a");
+               writeTrashFile("b", "New Hello world b");
+               return commit;
+       }
+
+       protected RevCommit createStagedChanges() throws Exception {
+               RevCommit commit = createUnstagedChanges();
+               git.add().addFilepattern(".").call();
+               return commit;
+       }
+
+       protected List<DiffEntry> getRepositoryChanges(RevCommit commit)
+                       throws Exception {
+               TreeWalk tw = new TreeWalk(db);
+               tw.addTree(commit.getTree());
+               FileTreeIterator modifiedTree = new FileTreeIterator(db);
+               tw.addTree(modifiedTree);
+               List<DiffEntry> changes = DiffEntry.scan(tw);
+               return changes;
+       }
+
+       protected static InputStream createInputStream(String[] inputLines) {
+               return createInputStream(Arrays.asList(inputLines));
+       }
+
+       protected static InputStream createInputStream(List<String> inputLines) {
+               String input = String.join(System.lineSeparator(), inputLines);
+               InputStream inputStream = new ByteArrayInputStream(input.getBytes());
+               return inputStream;
+       }
+
+       protected static void assertArrayOfLinesEquals(String failMessage,
+                       String[] expected, String[] actual) {
+               assertEquals(failMessage, toString(expected), toString(actual));
+       }
+
+       protected static String getEchoCommand() {
+               /*
+                * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be
+                * replaced with full paths to a temporary file during some of the tests
+                */
+               return "(echo \"$MERGED\")";
+       }
+}
index 8e2eef7eb2e36862beec10c818204bdc068dbd62..674185df2b04c3fb8e914c6c0f4e87e1c8514a0c 100644 (file)
@@ -58,8 +58,8 @@ couldNotCreateBranch=Could not create branch {0}: {1}
 dateInfo=Date:   {0}
 deletedBranch=Deleted branch {0}
 deletedRemoteBranch=Deleted remote branch {0}
-diffToolHelpSetToFollowing='git difftool --tool=<tool>' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
-diffToolLaunch=Viewing ({0}/{1}): '{2}'\nLaunch '{3}' [Y/n]?
+diffToolHelpSetToFollowing=''git difftool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
+diffToolLaunch=Viewing ({0}/{1}): ''{2}''\nLaunch ''{3}'' [Y/n]?
 diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1}
 doesNotExist={0} does not exist
 dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge:
@@ -91,6 +91,22 @@ listeningOn=Listening on {0}
 logNoSignatureVerifier="No signature verifier available"
 mergeConflict=CONFLICT(content): Merge conflict in {0}
 mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge:
+mergeToolHelpSetToFollowing=''git mergetool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
+mergeToolLaunch=Hit return to start merge resolution tool ({0}):
+mergeToolDied=local or remote cannot be found in cache, stopping at {0}
+mergeToolNoFiles=No files need merging
+mergeToolMerging=Merging:\n{0}
+mergeToolUnknownConflict=\nUnknown merge conflict for ''{0}'':
+mergeToolNormalConflict=\nNormal merge conflict for ''{0}'':\n  '{'local'}': modified file\n  '{'remote'}': modified file
+mergeToolMergeFailed=merge of {0} failed
+mergeToolExecutionError=excution error
+mergeToolFileUnchanged=\n{0} seems unchanged.
+mergeToolDeletedConflict=\nDeleted merge conflict for ''{0}'':
+mergeToolDeletedConflictByUs=  {local}: deleted\n  {remote}: modified file
+mergeToolDeletedConflictByThem=  {local}: modified file\n  {remote}: deleted
+mergeToolContinueUnresolvedPaths=\nContinue merging other unresolved paths [y/n]?
+mergeToolWasMergeSuccessfull=Was the merge successful [y/n]?
+mergeToolDeletedMergeDecision=Use (m)odified or (d)eleted file, or (a)bort?
 mergeFailed=Automatic merge failed; fix conflicts and then commit the result
 mergeCheckoutFailed=Please, commit your changes or stash them before you can merge.
 mergeMadeBy=Merge made by the ''{0}'' strategy.
index 2e90d52cba3d6b5a702ed9a1e51be11a2e9b99ce..ffba36fe20958f29b62bc3a4ba50264b00edcf6f 100644 (file)
@@ -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(
index 37afa54c783ff555f1b42ad0e719274a0679650f..dce5a7996d73e2620361017002606b5b85063c8c 100644 (file)
 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();
index 7fe5b0fa45494e2307dc1f8cb50c6a1ca3ec8198..989e649b7264d61e7dacd1cd2cb7b0cb7f3e3f2a 100644 (file)
@@ -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;
index 0dde9b5f39343e3b6721a134a8bec0ec0986ed56..ad79fe8fc6329f16147d3d3401b98cf09a6ad046 100644 (file)
@@ -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 {
index cdc8f015f6353378d81a0ee287d25a552fc653e0..1ae87aaa6227fb98d9d398818c8419ed4c3f9fc6 100644 (file)
@@ -11,6 +11,7 @@
 package org.eclipse.jgit.internal.diffmergetool;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -80,35 +81,27 @@ public 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;
+       }
+
 }
index cefefb8e75614114f9a6367a21755bd84683d433..c4c2ceccff6bcbe6d331c41fc5b0a77407ed7e93 100644 (file)
 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()) {
index 7862cf596794fc655b3cb1cd0333b9d260939073..1ae0780ac89fc88c2d093a01535c091cdaefbd00 100644 (file)
@@ -26,6 +26,8 @@ public class ToolException extends Exception {
 
        private final ExecutionResult result;
 
+       private final boolean commandExecutionError;
+
        /**
         * the serial version UID
         */
@@ -35,8 +37,7 @@ public class ToolException extends Exception {
         *
         */
        public ToolException() {
-               super();
-               result = null;
+               this(null, null, false);
        }
 
        /**
@@ -44,8 +45,7 @@ public 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;
        }
 
        /**
@@ -93,6 +99,13 @@ public class ToolException extends Exception {
                return result;
        }
 
+       /**
+        * @return true if command execution error appears, false otherwise
+        */
+       public boolean isCommandExecutionError() {
+               return commandExecutionError;
+       }
+
        /**
         * @return the result Stderr
         */