Teach JGit to handle external diff/merge tools defined in .gitattributes

Adds API that allows UI to find (and handle) diff/merge tools, specific
for the given path. The assumption is that user can specify file type
specific diff/merge tools via gitattributes.

Bug: 552840
Change-Id: I1daa091e9afa542a9ebb5417853dff0452ed52dd
Signed-off-by: Mykola Zakharchuk <zakharchuk.vn@gmail.com>
Signed-off-by: Andrey Loskutov <loskutov@gmx.de>
Signed-off-by: Andre Bossert <andre.bossert@siemens.com>
This commit is contained in:
Andre Bossert 2020-01-21 10:13:43 +01:00 committed by Andrey Loskutov
parent ff77d412a9
commit c32694e5ae
8 changed files with 382 additions and 12 deletions

View File

@ -79,7 +79,7 @@ public void testUserToolWithCommandNotFoundError() throws Exception {
+ errorReturnCode);
}
@Test
@Test(expected = Die.class)
public void testEmptyToolName() throws Exception {
String emptyToolName = "";
@ -95,6 +95,7 @@ public void testEmptyToolName() throws Exception {
String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput), DIFF_TOOL,
"--no-prompt");
fail("Expected exception to be thrown due to external tool exiting with an error");
}
@Test

View File

@ -24,6 +24,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
@ -34,6 +35,8 @@
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS.ExecutionResult;
@ -127,13 +130,20 @@ public void testUserDefinedToolWithPrompt() throws Exception {
@Test
public void testUserDefinedToolWithCancelledPrompt() throws Exception {
String command = getEchoCommand();
FileBasedConfig config = db.getConfig();
String customToolName = "customTool";
config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
CONFIG_KEY_CMD, command);
DiffTools manager = new DiffTools(db);
PromptHandler promptHandler = PromptHandler.cancelPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
Optional<ExecutionResult> result = manager.compare(local, remote,
Optional.empty(), BooleanTriState.TRUE, false,
Optional.of(customToolName), BooleanTriState.TRUE, false,
BooleanTriState.TRUE, promptHandler, noToolHandler);
assertFalse("Expected no result if user cancels the operation",
result.isPresent());
@ -245,8 +255,9 @@ public void testDefaultTool() throws Exception {
gui = true;
String defaultGuiToolName = manager.getDefaultToolName(gui);
assertNull("Expected default difftool to not be set",
defaultGuiToolName);
assertEquals(
"Expected default gui difftool to be the default tool if no gui tool is set",
toolName, defaultGuiToolName);
config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL,
guiToolName);
@ -296,6 +307,119 @@ public void testUndefinedTool() throws Exception {
fail("Expected exception to be thrown due to not defined external diff tool");
}
@Test
public void testDefaultToolExecutionWithPrompt() throws Exception {
FileBasedConfig config = db.getConfig();
// the default diff tool is configured without a subsection
String subsection = null;
config.setString("diff", subsection, "tool", "customTool");
String command = getEchoCommand();
config.setString("difftool", "customTool", "cmd", command);
DiffTools manager = new DiffTools(db);
PromptHandler promptHandler = PromptHandler.acceptPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
manager.compare(local, remote, Optional.empty(), BooleanTriState.TRUE,
false, BooleanTriState.TRUE, promptHandler, noToolHandler);
assertEchoCommandHasCorrectOutput();
}
@Test
public void testNoDefaultToolName() {
DiffTools manager = new DiffTools(db);
boolean gui = false;
String defaultToolName = manager.getDefaultToolName(gui);
assertNull("Expected no default tool when none is configured",
defaultToolName);
gui = true;
defaultToolName = manager.getDefaultToolName(gui);
assertNull("Expected no default tool when none is configured",
defaultToolName);
}
@Test
public void testExternalToolInGitAttributes() throws Exception {
String content = "attributes:\n*.txt difftool=customTool";
File gitattributes = writeTrashFile(".gitattributes", content);
gitattributes.deleteOnExit();
try (TestRepository<Repository> testRepository = new TestRepository<>(
db)) {
FileBasedConfig config = db.getConfig();
config.setString("difftool", "customTool", "cmd", "echo");
testRepository.git().add().addFilepattern(localFile.getName())
.call();
testRepository.git().add().addFilepattern(".gitattributes").call();
testRepository.branch("master").commit().message("first commit")
.create();
DiffTools manager = new DiffTools(db);
Optional<String> tool = manager
.getExternalToolFromAttributes(localFile.getName());
assertTrue("Failed to find user defined tool", tool.isPresent());
assertEquals("Failed to find user defined tool", "customTool",
tool.get());
} finally {
Files.delete(gitattributes.toPath());
}
}
@Test
public void testNotExternalToolInGitAttributes() throws Exception {
String content = "";
File gitattributes = writeTrashFile(".gitattributes", content);
gitattributes.deleteOnExit();
try (TestRepository<Repository> testRepository = new TestRepository<>(
db)) {
FileBasedConfig config = db.getConfig();
config.setString("difftool", "customTool", "cmd", "echo");
testRepository.git().add().addFilepattern(localFile.getName())
.call();
testRepository.git().add().addFilepattern(".gitattributes").call();
testRepository.branch("master").commit().message("first commit")
.create();
DiffTools manager = new DiffTools(db);
Optional<String> tool = manager
.getExternalToolFromAttributes(localFile.getName());
assertFalse(
"Expected no external tool if no default tool is specified in .gitattributes",
tool.isPresent());
} finally {
Files.delete(gitattributes.toPath());
}
}
@Test(expected = ToolException.class)
public void testNullTool() throws Exception {
DiffTools manager = new DiffTools(db);
boolean trustExitCode = true;
ExternalDiffTool tool = null;
manager.compare(local, remote, tool, trustExitCode);
}
@Test(expected = ToolException.class)
public void testNullToolWithPrompt() throws Exception {
DiffTools manager = new DiffTools(db);
PromptHandler promptHandler = PromptHandler.cancelPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
Optional<String> tool = null;
manager.compare(local, remote, tool, BooleanTriState.TRUE, false,
BooleanTriState.TRUE, promptHandler, noToolHandler);
}
private Optional<ExecutionResult> invokeCompare(String toolName)
throws ToolException {
DiffTools manager = new DiffTools(db);

View File

@ -329,6 +329,68 @@ public void testUndefinedTool() throws Exception {
fail("Expected exception to be thrown due to not defined external merge tool");
}
@Test
public void testDefaultToolExecutionWithPrompt() throws Exception {
FileBasedConfig config = db.getConfig();
// the default diff tool is configured without a subsection
String subsection = null;
config.setString("merge", subsection, "tool", "customTool");
String command = getEchoCommand();
config.setString("mergetool", "customTool", "cmd", command);
MergeTools manager = new MergeTools(db);
PromptHandler promptHandler = PromptHandler.acceptPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
manager.merge(local, remote, merged, base, null, Optional.empty(),
BooleanTriState.TRUE, false, promptHandler, noToolHandler);
assertEchoCommandHasCorrectOutput();
}
@Test
public void testNoDefaultToolName() {
MergeTools manager = new MergeTools(db);
boolean gui = false;
String defaultToolName = manager.getDefaultToolName(gui);
assertNull("Expected no default tool when none is configured",
defaultToolName);
gui = true;
defaultToolName = manager.getDefaultToolName(gui);
assertNull("Expected no default tool when none is configured",
defaultToolName);
}
@Test(expected = ToolException.class)
public void testNullTool() throws Exception {
MergeTools manager = new MergeTools(db);
PromptHandler promptHandler = null;
MissingToolHandler noToolHandler = null;
Optional<String> tool = null;
manager.merge(local, remote, merged, base, null, tool,
BooleanTriState.TRUE, false, promptHandler, noToolHandler);
}
@Test(expected = ToolException.class)
public void testNullToolWithPrompt() throws Exception {
MergeTools manager = new MergeTools(db);
PromptHandler promptHandler = PromptHandler.cancelPrompt();
MissingToolHandler noToolHandler = new MissingToolHandler();
Optional<String> tool = null;
manager.merge(local, remote, merged, base, null, tool,
BooleanTriState.TRUE, false, promptHandler, noToolHandler);
}
private Optional<ExecutionResult> invokeMerge(String toolName)
throws ToolException {
BooleanTriState prompt = BooleanTriState.UNSET;

View File

@ -237,6 +237,9 @@ deleteTagUnexpectedResult=Delete tag returned unexpected result {0}
deletingNotSupported=Deleting {0} not supported.
destinationIsNotAWildcard=Destination is not a wildcard.
detachedHeadDetected=HEAD is detached
diffToolNotGivenError=No diff tool provided and no defaults configured.
diffToolNotSpecifiedInGitAttributesError=Diff tool specified in git attributes cannot be found.
diffToolNullError=Parameter for diff tool cannot be null.
dirCacheDoesNotHaveABackingFile=DirCache does not have a backing file
dirCacheFileIsNotLocked=DirCache {0} not locked
dirCacheIsNotLocked=DirCache is not locked
@ -457,6 +460,8 @@ mergeStrategyDoesNotSupportHeads=merge strategy {0} does not support {1} heads t
mergeUsingStrategyResultedInDescription=Merge of revisions {0} with base {1} using strategy {2} resulted in: {3}. {4}
mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1}
mergeRecursiveTooManyMergeBasesFor = "More than {0} merge bases for:\n a {1}\n b {2} found:\n count {3}"
mergeToolNotGivenError=No merge tool provided and no defaults configured.
mergeToolNullError=Parameter for merge tool cannot be null.
messageAndTaggerNotAllowedInUnannotatedTags = Unannotated tags cannot have a message or tagger
minutesAgo={0} minutes ago
mismatchOffset=mismatch offset for object {0}

View File

@ -265,6 +265,9 @@ public static JGitText get() {
/***/ public String deletingNotSupported;
/***/ public String destinationIsNotAWildcard;
/***/ public String detachedHeadDetected;
/***/ public String diffToolNotGivenError;
/***/ public String diffToolNotSpecifiedInGitAttributesError;
/***/ public String diffToolNullError;
/***/ public String dirCacheDoesNotHaveABackingFile;
/***/ public String dirCacheFileIsNotLocked;
/***/ public String dirCacheIsNotLocked;
@ -485,6 +488,8 @@ public static JGitText get() {
/***/ public String mergeUsingStrategyResultedInDescription;
/***/ public String mergeRecursiveConflictsWhenMergingCommonAncestors;
/***/ public String mergeRecursiveTooManyMergeBasesFor;
/***/ public String mergeToolNotGivenError;
/***/ public String mergeToolNullError;
/***/ public String messageAndTaggerNotAllowedInUnannotatedTags;
/***/ public String minutesAgo;
/***/ public String mismatchOffset;

View File

@ -13,18 +13,22 @@
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.eclipse.jgit.util.StringUtils;
/**
* Manages diff tools.
@ -39,6 +43,8 @@ public class DiffTools {
private final DiffToolConfig config;
private final Repository repo;
private final Map<String, ExternalDiffTool> predefinedTools;
private final Map<String, ExternalDiffTool> userDefinedTools;
@ -64,6 +70,7 @@ public DiffTools(StoredConfig config) {
}
private DiffTools(Repository repo, StoredConfig config) {
this.repo = repo;
this.config = config.get(DiffToolConfig.KEY);
this.gitDir = repo == null ? null : repo.getDirectory();
this.fs = repo == null ? FS.DETECTED : repo.getFS();
@ -108,15 +115,18 @@ public Optional<ExecutionResult> compare(FileElement localFile,
String toolNameToUse;
if (toolName == null) {
throw new ToolException(JGitText.get().diffToolNullError);
}
if (toolName.isPresent()) {
toolNameToUse = toolName.get();
} else {
toolNameToUse = getDefaultToolName(gui);
}
if (toolNameToUse == null || toolNameToUse.isEmpty()) {
noToolHandler.inform(new ArrayList<>(predefinedTools.keySet()));
toolNameToUse = getFirstAvailableTool();
}
if (StringUtils.isEmptyOrNull(toolNameToUse)) {
throw new ToolException(JGitText.get().diffToolNotGivenError);
}
boolean doPrompt;
@ -167,6 +177,10 @@ public ExecutionResult compare(FileElement localFile,
FileElement remoteFile, ExternalDiffTool tool,
boolean trustExitCode) throws ToolException {
try {
if (tool == null) {
throw new ToolException(JGitText
.get().diffToolNotSpecifiedInGitAttributesError);
}
// prepare the command (replace the file paths)
String command = ExternalToolUtils.prepareCommand(tool.getCommand(),
localFile, remoteFile, null, null);
@ -217,6 +231,42 @@ public Set<String> getAllToolNames() {
getUserDefinedToolNames(), getPredefinedToolNames());
}
/**
* Provides {@link Optional} with the name of an external diff tool if
* specified in git configuration for a path.
*
* The formed git configuration results from global rules as well as merged
* rules from info and worktree attributes.
*
* Triggers {@link TreeWalk} until specified path found in the tree.
*
* @param path
* path to the node in repository to parse git attributes for
* @return name of the difftool if set
* @throws ToolException
*/
public Optional<String> getExternalToolFromAttributes(final String path)
throws ToolException {
return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
ExternalToolUtils.KEY_DIFF_TOOL);
}
/**
* Checks the availability of the predefined tools in the system.
*
* @return set of predefined available tools
*/
public Set<String> getPredefinedAvailableTools() {
Map<String, ExternalDiffTool> defTools = getPredefinedTools(true);
Set<String> availableTools = new LinkedHashSet<>();
for (Entry<String, ExternalDiffTool> elem : defTools.entrySet()) {
if (elem.getValue().isAvailable()) {
availableTools.add(elem.getKey());
}
}
return availableTools;
}
/**
* Get user defined tools map.
*
@ -272,8 +322,14 @@ public String getFirstAvailableTool() {
* @return the default tool name
*/
public String getDefaultToolName(boolean gui) {
return gui ? config.getDefaultGuiToolName()
: config.getDefaultToolName();
String guiToolName;
if (gui) {
guiToolName = config.getDefaultGuiToolName();
if (guiToolName != null) {
return guiToolName;
}
}
return config.getDefaultToolName();
}
/**

View File

@ -14,9 +14,17 @@
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.attributes.Attributes;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
import org.eclipse.jgit.util.FS;
/**
@ -24,6 +32,16 @@
*/
public class ExternalToolUtils {
/**
* Key for merge tool git configuration section
*/
public static final String KEY_MERGE_TOOL = "mergetool"; //$NON-NLS-1$
/**
* Key for diff tool git configuration section
*/
public static final String KEY_DIFF_TOOL = "difftool"; //$NON-NLS-1$
/**
* Prepare command for execution.
*
@ -174,4 +192,51 @@ public static Set<String> createSortedToolSet(String defaultName,
return names;
}
/**
* Provides {@link Optional} with the name of an external tool if specified
* in git configuration for a path.
*
* The formed git configuration results from global rules as well as merged
* rules from info and worktree attributes.
*
* Triggers {@link TreeWalk} until specified path found in the tree.
*
* @param repository
* target repository to traverse into
* @param path
* path to the node in repository to parse git attributes for
* @param toolKey
* config key name for the tool
* @return attribute value for the given tool key if set
* @throws ToolException
*/
public static Optional<String> getExternalToolFromAttributes(
final Repository repository, final String path,
final String toolKey) throws ToolException {
try {
WorkingTreeIterator treeIterator = new FileTreeIterator(repository);
try (TreeWalk walk = new TreeWalk(repository)) {
walk.addTree(treeIterator);
walk.setFilter(new NotIgnoredFilter(0));
while (walk.next()) {
String treePath = walk.getPathString();
if (treePath.equals(path)) {
Attributes attrs = walk.getAttributes();
if (attrs.containsKey(toolKey)) {
return Optional.of(attrs.getValue(toolKey));
}
}
if (walk.isSubtree()) {
walk.enterSubtree();
}
}
// no external tool specified
return Optional.empty();
}
} catch (RevisionSyntaxException | IOException e) {
throw new ToolException(e);
}
}
}

View File

@ -18,16 +18,21 @@
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.internal.BooleanTriState;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.StringUtils;
import org.eclipse.jgit.util.FS.ExecutionResult;
/**
@ -43,6 +48,8 @@ public class MergeTools {
private final MergeToolConfig config;
private final Repository repo;
private final Map<String, ExternalMergeTool> predefinedTools;
private final Map<String, ExternalMergeTool> userDefinedTools;
@ -68,6 +75,7 @@ public MergeTools(StoredConfig config) {
}
private MergeTools(Repository repo, StoredConfig config) {
this.repo = repo;
this.config = config.get(MergeToolConfig.KEY);
this.gitDir = repo == null ? null : repo.getDirectory();
this.fs = repo == null ? FS.DETECTED : repo.getFS();
@ -116,17 +124,25 @@ public Optional<ExecutionResult> merge(FileElement localFile,
String toolNameToUse;
if (toolName == null) {
throw new ToolException(JGitText.get().diffToolNullError);
}
if (toolName.isPresent()) {
toolNameToUse = toolName.get();
} else {
toolNameToUse = getDefaultToolName(gui);
if (toolNameToUse == null || toolNameToUse.isEmpty()) {
if (StringUtils.isEmptyOrNull(toolNameToUse)) {
noToolHandler.inform(new ArrayList<>(predefinedTools.keySet()));
toolNameToUse = getFirstAvailableTool();
}
}
if (StringUtils.isEmptyOrNull(toolNameToUse)) {
throw new ToolException(JGitText.get().diffToolNotGivenError);
}
boolean doPrompt;
if (prompt != BooleanTriState.UNSET) {
doPrompt = prompt == BooleanTriState.TRUE;
@ -276,6 +292,42 @@ public Set<String> getAllToolNames() {
getUserDefinedToolNames(), getPredefinedToolNames());
}
/**
* Provides {@link Optional} with the name of an external merge tool if
* specified in git configuration for a path.
*
* The formed git configuration results from global rules as well as merged
* rules from info and worktree attributes.
*
* Triggers {@link TreeWalk} until specified path found in the tree.
*
* @param path
* path to the node in repository to parse git attributes for
* @return name of the difftool if set
* @throws ToolException
*/
public Optional<String> getExternalToolFromAttributes(final String path)
throws ToolException {
return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
ExternalToolUtils.KEY_MERGE_TOOL);
}
/**
* Checks the availability of the predefined tools in the system.
*
* @return set of predefined available tools
*/
public Set<String> getPredefinedAvailableTools() {
Map<String, ExternalMergeTool> defTools = getPredefinedTools(true);
Set<String> availableTools = new LinkedHashSet<>();
for (Entry<String, ExternalMergeTool> elem : defTools.entrySet()) {
if (elem.getValue().isAvailable()) {
availableTools.add(elem.getKey());
}
}
return availableTools;
}
/**
* @return the user defined tools
*/