From 48f4d97a226a2c9be4577b916cc0b99ce870939a Mon Sep 17 00:00:00 2001 From: Andre Bossert Date: Sat, 23 Feb 2019 15:25:10 +0100 Subject: [PATCH] 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 Signed-off-by: Simeon Andreev --- .../META-INF/MANIFEST.MF | 1 + .../org/eclipse/jgit/pgm/DiffToolTest.java | 195 ++++++++++++++ org.eclipse.jgit.pgm/META-INF/MANIFEST.MF | 2 + .../services/org.eclipse.jgit.pgm.TextBuiltin | 1 + .../jgit/pgm/internal/CLIText.properties | 13 + .../src/org/eclipse/jgit/pgm/DiffTool.java | 255 ++++++++++++++++++ .../eclipse/jgit/pgm/internal/CLIText.java | 3 + org.eclipse.jgit.test/META-INF/MANIFEST.MF | 1 + .../diffmergetool/ExternalDiffToolTest.java | 114 ++++++++ .../diffmergetool/ExternalToolTest.java | 74 +++++ org.eclipse.jgit/META-INF/MANIFEST.MF | 3 + .../internal/diffmergetool/DiffTools.java | 121 +++++++++ .../diffmergetool/ExternalDiffTool.java | 33 +++ .../jgit/lib/internal/BooleanTriState.java | 28 ++ 14 files changed, 844 insertions(+) create mode 100644 org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java create mode 100644 org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BooleanTriState.java diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF index 2c53da8c8..3e0a4eaf2 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 @@ Import-Package: org.eclipse.jgit.api;version="[6.1.0,6.2.0)", 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 000000000..2ce50c782 --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2021, Simeon Andreev 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 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 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 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 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 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= 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 getRepositoryChanges(RevCommit commit) + throws Exception { + TreeWalk tw = new TreeWalk(db); + tw.addTree(commit.getTree()); + FileTreeIterator modifiedTree = new FileTreeIterator(db); + tw.addTree(modifiedTree); + List changes = DiffEntry.scan(tw); + return changes; + } + + private String[] getExpectedDiffToolOutput(List 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 1ebd3a3e4..fa0f452c5 100644 --- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF @@ -24,6 +24,7 @@ Import-Package: javax.servlet;version="[3.1.0,5.0.0)", 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 @@ Import-Package: javax.servlet;version="[3.1.0,5.0.0)", 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 e645255e9..8c44764c6 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.ConvertRefStorage 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 97450033e..d51daafde 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 @@ couldNotCreateBranch=Could not create branch {0}: {1} dateInfo=Date: {0} deletedBranch=Deleted branch {0} deletedRemoteBranch=Deleted remote branch {0} +diffToolHelpSetToFollowing='git difftool --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_s3StorageClass=STORAGE-CLASS metaVar_seconds=SECONDS metaVar_service=SERVICE metaVar_tagLocalUser= +metaVar_tool=TOOL metaVar_treeish=tree-ish metaVar_uriish=uri-ish metaVar_url=URL @@ -249,6 +253,8 @@ usage_DiffAlgorithms=Test performance of jgit's diff algorithms 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_ShowRef=List references in a local repository 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 . Run git difftool --tool-help for the list of valid 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_deleteFullyMergedBranch=delete fully merged branch 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_pathToXml=path to the repo manifest XML file 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_srcPrefix=show the source prefix instead of "a/" 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_tagMessage=create an annotated tag with the given message, unsigned unless 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 000000000..9fc26c935 --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert + * + * 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 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 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 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 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 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 8e49a76a3..7fe5b0fa4 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 e762fc1ad..95c03a0a4 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -33,6 +33,7 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", 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 000000000..f07d9d1af --- /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 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 actualToolNames = manager.getToolNames(); + Set expectedToolNames = Collections.emptySet(); + assertEquals("Incorrect set of external diff tool names", + expectedToolNames, + actualToolNames); + } + + @Test + public void testAllTools() { + DiffTools manager = new DiffTools(db); + Set actualToolNames = manager.getAvailableTools().keySet(); + Set expectedToolNames = Collections.emptySet(); + assertEquals("Incorrect set of available external diff tools", + expectedToolNames, + actualToolNames); + } + + @Test + public void testUserDefinedTools() { + DiffTools manager = new DiffTools(db); + Set actualToolNames = manager.getUserDefinedTools().keySet(); + Set expectedToolNames = Collections.emptySet(); + assertEquals("Incorrect set of user defined external diff tools", + expectedToolNames, + actualToolNames); + } + + @Test + public void testNotAvailableTools() { + DiffTools manager = new DiffTools(db); + Set actualToolNames = manager.getNotAvailableTools().keySet(); + Set 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 000000000..c7c8eca71 --- /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 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 dd6bae395..e674aded6 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -70,7 +70,10 @@ Export-Package: org.eclipse.jgit.annotations;version="6.1.0", 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 000000000..cb0640d2e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert + * + * 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 predefinedTools; + + private Map 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 getToolNames() { + return Collections.emptySet(); + } + + /** + * @return the user defined tools + */ + public Map getUserDefinedTools() { + return Collections.unmodifiableMap(userDefinedTools); + } + + /** + * @return the available predefined tools + */ + public Map getAvailableTools() { + return Collections.unmodifiableMap(predefinedTools); + } + + /** + * @return the NOT available predefined tools + */ + public Map 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 000000000..f2d7e828c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert + * + * 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 000000000..44d3bb36e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BooleanTriState.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 Simeon Andreev 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; +}