Add difftool compare feature (execute external tool)

see: http://git-scm.com/docs/git-difftool

* add CommandExecutor that handles tool execution with help of "jgit.FS"
  * it handles tool execution with temporary created "command file" -->
for for all "command interpreters" and parameters with spaces etc.
  * using of external bash.exe at Windows (MinGW) if shell-script is
used as difftool command. It can be enabled with parameter
"jgit.usemsys2bash=auto" that checks if command contains ".sh" or
enabled / disabled with "jgit.usemsys2bash=true|false"
* added special handling for empty files (e.g. deleted, added etc.) that
are named "/dev/null"
* added creation and deletion of temporary files needed for compare
* added own Exception class for reporting to pgm / command line / EGit
* added prompt option handling before executing difftool
* reworked trustExitCode option for specific difftool and override for
all difftools from config and command line
* tested with command line options "--[no]-trust-exit-code",
"--tool=<toolname>", "--[no]-gui", --[no]-prompt
* ContentSource
  * added close() methods to close / cleanup used resources
(like ObjectReader TreeWalk etc.)
  * added isWorkingTreeSource() methods to check if file can be used
from working tree instead of copy from "ObjectLoader / ObjectReader" to
temporary file (fixes "difftool <commit> <commit>")

Bug: 356832
Change-Id: I5462fb6dbe4ecfd9da7c74117fce4070bbfd4d7a
Signed-off-by: Andre Bossert <andre.bossert@siemens.com>
Signed-off-by: Simeon Andreev <simeon.danailov.andreev@gmail.com>
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 fda0bf6..3653b9d 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
@@ -60,7 +60,7 @@
 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]?
-diffToolDied=external diff died, stopping at {0}
+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:
 everythingUpToDate=Everything up-to-date
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 1288817..2f74177 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
@@ -21,8 +21,10 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
-
+import org.eclipse.jgit.diff.ContentSource;
+import org.eclipse.jgit.diff.ContentSource.Pair;
 import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffEntry.Side;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.AmbiguousObjectException;
@@ -30,8 +32,11 @@
 import org.eclipse.jgit.errors.RevisionSyntaxException;
 import org.eclipse.jgit.internal.diffmergetool.DiffTools;
 import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
+import org.eclipse.jgit.internal.diffmergetool.FileElement;
+import org.eclipse.jgit.internal.diffmergetool.ToolException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.ObjectStream;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
@@ -40,8 +45,10 @@
 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.WorkingTreeIterator;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.FS.ExecutionResult;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -145,40 +152,54 @@ protected void run() {
 
 	private void compare(List<DiffEntry> files, boolean showPrompt,
 			String toolNamePrompt) throws IOException {
-		for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
-			DiffEntry ent = files.get(fileIndex);
-			String mergedFilePath = ent.getNewPath();
-			if (mergedFilePath.equals(DiffEntry.DEV_NULL)) {
-				mergedFilePath = ent.getOldPath();
-			}
-			// check if user wants to launch compare
-			boolean launchCompare = true;
-			if (showPrompt) {
-				launchCompare = isLaunchCompare(fileIndex + 1, files.size(),
-						mergedFilePath, toolNamePrompt);
-			}
-			if (launchCompare) {
-				switch (ent.getChangeType()) {
-				case MODIFY:
-					outw.println("M\t" + ent.getNewPath() //$NON-NLS-1$
-							+ " (" + ent.getNewId().name() + ")" //$NON-NLS-1$ //$NON-NLS-2$
-							+ "\t" + ent.getOldPath() //$NON-NLS-1$
-							+ " (" + ent.getOldId().name() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
-					int ret = diffTools.compare(ent.getNewPath(),
-							ent.getOldPath(), ent.getNewId().name(),
-							ent.getOldId().name(), toolName, prompt, gui,
-							trustExitCode);
-					if (ret != 0) {
+		ContentSource.Pair sourcePair = new ContentSource.Pair(source(oldTree),
+				source(newTree));
+		try {
+			for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
+				DiffEntry ent = files.get(fileIndex);
+				String mergedFilePath = ent.getNewPath();
+				if (mergedFilePath.equals(DiffEntry.DEV_NULL)) {
+					mergedFilePath = ent.getOldPath();
+				}
+				FileElement local = new FileElement(ent.getOldPath(),
+						ent.getOldId().name(),
+						getObjectStream(sourcePair, Side.OLD, ent));
+				FileElement remote = new FileElement(ent.getNewPath(),
+						ent.getNewId().name(),
+						getObjectStream(sourcePair, Side.NEW, ent));
+				// check if user wants to launch compare
+				boolean launchCompare = true;
+				if (showPrompt) {
+					launchCompare = isLaunchCompare(fileIndex + 1, files.size(),
+							mergedFilePath, toolNamePrompt);
+				}
+				if (launchCompare) {
+					try {
+						// TODO: check how to return the exit-code of
+						// the
+						// tool
+						// to
+						// jgit / java runtime ?
+						// int rc =...
+						ExecutionResult result = diffTools.compare(db, local,
+								remote, mergedFilePath,
+								toolName, prompt, gui, trustExitCode);
+						outw.println(new String(result.getStdout().toByteArray()));
+						errw.println(
+								new String(result.getStderr().toByteArray()));
+					} catch (ToolException e) {
+						outw.println(e.getResultStdout());
+						outw.flush();
+						errw.println(e.getMessage());
 						throw die(MessageFormat.format(
-								CLIText.get().diffToolDied, mergedFilePath));
+								CLIText.get().diffToolDied, mergedFilePath, e));
 					}
-					break;
-				default:
+				} else {
 					break;
 				}
-			} else {
-				break;
 			}
+		} finally {
+			sourcePair.close();
 		}
 	}
 
@@ -254,4 +275,23 @@ private List<DiffEntry> getFiles()
 		return files;
 	}
 
+	private ObjectStream getObjectStream(Pair pair, Side side, DiffEntry ent) {
+		ObjectStream stream = null;
+		if (!pair.isWorkingTreeSource(side)) {
+			try {
+				stream = pair.open(side, ent).openStream();
+			} catch (Exception e) {
+				stream = null;
+			}
+		}
+		return stream;
+	}
+
+	private ContentSource source(AbstractTreeIterator iterator) {
+		if (iterator instanceof WorkingTreeIterator) {
+			return ContentSource.create((WorkingTreeIterator) iterator);
+		}
+		return ContentSource.create(db.newObjectReader());
+	}
+
 }