Merge "Add filtering with help of DirCacheCheckout.getContent()"
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
index dc34c0d..8daaa6a 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
@@ -21,10 +21,8 @@
 import java.util.Arrays;
 import java.util.List;
 
-import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool;
 import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -49,9 +47,8 @@
 				"y", // accept launching diff tool
 		};
 
-		RevCommit commit = createUnstagedChanges();
-		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedCompareOutput(changes);
+		String[] conflictingFilenames = createUnstagedChanges();
+		String[] expectedOutput = getExpectedCompareOutput(conflictingFilenames);
 
 		String option = "--tool";
 
@@ -68,10 +65,9 @@
 				"n", // don't launch diff tool
 		};
 
-		RevCommit commit = createUnstagedChanges();
-		List<DiffEntry> changes = getRepositoryChanges(commit);
+		String[] conflictingFilenames = createUnstagedChanges();
 		int abortIndex = 1;
-		String[] expectedOutput = getExpectedAbortOutput(changes, abortIndex);
+		String[] expectedOutput = getExpectedAbortOutput(conflictingFilenames, abortIndex);
 
 		String option = "--tool";
 
@@ -92,9 +88,8 @@
 
 	@Test
 	public void testTool() throws Exception {
-		RevCommit commit = createUnstagedChanges();
-		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
+		String[] conflictFilenames = createUnstagedChanges();
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictFilenames);
 
 		String[] options = {
 				"--tool",
@@ -111,9 +106,8 @@
 
 	@Test
 	public void testToolTrustExitCode() throws Exception {
-		RevCommit commit = createUnstagedChanges();
-		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
+		String[] conflictingFilenames = createUnstagedChanges();
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
 
 		String[] options = { "--tool", "-t", };
 
@@ -126,9 +120,8 @@
 
 	@Test
 	public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
-		RevCommit commit = createUnstagedChanges();
-		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
+		String[] conflictingFilenames = createUnstagedChanges();
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
 
 		String[] options = { "--tool", "-t", };
 
@@ -142,9 +135,8 @@
 
 	@Test
 	public void testToolCached() throws Exception {
-		RevCommit commit = createStagedChanges();
-		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedToolOutputNoPrompt(changes);
+		String[] conflictingFilenames = createStagedChanges();
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
 
 		String[] options = { "--cached", "--staged", };
 
@@ -201,23 +193,21 @@
 				String.valueOf(false));
 	}
 
-	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);
-			String newPath = change.getNewPath();
+	private static String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) {
+		String[] expectedToolOutput = new String[conflictingFilenames.length];
+		for (int i = 0; i < conflictingFilenames.length; ++i) {
+			String newPath = conflictingFilenames[i];
 			String expectedLine = newPath;
 			expectedToolOutput[i] = expectedLine;
 		}
 		return expectedToolOutput;
 	}
 
