Add command line support for "git difftool"
see: http://git-scm.com/docs/git-difftool
* add command line support for "jgit difftool"
* show supported commands with "jgit difftool --help"
* added "git difftool --tool-help" to show the tools (empty now)
* prepare for all other commands
Bug: 356832
Change-Id: Ice0c13ef7953a20feaf25e7746d62b94ff4e89e5
Signed-off-by: Andre Bossert <andre.bossert@siemens.com>
Signed-off-by: Simeon Andreev <simeon.danailov.andreev@gmail.com>
diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
index 2c53da8..3e0a4ea 100644
--- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
@@ -15,6 +15,7 @@
org.eclipse.jgit.internal.storage.file;version="6.1.0",
org.eclipse.jgit.junit;version="[6.1.0,6.2.0)",
org.eclipse.jgit.lib;version="[6.1.0,6.2.0)",
+ org.eclipse.jgit.lib.internal;version="[6.1.0,6.2.0)",
org.eclipse.jgit.merge;version="[6.1.0,6.2.0)",
org.eclipse.jgit.pgm;version="[6.1.0,6.2.0)",
org.eclipse.jgit.pgm.internal;version="[6.1.0,6.2.0)",
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
new file mode 100644
index 0000000..2ce50c7
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.pgm;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.eclipse.jgit.pgm.opt.CmdLineParser;
+import org.eclipse.jgit.pgm.opt.SubcommandHandler;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Before;
+import org.junit.Test;
+import org.kohsuke.args4j.Argument;
+
+/**
+ * Testing the {@code difftool} command.
+ */
+public class DiffToolTest extends CLIRepositoryTestCase {
+ public static class GitCliJGitWrapperParser {
+ @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
+ TextBuiltin subcommand;
+
+ @Argument(index = 1, metaVar = "metaVar_arg")
+ List<String> arguments = new ArrayList<>();
+ }
+
+ private String[] runAndCaptureUsingInitRaw(String... args)
+ throws Exception {
+ CLIGitCommand.Result result = new CLIGitCommand.Result();
+
+ GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
+ CmdLineParser clp = new CmdLineParser(bean);
+ clp.parseArgument(args);
+
+ TextBuiltin cmd = bean.subcommand;
+ cmd.initRaw(db, null, null, result.out, result.err);
+ cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()]));
+ if (cmd.getOutputWriter() != null) {
+ cmd.getOutputWriter().flush();
+ }
+ if (cmd.getErrorWriter() != null) {
+ cmd.getErrorWriter().flush();
+ }
+ return result.outLines().toArray(new String[0]);
+ }
+
+ private Git git;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ git = new Git(db);
+ git.commit().setMessage("initial commit").call();
+ }
+
+ @Test
+ public void testTool() throws Exception {
+ RevCommit commit = createUnstagedChanges();
+ List<DiffEntry> changes = getRepositoryChanges(commit);
+ String[] expectedOutput = getExpectedDiffToolOutput(changes);
+
+ String[] options = {
+ "--tool",
+ "-t",
+ };
+
+ for (String option : options) {
+ assertArrayOfLinesEquals("Incorrect output for option: " + option,
+ expectedOutput,
+ runAndCaptureUsingInitRaw("difftool", option,
+ "some_tool"));
+ }
+ }
+
+ @Test
+ public void testToolTrustExitCode() throws Exception {
+ RevCommit commit = createUnstagedChanges();
+ List<DiffEntry> changes = getRepositoryChanges(commit);
+ String[] expectedOutput = getExpectedDiffToolOutput(changes);
+
+ String[] options = { "--tool", "-t", };
+
+ for (String option : options) {
+ assertArrayOfLinesEquals("Incorrect output for option: " + option,
+ expectedOutput, runAndCaptureUsingInitRaw("difftool",
+ "--trust-exit-code", option, "some_tool"));
+ }
+ }
+
+ @Test
+ public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
+ RevCommit commit = createUnstagedChanges();
+ List<DiffEntry> changes = getRepositoryChanges(commit);
+ String[] expectedOutput = getExpectedDiffToolOutput(changes);
+
+ String[] options = { "--tool", "-t", };
+
+ for (String option : options) {
+ assertArrayOfLinesEquals("Incorrect output for option: " + option,
+ expectedOutput, runAndCaptureUsingInitRaw("difftool",
+ "--no-gui", "--no-prompt", "--no-trust-exit-code",
+ option, "some_tool"));
+ }
+ }
+
+ @Test
+ public void testToolCached() throws Exception {
+ RevCommit commit = createStagedChanges();
+ List<DiffEntry> changes = getRepositoryChanges(commit);
+ String[] expectedOutput = getExpectedDiffToolOutput(changes);
+
+ String[] options = { "--cached", "--staged", };
+
+ for (String option : options) {
+ assertArrayOfLinesEquals("Incorrect output for option: " + option,
+ expectedOutput, runAndCaptureUsingInitRaw("difftool",
+ option, "--tool", "some_tool"));
+ }
+ }
+
+ @Test
+ public void testToolHelp() throws Exception {
+ String[] expectedOutput = {
+ "git difftool --tool=<tool> may be set to one of the following:",
+ "user-defined:",
+ "The following tools are valid, but not currently available:",
+ "Some of the tools listed above only work in a windowed",
+ "environment. If run in a terminal-only session, they will fail.", };
+
+ String option = "--tool-help";
+ assertArrayOfLinesEquals("Incorrect output for option: " + option,
+ expectedOutput, runAndCaptureUsingInitRaw("difftool", option));
+ }
+
+ private RevCommit createUnstagedChanges() throws Exception {
+ writeTrashFile("a", "Hello world a");
+ writeTrashFile("b", "Hello world b");
+ git.add().addFilepattern(".").call();
+ RevCommit commit = git.commit().setMessage("files a & b").call();
+ writeTrashFile("a", "New Hello world a");
+ writeTrashFile("b", "New Hello world b");
+ return commit;
+ }
+
+ private RevCommit createStagedChanges() throws Exception {
+ RevCommit commit = createUnstagedChanges();
+ git.add().addFilepattern(".").call();
+ return commit;
+ }
+
+ private List<DiffEntry> getRepositoryChanges(RevCommit commit)
+ throws Exception {
+ TreeWalk tw = new TreeWalk(db);
+ tw.addTree(commit.getTree());
+ FileTreeIterator modifiedTree = new FileTreeIterator(db);
+ tw.addTree(modifiedTree);
+ List<DiffEntry> changes = DiffEntry.scan(tw);
+ return changes;
+ }
+
+ private String[] getExpectedDiffToolOutput(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();
+ String oldPath = change.getOldPath();
+ String newIdName = change.getNewId().name();
+ String oldIdName = change.getOldId().name();
+ String expectedLine = "M\t" + newPath + " (" + newIdName + ")"
+ + "\t" + oldPath + " (" + oldIdName + ")";
+ expectedToolOutput[i] = expectedLine;
+ }
+ return expectedToolOutput;
+ }
+
+ private static void assertArrayOfLinesEquals(String failMessage,
+ String[] expected, String[] actual) {
+ assertEquals(failMessage, toString(expected), toString(actual));
+ }
+}
diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
index 1ebd3a3..fa0f452 100644
--- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
@@ -24,6 +24,7 @@
org.eclipse.jgit.errors;version="[6.1.0,6.2.0)",
org.eclipse.jgit.gitrepo;version="[6.1.0,6.2.0)",
org.eclipse.jgit.internal.storage.file;version="[6.1.0,6.2.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="[6.1.0,6.2.0)",
org.eclipse.jgit.internal.storage.io;version="[6.1.0,6.2.0)",
org.eclipse.jgit.internal.storage.pack;version="[6.1.0,6.2.0)",
org.eclipse.jgit.internal.storage.reftable;version="[6.1.0,6.2.0)",
@@ -33,6 +34,7 @@
org.eclipse.jgit.lfs.server.s3;version="[6.1.0,6.2.0)",
org.eclipse.jgit.lib;version="[6.1.0,6.2.0)",
org.eclipse.jgit.merge;version="[6.1.0,6.2.0)",
+ org.eclipse.jgit.lib.internal;version="[6.1.0,6.2.0)",
org.eclipse.jgit.nls;version="[6.1.0,6.2.0)",
org.eclipse.jgit.notes;version="[6.1.0,6.2.0)",
org.eclipse.jgit.revplot;version="[6.1.0,6.2.0)",
diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
index e645255..8c44764 100644
--- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
+++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
@@ -12,6 +12,7 @@
org.eclipse.jgit.pgm.Daemon
org.eclipse.jgit.pgm.Describe
org.eclipse.jgit.pgm.Diff
+org.eclipse.jgit.pgm.DiffTool
org.eclipse.jgit.pgm.DiffTree
org.eclipse.jgit.pgm.Fetch
org.eclipse.jgit.pgm.Gc
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index 9745003..d51daaf 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -58,6 +58,9 @@
dateInfo=Date: {0}
deletedBranch=Deleted branch {0}
deletedRemoteBranch=Deleted remote branch {0}
+diffToolHelpSetToFollowing='git difftool --tool=<tool>' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
+diffToolLaunch=Viewing ({0}/{1}): '{2}'\nLaunch '{3}' [Y/n]?
+diffToolDied=external diff died, stopping at {0}
doesNotExist={0} does not exist
dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge:
everythingUpToDate=Everything up-to-date
@@ -145,6 +148,7 @@
metaVar_seconds=SECONDS
metaVar_service=SERVICE
metaVar_tagLocalUser=<GPG key ID>
+metaVar_tool=TOOL
metaVar_treeish=tree-ish
metaVar_uriish=uri-ish
metaVar_url=URL
@@ -249,6 +253,8 @@
usage_DisplayTheVersionOfJgit=Display the version of jgit
usage_Gc=Cleanup unnecessary files and optimize the local repository
usage_Glog=View commit history as a graph
+usage_DiffGuiTool=When git-difftool is invoked with the -g or --gui option the default diff tool will be read from the configured diff.guitool variable instead of diff.tool.
+usage_noGui=The --no-gui option can be used to override -g or --gui setting.
usage_IndexPack=Build pack index file for an existing packed archive
usage_LFSDirectory=Directory to store large objects
usage_LFSPort=Server http port
@@ -295,6 +301,7 @@
usage_Status=Show the working tree status
usage_StopTrackingAFile=Stop tracking a file
usage_TextHashFunctions=Scan repository to compute maximum number of collisions for hash functions
+usage_ToolForDiff=Use the diff tool specified by <tool>. Run git difftool --tool-help for the list of valid <tool> settings.\nIf a diff tool is not specified, git difftool will use the configuration variable diff.tool.
usage_UpdateRemoteRepositoryFromLocalRefs=Update remote repository from local refs
usage_UseAll=Use all refs found in refs/
usage_UseTags=Use any tag including lightweight tags
@@ -341,6 +348,7 @@
usage_date=date format, one of default, rfc, local, iso, short, raw (as defined by git-log(1) ), locale or localelocal (jgit extensions)
usage_detectRenames=detect renamed files
usage_diffAlgorithm=the diff algorithm to use. Currently supported are: 'myers', 'histogram'
+usage_DiffTool=git difftool is a Git command that allows you to compare and edit files between revisions using common diff tools.\ngit difftool is a frontend to git diff and accepts the same options and arguments.
usage_directoriesToExport=directories to export
usage_disableTheServiceInAllRepositories=disable the service in all repositories
usage_displayAListOfAllRegisteredJgitCommands=Display a list of all registered jgit commands
@@ -395,6 +403,8 @@
usage_performFsckStyleChecksOnReceive=perform fsck style checks on receive
usage_portNumberToListenOn=port number to listen on
usage_printOnlyBranchesThatContainTheCommit=print only branches that contain the commit
+usage_prompt=Prompt before each invocation of the diff tool. This is the default behaviour; the option is provided to override any configuration settings.
+usage_noPrompt=Do not prompt before launching a diff tool.
usage_pruneStaleTrackingRefs=prune stale tracking refs
usage_pushUrls=push URLs are manipulated
usage_quiet=don't show progress messages
@@ -422,6 +432,8 @@
usage_sshDriver=Selects the built-in ssh library to use, JSch or Apache MINA sshd.
usage_symbolicVersionForTheProject=Symbolic version for the project
usage_tags=fetch all tags
+usage_trustExitCode=git-difftool invokes a diff tool individually on each file. Errors reported by the diff tool are ignored by default. Use --trust-exit-code to make git-difftool exit when an invoked diff tool returns a non-zero exit code.\ngit-difftool will forward the exit code of the invoked tool when --trust-exit-code is used.
+usage_noTrustExitCode=This option can be used to override --trust-exit-code setting.
usage_notags=do not fetch tags
usage_tagAnnotated=create an annotated tag, unsigned unless -s or -u are given, or config tag.gpgSign is true
usage_tagDelete=delete tag
@@ -430,6 +442,7 @@
usage_tagSign=create a signed annotated tag
usage_tagNoSign=suppress signing the tag
usage_tagVerify=Verify the GPG signature
+usage_toolHelp=Print a list of diff tools that may be used with --tool.
usage_untrackedFilesMode=show untracked files
usage_updateRef=reference to update
usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repository
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
new file mode 100644
index 0000000..9fc26c9
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
@@ -0,0 +1,255 @@
+/*
+ * 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.pgm;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jgit.diff.DiffEntry;
+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.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+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.treewalk.AbstractTreeIterator;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.StringUtils;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+@Command(name = "difftool", common = true, usage = "usage_DiffTool")
+class DiffTool extends TextBuiltin {
+ private DiffFormatter diffFmt;
+
+ private DiffTools diffTools;
+
+ @Argument(index = 0, metaVar = "metaVar_treeish")
+ private AbstractTreeIterator oldTree;
+
+ @Argument(index = 1, metaVar = "metaVar_treeish")
+ private AbstractTreeIterator newTree;
+
+ @Option(name = "--tool", aliases = {
+ "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForDiff")
+ private String toolName;
+
+ @Option(name = "--cached", aliases = { "--staged" }, usage = "usage_cached")
+ private boolean cached;
+
+ private BooleanTriState prompt = BooleanTriState.UNSET;
+
+ @Option(name = "--prompt", usage = "usage_prompt")
+ void setPrompt(@SuppressWarnings("unused") boolean on) {
+ prompt = BooleanTriState.TRUE;
+ }
+
+ @Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt")
+ void noPrompt(@SuppressWarnings("unused") boolean on) {
+ prompt = BooleanTriState.FALSE;
+ }
+
+ @Option(name = "--tool-help", usage = "usage_toolHelp")
+ private boolean toolHelp;
+
+ private BooleanTriState gui = BooleanTriState.UNSET;
+
+ @Option(name = "--gui", aliases = { "-g" }, usage = "usage_DiffGuiTool")
+ void setGui(@SuppressWarnings("unused") boolean on) {
+ gui = BooleanTriState.TRUE;
+ }
+
+ @Option(name = "--no-gui", usage = "usage_noGui")
+ void noGui(@SuppressWarnings("unused") boolean on) {
+ gui = BooleanTriState.FALSE;
+ }
+
+ private BooleanTriState trustExitCode = BooleanTriState.UNSET;
+
+ @Option(name = "--trust-exit-code", usage = "usage_trustExitCode")
+ void setTrustExitCode(@SuppressWarnings("unused") boolean on) {
+ trustExitCode = BooleanTriState.TRUE;
+ }
+
+ @Option(name = "--no-trust-exit-code", usage = "usage_noTrustExitCode")
+ void noTrustExitCode(@SuppressWarnings("unused") boolean on) {
+ trustExitCode = BooleanTriState.FALSE;
+ }
+
+ @Option(name = "--", metaVar = "metaVar_paths", handler = PathTreeFilterHandler.class)
+ private TreeFilter pathFilter = TreeFilter.ALL;
+
+ @Override
+ protected void init(Repository repository, String gitDir) {
+ super.init(repository, gitDir);
+ diffFmt = new DiffFormatter(new BufferedOutputStream(outs));
+ diffTools = new DiffTools(repository);
+ }
+
+ @Override
+ protected void run() {
+ try {
+ if (toolHelp) {
+ showToolHelp();
+ } else {
+ boolean showPrompt = diffTools.isInteractive();
+ if (prompt != BooleanTriState.UNSET) {
+ showPrompt = prompt == BooleanTriState.TRUE;
+ }
+ String toolNamePrompt = toolName;
+ if (showPrompt) {
+ if (StringUtils.isEmptyOrNull(toolNamePrompt)) {
+ toolNamePrompt = diffTools.getDefaultToolName(gui);
+ }
+ }
+ // get the changed files
+ List<DiffEntry> files = getFiles();
+ if (files.size() > 0) {
+ compare(files, showPrompt, toolNamePrompt);
+ }
+ }
+ outw.flush();
+ } catch (RevisionSyntaxException | IOException e) {
+ throw die(e.getMessage(), e);
+ } finally {
+ diffFmt.close();
+ }
+ }
+
+ private void compare(List<DiffEntry> files, boolean showPrompt,
+ String toolNamePrompt) throws IOException {
+ for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
+ DiffEntry ent = files.get(fileIndex);
+ String mergedFilePath = ent.getNewPath();
+ if (mergedFilePath.equals(DiffEntry.DEV_NULL)) {
+ mergedFilePath = ent.getOldPath();
+ }
+ // check if user wants to launch compare
+ boolean launchCompare = true;
+ if (showPrompt) {
+ launchCompare = isLaunchCompare(fileIndex + 1, files.size(),
+ mergedFilePath, toolNamePrompt);
+ }
+ if (launchCompare) {
+ switch (ent.getChangeType()) {
+ case MODIFY:
+ outw.println("M\t" + ent.getNewPath() //$NON-NLS-1$
+ + " (" + ent.getNewId().name() + ")" //$NON-NLS-1$ //$NON-NLS-2$
+ + "\t" + ent.getOldPath() //$NON-NLS-1$
+ + " (" + ent.getOldId().name() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
+ int ret = diffTools.compare(ent.getNewPath(),
+ ent.getOldPath(), ent.getNewId().name(),
+ ent.getOldId().name(), toolName, prompt, gui,
+ trustExitCode);
+ if (ret != 0) {
+ throw die(MessageFormat.format(
+ CLIText.get().diffToolDied, mergedFilePath));
+ }
+ break;
+ default:
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ @SuppressWarnings("boxing")
+ private boolean isLaunchCompare(int fileIndex, int fileCount,
+ String fileName, String toolNamePrompt) throws IOException {
+ boolean launchCompare = true;
+ outw.println(MessageFormat.format(CLIText.get().diffToolLaunch,
+ fileIndex, fileCount, fileName, toolNamePrompt));
+ outw.flush();
+ BufferedReader br = new BufferedReader(new InputStreamReader(ins));
+ String line = null;
+ if ((line = br.readLine()) != null) {
+ if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$
+ launchCompare = false;
+ }
+ }
+ return launchCompare;
+ }
+
+ private void showToolHelp() throws IOException {
+ String availableToolNames = new String();
+ for (String name : diffTools.getAvailableTools().keySet()) {
+ availableToolNames += String.format("\t\t{0}\n", name); //$NON-NLS-1$
+ }
+ String notAvailableToolNames = new String();
+ for (String name : diffTools.getNotAvailableTools().keySet()) {
+ notAvailableToolNames += String.format("\t\t{0}\n", name); //$NON-NLS-1$
+ }
+ String userToolNames = new String();
+ Map<String, ExternalDiffTool> userTools = diffTools
+ .getUserDefinedTools();
+ for (String name : userTools.keySet()) {
+ availableToolNames += String.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$
+ name, userTools.get(name).getCommand());
+ }
+ outw.println(MessageFormat.format(
+ CLIText.get().diffToolHelpSetToFollowing, availableToolNames,
+ userToolNames, notAvailableToolNames));
+ }
+
+ private List<DiffEntry> getFiles()
+ throws RevisionSyntaxException, AmbiguousObjectException,
+ IncorrectObjectTypeException, IOException {
+ diffFmt.setRepository(db);
+ if (cached) {
+ if (oldTree == null) {
+ ObjectId head = db.resolve(HEAD + "^{tree}"); //$NON-NLS-1$
+ if (head == null) {
+ die(MessageFormat.format(CLIText.get().notATree, HEAD));
+ }
+ CanonicalTreeParser p = new CanonicalTreeParser();
+ try (ObjectReader reader = db.newObjectReader()) {
+ p.reset(reader, head);
+ }
+ oldTree = p;
+ }
+ newTree = new DirCacheIterator(db.readDirCache());
+ } else if (oldTree == null) {
+ oldTree = new DirCacheIterator(db.readDirCache());
+ newTree = new FileTreeIterator(db);
+ } else if (newTree == null) {
+ newTree = new FileTreeIterator(db);
+ }
+
+ TextProgressMonitor pm = new TextProgressMonitor(errw);
+ pm.setDelayStart(2, TimeUnit.SECONDS);
+ diffFmt.setProgressMonitor(pm);
+ diffFmt.setPathFilter(pathFilter);
+
+ List<DiffEntry> files = diffFmt.scan(oldTree, newTree);
+ return files;
+ }
+
+}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
index 8e49a76..7fe5b0f 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
@@ -136,6 +136,9 @@ public static String fatalError(String message) {
/***/ public String dateInfo;
/***/ public String deletedBranch;
/***/ public String deletedRemoteBranch;
+ /***/ public String diffToolHelpSetToFollowing;
+ /***/ public String diffToolLaunch;
+ /***/ public String diffToolDied;
/***/ public String doesNotExist;
/***/ public String dontOverwriteLocalChanges;
/***/ public String everythingUpToDate;
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index e762fc1..95c03a0 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -33,6 +33,7 @@
org.eclipse.jgit.ignore;version="[6.1.0,6.2.0)",
org.eclipse.jgit.ignore.internal;version="[6.1.0,6.2.0)",
org.eclipse.jgit.internal;version="[6.1.0,6.2.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="[6.1.0,6.2.0)",
org.eclipse.jgit.internal.fsck;version="[6.1.0,6.2.0)",
org.eclipse.jgit.internal.revwalk;version="[6.1.0,6.2.0)",
org.eclipse.jgit.internal.storage.dfs;version="[6.1.0,6.2.0)",
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
new file mode 100644
index 0000000..f07d9d1
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020-2021, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.diffmergetool;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.junit.Test;
+
+/**
+ * Testing external diff tools.
+ */
+public class ExternalDiffToolTest extends ExternalToolTest {
+
+ @Test
+ public void testToolNames() {
+ DiffTools manager = new DiffTools(db);
+ Set<String> actualToolNames = manager.getToolNames();
+ Set<String> expectedToolNames = Collections.emptySet();
+ assertEquals("Incorrect set of external diff tool names",
+ expectedToolNames,
+ actualToolNames);
+ }
+
+ @Test
+ public void testAllTools() {
+ DiffTools manager = new DiffTools(db);
+ Set<String> actualToolNames = manager.getAvailableTools().keySet();
+ Set<String> expectedToolNames = Collections.emptySet();
+ assertEquals("Incorrect set of available external diff tools",
+ expectedToolNames,
+ actualToolNames);
+ }
+
+ @Test
+ public void testUserDefinedTools() {
+ DiffTools manager = new DiffTools(db);
+ Set<String> actualToolNames = manager.getUserDefinedTools().keySet();
+ Set<String> expectedToolNames = Collections.emptySet();
+ assertEquals("Incorrect set of user defined external diff tools",
+ expectedToolNames,
+ actualToolNames);
+ }
+
+ @Test
+ public void testNotAvailableTools() {
+ DiffTools manager = new DiffTools(db);
+ Set<String> actualToolNames = manager.getNotAvailableTools().keySet();
+ Set<String> expectedToolNames = Collections.emptySet();
+ assertEquals("Incorrect set of not available external diff tools",
+ expectedToolNames,
+ actualToolNames);
+ }
+
+ @Test
+ public void testCompare() {
+ DiffTools manager = new DiffTools(db);
+
+ String newPath = "";
+ String oldPath = "";
+ String newId = "";
+ String oldId = "";
+ String toolName = "";
+ BooleanTriState prompt = BooleanTriState.UNSET;
+ BooleanTriState gui = BooleanTriState.UNSET;
+ BooleanTriState trustExitCode = BooleanTriState.UNSET;
+
+ int expectedCompareResult = 0;
+ int compareResult = manager.compare(newPath, oldPath, newId, oldId,
+ toolName, prompt, gui, trustExitCode);
+ assertEquals("Incorrect compare result for external diff tool",
+ expectedCompareResult,
+ compareResult);
+ }
+
+ @Test
+ public void testDefaultTool() throws Exception {
+ FileBasedConfig config = db.getConfig();
+ // the default diff tool is configured without a subsection
+ String subsection = null;
+ config.setString("diff", subsection, "tool", "customTool");
+
+ DiffTools manager = new DiffTools(db);
+ BooleanTriState gui = BooleanTriState.UNSET;
+ String defaultToolName = manager.getDefaultToolName(gui);
+ assertEquals(
+ "Expected configured difftool to be the default external diff tool",
+ "my_default_toolname", defaultToolName);
+
+ gui = BooleanTriState.TRUE;
+ String defaultGuiToolName = manager.getDefaultToolName(gui);
+ assertEquals(
+ "Expected configured difftool to be the default external diff tool",
+ "my_gui_tool", defaultGuiToolName);
+
+ config.setString("diff", subsection, "guitool", "customGuiTool");
+ manager = new DiffTools(db);
+ defaultGuiToolName = manager.getDefaultToolName(gui);
+ assertEquals(
+ "Expected configured difftool to be the default external diff guitool",
+ "my_gui_tool", defaultGuiToolName);
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTest.java
new file mode 100644
index 0000000..c7c8eca
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020-2021, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.io.File;
+import java.nio.file.Files;
+
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS_POSIX;
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+
+/**
+ * Base test case for external merge and diff tool tests.
+ */
+public abstract class ExternalToolTest extends RepositoryTestCase {
+
+ protected static final String DEFAULT_CONTENT = "line1";
+
+ protected File localFile;
+
+ protected File remoteFile;
+
+ protected File mergedFile;
+
+ protected File baseFile;
+
+ protected File commandResult;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ localFile = writeTrashFile("localFile.txt", DEFAULT_CONTENT + "\n");
+ localFile.deleteOnExit();
+ remoteFile = writeTrashFile("remoteFile.txt", DEFAULT_CONTENT + "\n");
+ remoteFile.deleteOnExit();
+ mergedFile = writeTrashFile("mergedFile.txt", "");
+ mergedFile.deleteOnExit();
+ baseFile = writeTrashFile("baseFile.txt", "");
+ baseFile.deleteOnExit();
+ commandResult = writeTrashFile("commandResult.txt", "");
+ commandResult.deleteOnExit();
+ }
+
+ @After
+ @Override
+ public void tearDown() throws Exception {
+ Files.delete(localFile.toPath());
+ Files.delete(remoteFile.toPath());
+ Files.delete(mergedFile.toPath());
+ Files.delete(baseFile.toPath());
+ Files.delete(commandResult.toPath());
+
+ super.tearDown();
+ }
+
+
+ protected static void assumePosixPlatform() {
+ Assume.assumeTrue(
+ "This test can run only in Linux tests",
+ FS.DETECTED instanceof FS_POSIX);
+ }
+}
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index dd6bae3..e674ade 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -70,7 +70,10 @@
org.eclipse.jgit.internal;version="6.1.0";
x-friends:="org.eclipse.jgit.test,
org.eclipse.jgit.http.test",
+ org.eclipse.jgit.internal.diffmergetool;version="6.1.0";
org.eclipse.jgit.internal.fsck;version="6.1.0";
+ x-friends:="org.eclipse.jgit.test,
+ org.eclipse.jgit.pgm",
x-friends:="org.eclipse.jgit.test",
org.eclipse.jgit.internal.revwalk;version="6.1.0";
x-friends:="org.eclipse.jgit.test",
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
new file mode 100644
index 0000000..cb0640d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
@@ -0,0 +1,121 @@
+/*
+ * 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.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+
+/**
+ * Manages diff tools.
+ */
+public class DiffTools {
+
+ private Map<String, ExternalDiffTool> predefinedTools;
+
+ private Map<String, ExternalDiffTool> userDefinedTools;
+
+ /**
+ * Creates the external diff-tools manager for given repository.
+ *
+ * @param repo
+ * the repository database
+ */
+ public DiffTools(Repository repo) {
+ setupPredefinedTools();
+ setupUserDefinedTools();
+ }
+
+ /**
+ * Compare two versions of a file.
+ *
+ * @param newPath
+ * the new file path
+ * @param oldPath
+ * the old file path
+ * @param newId
+ * the new object ID
+ * @param oldId
+ * the old object ID
+ * @param toolName
+ * the selected tool name (can be null)
+ * @param prompt
+ * the prompt option
+ * @param gui
+ * the GUI option
+ * @param trustExitCode
+ * the "trust exit code" option
+ * @return the return code from executed tool
+ */
+ public int compare(String newPath, String oldPath, String newId,
+ String oldId, String toolName, BooleanTriState prompt,
+ BooleanTriState gui, BooleanTriState trustExitCode) {
+ return 0;
+ }
+
+ /**
+ * @return the tool names
+ */
+ public Set<String> getToolNames() {
+ return Collections.emptySet();
+ }
+
+ /**
+ * @return the user defined tools
+ */
+ public Map<String, ExternalDiffTool> getUserDefinedTools() {
+ return Collections.unmodifiableMap(userDefinedTools);
+ }
+
+ /**
+ * @return the available predefined tools
+ */
+ public Map<String, ExternalDiffTool> getAvailableTools() {
+ return Collections.unmodifiableMap(predefinedTools);
+ }
+
+ /**
+ * @return the NOT available predefined tools
+ */
+ public Map<String, ExternalDiffTool> getNotAvailableTools() {
+ return Collections.unmodifiableMap(new TreeMap<>());
+ }
+
+ /**
+ * @param gui
+ * use the diff.guitool setting ?
+ * @return the default tool name
+ */
+ public String getDefaultToolName(BooleanTriState gui) {
+ return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$
+ : "my_default_toolname"; //$NON-NLS-1$
+ }
+
+ /**
+ * @return is interactive (config prompt enabled) ?
+ */
+ public boolean isInteractive() {
+ return false;
+ }
+
+ private void setupPredefinedTools() {
+ predefinedTools = new TreeMap<>();
+ }
+
+ private void setupUserDefinedTools() {
+ userDefinedTools = new TreeMap<>();
+ }
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java
new file mode 100644
index 0000000..f2d7e82
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * The external tool interface.
+ */
+public interface ExternalDiffTool {
+
+ /**
+ * @return the tool name
+ */
+ String getName();
+
+ /**
+ * @return the tool path
+ */
+ String getPath();
+
+ /**
+ * @return the tool command
+ */
+ String getCommand();
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BooleanTriState.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BooleanTriState.java
new file mode 100644
index 0000000..44d3bb3
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BooleanTriState.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 Simeon Andreev <simeon.danailov.andreev@gmail.com> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lib.internal;
+
+/**
+ * A boolean value that can also have an unset state.
+ */
+public enum BooleanTriState {
+ /**
+ * Value equivalent to {@code true}.
+ */
+ TRUE,
+ /**
+ * Value equivalent to {@code false}.
+ */
+ FALSE,
+ /**
+ * Value is not set.
+ */
+ UNSET;
+}