Add filtering with help of DirCacheCheckout.getContent()

see: https://git-scm.com/docs/git-mergetool

* refactoring of content (FileElement) handling
* now the temporary files are already filled with filtered content in
the calling classes (PGM), that can be used with EGit content too

TODO:
 * keep the temporaries when no change detected and the user answers no
to the question if the merge was successful

Bug: 356832
Change-Id: I86a0a052d059957d4d152c1bb94c262902c377d2
Signed-off-by: Andre Bossert <andre.bossert@siemens.com>
This commit is contained in:
Andre Bossert 2020-01-19 20:54:17 +01:00 committed by Andrey Loskutov
parent d128c3112d
commit e81085944f
12 changed files with 487 additions and 261 deletions

View File

@ -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 @@ public void testToolWithPrompt() throws Exception {
"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 @@ public void testToolAbortLaunch() throws Exception {
"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 @@ public void testNotDefinedTool() throws Exception {
@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 @@ public void testTool() throws Exception {
@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 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
@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 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
@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 @@ private void configureEchoTool(String toolName) {
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 @@ private static String[] getExpectedCompareOutput(List<DiffEntry> changes) {
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]?");

View File

@ -94,15 +94,15 @@ protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
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 @@ protected String[] createMergeConflict() throws Exception {
// 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 @@ protected String[] createDeletedConflict() throws Exception {
// 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)

View File

@ -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 @@ private void compare(List<DiffEntry> files, boolean showPrompt,
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 @@ private void compare(List<DiffEntry> files, boolean showPrompt,
}
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 @@ 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;
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) {

View File

@ -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 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
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 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
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 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt,
}
// 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,

View File

@ -54,8 +54,8 @@ public void testUserToolWithError() throws Exception {
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 @@ public void testUserToolWithCommandNotFoundError() throws Exception {
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 @@ public void testCompare() throws ToolException {
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 @@ public void testUndefinedTool() throws Exception {
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");
}

View File

@ -55,8 +55,7 @@ public void testUserToolWithError() throws Exception {
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 @@ public void testUserToolWithCommandNotFoundError() throws Exception {
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 @@ public void testCompare() throws ToolException {
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 @@ public void testUndefinedTool() throws Exception {
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");
}

View File

@ -60,10 +60,14 @@ public void setUp() throws Exception {
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

View File

@ -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 @@ public class DiffTools {
* 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 @@ public DiffTools(Repository repo) {
/**
* 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 @@ public DiffTools(Repository repo) {
* @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();
}
}

View File

@ -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;
}
}

View File

@ -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 @@ public String getPath() {
}
/**
* @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 @@ public void cleanTemporaries() {
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 @@ private void copyFromStream() throws IOException, FileNotFoundException {
} 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());
}
}

View File

@ -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 @@ public class MergeTools {
/**
* @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 @@ public MergeTools(Repository repo) {
* @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,18 +123,29 @@ public ExecutionResult merge(Repository repo, FileElement localFile,
}
}
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
*/
@ -208,27 +211,6 @@ private ExternalMergeTool getTool(final String name) {
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()) {

View File

@ -113,7 +113,7 @@ public String getResultStderr() {
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 @@ public String getResultStdout() {
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$
}