-	private static String[] getExpectedCompareOutput(List<DiffEntry> changes) {
+	private static String[] getExpectedCompareOutput(String[] conflictingFilenames) {
 		List<String> expected = new ArrayList<>();
-		int n = changes.size();
+		int n = conflictingFilenames.length;
 		for (int i = 0; i < n; ++i) {
-			DiffEntry change = changes.get(i);
-			String newPath = change.getNewPath();
+			String newPath = conflictingFilenames[i];
 			expected.add(
 					"Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'");
 			expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
@@ -226,13 +216,12 @@
 		return expected.toArray(new String[0]);
 	}
 
-	private static String[] getExpectedAbortOutput(List<DiffEntry> changes,
+	private static String[] getExpectedAbortOutput(String[] conflictingFilenames,
 			int abortIndex) {
 		List<String> expected = new ArrayList<>();
-		int n = changes.size();
+		int n = conflictingFilenames.length;
 		for (int i = 0; i < n; ++i) {
-			DiffEntry change = changes.get(i);
-			String newPath = change.getNewPath();
+			String newPath = conflictingFilenames[i];
 			expected.add(
 					"Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'");
 			expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
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
index d13eeb7..933f19b 100644
--- 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
@@ -94,15 +94,15 @@
 	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");
+		writeTrashFile("dir1/a", "Hello world a");
+		writeTrashFile("dir2/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");
+		writeTrashFile("dir1/a", "Hello world a 1");
+		writeTrashFile("dir2/b", "Hello world b 1");
 		git.add().addFilepattern(".").call();
 		RevCommit commit1 = git.commit()
 				.setMessage("files a & b modified commit 1").call();
@@ -111,28 +111,28 @@
 		// 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");
+		writeTrashFile("dir1/a", "Hello world a 2");
+		writeTrashFile("dir2/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" };
+		String[] conflictingFilenames = { "dir1/a", "dir2/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");
+		writeTrashFile("dir1/a", "Hello world a");
+		writeTrashFile("dir2/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");
+		writeTrashFile("dir1/a", "Hello world a 1");
+		writeTrashFile("dir2/b", "Hello world b 1");
 		git.add().addFilepattern(".").call();
 		RevCommit commit1 = git.commit()
 				.setMessage("files a & b modified commit 1").call();
@@ -141,29 +141,30 @@
 		// 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.rm().addFilepattern("dir1/a").call();
+		git.rm().addFilepattern("dir2/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" };
+		String[] conflictingFilenames = { "dir1/a", "dir2/b" };
 		return conflictingFilenames;
 	}
 
-	protected RevCommit createUnstagedChanges() throws Exception {
-		writeTrashFile("a", "Hello world a");
-		writeTrashFile("b", "Hello world b");
+	protected String[] createUnstagedChanges() throws Exception {
+		writeTrashFile("dir1/a", "Hello world a");
+		writeTrashFile("dir2/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;
+		git.commit().setMessage("files a & b").call();
+		writeTrashFile("dir1/a", "New Hello world a");
+		writeTrashFile("dir2/b", "New Hello world b");
+		String[] conflictingFilenames = { "dir1/a", "dir2/b" };
+		return conflictingFilenames;
 	}
 
-	protected RevCommit createStagedChanges() throws Exception {
-		RevCommit commit = createUnstagedChanges();
+	protected String[] createStagedChanges() throws Exception {
+		String[] conflictingFilenames = createUnstagedChanges();
 		git.add().addFilepattern(".").call();
-		return commit;
+		return conflictingFilenames;
 	}
 
 	protected List<DiffEntry> getRepositoryChanges(RevCommit commit)
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 ffba36f..74d91cd 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
@@ -11,9 +11,11 @@
 package org.eclipse.jgit.pgm;
 
 import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
 
 import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
@@ -25,27 +27,36 @@
 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;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-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.internal.diffmergetool.DiffTools;
+import org.eclipse.jgit.internal.diffmergetool.FileElement;
+import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
+import org.eclipse.jgit.errors.AmbiguousObjectException;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.NoWorkTreeException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.lib.Constants;
 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.CoreConfig.EolStreamType;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.treewalk.WorkingTreeOptions;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.eclipse.jgit.util.StringUtils;
 import org.eclipse.jgit.util.FS.ExecutionResult;
@@ -164,12 +175,6 @@
 				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) {
@@ -178,15 +183,20 @@
 				}
 				if (launchCompare) {
 					try {
-						// TODO: check how to return the exit-code of
-						// the
-						// tool
-						// to
-						// jgit / java runtime ?
+						FileElement local = createFileElement(
+								FileElement.Type.LOCAL, sourcePair, Side.OLD,
+								ent);
+						FileElement remote = createFileElement(
+								FileElement.Type.REMOTE, sourcePair, Side.NEW,
+								ent);
+						FileElement merged = new FileElement(mergedFilePath,
+								FileElement.Type.MERGED);
+						// 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);
+						ExecutionResult result = diffTools.compare(local,
+								remote, merged, toolName, prompt, gui,
+								trustExitCode);
 						outw.println(new String(result.getStdout().toByteArray()));
 						errw.println(
 								new String(result.getStderr().toByteArray()));
@@ -278,16 +288,46 @@
 		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;
+	private FileElement createFileElement(FileElement.Type elementType,
+			Pair pair, Side side, DiffEntry entry)
+			throws NoWorkTreeException, CorruptObjectException, IOException,
+			ToolException {
+		String entryPath = side == Side.NEW ? entry.getNewPath()
+				: entry.getOldPath();
+		FileElement fileElement = new FileElement(entryPath, elementType);
+		if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) {
+			try (RevWalk revWalk = new RevWalk(db);
+					TreeWalk treeWalk = new TreeWalk(db,
+							revWalk.getObjectReader())) {
+				treeWalk.setFilter(
+						PathFilterGroup.createFromStrings(entryPath));
+				if (side == Side.NEW) {
+					newTree.reset();
+					treeWalk.addTree(newTree);
+				} else {
+					oldTree.reset();
+					treeWalk.addTree(oldTree);
+				}
+				if (treeWalk.next()) {
+					final EolStreamType eolStreamType = treeWalk
+							.getEolStreamType(CHECKOUT_OP);
+					final String filterCommand = treeWalk.getFilterCommand(
+							Constants.ATTR_FILTER_TYPE_SMUDGE);
+					WorkingTreeOptions opt = db.getConfig()
+							.get(WorkingTreeOptions.KEY);
+					CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
+							eolStreamType, filterCommand);
+					DirCacheCheckout.getContent(db, entryPath,
+							checkoutMetadata, pair.open(side, entry), opt,
+							new FileOutputStream(
+									fileElement.createTempFile(null)));
+				} else {
+					throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$
+							+ "' in staging area!", null); //$NON-NLS-1$
+				}
 			}
 		}
-		return stream;
+		return fileElement;
 	}
 
 	private ContentSource source(AbstractTreeIterator iterator) {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
index dce5a79..9712770 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
@@ -10,8 +10,11 @@
 
 package org.eclipse.jgit.pgm;
 
+import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
+
 import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.text.MessageFormat;
@@ -26,8 +29,12 @@
 import org.eclipse.jgit.api.StatusCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.diff.ContentSource;
+import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
 import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheCheckout;
 import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
 import org.eclipse.jgit.errors.NoWorkTreeException;
 import org.eclipse.jgit.errors.RevisionSyntaxException;
 import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
@@ -35,9 +42,15 @@
 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.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeOptions;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.util.FS.ExecutionResult;
 import org.kohsuke.args4j.Argument;
@@ -188,32 +201,67 @@
 		ContentSource baseSource = ContentSource.create(db.newObjectReader());
 		ContentSource localSource = ContentSource.create(db.newObjectReader());
 		ContentSource remoteSource = ContentSource.create(db.newObjectReader());
+		// temporary directory if mergetool.writeToTemp == true
+		File tempDir = mergeTools.createTempDirectory();
+		// the parent directory for temp files (can be same as tempDir or just
+		// the worktree dir)
+		File tempFilesParent = tempDir != null ? tempDir : db.getWorkTree();
 		try {
 			FileElement base = null;
 			FileElement local = null;
 			FileElement remote = null;
+			FileElement merged = new FileElement(mergedFilePath,
+					Type.MERGED);
 			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);
+			try (RevWalk revWalk = new RevWalk(db);
+					TreeWalk treeWalk = new TreeWalk(db,
+							revWalk.getObjectReader())) {
+				treeWalk.setFilter(
+						PathFilterGroup.createFromStrings(mergedFilePath));
+				DirCacheIterator cacheIter = new DirCacheIterator(cache);
+				treeWalk.addTree(cacheIter);
+				while (treeWalk.next()) {
+					if (treeWalk.isSubtree()) {
+						treeWalk.enterSubtree();
+						continue;
+					}
+					final EolStreamType eolStreamType = treeWalk
+							.getEolStreamType(CHECKOUT_OP);
+					final String filterCommand = treeWalk.getFilterCommand(
+							Constants.ATTR_FILTER_TYPE_SMUDGE);
+					WorkingTreeOptions opt = db.getConfig()
+							.get(WorkingTreeOptions.KEY);
+					CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
+							eolStreamType, filterCommand);
+					DirCacheEntry entry = treeWalk.getTree(DirCacheIterator.class).getDirCacheEntry();
+					if (entry == null) {
+						continue;
+					}
 					ObjectId id = entry.getObjectId();
 					switch (entry.getStage()) {
 					case DirCacheEntry.STAGE_1:
-						base = new FileElement(mergedFilePath, id.name(),
-								baseSource.open(mergedFilePath, id)
-										.openStream());
+						base = new FileElement(mergedFilePath, Type.BASE);
+						DirCacheCheckout.getContent(db, mergedFilePath,
+								checkoutMetadata,
+								baseSource.open(mergedFilePath, id), opt,
+								new FileOutputStream(
+										base.createTempFile(tempFilesParent)));
 						break;
 					case DirCacheEntry.STAGE_2:
-						local = new FileElement(mergedFilePath, id.name(),
-								localSource.open(mergedFilePath, id)
-										.openStream());
+						local = new FileElement(mergedFilePath, Type.LOCAL);
+						DirCacheCheckout.getContent(db, mergedFilePath,
+								checkoutMetadata,
+								localSource.open(mergedFilePath, id), opt,
+								new FileOutputStream(
+										local.createTempFile(tempFilesParent)));
 						break;
 					case DirCacheEntry.STAGE_3:
-						remote = new FileElement(mergedFilePath, id.name(),
-								remoteSource.open(mergedFilePath, id)
-										.openStream());
+						remote = new FileElement(mergedFilePath, Type.REMOTE);
+						DirCacheCheckout.getContent(db, mergedFilePath,
+								checkoutMetadata,
+								remoteSource.open(mergedFilePath, id), opt,
+								new FileOutputStream(remote
+										.createTempFile(tempFilesParent)));
 						break;
 					}
 				}
@@ -222,14 +270,13 @@
 				throw die(MessageFormat.format(CLIText.get().mergeToolDied,
 						mergedFilePath));
 			}
-			File merged = new File(mergedFilePath);
-			long modifiedBefore = merged.lastModified();
+			long modifiedBefore = merged.getFile().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);
+				ExecutionResult executionResult = mergeTools.merge(local,
+						remote, merged, base, tempDir, toolName, prompt, gui);
 				outw.println(
 						new String(executionResult.getStdout().toByteArray()));
 				outw.flush();
@@ -250,7 +297,7 @@
 			}
 			// if merge was successful check file modified
 			if (isMergeSuccessful) {
-				long modifiedAfter = merged.lastModified();
+				long modifiedAfter = merged.getFile().lastModified();
 				if (modifiedBefore == modifiedAfter) {
 					outw.println(MessageFormat.format(
 							CLIText.get().mergeToolFileUnchanged,
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
index ebc67c8..4fd55c6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
@@ -54,8 +54,8 @@
 		BooleanTriState gui = BooleanTriState.UNSET;
 		BooleanTriState trustExitCode = BooleanTriState.TRUE;
 
-		manager.compare(db, local, remote, merged.getPath(), toolName, prompt,
-				gui, trustExitCode);
+		manager.compare(local, remote, merged, toolName, prompt, gui,
+				trustExitCode);
 
 		fail("Expected exception to be thrown due to external tool exiting with error code: "
 				+ errorReturnCode);
@@ -78,8 +78,8 @@
 		BooleanTriState gui = BooleanTriState.UNSET;
 		BooleanTriState trustExitCode = BooleanTriState.FALSE;
 
-		manager.compare(db, local, remote, merged.getPath(), toolName, prompt,
-				gui, trustExitCode);
+		manager.compare(local, remote, merged, toolName, prompt, gui,
+				trustExitCode);
 
 		fail("Expected exception to be thrown due to external tool exiting with error code: "
 				+ errorReturnCode);
@@ -183,8 +183,8 @@
 		DiffTools manager = new DiffTools(db);
 
 		int expectedCompareResult = 0;
-		ExecutionResult compareResult = manager.compare(db, local, remote,
-				merged.getPath(), toolName, prompt, gui, trustExitCode);
+		ExecutionResult compareResult = manager.compare(local, remote, merged,
+				toolName, prompt, gui, trustExitCode);
 		assertEquals("Incorrect compare result for external diff tool",
 				expectedCompareResult, compareResult.getRc());
 	}
@@ -263,8 +263,8 @@
 		BooleanTriState gui = BooleanTriState.UNSET;
 		BooleanTriState trustExitCode = BooleanTriState.UNSET;
 
-		manager.compare(db, local, remote, merged.getPath(), toolName, prompt,
-				gui, trustExitCode);
+		manager.compare(local, remote, merged, toolName, prompt, gui,
+				trustExitCode);
 		fail("Expected exception to be thrown due to not defined external diff tool");
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java
index 1dea44e..5057668 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java
@@ -55,8 +55,7 @@
 		BooleanTriState prompt = BooleanTriState.UNSET;
 		BooleanTriState gui = BooleanTriState.UNSET;
 
-		manager.merge(db, local, remote, base, merged.getPath(), toolName,
-				prompt, gui);
+		manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
 
 		fail("Expected exception to be thrown due to external tool exiting with error code: "
 				+ errorReturnCode);
@@ -78,8 +77,7 @@
 		BooleanTriState prompt = BooleanTriState.UNSET;
 		BooleanTriState gui = BooleanTriState.UNSET;
 
-		manager.merge(db, local, remote, base, merged.getPath(), toolName,
-				prompt, gui);
+		manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
 
 		fail("Expected exception to be thrown due to external tool exiting with error code: "
 				+ errorReturnCode);
@@ -182,8 +180,8 @@
 		MergeTools manager = new MergeTools(db);
 
 		int expectedCompareResult = 0;
-		ExecutionResult compareResult = manager.merge(db, local, remote, base,
-				merged.getPath(), toolName, prompt, gui);
+		ExecutionResult compareResult = manager.merge(local, remote, merged,
+				base, null, toolName, prompt, gui);
 		assertEquals("Incorrect compare result for external merge tool",
 				expectedCompareResult, compareResult.getRc());
 	}
@@ -262,8 +260,7 @@
 		BooleanTriState prompt = BooleanTriState.UNSET;
 		BooleanTriState gui = BooleanTriState.UNSET;
 
-		manager.merge(db, local, remote, base, merged.getPath(), toolName,
-				prompt, gui);
+		manager.merge(local, remote, merged, base, null, toolName, prompt, gui);
 		fail("Expected exception to be thrown due to not defined external merge tool");
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java
index 6757eb4..0fd85cb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java
@@ -60,10 +60,14 @@
 		commandResult = writeTrashFile("commandResult.txt", "");
 		commandResult.deleteOnExit();
 
-		local = new FileElement(localFile.getAbsolutePath(), "LOCAL");
-		remote = new FileElement(remoteFile.getAbsolutePath(), "REMOTE");
-		merged = new FileElement(mergedFile.getAbsolutePath(), "MERGED");
-		base = new FileElement(baseFile.getAbsolutePath(), "BASE");
+		local = new FileElement(localFile.getAbsolutePath(),
+				FileElement.Type.LOCAL);
+		remote = new FileElement(remoteFile.getAbsolutePath(),
+				FileElement.Type.REMOTE);
+		merged = new FileElement(mergedFile.getAbsolutePath(),
+				FileElement.Type.MERGED);
+		base = new FileElement(baseFile.getAbsolutePath(),
+				FileElement.Type.BASE);
 	}
 
 	@After
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
index 2f2b9de..1dcc523 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
@@ -12,12 +12,10 @@
 
 import java.util.TreeMap;
 import java.util.Collections;
-import java.io.File;
 import java.io.IOException;
 import java.util.Map;
 import java.util.Set;
 
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
 import org.eclipse.jgit.util.FS.ExecutionResult;
@@ -28,6 +26,8 @@
  */
 public class DiffTools {
 
+	private final Repository repo;
+
 	private final DiffToolConfig config;
 
 	private final Map<String, ExternalDiffTool> predefinedTools;
@@ -41,6 +41,7 @@
 	 *            the repository
 	 */
 	public DiffTools(Repository repo) {
+		this.repo = repo;
 		config = repo.getConfig().get(DiffToolConfig.KEY);
 		predefinedTools = setupPredefinedTools();
 		userDefinedTools = setupUserDefinedTools(config, predefinedTools);
@@ -49,14 +50,13 @@
 	/**
 	 * Compare two versions of a file.
 	 *
-	 * @param repo
-	 *            the repository
 	 * @param localFile
 	 *            the local file element
 	 * @param remoteFile
 	 *            the remote file element
-	 * @param mergedFilePath
-	 *            the path of 'merged' file, it equals local or remote path
+	 * @param mergedFile
+	 *            the merged file element, it's path equals local or remote
+	 *            element path
 	 * @param toolName
 	 *            the selected tool name (can be null)
 	 * @param prompt
@@ -68,36 +68,31 @@
 	 * @return the execution result from tool
 	 * @throws ToolException
 	 */
-	public ExecutionResult compare(Repository repo, FileElement localFile,
-			FileElement remoteFile, String mergedFilePath, String toolName,
+	public ExecutionResult compare(FileElement localFile,
+			FileElement remoteFile, FileElement mergedFile, String toolName,
 			BooleanTriState prompt, BooleanTriState gui,
 			BooleanTriState trustExitCode) throws ToolException {
-		ExternalDiffTool tool = guessTool(toolName, gui);
 		try {
-			File workingDir = repo.getWorkTree();
-			String localFilePath = localFile.getFile().getPath();
-			String remoteFilePath = remoteFile.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$
-			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$
+			// prepare the command (replace the file paths)
+			String command = ExternalToolUtils.prepareCommand(
+					guessTool(toolName, gui).getCommand(), localFile,
+					remoteFile, mergedFile, null);
+			// prepare the environment
+			Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo,
+					localFile, remoteFile, mergedFile, null);
 			boolean trust = config.isTrustExitCode();
 			if (trustExitCode != BooleanTriState.UNSET) {
 				trust = trustExitCode == BooleanTriState.TRUE;
 			}
+			// execute the tool
 			CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
-			return cmdExec.run(command, workingDir, env);
+			return cmdExec.run(command, repo.getWorkTree(), env);
 		} catch (IOException | InterruptedException e) {
 			throw new ToolException(e);
 		} finally {
 			localFile.cleanTemporaries();
 			remoteFile.cleanTemporaries();
+			mergedFile.cleanTemporaries();
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java
new file mode 100644
index 0000000..3efb90c
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * 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.internal.diffmergetool;
+
+import java.util.TreeMap;
+import java.io.IOException;
+import java.util.Map;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Utilities for diff- and merge-tools.
+ */
+public class ExternalToolUtils {
+
+	/**
+	 * Prepare command for execution.
+	 *
+	 * @param command
+	 *            the input "command" string
+	 * @param localFile
+	 *            the local file (ours)
+	 * @param remoteFile
+	 *            the remote file (theirs)
+	 * @param mergedFile
+	 *            the merged file (worktree)
+	 * @param baseFile
+	 *            the base file (can be null)
+	 * @return the prepared (with replaced variables) command string
+	 * @throws IOException
+	 */
+	public static String prepareCommand(String command, FileElement localFile,
+			FileElement remoteFile, FileElement mergedFile,
+			FileElement baseFile) throws IOException {
+		command = localFile.replaceVariable(command);
+		command = remoteFile.replaceVariable(command);
+		command = mergedFile.replaceVariable(command);
+		if (baseFile != null) {
+			command = baseFile.replaceVariable(command);
+		}
+		return command;
+	}
+
+	/**
+	 * Prepare environment needed for execution.
+	 *
+	 * @param repo
+	 *            the repository
+	 * @param localFile
+	 *            the local file (ours)
+	 * @param remoteFile
+	 *            the remote file (theirs)
+	 * @param mergedFile
+	 *            the merged file (worktree)
+	 * @param baseFile
+	 *            the base file (can be null)
+	 * @return the environment map with variables and values (file paths)
+	 * @throws IOException
+	 */
+	public static Map<String, String> prepareEnvironment(Repository repo,
+			FileElement localFile, FileElement remoteFile,
+			FileElement mergedFile, FileElement baseFile) throws IOException {
+		Map<String, String> env = new TreeMap<>();
+		env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath());
+		localFile.addToEnv(env);
+		remoteFile.addToEnv(env);
+		mergedFile.addToEnv(env);
+		if (baseFile != null) {
+			baseFile.addToEnv(env);
+		}
+		return env;
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
index 1ae87aa..5902c1e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
@@ -14,10 +14,11 @@
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.Map;
 
 import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.lib.ObjectStream;
 
 /**
  * The element used as left or right file for compare.
@@ -25,36 +26,71 @@
  */
 public class FileElement {
 
+	/**
+	 * The file element type.
+	 *
+	 */
+	public enum Type {
+		/**
+		 * The local file element (ours).
+		 */
+		LOCAL,
+		/**
+		 * The remote file element (theirs).
+		 */
+		REMOTE,
+		/**
+		 * The merged file element (path in worktree).
+		 */
+		MERGED,
+		/**
+		 * The base file element (of ours and theirs).
+		 */
+		BASE,
+		/**
+		 * The backup file element (copy of merged / conflicted).
+		 */
+		BACKUP
+	}
+
 	private final String path;
 
-	private final String id;
+	private final Type type;
 
-	private ObjectStream stream;
+	private InputStream stream;
 
 	private File tempFile;
 
 	/**
+	 * Creates file element for path.
+	 *
 	 * @param path
 	 *            the file path
-	 * @param id
-	 *            the file id
+	 * @param type
+	 *            the element type
 	 */
-	public FileElement(final String path, final String id) {
-		this(path, id, null);
+	public FileElement(String path, Type type) {
+		this(path, type, null, null);
 	}
 
 	/**
+	 * Creates file element for path.
+	 *
 	 * @param path
 	 *            the file path
-	 * @param id
-	 *            the file id
+	 * @param type
+	 *            the element type
+	 * @param tempFile
+	 *            the temporary file to be used (can be null and will be created
+	 *            then)
 	 * @param stream
 	 *            the object stream to load instead of file
 	 */
-	public FileElement(final String path, final String id,
-			ObjectStream stream) {
+	public FileElement(String path, Type type, File tempFile,
+			InputStream stream) {
 		this.path = path;
-		this.id = id;
+		this.type = type;
+		this.tempFile = tempFile;
 		this.stream = stream;
 	}
 
@@ -66,71 +102,101 @@
 	}
 
 	/**
-	 * @return the file id
+	 * @return the element type
 	 */
-	public String getId() {
-		return id;
+	public Type getType() {
+		return type;
 	}
 
 	/**
-	 * @param stream
-	 *            the object stream
-	 */
-	public void setStream(ObjectStream stream) {
-		this.stream = stream;
-	}
-
-	/**
-	 * Returns a temporary file with in passed working directory and fills it
-	 * with stream if valid.
+	 * Return a temporary file within passed directory and fills it with stream
+	 * if valid.
 	 *
 	 * @param directory
-	 *            the working directory where the temporary file is created
+	 *            the 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 directory, String midName) throws IOException {
-		if (tempFile != null) {
+		if ((tempFile != null) && (stream == null)) {
 			return tempFile;
 		}
-		String[] fileNameAndExtension = splitBaseFileNameAndExtension(
-				new File(path));
-		tempFile = File.createTempFile(
-				fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
-				fileNameAndExtension[1], directory);
-		copyFromStream();
-		return tempFile;
+		tempFile = getTempFile(path, directory, midName);
+		return copyFromStream(tempFile, stream);
 	}
 
 	/**
-	 * Returns a real file from work tree or a temporary file with content if
+	 * Return a real file from work tree or a temporary file with content if
 	 * stream is valid or if path is "/dev/null"
 	 *
 	 * @return the object stream
 	 * @throws IOException
 	 */
 	public File getFile() throws IOException {
-		if (tempFile != null) {
+		if ((tempFile != null) && (stream == null)) {
 			return tempFile;
 		}
 		File file = new File(path);
-		String name = file.getName();
 		// if we have a stream or file is missing ("/dev/null") then create
 		// temporary file
-		if ((stream != null) || path.equals(DiffEntry.DEV_NULL)) {
-			// TODO: avoid long random file name (number generated by
-			// createTempFile)
-			tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$
-			copyFromStream();
-			return tempFile;
+		if ((stream != null) || isNullPath()) {
+			tempFile = getTempFile(file);
+			return copyFromStream(tempFile, stream);
 		}
 		return file;
 	}
 
 	/**
-	 * Deletes and invalidates temporary file if necessary.
+	 * Check if path id "/dev/null"
+	 *
+	 * @return true if path is "/dev/null"
+	 */
+	public boolean isNullPath() {
+		return path.equals(DiffEntry.DEV_NULL);
+	}
+
+	/**
+	 * Create temporary file in given or system temporary directory
+	 *
+	 * @param directory
+	 *            the directory for the file (can be null); if null system
+	 *            temporary directory is used
+	 * @return temporary file in directory or in the system temporary directory
+	 * @throws IOException
+	 */
+	public File createTempFile(File directory) throws IOException {
+		if (tempFile == null) {
+			File file = new File(path);
+			if (directory != null) {
+				tempFile = getTempFile(file, directory, type.name());
+			} else {
+				tempFile = getTempFile(file);
+			}
+		}
+		return tempFile;
+	}
+
+	private static File getTempFile(File file) throws IOException {
+		return File.createTempFile(".__", "__" + file.getName()); //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	private static File getTempFile(File file, File directory, String midName)
+			throws IOException {
+		String[] fileNameAndExtension = splitBaseFileNameAndExtension(file);
+		return File.createTempFile(
+				fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
+				fileNameAndExtension[1], directory);
+	}
+
+	private static File getTempFile(String path, File directory, String midName)
+			throws IOException {
+		return getTempFile(new File(path), directory, midName);
+	}
+
+	/**
+	 * Delete and invalidate temporary file if necessary.
 	 */
 	public void cleanTemporaries() {
 		if (tempFile != null && tempFile.exists())
@@ -138,9 +204,10 @@
 		tempFile = null;
 	}
 
-	private void copyFromStream() throws IOException, FileNotFoundException {
+	private static File copyFromStream(File file, final InputStream stream)
+			throws IOException, FileNotFoundException {
 		if (stream != null) {
-			try (OutputStream outStream = new FileOutputStream(tempFile)) {
+			try (OutputStream outStream = new FileOutputStream(file)) {
 				int read = 0;
 				byte[] bytes = new byte[8 * 1024];
 				while ((read = stream.read(bytes)) != -1) {
@@ -149,23 +216,46 @@
 			} finally {
 				// stream can only be consumed once --> close it
 				stream.close();
-				stream = null;
 			}
 		}
+		return file;
 	}
 
 	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);
-			}
+		int idx = result[0].lastIndexOf("."); //$NON-NLS-1$
+		// if "." was found (>-1) and last-index is not first char (>0), then
+		// split (same behavior like cgit)
+		if (idx > 0) {
+			result[1] = result[0].substring(idx, result[0].length());
+			result[0] = result[0].substring(0, idx);
 		}
 		return result;
 	}
 
+	/**
+	 * Replace variable in input
+	 *
+	 * @param input
+	 *            the input string
+	 * @return the replaced input string
+	 * @throws IOException
+	 */
+	public String replaceVariable(String input) throws IOException {
+		return input.replace("$" + type.name(), getFile().getPath()); //$NON-NLS-1$
+	}
+
+	/**
+	 * Add variable to environment map.
+	 *
+	 * @param env
+	 *            the environment where this element should be added
+	 * @throws IOException
+	 */
+	public void addToEnv(Map<String, String> env) throws IOException {
+		env.put(type.name(), getFile().getPath());
+	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
index c4c2cec..9a2a830 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
@@ -19,7 +19,7 @@
 import java.util.Set;
 import java.util.TreeMap;
 
-import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
 import org.eclipse.jgit.util.FS.ExecutionResult;
@@ -29,6 +29,8 @@
  */
 public class MergeTools {
 
+	Repository repo;
+
 	private final MergeToolConfig config;
 
 	private final Map<String, ExternalMergeTool> predefinedTools;
@@ -37,25 +39,27 @@
 
 	/**
 	 * @param repo
-	 *            the repository database
+	 *            the repository
 	 */
 	public MergeTools(Repository repo) {
+		this.repo = repo;
 		config = repo.getConfig().get(MergeToolConfig.KEY);
 		predefinedTools = setupPredefinedTools();
 		userDefinedTools = setupUserDefinedTools(config, predefinedTools);
 	}
 
 	/**
-	 * @param repo
-	 *            the repository
 	 * @param localFile
 	 *            the local file element
 	 * @param remoteFile
 	 *            the remote file element
+	 * @param mergedFile
+	 *            the merged file element
 	 * @param baseFile
 	 *            the base file element (can be null)
-	 * @param mergedFilePath
-	 *            the path of 'merged' file
+	 * @param tempDir
+	 *            the temporary directory (needed for backup and auto-remove,
+	 *            can be null)
 	 * @param toolName
 	 *            the selected tool name (can be null)
 	 * @param prompt
@@ -65,47 +69,35 @@
 	 * @return the execution result from tool
 	 * @throws ToolException
 	 */
-	public ExecutionResult merge(Repository repo, FileElement localFile,
-			FileElement remoteFile, FileElement baseFile, String mergedFilePath,
+	public ExecutionResult merge(FileElement localFile, FileElement remoteFile,
+			FileElement mergedFile, FileElement baseFile, File tempDir,
 			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();
-			// 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$
-			}
+			backup = createBackupFile(mergedFile.getPath(),
+					tempDir != null ? tempDir : workingDir);
 			// prepare the command (replace the file paths)
 			boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
-			String command = prepareCommand(mergedFilePath, localFilePath,
-					remoteFilePath, baseFilePath,
-					tool.getCommand(baseFile != null));
+			String command = ExternalToolUtils.prepareCommand(
+					tool.getCommand(baseFile != null), localFile, remoteFile,
+					mergedFile, baseFile);
 			// prepare the environment
-			Map<String, String> env = prepareEnvironment(repo, mergedFilePath,
-					localFilePath, remoteFilePath, baseFilePath);
+			Map<String, String> env = ExternalToolUtils.prepareEnvironment(repo,
+					localFile, remoteFile, mergedFile, baseFile);
+			// execute the tool
 			CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust);
 			result = cmdExec.run(command, workingDir, env);
 			// keep backup as .orig file
 			if (backup != null) {
-				keepBackupFile(mergedFilePath, backup);
+				keepBackupFile(mergedFile.getPath(), backup);
 			}
 			return result;
-		} catch (Exception e) {
+		} catch (IOException | InterruptedException e) {
 			throw new ToolException(e);
 		} finally {
 			// always delete backup file (ignore that it was may be already
@@ -131,19 +123,30 @@
 		}
 	}
 
-	private static FileElement createBackupFile(String mergedFilePath,
-			File tempDir) throws IOException {
+	private FileElement createBackupFile(String filePath, File parentDir)
+			throws IOException {
 		FileElement backup = null;
-		Path path = Paths.get(tempDir.getPath(), mergedFilePath);
+		Path path = Paths.get(filePath);
 		if (Files.exists(path)) {
-			backup = new FileElement(mergedFilePath, "NOID", null); //$NON-NLS-1$
-			Files.copy(path, backup.getFile(tempDir, "BACKUP").toPath(), //$NON-NLS-1$
+			backup = new FileElement(filePath, Type.BACKUP);
+			Files.copy(path, backup.createTempFile(parentDir).toPath(),
 					StandardCopyOption.REPLACE_EXISTING);
 		}
 		return backup;
 	}
 
 	/**
+	 * @return the created temporary directory if (mergetol.writeToTemp == true)
+	 *         or null if not configured or false.
+	 * @throws IOException
+	 */
+	public File createTempDirectory() throws IOException {
+		return config.isWriteToTemp()
+				? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$
+				: null;
+	}
+
+	/**
 	 * @return the tool names
 	 */
 	public Set<String> getToolNames() {
@@ -208,27 +211,6 @@
 		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()) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
index 1ae0780..27f7d12 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
@@ -113,7 +113,7 @@
 		try {
 			return new String(result.getStderr().toByteArray());
 		} catch (Exception e) {
-			LOG.warn(e.getMessage());
+			LOG.warn("Failed to retrieve standard error output", e); //$NON-NLS-1$
 		}
 		return ""; //$NON-NLS-1$
 	}
@@ -125,7 +125,7 @@
 		try {
 			return new String(result.getStdout().toByteArray());
 		} catch (Exception e) {
-			LOG.warn(e.getMessage());
+			LOG.warn("Failed to retrieve standard output", e); //$NON-NLS-1$
 		}
 		return ""; //$NON-NLS-1$
 	}