From b63c2f39a16f1607cc65e82c0271c8c29a6038f1 Mon Sep 17 00:00:00 2001 From: Andre Bossert Date: Sun, 19 Jan 2020 20:50:14 +0100 Subject: [PATCH 1/7] Add difftool compare feature (execute external tool) see: http://git-scm.com/docs/git-difftool * add CommandExecutor that handles tool execution with help of "jgit.FS" * it handles tool execution with temporary created "command file" --> for for all "command interpreters" and parameters with spaces etc. * using of external bash.exe at Windows (MinGW) if shell-script is used as difftool command. It can be enabled with parameter "jgit.usemsys2bash=auto" that checks if command contains ".sh" or enabled / disabled with "jgit.usemsys2bash=true|false" * added special handling for empty files (e.g. deleted, added etc.) that are named "/dev/null" * added creation and deletion of temporary files needed for compare * added own Exception class for reporting to pgm / command line / EGit * added prompt option handling before executing difftool * reworked trustExitCode option for specific difftool and override for all difftools from config and command line * tested with command line options "--[no]-trust-exit-code", "--tool=", "--[no]-gui", --[no]-prompt * ContentSource * added close() methods to close / cleanup used resources (like ObjectReader TreeWalk etc.) * added isWorkingTreeSource() methods to check if file can be used from working tree instead of copy from "ObjectLoader / ObjectReader" to temporary file (fixes "difftool ") Bug: 356832 Change-Id: I5462fb6dbe4ecfd9da7c74117fce4070bbfd4d7a Signed-off-by: Andre Bossert Signed-off-by: Simeon Andreev --- .../org/eclipse/jgit/pgm/DiffToolTest.java | 63 +++++- .../jgit/pgm/internal/CLIText.properties | 2 +- .../src/org/eclipse/jgit/pgm/DiffTool.java | 100 +++++++--- .../diffmergetool/ExternalDiffToolTest.java | 149 ++++++++++++-- .../diffmergetool/ExternalToolTestCase.java | 13 ++ .../org/eclipse/jgit/diff/ContentSource.java | 77 +++++++- .../diffmergetool/CommandExecutor.java | 182 ++++++++++++++++++ .../diffmergetool/DiffToolConfig.java | 5 +- .../internal/diffmergetool/DiffTools.java | 111 ++++++++--- .../internal/diffmergetool/FileElement.java | 160 +++++++++++++++ .../diffmergetool/PreDefinedDiffTool.java | 11 -- .../internal/diffmergetool/ToolException.java | 120 ++++++++++++ 12 files changed, 895 insertions(+), 98 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java index e7bf48417..e2ff18927 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java @@ -9,7 +9,13 @@ */ package org.eclipse.jgit.pgm; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.Arrays; @@ -19,6 +25,7 @@ import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool; import org.eclipse.jgit.lib.CLIRepositoryTestCase; +import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.pgm.opt.CmdLineParser; import org.eclipse.jgit.pgm.opt.SubcommandHandler; import org.eclipse.jgit.revwalk.RevCommit; @@ -60,6 +67,7 @@ private String[] runAndCaptureUsingInitRaw(String... args) return result.outLines().toArray(new String[0]); } + private static final String TOOL_NAME = "some_tool"; private Git git; @Override @@ -68,6 +76,15 @@ public void setUp() throws Exception { super.setUp(); git = new Git(db); git.commit().setMessage("initial commit").call(); + configureEchoTool(TOOL_NAME); + } + + @Test(expected = Die.class) + public void testNotDefinedTool() throws Exception { + createUnstagedChanges(); + + runAndCaptureUsingInitRaw("difftool", "--tool", "undefined"); + fail("Expected exception when trying to run undefined tool"); } @Test @@ -85,7 +102,7 @@ public void testTool() throws Exception { assertArrayOfLinesEquals("Incorrect output for option: " + option, expectedOutput, runAndCaptureUsingInitRaw("difftool", option, - "some_tool")); + TOOL_NAME)); } } @@ -100,7 +117,7 @@ public void testToolTrustExitCode() throws Exception { for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, expectedOutput, runAndCaptureUsingInitRaw("difftool", - "--trust-exit-code", option, "some_tool")); + "--trust-exit-code", option, TOOL_NAME)); } } @@ -116,7 +133,7 @@ public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception { assertArrayOfLinesEquals("Incorrect output for option: " + option, expectedOutput, runAndCaptureUsingInitRaw("difftool", "--no-gui", "--no-prompt", "--no-trust-exit-code", - option, "some_tool")); + option, TOOL_NAME)); } } @@ -131,7 +148,7 @@ public void testToolCached() throws Exception { for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, expectedOutput, runAndCaptureUsingInitRaw("difftool", - option, "--tool", "some_tool")); + option, "--tool", TOOL_NAME)); } } @@ -144,8 +161,11 @@ public void testToolHelp() throws Exception { String toolName = defaultTool.name(); expectedOutput.add(toolName); } + String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " " + + getEchoCommand(); + expectedOutput.add("user-defined:"); + expectedOutput.add(customToolHelpLine); String[] userDefinedToolsHelp = { - "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.", @@ -157,6 +177,25 @@ public void testToolHelp() throws Exception { expectedOutput.toArray(new String[0]), runAndCaptureUsingInitRaw("difftool", option)); } + private void configureEchoTool(String toolName) { + StoredConfig config = db.getConfig(); + // the default diff tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + String command = getEchoCommand(); + + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + /* + * prevent prompts as we are running in tests and there is no user to + * interact with on the command line + */ + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_PROMPT, + String.valueOf(false)); + } + private RevCommit createUnstagedChanges() throws Exception { writeTrashFile("a", "Hello world a"); writeTrashFile("b", "Hello world b"); @@ -188,11 +227,7 @@ private String[] getExpectedDiffToolOutput(List changes) { 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 + ")"; + String expectedLine = newPath; expectedToolOutput[i] = expectedLine; } return expectedToolOutput; @@ -202,4 +237,12 @@ private static void assertArrayOfLinesEquals(String failMessage, String[] expected, String[] actual) { assertEquals(failMessage, toString(expected), toString(actual)); } + + private static String getEchoCommand() { + /* + * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be + * replaced with full paths to a temporary file during some of the tests + */ + return "(echo \"$MERGED\")"; + } } 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 fda0bf6ff..3653b9d8f 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 @@ -60,7 +60,7 @@ 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} +diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1} doesNotExist={0} does not exist dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge: everythingUpToDate=Everything up-to-date diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java index 128881779..2f7417745 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java @@ -21,8 +21,10 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; - +import org.eclipse.jgit.diff.ContentSource; +import org.eclipse.jgit.diff.ContentSource.Pair; import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.Side; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.AmbiguousObjectException; @@ -30,8 +32,11 @@ import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.internal.diffmergetool.DiffTools; import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool; +import org.eclipse.jgit.internal.diffmergetool.FileElement; +import org.eclipse.jgit.internal.diffmergetool.ToolException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.ObjectStream; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.lib.internal.BooleanTriState; @@ -40,8 +45,10 @@ import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.WorkingTreeIterator; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.FS.ExecutionResult; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; @@ -145,40 +152,54 @@ protected void run() { 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) { + ContentSource.Pair sourcePair = new ContentSource.Pair(source(oldTree), + source(newTree)); + try { + 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(); + } + FileElement local = new FileElement(ent.getOldPath(), + ent.getOldId().name(), + getObjectStream(sourcePair, Side.OLD, ent)); + FileElement remote = new FileElement(ent.getNewPath(), + ent.getNewId().name(), + getObjectStream(sourcePair, Side.NEW, ent)); + // check if user wants to launch compare + boolean launchCompare = true; + if (showPrompt) { + launchCompare = isLaunchCompare(fileIndex + 1, files.size(), + mergedFilePath, toolNamePrompt); + } + if (launchCompare) { + try { + // TODO: check how to return the exit-code of + // the + // tool + // to + // jgit / java runtime ? + // int rc =... + ExecutionResult result = diffTools.compare(db, local, + remote, mergedFilePath, + toolName, prompt, gui, trustExitCode); + outw.println(new String(result.getStdout().toByteArray())); + errw.println( + new String(result.getStderr().toByteArray())); + } catch (ToolException e) { + outw.println(e.getResultStdout()); + outw.flush(); + errw.println(e.getMessage()); throw die(MessageFormat.format( - CLIText.get().diffToolDied, mergedFilePath)); + CLIText.get().diffToolDied, mergedFilePath, e)); } - break; - default: + } else { break; } - } else { - break; } + } finally { + sourcePair.close(); } } @@ -254,4 +275,23 @@ private List getFiles() return files; } + private ObjectStream getObjectStream(Pair pair, Side side, DiffEntry ent) { + ObjectStream stream = null; + if (!pair.isWorkingTreeSource(side)) { + try { + stream = pair.open(side, ent).openStream(); + } catch (Exception e) { + stream = null; + } + } + return stream; + } + + private ContentSource source(AbstractTreeIterator iterator) { + if (iterator instanceof WorkingTreeIterator) { + return ContentSource.create((WorkingTreeIterator) iterator); + } + return ContentSource.create(db.newObjectReader()); + } + } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java index c9ebec763..ebc67c81c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java @@ -10,13 +10,17 @@ package org.eclipse.jgit.internal.diffmergetool; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.util.Collections; import java.util.LinkedHashSet; @@ -25,6 +29,7 @@ import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS.ExecutionResult; import org.junit.Test; /** @@ -32,6 +37,54 @@ */ public class ExternalDiffToolTest extends ExternalToolTestCase { + @Test(expected = ToolException.class) + public void testUserToolWithError() throws Exception { + String toolName = "customTool"; + + int errorReturnCode = 1; + String command = "exit " + errorReturnCode; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + DiffTools manager = new DiffTools(db); + + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + BooleanTriState trustExitCode = BooleanTriState.TRUE; + + manager.compare(db, local, remote, merged.getPath(), toolName, prompt, + gui, trustExitCode); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + + @Test(expected = ToolException.class) + public void testUserToolWithCommandNotFoundError() throws Exception { + String toolName = "customTool"; + + int errorReturnCode = 127; // command not found + String command = "exit " + errorReturnCode; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + DiffTools manager = new DiffTools(db); + + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + BooleanTriState trustExitCode = BooleanTriState.FALSE; + + manager.compare(db, local, remote, merged.getPath(), toolName, prompt, + gui, trustExitCode); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + @Test public void testToolNames() { DiffTools manager = new DiffTools(db); @@ -86,11 +139,11 @@ public void testUserDefinedTools() { config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, CONFIG_KEY_PATH, "/usr/bin/echo"); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, - CONFIG_KEY_PROMPT, "--no-prompt"); + CONFIG_KEY_PROMPT, String.valueOf(false)); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, - CONFIG_KEY_GUITOOL, "--no-gui"); + CONFIG_KEY_GUITOOL, String.valueOf(false)); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, - CONFIG_KEY_TRUST_EXIT_CODE, "--no-trust-exit-code"); + CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false)); DiffTools manager = new DiffTools(db); Set actualToolNames = manager.getUserDefinedTools().keySet(); Set expectedToolNames = new LinkedHashSet<>(); @@ -109,38 +162,50 @@ public void testNotAvailableTools() { } @Test - public void testCompare() { - DiffTools manager = new DiffTools(db); + public void testCompare() throws ToolException { + String toolName = "customTool"; + + FileBasedConfig config = db.getConfig(); + // the default diff tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + String command = getEchoCommand(); + + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); - String newPath = ""; - String oldPath = ""; - String newId = ""; - String oldId = ""; - String toolName = ""; BooleanTriState prompt = BooleanTriState.UNSET; BooleanTriState gui = BooleanTriState.UNSET; BooleanTriState trustExitCode = BooleanTriState.UNSET; + DiffTools manager = new DiffTools(db); + int expectedCompareResult = 0; - int compareResult = manager.compare(newPath, oldPath, newId, oldId, - toolName, prompt, gui, trustExitCode); + ExecutionResult compareResult = manager.compare(db, local, remote, + merged.getPath(), toolName, prompt, gui, trustExitCode); assertEquals("Incorrect compare result for external diff tool", - expectedCompareResult, compareResult); + expectedCompareResult, compareResult.getRc()); } @Test public void testDefaultTool() throws Exception { + String toolName = "customTool"; + String guiToolName = "customGuiTool"; + FileBasedConfig config = db.getConfig(); // the default diff tool is configured without a subsection String subsection = null; - config.setString("diff", subsection, "tool", "customTool"); + config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); 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); + toolName, defaultToolName); gui = BooleanTriState.TRUE; String defaultGuiToolName = manager.getDefaultToolName(gui); @@ -148,11 +213,63 @@ public void testDefaultTool() throws Exception { "Expected configured difftool to be the default external diff tool", "my_gui_tool", defaultGuiToolName); - config.setString("diff", subsection, "guitool", "customGuiTool"); + config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL, + guiToolName); manager = new DiffTools(db); defaultGuiToolName = manager.getDefaultToolName(gui); assertEquals( "Expected configured difftool to be the default external diff guitool", "my_gui_tool", defaultGuiToolName); } + + @Test + public void testOverridePreDefinedToolPath() { + String newToolPath = "/tmp/path/"; + + CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); + assertTrue("Expected to find pre-defined external diff tools", + defaultTools.length > 0); + + CommandLineDiffTool overridenTool = defaultTools[0]; + String overridenToolName = overridenTool.name(); + String overridenToolPath = newToolPath + overridenToolName; + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_DIFFTOOL_SECTION, overridenToolName, + CONFIG_KEY_PATH, overridenToolPath); + + DiffTools manager = new DiffTools(db); + Map availableTools = manager + .getAvailableTools(); + ExternalDiffTool externalDiffTool = availableTools + .get(overridenToolName); + String actualDiffToolPath = externalDiffTool.getPath(); + assertEquals( + "Expected pre-defined external diff tool to have overriden path", + overridenToolPath, actualDiffToolPath); + String expectedDiffToolCommand = overridenToolPath + " " + + overridenTool.getParameters(); + String actualDiffToolCommand = externalDiffTool.getCommand(); + assertEquals( + "Expected pre-defined external diff tool to have overriden command", + expectedDiffToolCommand, actualDiffToolCommand); + } + + @Test(expected = ToolException.class) + public void testUndefinedTool() throws Exception { + DiffTools manager = new DiffTools(db); + + String toolName = "undefined"; + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + BooleanTriState trustExitCode = BooleanTriState.UNSET; + + manager.compare(db, local, remote, merged.getPath(), toolName, prompt, + gui, trustExitCode); + fail("Expected exception to be thrown due to not defined external diff tool"); + } + + private String getEchoCommand() { + return "(echo \"$LOCAL\" \"$REMOTE\") > " + + commandResult.getAbsolutePath(); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java index 0cc12978a..6757eb463 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java @@ -36,6 +36,14 @@ public abstract class ExternalToolTestCase extends RepositoryTestCase { protected File commandResult; + protected FileElement local; + + protected FileElement remote; + + protected FileElement merged; + + protected FileElement base; + @Before @Override public void setUp() throws Exception { @@ -51,6 +59,11 @@ public void setUp() throws Exception { baseFile.deleteOnExit(); commandResult = writeTrashFile("commandResult.txt", ""); commandResult.deleteOnExit(); + + local = new FileElement(localFile.getAbsolutePath(), "LOCAL"); + remote = new FileElement(remoteFile.getAbsolutePath(), "REMOTE"); + merged = new FileElement(mergedFile.getAbsolutePath(), "MERGED"); + base = new FileElement(baseFile.getAbsolutePath(), "BASE"); } @After diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java index 1a41df3d0..64ff19c9c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, 2020 Google Inc. and others + * Copyright (C) 2010, 2021 Google Inc. 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 @@ -91,6 +91,29 @@ public static ContentSource create(WorkingTreeIterator iterator) { public abstract ObjectLoader open(String path, ObjectId id) throws IOException; + /** + * Closes the used resources like ObjectReader, TreeWalk etc. Default + * implementation does nothing. + * + * @since 6.2 + */ + public void close() { + // Do nothing + } + + /** + * Checks if the source is from "working tree", so it can be accessed as a + * file directly. + * + * @since 6.2 + * + * @return true if working tree source and false otherwise (loader must be + * used) + */ + public boolean isWorkingTreeSource() { + return false; + } + private static class ObjectReaderSource extends ContentSource { private final ObjectReader reader; @@ -111,6 +134,16 @@ public long size(String path, ObjectId id) throws IOException { public ObjectLoader open(String path, ObjectId id) throws IOException { return reader.open(id, Constants.OBJ_BLOB); } + + @Override + public void close() { + reader.close(); + } + + @Override + public boolean isWorkingTreeSource() { + return false; + } } private static class WorkingTreeSource extends ContentSource { @@ -194,6 +227,16 @@ private void seek(String path) throws IOException { throw new FileNotFoundException(path); } } + + @Override + public void close() { + tw.close(); + } + + @Override + public boolean isWorkingTreeSource() { + return true; + } } /** A pair of sources to access the old and new sides of a DiffEntry. */ @@ -261,5 +304,37 @@ public ObjectLoader open(DiffEntry.Side side, DiffEntry ent) throw new IllegalArgumentException(); } } + + /** + * Closes used resources. + * + * @since 6.2 + */ + public void close() { + oldSource.close(); + newSource.close(); + } + + /** + * Checks if source (side) is a "working tree". + * + * @since 6.2 + * + * @param side + * which side of the entry to read (OLD or NEW). + * @return is the source a "working tree" + * + */ + public boolean isWorkingTreeSource(DiffEntry.Side side) { + switch (side) { + case OLD: + return oldSource.isWorkingTreeSource(); + case NEW: + return newSource.isWorkingTreeSource(); + default: + throw new IllegalArgumentException(); + } + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java new file mode 100644 index 000000000..0dde9b5f3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java @@ -0,0 +1,182 @@ +/* + * 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.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Map; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.FS_POSIX; +import org.eclipse.jgit.util.FS_Win32; +import org.eclipse.jgit.util.FS_Win32_Cygwin; + +/** + * Runs a command with help of FS. + */ +public class CommandExecutor { + + private FS fs; + + private boolean checkExitCode; + + private File commandFile; + + private boolean useMsys2; + + /** + * @param fs + * the file system + * @param checkExitCode + * should the exit code be checked for errors ? + */ + public CommandExecutor(FS fs, boolean checkExitCode) { + this.fs = fs; + this.checkExitCode = checkExitCode; + } + + /** + * @param command + * the command string + * @param workingDir + * the working directory + * @param env + * the environment + * @return the execution result + * @throws ToolException + * @throws InterruptedException + * @throws IOException + */ + public ExecutionResult run(String command, File workingDir, + Map env) + throws ToolException, IOException, InterruptedException { + String[] commandArray = createCommandArray(command); + try { + ProcessBuilder pb = fs.runInShell(commandArray[0], + Arrays.copyOfRange(commandArray, 1, commandArray.length)); + pb.directory(workingDir); + Map envp = pb.environment(); + if (env != null) { + envp.putAll(env); + } + ExecutionResult result = fs.execute(pb, null); + int rc = result.getRc(); + if ((rc != 0) && (checkExitCode + || isCommandExecutionError(rc))) { + throw new ToolException( + new String(result.getStderr().toByteArray()), result); + } + return result; + } finally { + deleteCommandArray(); + } + } + + private void deleteCommandArray() { + deleteCommandFile(); + } + + private String[] createCommandArray(String command) + throws ToolException, IOException { + String[] commandArray = null; + checkUseMsys2(command); + createCommandFile(command); + if (fs instanceof FS_POSIX) { + commandArray = new String[1]; + commandArray[0] = commandFile.getCanonicalPath(); + } else if (fs instanceof FS_Win32) { + if (useMsys2) { + commandArray = new String[3]; + commandArray[0] = "bash.exe"; //$NON-NLS-1$ + commandArray[1] = "-c"; //$NON-NLS-1$ + commandArray[2] = commandFile.getCanonicalPath().replace("\\", //$NON-NLS-1$ + "/"); //$NON-NLS-1$ + } else { + commandArray = new String[1]; + commandArray[0] = commandFile.getCanonicalPath(); + } + } else if (fs instanceof FS_Win32_Cygwin) { + commandArray = new String[1]; + commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + throw new ToolException( + "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$ + } + return commandArray; + } + + private void checkUseMsys2(String command) { + useMsys2 = false; + String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$ + if (useMsys2Str != null && !useMsys2Str.isEmpty()) { + if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$ + useMsys2 = command.contains(".sh"); //$NON-NLS-1$ + } else { + useMsys2 = Boolean.parseBoolean(useMsys2Str); + } + } + } + + private void createCommandFile(String command) + throws ToolException, IOException { + String fileExtension = null; + if (useMsys2 || fs instanceof FS_POSIX + || fs instanceof FS_Win32_Cygwin) { + fileExtension = ".sh"; //$NON-NLS-1$ + } else if (fs instanceof FS_Win32) { + fileExtension = ".cmd"; //$NON-NLS-1$ + command = "@echo off" + System.lineSeparator() + command //$NON-NLS-1$ + + System.lineSeparator() + "exit /B %ERRORLEVEL%"; //$NON-NLS-1$ + } else { + throw new ToolException( + "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$ + } + commandFile = File.createTempFile(".__", //$NON-NLS-1$ + "__jgit_tool" + fileExtension); //$NON-NLS-1$ + try (OutputStream outStream = new FileOutputStream(commandFile)) { + byte[] strToBytes = command.getBytes(); + outStream.write(strToBytes); + outStream.close(); + } + commandFile.setExecutable(true); + } + + private void deleteCommandFile() { + if (commandFile != null && commandFile.exists()) { + commandFile.delete(); + } + } + + private boolean isCommandExecutionError(int rc) { + if (useMsys2 || fs instanceof FS_POSIX + || fs instanceof FS_Win32_Cygwin) { + // 126: permission for executing command denied + // 127: command not found + if ((rc == 126) || (rc == 127)) { + return true; + } + } + else if (fs instanceof FS_Win32) { + // 9009, 0x2331: Program is not recognized as an internal or + // external command, operable program or batch file. Indicates that + // command, application name or path has been misspelled when + // configuring the Action. + if (rc == 9009) { + return true; + } + } + return false; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java index 551f634f2..c8b04f90f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java @@ -49,9 +49,10 @@ private DiffToolConfig(Config rc) { toolName = rc.getString(CONFIG_DIFF_SECTION, null, CONFIG_KEY_TOOL); guiToolName = rc.getString(CONFIG_DIFF_SECTION, null, CONFIG_KEY_GUITOOL); - prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, CONFIG_KEY_PROMPT, + prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, toolName, + CONFIG_KEY_PROMPT, true); - String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, null, + String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_TRUST_EXIT_CODE); if (trustStr != null) { trustExitCode = Boolean.parseBoolean(trustStr) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index 39729a4ee..b15cbdc34 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -12,11 +12,16 @@ import java.util.TreeMap; import java.util.Collections; +import java.io.File; +import java.io.IOException; import java.util.Map; import java.util.Set; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.StringUtils; /** * Manages diff tools. @@ -25,9 +30,9 @@ public class DiffTools { private final DiffToolConfig config; - private Map predefinedTools; + private final Map predefinedTools; - private Map userDefinedTools; + private final Map userDefinedTools; /** * Creates the external diff-tools manager for given repository. @@ -37,21 +42,22 @@ public class DiffTools { */ public DiffTools(Repository repo) { config = repo.getConfig().get(DiffToolConfig.KEY); - setupPredefinedTools(); - setupUserDefinedTools(); + predefinedTools = setupPredefinedTools(); + userDefinedTools = setupUserDefinedTools(config, predefinedTools); } /** * 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 repo + * the repository + * @param localFile + * the local file element + * @param remoteFile + * the remote file element + * @param mergedFilePath + * the path of 'merged' file, it equals local or remote path for + * difftool * @param toolName * the selected tool name (can be null) * @param prompt @@ -61,11 +67,39 @@ public DiffTools(Repository repo) { * @param trustExitCode * the "trust exit code" option * @return the return code from executed tool + * @throws ToolException */ - public int compare(String newPath, String oldPath, String newId, - String oldId, String toolName, BooleanTriState prompt, - BooleanTriState gui, BooleanTriState trustExitCode) { - return 0; + public ExecutionResult compare(Repository repo, FileElement localFile, + FileElement remoteFile, String mergedFilePath, String toolName, + BooleanTriState prompt, BooleanTriState gui, + BooleanTriState trustExitCode) throws ToolException { + ExternalDiffTool tool = guessTool(toolName, gui); + try { + File workingDir = repo.getWorkTree(); + String localFilePath = localFile.getFile().getPath(); + String remoteFilePath = remoteFile.getFile().getPath(); + String command = tool.getCommand(); + command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$ + command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$ + command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$ + Map env = new TreeMap<>(); + env.put(Constants.GIT_DIR_KEY, + repo.getDirectory().getAbsolutePath()); + env.put("LOCAL", localFilePath); //$NON-NLS-1$ + env.put("REMOTE", remoteFilePath); //$NON-NLS-1$ + env.put("MERGED", mergedFilePath); //$NON-NLS-1$ + boolean trust = config.isTrustExitCode(); + if (trustExitCode != BooleanTriState.UNSET) { + trust = trustExitCode == BooleanTriState.TRUE; + } + CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); + return cmdExec.run(command, workingDir, env); + } catch (IOException | InterruptedException e) { + throw new ToolException(e); + } finally { + localFile.cleanTemporaries(); + remoteFile.cleanTemporaries(); + } } /** @@ -103,41 +137,64 @@ public Map getNotAvailableTools() { */ public String getDefaultToolName(BooleanTriState gui) { return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ - : "my_default_toolname"; //$NON-NLS-1$ + : config.getDefaultToolName(); } /** * @return is interactive (config prompt enabled) ? */ public boolean isInteractive() { - return false; + return config.isPrompt(); } - private void setupPredefinedTools() { - predefinedTools = new TreeMap<>(); - for (CommandLineDiffTool tool : CommandLineDiffTool.values()) { - predefinedTools.put(tool.name(), new PreDefinedDiffTool(tool)); + private ExternalDiffTool guessTool(String toolName, BooleanTriState gui) + throws ToolException { + if (StringUtils.isEmptyOrNull(toolName)) { + toolName = getDefaultToolName(gui); } + ExternalDiffTool tool = getTool(toolName); + if (tool == null) { + throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$ + } + return tool; } - private void setupUserDefinedTools() { - userDefinedTools = new TreeMap<>(); - Map userTools = config.getTools(); + private ExternalDiffTool getTool(final String name) { + ExternalDiffTool tool = userDefinedTools.get(name); + if (tool == null) { + tool = predefinedTools.get(name); + } + return tool; + } + + private static Map setupPredefinedTools() { + Map tools = new TreeMap<>(); + for (CommandLineDiffTool tool : CommandLineDiffTool.values()) { + tools.put(tool.name(), new PreDefinedDiffTool(tool)); + } + return tools; + } + + private static Map setupUserDefinedTools( + DiffToolConfig cfg, Map predefTools) { + Map tools = new TreeMap<>(); + Map userTools = cfg.getTools(); for (String name : userTools.keySet()) { ExternalDiffTool userTool = userTools.get(name); // if difftool..cmd is defined we have user defined tool if (userTool.getCommand() != null) { - userDefinedTools.put(name, userTool); + tools.put(name, userTool); } else if (userTool.getPath() != null) { // if difftool..path is defined we just overload the path // of predefined tool - PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefinedTools + PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefTools .get(name); if (predefTool != null) { predefTool.setPath(userTool.getPath()); } } } + return tools; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java new file mode 100644 index 000000000..cdc8f015f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java @@ -0,0 +1,160 @@ +/* + * 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.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.ObjectStream; + +/** + * The element used as left or right file for compare. + * + */ +public class FileElement { + + private final String path; + + private final String id; + + private ObjectStream stream; + + private File tempFile; + + /** + * @param path + * the file path + * @param id + * the file id + */ + public FileElement(final String path, final String id) { + this(path, id, null); + } + + /** + * @param path + * the file path + * @param id + * the file id + * @param stream + * the object stream to load instead of file + */ + public FileElement(final String path, final String id, + ObjectStream stream) { + this.path = path; + this.id = id; + this.stream = stream; + } + + /** + * @return the file path + */ + public String getPath() { + return path; + } + + /** + * @return the file id + */ + public String getId() { + return id; + } + + /** + * @param stream + * the object stream + */ + public void setStream(ObjectStream stream) { + this.stream = stream; + } + + /** + * @param workingDir the working directory used if file cannot be found (e.g. /dev/null) + * @return the object stream + * @throws IOException + */ + public File getFile(File workingDir) throws IOException { + if (tempFile != null) { + return tempFile; + } + File file = new File(path); + String name = file.getName(); + if (path.equals(DiffEntry.DEV_NULL)) { + file = new File(workingDir, "nul"); //$NON-NLS-1$ + } + else if (stream != null) { + tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$ + try (OutputStream outStream = new FileOutputStream(tempFile)) { + int read = 0; + byte[] bytes = new byte[8 * 1024]; + while ((read = stream.read(bytes)) != -1) { + outStream.write(bytes, 0, read); + } + } finally { + // stream can only be consumed once --> close it + stream.close(); + stream = null; + } + return tempFile; + } + return file; + } + + /** + * Returns a real file from work tree or a temporary file with content if + * stream is valid or if path is "/dev/null" + * + * @return the object stream + * @throws IOException + */ + public File getFile() throws IOException { + if (tempFile != null) { + return tempFile; + } + File file = new File(path); + String name = file.getName(); + // if we have a stream or file is missing ("/dev/null") then create + // temporary file + if ((stream != null) || path.equals(DiffEntry.DEV_NULL)) { + // TODO: avoid long random file name (number generated by + // createTempFile) + tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$ + if (stream != null) { + try (OutputStream outStream = new FileOutputStream(tempFile)) { + int read = 0; + byte[] bytes = new byte[8 * 1024]; + while ((read = stream.read(bytes)) != -1) { + outStream.write(bytes, 0, read); + } + } finally { + // stream can only be consumed once --> close it + stream.close(); + stream = null; + } + } + return tempFile; + } + return file; + } + + /** + * Deletes and invalidates temporary file if necessary. + */ + public void cleanTemporaries() { + if (tempFile != null && tempFile.exists()) + tempFile.delete(); + tempFile = null; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java index 1c69fb491..092cb605b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java @@ -46,17 +46,6 @@ public PreDefinedDiffTool(CommandLineDiffTool tool) { */ @Override public void setPath(String path) { - // handling of spaces in path - if (path.contains(" ")) { //$NON-NLS-1$ - // add quotes before if needed - if (!path.startsWith("\"")) { //$NON-NLS-1$ - path = "\"" + path; //$NON-NLS-1$ - } - // add quotes after if needed - if (!path.endsWith("\"")) { //$NON-NLS-1$ - path = path + "\""; //$NON-NLS-1$ - } - } super.setPath(path); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java new file mode 100644 index 000000000..7862cf596 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java @@ -0,0 +1,120 @@ +/* + * 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 org.eclipse.jgit.util.FS.ExecutionResult; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tool exception for differentiation. + * + */ +public class ToolException extends Exception { + + private final static Logger LOG = LoggerFactory + .getLogger(ToolException.class); + + private final ExecutionResult result; + + /** + * the serial version UID + */ + private static final long serialVersionUID = 1L; + + /** + * + */ + public ToolException() { + super(); + result = null; + } + + /** + * @param message + * the exception message + */ + public ToolException(String message) { + super(message); + result = null; + } + + /** + * @param message + * the exception message + * @param result + * the execution result + */ + public ToolException(String message, ExecutionResult result) { + super(message); + this.result = result; + } + + /** + * @param message + * the exception message + * @param cause + * the cause for throw + */ + public ToolException(String message, Throwable cause) { + super(message, cause); + result = null; + } + + /** + * @param cause + * the cause for throw + */ + public ToolException(Throwable cause) { + super(cause); + result = null; + } + + /** + * @return true if result is valid, false else + */ + public boolean isResult() { + return result != null; + } + + /** + * @return the execution result + */ + public ExecutionResult getResult() { + return result; + } + + /** + * @return the result Stderr + */ + public String getResultStderr() { + try { + return new String(result.getStderr().toByteArray()); + } catch (Exception e) { + LOG.warn(e.getMessage()); + } + return ""; //$NON-NLS-1$ + } + + /** + * @return the result Stdout + */ + public String getResultStdout() { + try { + return new String(result.getStdout().toByteArray()); + } catch (Exception e) { + LOG.warn(e.getMessage()); + } + return ""; //$NON-NLS-1$ + } + +} From 24171b05f0db8487d0a1dc40072cc313bb6e2087 Mon Sep 17 00:00:00 2001 From: Andre Bossert Date: Fri, 8 Mar 2019 21:32:57 +0100 Subject: [PATCH 2/7] Add config reader for user-defined mergetools see: https://git-scm.com/docs/git-mergetool see: https://git-scm.com/docs/git-config * add config reader for user-defined mergetools * merge.tool * merge.guitool * mergetool.prompt * mergetool.keepBackup * mergetool.keepTemporaries * mergetool.writeToTemp * mergetool..path * mergetool..cmd * mergetool..trustExitCode Bug: 356832 Change-Id: Ic4f07376630713d8e06cbad284d9c72c9ecc0405 Signed-off-by: Andre Bossert --- .../diffmergetool/ExternalMergeToolTest.java | 109 ++++++++++++++ .../diffmergetool/ExternalMergeTool.java | 23 +++ .../diffmergetool/MergeToolConfig.java | 141 ++++++++++++++++++ .../internal/diffmergetool/MergeTools.java | 119 +++++++++++++++ .../diffmergetool/UserDefinedMergeTool.java | 59 ++++++++ .../org/eclipse/jgit/lib/ConfigConstants.java | 36 ++++- 6 files changed, 483 insertions(+), 4 deletions(-) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java new file mode 100644 index 000000000..96fd1026c --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020-2022, 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 merge tools. + */ +public class ExternalMergeToolTest extends ExternalToolTestCase { + + @Test + public void testToolNames() { + MergeTools manager = new MergeTools(db); + Set actualToolNames = manager.getToolNames(); + Set expectedToolNames = Collections.emptySet(); + assertEquals("Incorrect set of external diff tool names", + expectedToolNames, actualToolNames); + } + + @Test + public void testAllTools() { + MergeTools manager = new MergeTools(db); + Set actualToolNames = manager.getAvailableTools().keySet(); + Set expectedToolNames = Collections.emptySet(); + assertEquals("Incorrect set of available external diff tools", + expectedToolNames, actualToolNames); + } + + @Test + public void testUserDefinedTools() { + MergeTools manager = new MergeTools(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() { + MergeTools manager = new MergeTools(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() throws ToolException { + MergeTools manager = new MergeTools(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.merge(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"); + + MergeTools manager = new MergeTools(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 MergeTools(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/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java new file mode 100644 index 000000000..bcc749ada --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018-2022, 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 merge tool interface. + */ +public interface ExternalMergeTool extends ExternalDiffTool { + + /** + * @return the tool "trust exit code" option + */ + boolean isTrustExitCode(); + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java new file mode 100644 index 000000000..e91282261 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2018-2022, 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.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Config.SectionParser; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * Keeps track of difftool related configuration options. + */ +public class MergeToolConfig { + + /** Key for {@link Config#get(SectionParser)}. */ + public static final Config.SectionParser KEY = MergeToolConfig::new; + + private final String toolName; + + private final String guiToolName; + + private final boolean prompt; + + private final boolean keepBackup; + + private final boolean keepTemporaries; + + private final boolean writeToTemp; + + private final Map tools; + + private MergeToolConfig(Config rc) { + toolName = rc.getString(ConfigConstants.CONFIG_MERGE_SECTION, null, + ConfigConstants.CONFIG_KEY_TOOL); + guiToolName = rc.getString(ConfigConstants.CONFIG_MERGE_SECTION, null, + ConfigConstants.CONFIG_KEY_GUITOOL); + prompt = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION, + ConfigConstants.CONFIG_KEY_PROMPT, true); + keepBackup = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION, + ConfigConstants.CONFIG_KEY_KEEP_BACKUP, true); + keepTemporaries = rc.getBoolean( + ConfigConstants.CONFIG_MERGETOOL_SECTION, + ConfigConstants.CONFIG_KEY_KEEP_TEMPORARIES, false); + writeToTemp = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION, + ConfigConstants.CONFIG_KEY_WRITE_TO_TEMP, false); + tools = new HashMap<>(); + Set subsections = rc + .getSubsections(ConfigConstants.CONFIG_MERGETOOL_SECTION); + for (String name : subsections) { + String cmd = rc.getString(ConfigConstants.CONFIG_MERGETOOL_SECTION, + name, ConfigConstants.CONFIG_KEY_CMD); + String path = rc.getString(ConfigConstants.CONFIG_MERGETOOL_SECTION, + name, ConfigConstants.CONFIG_KEY_PATH); + BooleanTriState trustExitCode = BooleanTriState.FALSE; + String trustStr = rc.getString( + ConfigConstants.CONFIG_MERGETOOL_SECTION, name, + ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE); + if (trustStr != null) { + trustExitCode = Boolean.valueOf(trustStr).booleanValue() + ? BooleanTriState.TRUE + : BooleanTriState.FALSE; + } else { + trustExitCode = BooleanTriState.UNSET; + } + if ((cmd != null) || (path != null)) { + tools.put(name, + new UserDefinedMergeTool(name, path, cmd, + trustExitCode)); + } + } + } + + /** + * @return the default merge tool name (merge.tool) + */ + public String getDefaultToolName() { + return toolName; + } + + /** + * @return the default GUI merge tool name (merge.guitool) + */ + public String getDefaultGuiToolName() { + return guiToolName; + } + + /** + * @return the merge tool "prompt" option (mergetool.prompt) + */ + public boolean isPrompt() { + return prompt; + } + + /** + * @return the tool "keep backup" option + */ + public boolean isKeepBackup() { + return keepBackup; + } + + /** + * @return the tool "keepTemporaries" option + */ + public boolean isKeepTemporaries() { + return keepTemporaries; + } + + /** + * @return the tool "write to temp" option + */ + public boolean isWriteToTemp() { + return writeToTemp; + } + + /** + * @return the tools map + */ + public Map getTools() { + return tools; + } + + /** + * @return the tool names + */ + public Set getToolNames() { + return tools.keySet(); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java new file mode 100644 index 000000000..bb5d73eeb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2018-2022, 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.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * Manages merge tools. + */ +public class MergeTools { + private final MergeToolConfig config; + + private final Map predefinedTools; + + private final Map userDefinedTools; + + /** + * @param repo + * the repository database + */ + public MergeTools(Repository repo) { + config = repo.getConfig().get(MergeToolConfig.KEY); + predefinedTools = setupPredefinedTools(); + userDefinedTools = setupUserDefinedTools(); + } + + /** + * @param localFile + * the local file element + * @param remoteFile + * the remote file element + * @param baseFile + * the base file element + * @param mergedFilePath + * the path of 'merged' file + * @param toolName + * the selected tool name (can be null) + * @param prompt + * the prompt option + * @param trustExitCode + * the "trust exit code" option + * @param gui + * the GUI option + * @return the execution result from tool + * @throws ToolException + */ + public int merge(String localFile, + String remoteFile, String baseFile, String mergedFilePath, + String toolName, BooleanTriState prompt, BooleanTriState gui, + BooleanTriState trustExitCode) + throws ToolException { + return 0; + } + + /** + * @return the tool names + */ + public Set getToolNames() { + return config.getToolNames(); + } + + /** + * @return the user defined tools + */ + public Map getUserDefinedTools() { + return userDefinedTools; + } + + /** + * @return the available predefined tools + */ + public Map getAvailableTools() { + return predefinedTools; + } + + /** + * @return the NOT available predefined tools + */ + public Map getNotAvailableTools() { + return 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 config.isPrompt(); + } + + private Map setupPredefinedTools() { + return new TreeMap<>(); + } + + private Map setupUserDefinedTools() { + return new TreeMap<>(); + } +} \ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java new file mode 100644 index 000000000..df4d8cb8c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018-2022, 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 org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * The user-defined merge tool. + */ +public class UserDefinedMergeTool extends UserDefinedDiffTool + implements ExternalMergeTool { + + /** + * the merge tool "trust exit code" option + */ + private final BooleanTriState trustExitCode; + + /** + * Creates the merge tool + * + * @param name + * the name + * @param path + * the path + * @param cmd + * the command + * @param trustExitCode + * the "trust exit code" option + */ + public UserDefinedMergeTool(String name, String path, String cmd, + BooleanTriState trustExitCode) { + super(name, path, cmd); + this.trustExitCode = trustExitCode; + } + + /** + * @return the "trust exit code" flag + */ + @Override + public boolean isTrustExitCode() { + return trustExitCode == BooleanTriState.TRUE; + } + + /** + * @return the "trust exit code" option + */ + public BooleanTriState getTrustExitCode() { + return trustExitCode; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 8ad32d41c..e982a33b2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -31,14 +31,14 @@ public final class ConfigConstants { public static final String CONFIG_DIFF_SECTION = "diff"; /** - * The "tool" key within "diff" section + * The "tool" key within "diff" or "merge" section * * @since 6.1 */ public static final String CONFIG_KEY_TOOL = "tool"; /** - * The "guitool" key within "diff" section + * The "guitool" key within "diff" or "merge" section * * @since 6.1 */ @@ -52,14 +52,14 @@ public final class ConfigConstants { public static final String CONFIG_DIFFTOOL_SECTION = "difftool"; /** - * The "prompt" key within "difftool" section + * The "prompt" key within "difftool" or "mergetool" section * * @since 6.1 */ public static final String CONFIG_KEY_PROMPT = "prompt"; /** - * The "trustExitCode" key within "difftool" section + * The "trustExitCode" key within "difftool" or "mergetool.." section * * @since 6.1 */ @@ -123,6 +123,34 @@ public final class ConfigConstants { */ public static final String CONFIG_MERGE_SECTION = "merge"; + /** + * The "mergetool" section + * + * @since 5.13 + */ + public static final String CONFIG_MERGETOOL_SECTION = "mergetool"; + + /** + * The "keepBackup" key within "mergetool" section + * + * @since 5.13 + */ + public static final String CONFIG_KEY_KEEP_BACKUP = "keepBackup"; + + /** + * The "keepTemporaries" key within "mergetool" section + * + * @since 5.13 + */ + public static final String CONFIG_KEY_KEEP_TEMPORARIES = "keepTemporaries"; + + /** + * The "writeToTemp" key within "mergetool" section + * + * @since 5.13 + */ + public static final String CONFIG_KEY_WRITE_TO_TEMP = "writeToTemp"; + /** * The "filter" section * @since 4.6 From 85734356351ec2df4067b2472a37f6d9bcbb7350 Mon Sep 17 00:00:00 2001 From: Andre Bossert Date: Sun, 19 Jan 2020 20:52:56 +0100 Subject: [PATCH 3/7] Add command line support for "git mergetool" see: https://git-scm.com/docs/git-mergetool see: https://git-scm.com/docs/git-config * add command line support for "git mergetool" * add option handling for "--tool-help", "--tool=", "--[no-]prompt", "--[no-]gui" * handle prompt * add MergeTools * add pre-defined mergetools * print merge actions --> no execute, will be done later Bug: 356832 Change-Id: I6e505ffc3d03f75ecf4bba452a25d25dfcf5793f Signed-off-by: Andre Bossert --- .../org/eclipse/jgit/pgm/DiffToolTest.java | 106 +----- .../jgit/pgm/ExternalToolTestCase.java | 136 ++++++++ .../org/eclipse/jgit/pgm/MergeToolTest.java | 136 ++++++++ .../services/org.eclipse.jgit.pgm.TextBuiltin | 1 + .../jgit/pgm/internal/CLIText.properties | 3 + .../src/org/eclipse/jgit/pgm/DiffTool.java | 4 +- .../src/org/eclipse/jgit/pgm/MergeTool.java | 212 ++++++++++++ .../diffmergetool/ExternalMergeToolTest.java | 217 ++++++++++-- .../diffmergetool/CommandLineMergeTool.java | 327 ++++++++++++++++++ .../internal/diffmergetool/DiffTools.java | 7 +- .../diffmergetool/ExternalMergeTool.java | 12 +- .../diffmergetool/MergeToolConfig.java | 58 ++-- .../internal/diffmergetool/MergeTools.java | 105 +++++- .../diffmergetool/PreDefinedMergeTool.java | 91 +++++ .../diffmergetool/UserDefinedMergeTool.java | 28 +- .../org/eclipse/jgit/lib/ConfigConstants.java | 3 +- 16 files changed, 1272 insertions(+), 174 deletions(-) create mode 100644 org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java create mode 100644 org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java create mode 100644 org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java index e2ff18927..017a5d994 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021, Simeon Andreev and others. + * Copyright (C) 2021-2022, 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 @@ -14,68 +14,30 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.eclipse.jgit.api.Git; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool; -import org.eclipse.jgit.lib.CLIRepositoryTestCase; import org.eclipse.jgit.lib.StoredConfig; -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; +public class DiffToolTest extends ExternalToolTestCase { - @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 static final String TOOL_NAME = "some_tool"; - private Git git; + private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION; @Override @Before public void setUp() throws Exception { super.setUp(); - git = new Git(db); - git.commit().setMessage("initial commit").call(); configureEchoTool(TOOL_NAME); } @@ -83,7 +45,7 @@ public void setUp() throws Exception { public void testNotDefinedTool() throws Exception { createUnstagedChanges(); - runAndCaptureUsingInitRaw("difftool", "--tool", "undefined"); + runAndCaptureUsingInitRaw(DIFF_TOOL, "--tool", "undefined"); fail("Expected exception when trying to run undefined tool"); } @@ -91,7 +53,7 @@ public void testNotDefinedTool() throws Exception { public void testTool() throws Exception { RevCommit commit = createUnstagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] expectedOutput = getExpectedToolOutput(changes); String[] options = { "--tool", @@ -101,7 +63,7 @@ public void testTool() throws Exception { for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, expectedOutput, - runAndCaptureUsingInitRaw("difftool", option, + runAndCaptureUsingInitRaw(DIFF_TOOL, option, TOOL_NAME)); } } @@ -110,13 +72,13 @@ public void testTool() throws Exception { public void testToolTrustExitCode() throws Exception { RevCommit commit = createUnstagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] expectedOutput = getExpectedToolOutput(changes); String[] options = { "--tool", "-t", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, "--trust-exit-code", option, TOOL_NAME)); } } @@ -125,13 +87,13 @@ expectedOutput, runAndCaptureUsingInitRaw("difftool", public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception { RevCommit commit = createUnstagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] expectedOutput = getExpectedToolOutput(changes); String[] options = { "--tool", "-t", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, "--no-gui", "--no-prompt", "--no-trust-exit-code", option, TOOL_NAME)); } @@ -141,13 +103,13 @@ expectedOutput, runAndCaptureUsingInitRaw("difftool", public void testToolCached() throws Exception { RevCommit commit = createStagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] expectedOutput = getExpectedToolOutput(changes); String[] options = { "--cached", "--staged", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, option, "--tool", TOOL_NAME)); } } @@ -174,7 +136,8 @@ public void testToolHelp() throws Exception { String option = "--tool-help"; assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput.toArray(new String[0]), runAndCaptureUsingInitRaw("difftool", option)); + expectedOutput.toArray(new String[0]), + runAndCaptureUsingInitRaw(DIFF_TOOL, option)); } private void configureEchoTool(String toolName) { @@ -196,33 +159,7 @@ private void configureEchoTool(String toolName) { String.valueOf(false)); } - 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) { + private String[] getExpectedToolOutput(List changes) { String[] expectedToolOutput = new String[changes.size()]; for (int i = 0; i < changes.size(); ++i) { DiffEntry change = changes.get(i); @@ -232,17 +169,4 @@ private String[] getExpectedDiffToolOutput(List changes) { } return expectedToolOutput; } - - private static void assertArrayOfLinesEquals(String failMessage, - String[] expected, String[] actual) { - assertEquals(failMessage, toString(expected), toString(actual)); - } - - private static String getEchoCommand() { - /* - * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be - * replaced with full paths to a temporary file during some of the tests - */ - return "(echo \"$MERGED\")"; - } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java new file mode 100644 index 000000000..e10b13efb --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022, 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.CherryPickResult; +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.kohsuke.args4j.Argument; + +/** + * Base test case for the {@code difftool} and {@code mergetool} commands. + */ +public abstract class ExternalToolTestCase 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<>(); + } + + protected static final String TOOL_NAME = "some_tool"; + + private static final String TEST_BRANCH_NAME = "test_branch"; + + private Git git; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + git = new Git(db); + git.commit().setMessage("initial commit").call(); + git.branchCreate().setName(TEST_BRANCH_NAME).call(); + } + + protected 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]); + } + + protected CherryPickResult createMergeConflict() throws Exception { + writeTrashFile("a", "Hello world a"); + writeTrashFile("b", "Hello world b"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b added").call(); + writeTrashFile("a", "Hello world a 1"); + writeTrashFile("b", "Hello world b 1"); + git.add().addFilepattern(".").call(); + RevCommit commit1 = git.commit().setMessage("files a & b commit 1") + .call(); + git.branchCreate().setName("branch_1").call(); + git.checkout().setName(TEST_BRANCH_NAME).call(); + writeTrashFile("a", "Hello world a 2"); + writeTrashFile("b", "Hello world b 2"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b commit 2").call(); + git.branchCreate().setName("branch_2").call(); + CherryPickResult result = git.cherryPick().include(commit1).call(); + return result; + } + + protected 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; + } + + protected RevCommit createStagedChanges() throws Exception { + RevCommit commit = createUnstagedChanges(); + git.add().addFilepattern(".").call(); + return commit; + } + + protected 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; + } + + protected static void assertArrayOfLinesEquals(String failMessage, + String[] expected, String[] actual) { + assertEquals(failMessage, toString(expected), toString(actual)); + } + + protected static String getEchoCommand() { + /* + * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be + * replaced with full paths to a temporary file during some of the tests + */ + return "(echo \"$MERGED\")"; + } +} diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java new file mode 100644 index 000000000..32cd60415 --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022, 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.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jgit.internal.diffmergetool.CommandLineMergeTool; +import org.eclipse.jgit.lib.StoredConfig; +import org.junit.Before; +import org.junit.Test; + +/** + * Testing the {@code mergetool} command. + */ +public class MergeToolTest extends ExternalToolTestCase { + + private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + configureEchoTool(TOOL_NAME); + } + + @Test + public void testTool() throws Exception { + createMergeConflict(); + String[] expectedOutput = getExpectedToolOutput(); + + String[] options = { + "--tool", + "-t", + }; + + for (String option : options) { + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, + runAndCaptureUsingInitRaw(MERGE_TOOL, option, + TOOL_NAME)); + } + } + + @Test + public void testToolNoGuiNoPrompt() throws Exception { + createMergeConflict(); + String[] expectedOutput = getExpectedToolOutput(); + + String[] options = { "--tool", "-t", }; + + for (String option : options) { + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL, + "--no-gui", "--no-prompt", option, TOOL_NAME)); + } + } + + @Test + public void testToolHelp() throws Exception { + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + List expectedOutput = new ArrayList<>(); + expectedOutput.add( + "'git mergetool --tool=' may be set to one of the following:"); + for (CommandLineMergeTool defaultTool : defaultTools) { + String toolName = defaultTool.name(); + expectedOutput.add(toolName); + } + String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " " + + getEchoCommand(); + expectedOutput.add("user-defined:"); + expectedOutput.add(customToolHelpLine); + String[] userDefinedToolsHelp = { + "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.", + }; + expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp)); + + String option = "--tool-help"; + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput.toArray(new String[0]), + runAndCaptureUsingInitRaw(MERGE_TOOL, option)); + } + + private void configureEchoTool(String toolName) { + StoredConfig config = db.getConfig(); + // the default merge tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + String command = getEchoCommand(); + + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + /* + * prevent prompts as we are running in tests and there is no user to + * interact with on the command line + */ + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PROMPT, + String.valueOf(false)); + } + + private String[] getExpectedToolOutput() { + String[] mergeConflictFilenames = { "a", "b", }; + List expectedOutput = new ArrayList<>(); + expectedOutput.add("Merging:"); + for (String mergeConflictFilename : mergeConflictFilenames) { + expectedOutput.add(mergeConflictFilename); + } + for (String mergeConflictFilename : mergeConflictFilenames) { + expectedOutput.add("Normal merge conflict for '" + + mergeConflictFilename + "':"); + expectedOutput.add("{local}: modified file"); + expectedOutput.add("{remote}: modified file"); + expectedOutput.add("TODO: Launch mergetool '" + TOOL_NAME + + "' for path '" + mergeConflictFilename + "'..."); + } + return expectedOutput.toArray(new String[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 8c44764c6..ea1d1e3fa 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 @@ -25,6 +25,7 @@ org.eclipse.jgit.pgm.LsRemote org.eclipse.jgit.pgm.LsTree org.eclipse.jgit.pgm.Merge org.eclipse.jgit.pgm.MergeBase +org.eclipse.jgit.pgm.MergeTool org.eclipse.jgit.pgm.Push org.eclipse.jgit.pgm.ReceivePack org.eclipse.jgit.pgm.Reflog 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 3653b9d8f..8e2eef7eb 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 @@ -255,6 +255,7 @@ 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_MergeGuiTool=When git-mergetool is invoked with the -g or --gui option the default merge tool will be read from the configured merge.guitool variable instead of merge.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 @@ -303,6 +304,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 . 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_ToolForMerge=Use the merge resolution program specified by . Run git mergetool --tool-help for the list of valid settings.\nIf a merge resolution program is not specified, git mergetool will use the configuration variable merge.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 @@ -350,6 +352,7 @@ usage_date=date format, one of default, rfc, local, iso, short, raw (as defined 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_MergeTool=git-mergetool - Run merge conflict resolution tools to resolve merge conflicts.\nUse git mergetool to run one of several merge utilities to resolve merge conflicts. It is typically run after git merge. usage_directoriesToExport=directories to export usage_disableTheServiceInAllRepositories=disable the service in all repositories usage_displayAListOfAllRegisteredJgitCommands=Display a list of all registered jgit commands diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java index 2f7417745..2e90d52cb 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2021, Andre Bossert + * Copyright (C) 2018-2022, 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 @@ -192,7 +192,7 @@ private void compare(List files, boolean showPrompt, outw.flush(); errw.println(e.getMessage()); throw die(MessageFormat.format( - CLIText.get().diffToolDied, mergedFilePath, e)); + CLIText.get().diffToolDied, mergedFilePath), e); } } else { break; diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java new file mode 100644 index 000000000..37afa54c7 --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2018-2022, 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 java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.Status; +import org.eclipse.jgit.api.StatusCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.internal.diffmergetool.MergeTools; +import org.eclipse.jgit.lib.IndexDiff.StageState; +import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.lib.Repository; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.spi.RestOfArgumentsHandler; + +@Command(name = "mergetool", common = true, usage = "usage_MergeTool") +class MergeTool extends TextBuiltin { + private MergeTools mergeTools; + + @Option(name = "--tool", aliases = { + "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge") + private String toolName; + + private Optional prompt = Optional.empty(); + + @Option(name = "--prompt", usage = "usage_prompt") + void setPrompt(@SuppressWarnings("unused") boolean on) { + prompt = Optional.of(Boolean.TRUE); + } + + @Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt") + void noPrompt(@SuppressWarnings("unused") boolean on) { + prompt = Optional.of(Boolean.FALSE); + } + + @Option(name = "--tool-help", usage = "usage_toolHelp") + private boolean toolHelp; + + private BooleanTriState gui = BooleanTriState.UNSET; + + @Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool") + 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; + } + + @Argument(required = false, index = 0, metaVar = "metaVar_paths") + @Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class) + protected List filterPaths; + + @Override + protected void init(Repository repository, String gitDir) { + super.init(repository, gitDir); + mergeTools = new MergeTools(repository); + } + + @Override + protected void run() { + try { + if (toolHelp) { + showToolHelp(); + } else { + // get prompt + boolean showPrompt = mergeTools.isInteractive(); + if (prompt.isPresent()) { + showPrompt = prompt.get().booleanValue(); + } + // get passed or default tool name + String toolNameSelected = toolName; + if ((toolNameSelected == null) || toolNameSelected.isEmpty()) { + toolNameSelected = mergeTools.getDefaultToolName(gui); + } + // get the changed files + Map files = getFiles(); + if (files.size() > 0) { + merge(files, showPrompt, toolNameSelected); + } else { + outw.println("No files need merging"); //$NON-NLS-1$ + } + } + outw.flush(); + } catch (Exception e) { + throw die(e.getMessage(), e); + } + } + + private void merge(Map files, boolean showPrompt, + String toolNamePrompt) throws Exception { + // sort file names + List fileNames = new ArrayList<>(files.keySet()); + Collections.sort(fileNames); + // show the files + outw.println("Merging:"); //$NON-NLS-1$ + for (String fileName : fileNames) { + outw.println(fileName); + } + outw.flush(); + for (String fileName : fileNames) { + StageState fileState = files.get(fileName); + // only both-modified is valid for mergetool + if (fileState == StageState.BOTH_MODIFIED) { + outw.println("\nNormal merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ + outw.println(" {local}: modified file"); //$NON-NLS-1$ + outw.println(" {remote}: modified file"); //$NON-NLS-1$ + // check if user wants to launch merge resolution tool + boolean launch = true; + if (showPrompt) { + launch = isLaunch(toolNamePrompt); + } + if (launch) { + outw.println("TODO: Launch mergetool '" + toolNamePrompt //$NON-NLS-1$ + + "' for path '" + fileName + "'..."); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + break; + } + } else if ((fileState == StageState.DELETED_BY_US) || (fileState == StageState.DELETED_BY_THEM)) { + outw.println("\nDeleted merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + outw.println( + "\nUnknown merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ + break; + } + } + } + + private boolean isLaunch(String toolNamePrompt) + throws IOException { + boolean launch = true; + outw.println("Hit return to start merge resolution tool (" //$NON-NLS-1$ + + toolNamePrompt + "): "); //$NON-NLS-1$ + outw.flush(); + BufferedReader br = new BufferedReader(new InputStreamReader(ins)); + String line = null; + if ((line = br.readLine()) != null) { + if (!line.equalsIgnoreCase("Y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$ + launch = false; + } + } + return launch; + } + + private void showToolHelp() throws IOException { + outw.println( + "'git mergetool --tool=' may be set to one of the following:"); //$NON-NLS-1$ + for (String name : mergeTools.getAvailableTools().keySet()) { + outw.println("\t\t" + name); //$NON-NLS-1$ + } + outw.println(""); //$NON-NLS-1$ + outw.println("\tuser-defined:"); //$NON-NLS-1$ + Map userTools = mergeTools + .getUserDefinedTools(); + for (String name : userTools.keySet()) { + outw.println("\t\t" + name + ".cmd " //$NON-NLS-1$ //$NON-NLS-2$ + + userTools.get(name).getCommand()); + } + outw.println(""); //$NON-NLS-1$ + outw.println( + "The following tools are valid, but not currently available:"); //$NON-NLS-1$ + for (String name : mergeTools.getNotAvailableTools().keySet()) { + outw.println("\t\t" + name); //$NON-NLS-1$ + } + outw.println(""); //$NON-NLS-1$ + outw.println("Some of the tools listed above only work in a windowed"); //$NON-NLS-1$ + outw.println( + "environment. If run in a terminal-only session, they will fail."); //$NON-NLS-1$ + return; + } + + private Map getFiles() + throws RevisionSyntaxException, NoWorkTreeException, + GitAPIException { + Map files = new TreeMap<>(); + try (Git git = new Git(db)) { + StatusCommand statusCommand = git.status(); + if (filterPaths != null && filterPaths.size() > 0) { + for (String path : filterPaths) { + statusCommand.addPath(path); + } + } + Status status = statusCommand.call(); + files = status.getConflictingStageState(); + } + return files; + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java index 96fd1026c..1dea44eaa 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java @@ -9,13 +9,27 @@ */ package org.eclipse.jgit.internal.diffmergetool; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS.ExecutionResult; import org.junit.Test; /** @@ -23,12 +37,60 @@ */ public class ExternalMergeToolTest extends ExternalToolTestCase { + @Test(expected = ToolException.class) + public void testUserToolWithError() throws Exception { + String toolName = "customTool"; + + int errorReturnCode = 1; + String command = "exit " + errorReturnCode; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, + CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(Boolean.TRUE)); + + MergeTools manager = new MergeTools(db); + + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + + manager.merge(db, local, remote, base, merged.getPath(), toolName, + prompt, gui); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + + @Test(expected = ToolException.class) + public void testUserToolWithCommandNotFoundError() throws Exception { + String toolName = "customTool"; + + int errorReturnCode = 127; // command not found + String command = "exit " + errorReturnCode; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + MergeTools manager = new MergeTools(db); + + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + + manager.merge(db, local, remote, base, merged.getPath(), toolName, + prompt, gui); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + @Test public void testToolNames() { MergeTools manager = new MergeTools(db); Set actualToolNames = manager.getToolNames(); Set expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of external diff tool names", + assertEquals("Incorrect set of external merge tool names", expectedToolNames, actualToolNames); } @@ -36,18 +98,58 @@ public void testToolNames() { public void testAllTools() { MergeTools manager = new MergeTools(db); Set actualToolNames = manager.getAvailableTools().keySet(); - Set expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of available external diff tools", - expectedToolNames, actualToolNames); + Set expectedToolNames = new LinkedHashSet<>(); + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + for (CommandLineMergeTool defaultTool : defaultTools) { + String toolName = defaultTool.name(); + expectedToolNames.add(toolName); + } + assertEquals("Incorrect set of external merge tools", expectedToolNames, + actualToolNames); + } + + @Test + public void testOverridePredefinedToolPath() { + String toolName = CommandLineMergeTool.guiffy.name(); + String customToolPath = "/usr/bin/echo"; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + "echo"); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PATH, + customToolPath); + + MergeTools manager = new MergeTools(db); + Map tools = manager.getUserDefinedTools(); + ExternalMergeTool mergeTool = tools.get(toolName); + assertNotNull("Expected tool \"" + toolName + "\" to be user defined", + mergeTool); + + String toolPath = mergeTool.getPath(); + assertEquals("Expected external merge tool to have an overriden path", + customToolPath, toolPath); } @Test public void testUserDefinedTools() { + FileBasedConfig config = db.getConfig(); + String customToolname = "customTool"; + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_CMD, "echo"); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_PATH, "/usr/bin/echo"); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_PROMPT, String.valueOf(false)); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_GUITOOL, String.valueOf(false)); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false)); MergeTools manager = new MergeTools(db); Set actualToolNames = manager.getUserDefinedTools().keySet(); - Set expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of user defined external diff tools", - expectedToolNames, actualToolNames); + Set expectedToolNames = new LinkedHashSet<>(); + expectedToolNames.add(customToolname); + assertEquals("Incorrect set of external merge tools", expectedToolNames, + actualToolNames); } @Test @@ -55,55 +157,118 @@ public void testNotAvailableTools() { MergeTools manager = new MergeTools(db); Set actualToolNames = manager.getNotAvailableTools().keySet(); Set expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of not available external diff tools", + assertEquals("Incorrect set of not available external merge tools", expectedToolNames, actualToolNames); } @Test public void testCompare() throws ToolException { - MergeTools manager = new MergeTools(db); + String toolName = "customTool"; + + FileBasedConfig config = db.getConfig(); + // the default merge tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + String command = getEchoCommand(); + + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); - String newPath = ""; - String oldPath = ""; - String newId = ""; - String oldId = ""; - String toolName = ""; BooleanTriState prompt = BooleanTriState.UNSET; BooleanTriState gui = BooleanTriState.UNSET; - BooleanTriState trustExitCode = BooleanTriState.UNSET; + + MergeTools manager = new MergeTools(db); int expectedCompareResult = 0; - int compareResult = manager.merge(newPath, oldPath, newId, oldId, - toolName, prompt, gui, trustExitCode); - assertEquals("Incorrect compare result for external diff tool", - expectedCompareResult, compareResult); + ExecutionResult compareResult = manager.merge(db, local, remote, base, + merged.getPath(), toolName, prompt, gui); + assertEquals("Incorrect compare result for external merge tool", + expectedCompareResult, compareResult.getRc()); } @Test public void testDefaultTool() throws Exception { + String toolName = "customTool"; + String guiToolName = "customGuiTool"; + FileBasedConfig config = db.getConfig(); - // the default diff tool is configured without a subsection + // the default merge tool is configured without a subsection String subsection = null; - config.setString("diff", subsection, "tool", "customTool"); + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); MergeTools manager = new MergeTools(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); + "Expected configured mergetool to be the default external merge tool", + toolName, defaultToolName); gui = BooleanTriState.TRUE; String defaultGuiToolName = manager.getDefaultToolName(gui); assertEquals( - "Expected configured difftool to be the default external diff tool", + "Expected configured mergetool to be the default external merge tool", "my_gui_tool", defaultGuiToolName); - config.setString("diff", subsection, "guitool", "customGuiTool"); + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL, + guiToolName); manager = new MergeTools(db); defaultGuiToolName = manager.getDefaultToolName(gui); assertEquals( - "Expected configured difftool to be the default external diff guitool", + "Expected configured mergetool to be the default external merge guitool", "my_gui_tool", defaultGuiToolName); } + + @Test + public void testOverridePreDefinedToolPath() { + String newToolPath = "/tmp/path/"; + + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + assertTrue("Expected to find pre-defined external merge tools", + defaultTools.length > 0); + + CommandLineMergeTool overridenTool = defaultTools[0]; + String overridenToolName = overridenTool.name(); + String overridenToolPath = newToolPath + overridenToolName; + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, overridenToolName, + CONFIG_KEY_PATH, overridenToolPath); + + MergeTools manager = new MergeTools(db); + Map availableTools = manager + .getAvailableTools(); + ExternalMergeTool externalMergeTool = availableTools + .get(overridenToolName); + String actualMergeToolPath = externalMergeTool.getPath(); + assertEquals( + "Expected pre-defined external merge tool to have overriden path", + overridenToolPath, actualMergeToolPath); + boolean withBase = true; + String expectedMergeToolCommand = overridenToolPath + " " + + overridenTool.getParameters(withBase); + String actualMergeToolCommand = externalMergeTool.getCommand(); + assertEquals( + "Expected pre-defined external merge tool to have overriden command", + expectedMergeToolCommand, actualMergeToolCommand); + } + + @Test(expected = ToolException.class) + public void testUndefinedTool() throws Exception { + MergeTools manager = new MergeTools(db); + + String toolName = "undefined"; + BooleanTriState prompt = BooleanTriState.UNSET; + BooleanTriState gui = BooleanTriState.UNSET; + + manager.merge(db, local, remote, base, merged.getPath(), toolName, + prompt, gui); + fail("Expected exception to be thrown due to not defined external merge tool"); + } + + private String getEchoCommand() { + return "(echo \"$LOCAL\" \"$REMOTE\") > " + + commandResult.getAbsolutePath(); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java new file mode 100644 index 000000000..3a2212432 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2018-2022, 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; + +/** + * Pre-defined merge tools. + * + * Adds same merge tools as also pre-defined in C-Git see "git-core\mergetools\" + * see links to command line parameter description for the tools + * + *
+ * araxis
+ * bc
+ * bc3
+ * codecompare
+ * deltawalker
+ * diffmerge
+ * diffuse
+ * ecmerge
+ * emerge
+ * examdiff
+ * guiffy
+ * gvimdiff
+ * gvimdiff2
+ * gvimdiff3
+ * kdiff3
+ * kompare
+ * meld
+ * opendiff
+ * p4merge
+ * tkdiff
+ * tortoisemerge
+ * vimdiff
+ * vimdiff2
+ * vimdiff3
+ * winmerge
+ * xxdiff
+ * 
+ * + */ +@SuppressWarnings("nls") +public enum CommandLineMergeTool { + /** + * See: https://www.araxis.com/merge/documentation-windows/command-line.en + */ + araxis("compare", + "-wait -merge -3 -a1 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + "-wait -2 \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + false), + /** + * See: https://www.scootersoftware.com/v4help/index.html?command_line_reference.html + */ + bc("bcomp", "\"$LOCAL\" \"$REMOTE\" \"$BASE\" --mergeoutput=\"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" --mergeoutput=\"$MERGED\"", + false), + /** + * See: https://www.scootersoftware.com/v4help/index.html?command_line_reference.html + */ + bc3("bcompare", bc), + /** + * See: https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm + */ + codecompare("CodeMerge", + "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -BF=\"$BASE\" -RF=\"$MERGED\"", + "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -RF=\"$MERGED\"", + false), + /** + * See: https://www.deltawalker.com/integrate/command-line + *

+ * Hint: $(pwd) command must be defined + *

+ */ + deltawalker("DeltaWalker", + "\"$LOCAL\" \"$REMOTE\" \"$BASE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"", + true), + /** + * See: https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html + */ + diffmerge("diffmerge", //$NON-NLS-1$ + "--merge --result=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "--merge --result=\"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: http://diffuse.sourceforge.net/manual.html#introduction-usage + *

+ * Hint: check the ' | cat' for the call + *

+ */ + diffuse("diffuse", "\"$LOCAL\" \"$MERGED\" \"$REMOTE\" \"$BASE\"", + "\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", false), + /** + * See: http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp + */ + ecmerge("ecmerge", + "--default --mode=merge3 \"$BASE\" \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"", + "--default --mode=merge2 \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"", + false), + /** + * See: https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html + *

+ * Hint: $(basename) command must be defined + *

+ */ + emerge("emacs", + "-f emerge-files-with-ancestor-command \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$(basename \"$MERGED\")\"", + "-f emerge-files-command \"$LOCAL\" \"$REMOTE\" \"$(basename \"$MERGED\")\"", + true), + /** + * See: https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options + */ + examdiff("ExamDiff", + "-merge \"$LOCAL\" \"$BASE\" \"$REMOTE\" -o:\"$MERGED\" -nh", + "-merge \"$LOCAL\" \"$REMOTE\" -o:\"$MERGED\" -nh", + false), + /** + * See: https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html + */ + guiffy("guiffy", "-s \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"", + "-m \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true), + /** + * See: http://vimdoc.sourceforge.net/htmldoc/diff.html + */ + gvimdiff("gvim", + "-f -d -c '4wincmd w | wincmd J' \"$LOCAL\" \"$BASE\" \"$REMOTE\" \"$MERGED\"", + "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + true), + /** + * See: http://vimdoc.sourceforge.net/htmldoc/diff.html + */ + gvimdiff2("gvim", "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", true), + /** + * See: + */ + gvimdiff3("gvim", + "-f -d -c 'hid | hid | hid' \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"", + "-f -d -c 'hid | hid' \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true), + /** + * See: http://kdiff3.sourceforge.net/doc/documentation.html + */ + kdiff3("kdiff3", + "--auto --L1 \"$MERGED (Base)\" --L2 \"$MERGED (Local)\" --L3 \"$MERGED (Remote)\" -o \"$MERGED\" \"$BASE\" \"$LOCAL\" \"$REMOTE\"", + "--auto --L1 \"$MERGED (Local)\" --L2 \"$MERGED (Remote)\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: http://meldmerge.org/help/file-mode.html + *

+ * Hint: use meld with output option only (new versions) + *

+ */ + meld("meld", "--output=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + false), + /** + * See: http://www.manpagez.com/man/1/opendiff/ + *

+ * Hint: check the ' | cat' for the call + *

+ */ + opendiff("opendiff", + "\"$LOCAL\" \"$REMOTE\" -ancestor \"$BASE\" -merge \"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" -merge \"$MERGED\"", + false), + /** + * See: https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html + *

+ * Hint: check how to fix "no base present" / create_virtual_base problem + *

+ */ + p4merge("p4merge", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", + "\"$REMOTE\" \"$LOCAL\" \"$MERGED\"", false), + /** + * See: http://linux.math.tifr.res.in/manuals/man/tkdiff.html + */ + tkdiff("tkdiff", "-a \"$BASE\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + "-o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics + *

+ * Hint: merge without base is not supported + *

+ *

+ * Hint: cannot diff + *

+ */ + tortoisegitmerge("tortoisegitmerge", + "-base \"$BASE\" -mine \"$LOCAL\" -theirs \"$REMOTE\" -merged \"$MERGED\"", + null, false), + /** + * See: https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics + *

+ * Hint: merge without base is not supported + *

+ *

+ * Hint: cannot diff + *

+ */ + tortoisemerge("tortoisemerge", + "-base:\"$BASE\" -mine:\"$LOCAL\" -theirs:\"$REMOTE\" -merged:\"$MERGED\"", + null, false), + /** + * See: http://vimdoc.sourceforge.net/htmldoc/diff.html + */ + vimdiff("vim", gvimdiff), + /** + * See: http://vimdoc.sourceforge.net/htmldoc/diff.html + */ + vimdiff2("vim", gvimdiff2), + /** + * See: http://vimdoc.sourceforge.net/htmldoc/diff.html + */ + vimdiff3("vim", gvimdiff3), + /** + * See: http://manual.winmerge.org/Command_line.html + *

+ * Hint: check how 'mergetool_find_win32_cmd "WinMergeU.exe" "WinMerge"' + * works + *

+ */ + winmerge("WinMergeU", + "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + false), + /** + * See: http://furius.ca/xxdiff/doc/xxdiff-doc.html + */ + xxdiff("xxdiff", + "-X --show-merged-pane -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "-X -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + false); + + CommandLineMergeTool(String path, String parametersWithBase, + String parametersWithoutBase, + boolean exitCodeTrustable) { + this.path = path; + this.parametersWithBase = parametersWithBase; + this.parametersWithoutBase = parametersWithoutBase; + this.exitCodeTrustable = exitCodeTrustable; + } + + CommandLineMergeTool(CommandLineMergeTool from) { + this(from.getPath(), from.getParameters(true), + from.getParameters(false), from.isExitCodeTrustable()); + } + + CommandLineMergeTool(String path, CommandLineMergeTool from) { + this(path, from.getParameters(true), from.getParameters(false), + from.isExitCodeTrustable()); + } + + private final String path; + + private final String parametersWithBase; + + private final String parametersWithoutBase; + + private final boolean exitCodeTrustable; + + /** + * @return path + */ + public String getPath() { + return path; + } + + /** + * @param withBase + * return parameters with base present? + * @return parameters with or without base present + */ + public String getParameters(boolean withBase) { + if (withBase) { + return parametersWithBase; + } + return parametersWithoutBase; + } + + /** + * @return parameters + */ + public boolean isExitCodeTrustable() { + return exitCodeTrustable; + } + + /** + * @return true if command with base present is valid, false otherwise + */ + public boolean canMergeWithoutBasePresent() { + return parametersWithoutBase != null; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index b15cbdc34..2f2b9de81 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2021, Andre Bossert + * Copyright (C) 2018-2022, 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 @@ -56,8 +56,7 @@ public DiffTools(Repository repo) { * @param remoteFile * the remote file element * @param mergedFilePath - * the path of 'merged' file, it equals local or remote path for - * difftool + * the path of 'merged' file, it equals local or remote path * @param toolName * the selected tool name (can be null) * @param prompt @@ -66,7 +65,7 @@ public DiffTools(Repository repo) { * the GUI option * @param trustExitCode * the "trust exit code" option - * @return the return code from executed tool + * @return the execution result from tool * @throws ToolException */ public ExecutionResult compare(Repository repo, FileElement localFile, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java index bcc749ada..0c3ddf9af 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.internal.diffmergetool; +import org.eclipse.jgit.lib.internal.BooleanTriState; + /** * The merge tool interface. */ @@ -18,6 +20,14 @@ public interface ExternalMergeTool extends ExternalDiffTool { /** * @return the tool "trust exit code" option */ - boolean isTrustExitCode(); + BooleanTriState getTrustExitCode(); + + /** + * @param withBase + * get command with base present (true) or without base present + * (false) + * @return the tool command + */ + String getCommand(boolean withBase); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java index e91282261..9be20b75a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java @@ -10,13 +10,24 @@ package org.eclipse.jgit.internal.diffmergetool; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_BACKUP; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_TEMPORARIES; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WRITE_TO_TEMP; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; + import java.util.HashMap; import java.util.Map; import java.util.Set; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config.SectionParser; -import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.internal.BooleanTriState; /** @@ -42,31 +53,27 @@ public class MergeToolConfig { private final Map tools; private MergeToolConfig(Config rc) { - toolName = rc.getString(ConfigConstants.CONFIG_MERGE_SECTION, null, - ConfigConstants.CONFIG_KEY_TOOL); - guiToolName = rc.getString(ConfigConstants.CONFIG_MERGE_SECTION, null, - ConfigConstants.CONFIG_KEY_GUITOOL); - prompt = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION, - ConfigConstants.CONFIG_KEY_PROMPT, true); - keepBackup = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION, - ConfigConstants.CONFIG_KEY_KEEP_BACKUP, true); - keepTemporaries = rc.getBoolean( - ConfigConstants.CONFIG_MERGETOOL_SECTION, - ConfigConstants.CONFIG_KEY_KEEP_TEMPORARIES, false); - writeToTemp = rc.getBoolean(ConfigConstants.CONFIG_MERGETOOL_SECTION, - ConfigConstants.CONFIG_KEY_WRITE_TO_TEMP, false); + toolName = rc.getString(CONFIG_MERGE_SECTION, null, CONFIG_KEY_TOOL); + guiToolName = rc.getString(CONFIG_MERGE_SECTION, null, + CONFIG_KEY_GUITOOL); + prompt = rc.getBoolean(CONFIG_MERGETOOL_SECTION, toolName, + CONFIG_KEY_PROMPT, true); + keepBackup = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_KEEP_BACKUP, true); + keepTemporaries = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_KEEP_TEMPORARIES, false); + writeToTemp = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_WRITE_TO_TEMP, false); tools = new HashMap<>(); - Set subsections = rc - .getSubsections(ConfigConstants.CONFIG_MERGETOOL_SECTION); + Set subsections = rc.getSubsections(CONFIG_MERGETOOL_SECTION); for (String name : subsections) { - String cmd = rc.getString(ConfigConstants.CONFIG_MERGETOOL_SECTION, - name, ConfigConstants.CONFIG_KEY_CMD); - String path = rc.getString(ConfigConstants.CONFIG_MERGETOOL_SECTION, - name, ConfigConstants.CONFIG_KEY_PATH); + String cmd = rc.getString(CONFIG_MERGETOOL_SECTION, name, + CONFIG_KEY_CMD); + String path = rc.getString(CONFIG_MERGETOOL_SECTION, name, + CONFIG_KEY_PATH); BooleanTriState trustExitCode = BooleanTriState.FALSE; - String trustStr = rc.getString( - ConfigConstants.CONFIG_MERGETOOL_SECTION, name, - ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE); + String trustStr = rc.getString(CONFIG_MERGETOOL_SECTION, name, + CONFIG_KEY_TRUST_EXIT_CODE); if (trustStr != null) { trustExitCode = Boolean.valueOf(trustStr).booleanValue() ? BooleanTriState.TRUE @@ -75,9 +82,8 @@ private MergeToolConfig(Config rc) { trustExitCode = BooleanTriState.UNSET; } if ((cmd != null) || (path != null)) { - tools.put(name, - new UserDefinedMergeTool(name, path, cmd, - trustExitCode)); + tools.put(name, new UserDefinedMergeTool(name, path, cmd, + trustExitCode)); } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java index bb5d73eeb..cefefb8e7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -9,17 +9,21 @@ */ package org.eclipse.jgit.internal.diffmergetool; +import java.io.File; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.util.FS.ExecutionResult; /** * Manages merge tools. */ public class MergeTools { + private final MergeToolConfig config; private final Map predefinedTools; @@ -33,10 +37,12 @@ public class MergeTools { public MergeTools(Repository repo) { config = repo.getConfig().get(MergeToolConfig.KEY); predefinedTools = setupPredefinedTools(); - userDefinedTools = setupUserDefinedTools(); + userDefinedTools = setupUserDefinedTools(config, predefinedTools); } /** + * @param repo + * the repository * @param localFile * the local file element * @param remoteFile @@ -49,19 +55,43 @@ public MergeTools(Repository repo) { * the selected tool name (can be null) * @param prompt * the prompt option - * @param trustExitCode - * the "trust exit code" option * @param gui * the GUI option * @return the execution result from tool * @throws ToolException */ - public int merge(String localFile, - String remoteFile, String baseFile, String mergedFilePath, - String toolName, BooleanTriState prompt, BooleanTriState gui, - BooleanTriState trustExitCode) + public ExecutionResult merge(Repository repo, FileElement localFile, + FileElement remoteFile, FileElement baseFile, String mergedFilePath, + String toolName, BooleanTriState prompt, BooleanTriState gui) throws ToolException { - return 0; + ExternalMergeTool tool = guessTool(toolName, gui); + try { + File workingDir = repo.getWorkTree(); + String localFilePath = localFile.getFile().getPath(); + String remoteFilePath = remoteFile.getFile().getPath(); + String baseFilePath = baseFile.getFile().getPath(); + String command = tool.getCommand(); + command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$ + command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$ + command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$ + command = command.replace("$BASE", baseFilePath); //$NON-NLS-1$ + Map env = new TreeMap<>(); + env.put(Constants.GIT_DIR_KEY, + repo.getDirectory().getAbsolutePath()); + env.put("LOCAL", localFilePath); //$NON-NLS-1$ + env.put("REMOTE", remoteFilePath); //$NON-NLS-1$ + env.put("MERGED", mergedFilePath); //$NON-NLS-1$ + env.put("BASE", baseFilePath); //$NON-NLS-1$ + boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; + CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); + return cmdExec.run(command, workingDir, env); + } catch (Exception e) { + throw new ToolException(e); + } finally { + localFile.cleanTemporaries(); + remoteFile.cleanTemporaries(); + baseFile.cleanTemporaries(); + } } /** @@ -99,7 +129,7 @@ public Map getNotAvailableTools() { */ public String getDefaultToolName(BooleanTriState gui) { return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ - : "my_default_toolname"; //$NON-NLS-1$ + : config.getDefaultToolName(); } /** @@ -109,11 +139,58 @@ public boolean isInteractive() { return config.isPrompt(); } - private Map setupPredefinedTools() { - return new TreeMap<>(); + private ExternalMergeTool guessTool(String toolName, BooleanTriState gui) + throws ToolException { + if ((toolName == null) || toolName.isEmpty()) { + toolName = getDefaultToolName(gui); + } + ExternalMergeTool tool = getTool(toolName); + if (tool == null) { + throw new ToolException("Unknown diff tool " + toolName); //$NON-NLS-1$ + } + return tool; } - private Map setupUserDefinedTools() { - return new TreeMap<>(); + private ExternalMergeTool getTool(final String name) { + ExternalMergeTool tool = userDefinedTools.get(name); + if (tool == null) { + tool = predefinedTools.get(name); + } + return tool; } -} \ No newline at end of file + + private Map setupPredefinedTools() { + Map tools = new TreeMap<>(); + for (CommandLineMergeTool tool : CommandLineMergeTool.values()) { + tools.put(tool.name(), new PreDefinedMergeTool(tool)); + } + return tools; + } + + private Map setupUserDefinedTools( + MergeToolConfig cfg, Map predefTools) { + Map tools = new TreeMap<>(); + Map userTools = cfg.getTools(); + for (String name : userTools.keySet()) { + ExternalMergeTool userTool = userTools.get(name); + // if mergetool..cmd is defined we have user defined tool + if (userTool.getCommand() != null) { + tools.put(name, userTool); + } else if (userTool.getPath() != null) { + // if mergetool..path is defined we just overload the path + // of predefined tool + PreDefinedMergeTool predefTool = (PreDefinedMergeTool) predefTools + .get(name); + if (predefTool != null) { + predefTool.setPath(userTool.getPath()); + if (userTool.getTrustExitCode() != BooleanTriState.UNSET) { + predefTool + .setTrustExitCode(userTool.getTrustExitCode()); + } + } + } + } + return tools; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java new file mode 100644 index 000000000..2c64c1666 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018-2022, 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 org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * The pre-defined merge tool. + */ +public class PreDefinedMergeTool extends UserDefinedMergeTool { + + /** + * the tool parameters without base + */ + private final String parametersWithoutBase; + + /** + * Creates the pre-defined merge tool + * + * @param name + * the name + * @param path + * the path + * @param parametersWithBase + * the tool parameters that are used together with path as + * command and "base is present" ($BASE) + * @param parametersWithoutBase + * the tool parameters that are used together with path as + * command and "base is present" ($BASE) + * @param trustExitCode + * the "trust exit code" option + */ + public PreDefinedMergeTool(String name, String path, + String parametersWithBase, String parametersWithoutBase, + BooleanTriState trustExitCode) { + super(name, path, parametersWithBase, trustExitCode); + this.parametersWithoutBase = parametersWithoutBase; + } + + /** + * Creates the pre-defined merge tool + * + * @param tool + * the command line merge tool + * + */ + public PreDefinedMergeTool(CommandLineMergeTool tool) { + this(tool.name(), tool.getPath(), tool.getParameters(true), + tool.getParameters(false), + tool.isExitCodeTrustable() ? BooleanTriState.TRUE + : BooleanTriState.FALSE); + } + + /** + * @param trustExitCode + * the "trust exit code" option + */ + @Override + public void setTrustExitCode(BooleanTriState trustExitCode) { + super.setTrustExitCode(trustExitCode); + } + + /** + * @return the tool command (with base present) + */ + @Override + public String getCommand() { + return getCommand(true); + } + + /** + * @param withBase + * get command with base present (true) or without base present + * (false) + * @return the tool command + */ + @Override + public String getCommand(boolean withBase) { + return getPath() + " " //$NON-NLS-1$ + + (withBase ? super.getCommand() : parametersWithoutBase); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java index df4d8cb8c..1dd2f0d79 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java @@ -21,7 +21,7 @@ public class UserDefinedMergeTool extends UserDefinedDiffTool /** * the merge tool "trust exit code" option */ - private final BooleanTriState trustExitCode; + private BooleanTriState trustExitCode; /** * Creates the merge tool @@ -40,20 +40,30 @@ public UserDefinedMergeTool(String name, String path, String cmd, super(name, path, cmd); this.trustExitCode = trustExitCode; } - /** * @return the "trust exit code" flag */ @Override - public boolean isTrustExitCode() { - return trustExitCode == BooleanTriState.TRUE; - } - - /** - * @return the "trust exit code" option - */ public BooleanTriState getTrustExitCode() { return trustExitCode; } + /** + * @param trustExitCode + * the new "trust exit code" flag + */ + protected void setTrustExitCode(BooleanTriState trustExitCode) { + this.trustExitCode = trustExitCode; + } + + /** + * @param withBase + * not used, because user-defined merge tool can only define one + * cmd -> it must handle with and without base present (empty) + * @return the tool command + */ + @Override + public String getCommand(boolean withBase) { + return getCommand(); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index e982a33b2..29c66f516 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -10,6 +10,7 @@ * * SPDX-License-Identifier: BSD-3-Clause */ + package org.eclipse.jgit.lib; /** @@ -66,7 +67,7 @@ public final class ConfigConstants { public static final String CONFIG_KEY_TRUST_EXIT_CODE = "trustExitCode"; /** - * The "cmd" key within "difftool.*." section + * The "cmd" key within "difftool.*." or "mergetool.*." section * * @since 6.1 */ From eaf4d500b886a7e776f50bf53497fe463e714b25 Mon Sep 17 00:00:00 2001 From: Andre Bossert Date: Fri, 8 Mar 2019 22:31:34 +0100 Subject: [PATCH 4/7] Add mergetool merge feature (execute external tool) see: https://git-scm.com/docs/git-mergetool * implement mergetool merge function (execute external tool) * add ExecutionResult and commandExecutionError to ToolException * handle "base not present" case (empty or null base file path) * handle deleted (rm) and modified (add) conflicts * handle settings * keepBackup * keepTemporaries * writeToTemp Bug: 356832 Change-Id: Id323c2fcb1c24d12ceb299801df8bac51a6d463f Signed-off-by: Andre Bossert --- .../org/eclipse/jgit/pgm/DiffToolTest.java | 88 ++++- .../org/eclipse/jgit/pgm/MergeToolTest.java | 244 +++++++++++-- ...nalToolTestCase.java => ToolTestCase.java} | 85 ++++- .../jgit/pgm/internal/CLIText.properties | 20 +- .../src/org/eclipse/jgit/pgm/DiffTool.java | 15 +- .../src/org/eclipse/jgit/pgm/MergeTool.java | 333 ++++++++++++++---- .../eclipse/jgit/pgm/internal/CLIText.java | 16 + .../diffmergetool/CommandExecutor.java | 16 +- .../internal/diffmergetool/FileElement.java | 83 +++-- .../internal/diffmergetool/MergeTools.java | 121 +++++-- .../internal/diffmergetool/ToolException.java | 23 +- 11 files changed, 853 insertions(+), 191 deletions(-) rename org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/{ExternalToolTestCase.java => ToolTestCase.java} (57%) diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java index 017a5d994..dc34c0d67 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java @@ -16,6 +16,7 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; import static org.junit.Assert.fail; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -30,7 +31,7 @@ /** * Testing the {@code difftool} command. */ -public class DiffToolTest extends ExternalToolTestCase { +public class DiffToolTest extends ToolTestCase { private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION; @@ -41,6 +42,46 @@ public void setUp() throws Exception { configureEchoTool(TOOL_NAME); } + @Test + public void testToolWithPrompt() throws Exception { + String[] inputLines = { + "y", // accept launching diff tool + "y", // accept launching diff tool + }; + + RevCommit commit = createUnstagedChanges(); + List changes = getRepositoryChanges(commit); + String[] expectedOutput = getExpectedCompareOutput(changes); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + DIFF_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testToolAbortLaunch() throws Exception { + String[] inputLines = { + "y", // accept launching diff tool + "n", // don't launch diff tool + }; + + RevCommit commit = createUnstagedChanges(); + List changes = getRepositoryChanges(commit); + int abortIndex = 1; + String[] expectedOutput = getExpectedAbortOutput(changes, abortIndex); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, + runAndCaptureUsingInitRaw(inputStream, DIFF_TOOL, "--prompt", option, + TOOL_NAME)); + } + @Test(expected = Die.class) public void testNotDefinedTool() throws Exception { createUnstagedChanges(); @@ -53,7 +94,7 @@ public void testNotDefinedTool() throws Exception { public void testTool() throws Exception { RevCommit commit = createUnstagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedToolOutput(changes); + String[] expectedOutput = getExpectedToolOutputNoPrompt(changes); String[] options = { "--tool", @@ -72,7 +113,7 @@ public void testTool() throws Exception { public void testToolTrustExitCode() throws Exception { RevCommit commit = createUnstagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedToolOutput(changes); + String[] expectedOutput = getExpectedToolOutputNoPrompt(changes); String[] options = { "--tool", "-t", }; @@ -87,7 +128,7 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception { RevCommit commit = createUnstagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedToolOutput(changes); + String[] expectedOutput = getExpectedToolOutputNoPrompt(changes); String[] options = { "--tool", "-t", }; @@ -103,7 +144,7 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, public void testToolCached() throws Exception { RevCommit commit = createStagedChanges(); List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedToolOutput(changes); + String[] expectedOutput = getExpectedToolOutputNoPrompt(changes); String[] options = { "--cached", "--staged", }; @@ -118,7 +159,8 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, public void testToolHelp() throws Exception { CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); List expectedOutput = new ArrayList<>(); - expectedOutput.add("git difftool --tool= may be set to one of the following:"); + expectedOutput.add( + "'git difftool --tool=' may be set to one of the following:"); for (CommandLineDiffTool defaultTool : defaultTools) { String toolName = defaultTool.name(); expectedOutput.add(toolName); @@ -159,7 +201,7 @@ private void configureEchoTool(String toolName) { String.valueOf(false)); } - private String[] getExpectedToolOutput(List changes) { + private static String[] getExpectedToolOutputNoPrompt(List changes) { String[] expectedToolOutput = new String[changes.size()]; for (int i = 0; i < changes.size(); ++i) { DiffEntry change = changes.get(i); @@ -169,4 +211,36 @@ private String[] getExpectedToolOutput(List changes) { } return expectedToolOutput; } + + private static String[] getExpectedCompareOutput(List changes) { + List expected = new ArrayList<>(); + int n = changes.size(); + for (int i = 0; i < n; ++i) { + DiffEntry change = changes.get(i); + String newPath = change.getNewPath(); + expected.add( + "Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'"); + expected.add("Launch '" + TOOL_NAME + "' [Y/n]?"); + expected.add(newPath); + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedAbortOutput(List changes, + int abortIndex) { + List expected = new ArrayList<>(); + int n = changes.size(); + for (int i = 0; i < n; ++i) { + DiffEntry change = changes.get(i); + String newPath = change.getNewPath(); + expected.add( + "Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'"); + expected.add("Launch '" + TOOL_NAME + "' [Y/n]?"); + if (i == abortIndex) { + break; + } + expected.add(newPath); + } + return expected.toArray(new String[0]); + } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java index 32cd60415..2e50f0908 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java @@ -15,6 +15,7 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -27,7 +28,7 @@ /** * Testing the {@code mergetool} command. */ -public class MergeToolTest extends ExternalToolTestCase { +public class MergeToolTest extends ToolTestCase { private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION; @@ -39,37 +40,121 @@ public void setUp() throws Exception { } @Test - public void testTool() throws Exception { - createMergeConflict(); - String[] expectedOutput = getExpectedToolOutput(); - - String[] options = { - "--tool", - "-t", + public void testAbortMerge() throws Exception { + String[] inputLines = { + "y", // start tool for merge resolution + "n", // don't accept merge tool result + "n", // don't continue resolution }; + String[] conflictingFilenames = createMergeConflict(); + int abortIndex = 1; + String[] expectedOutput = getExpectedAbortMergeOutput( + conflictingFilenames, + abortIndex); - for (String option : options) { - assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, - runAndCaptureUsingInitRaw(MERGE_TOOL, option, - TOOL_NAME)); - } + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); } @Test - public void testToolNoGuiNoPrompt() throws Exception { - createMergeConflict(); - String[] expectedOutput = getExpectedToolOutput(); + public void testAbortLaunch() throws Exception { + String[] inputLines = { + "n", // abort merge tool launch + }; + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedAbortLaunchOutput( + conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testMergeConflict() throws Exception { + String[] inputLines = { + "y", // start tool for merge resolution + "y", // accept merge result as successful + "y", // start tool for merge resolution + "y", // accept merge result as successful + }; + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedMergeConflictOutput( + conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testDeletedConflict() throws Exception { + String[] inputLines = { + "d", // choose delete option to resolve conflict + "m", // choose merge option to resolve conflict + }; + String[] conflictingFilenames = createDeletedConflict(); + String[] expectedOutput = getExpectedDeletedConflictOutput( + conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testNoConflict() throws Exception { + createStagedChanges(); + String[] expectedOutput = { "No files need merging" }; String[] options = { "--tool", "-t", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL, - "--no-gui", "--no-prompt", option, TOOL_NAME)); + expectedOutput, + runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME)); } } + @Test + public void testMergeConflictNoPrompt() throws Exception { + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt( + conflictingFilenames); + + String option = "--tool"; + + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, + runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME)); + } + + @Test + public void testMergeConflictNoGuiNoPrompt() throws Exception { + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt( + conflictingFilenames); + + String option = "--tool"; + + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL, + "--no-gui", "--no-prompt", option, TOOL_NAME)); + } + @Test public void testToolHelp() throws Exception { CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); @@ -87,8 +172,7 @@ public void testToolHelp() throws Exception { String[] userDefinedToolsHelp = { "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.", - }; + "environment. If run in a terminal-only session, they will fail.", }; expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp)); String option = "--tool-help"; @@ -116,21 +200,111 @@ private void configureEchoTool(String toolName) { String.valueOf(false)); } - private String[] getExpectedToolOutput() { - String[] mergeConflictFilenames = { "a", "b", }; - List expectedOutput = new ArrayList<>(); - expectedOutput.add("Merging:"); - for (String mergeConflictFilename : mergeConflictFilenames) { - expectedOutput.add(mergeConflictFilename); + private static String[] getExpectedMergeConflictOutputNoPrompt( + String[] conflictFilenames) { + List expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); } - for (String mergeConflictFilename : mergeConflictFilenames) { - expectedOutput.add("Normal merge conflict for '" - + mergeConflictFilename + "':"); - expectedOutput.add("{local}: modified file"); - expectedOutput.add("{remote}: modified file"); - expectedOutput.add("TODO: Launch mergetool '" + TOOL_NAME - + "' for path '" + mergeConflictFilename + "'..."); + for (String conflictFilename : conflictFilenames) { + expected.add("Normal merge conflict for '" + conflictFilename + + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + expected.add(conflictFilename); + expected.add(conflictFilename + " seems unchanged."); } - return expectedOutput.toArray(new String[0]); + return expected.toArray(new String[0]); + } + + private static String[] getExpectedAbortLaunchOutput( + String[] conflictFilenames) { + List expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + if (conflictFilenames.length > 1) { + String conflictFilename = conflictFilenames[0]; + expected.add( + "Normal merge conflict for '" + conflictFilename + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + expected.add("Hit return to start merge resolution tool (" + + TOOL_NAME + "):"); + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedAbortMergeOutput( + String[] conflictFilenames, int abortIndex) { + List expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + for (int i = 0; i < conflictFilenames.length; ++i) { + if (i == abortIndex) { + break; + } + + String conflictFilename = conflictFilenames[i]; + expected.add( + "Normal merge conflict for '" + conflictFilename + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + expected.add("Hit return to start merge resolution tool (" + + TOOL_NAME + "): " + conflictFilename); + expected.add(conflictFilename + " seems unchanged."); + expected.add("Was the merge successful [y/n]?"); + if (i < conflictFilenames.length - 1) { + expected.add( + "\tContinue merging other unresolved paths [y/n]?"); + } + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedMergeConflictOutput( + String[] conflictFilenames) { + List expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + for (int i = 0; i < conflictFilenames.length; ++i) { + String conflictFilename = conflictFilenames[i]; + expected.add("Normal merge conflict for '" + conflictFilename + + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + expected.add("Hit return to start merge resolution tool (" + + TOOL_NAME + "): " + conflictFilename); + expected.add(conflictFilename + " seems unchanged."); + expected.add("Was the merge successful [y/n]?"); + if (i < conflictFilenames.length - 1) { + // expected.add( + // "\tContinue merging other unresolved paths [y/n]?"); + } + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedDeletedConflictOutput( + String[] conflictFilenames) { + List expected = new ArrayList<>(); + expected.add("Merging:"); + for (String mergeConflictFilename : conflictFilenames) { + expected.add(mergeConflictFilename); + } + for (int i = 0; i < conflictFilenames.length; ++i) { + String conflictFilename = conflictFilenames[i]; + expected.add(conflictFilename + " seems unchanged."); + expected.add("{local}: deleted"); + expected.add("{remote}: modified file"); + expected.add("Use (m)odified or (d)eleted file, or (a)bort?"); + } + return expected.toArray(new String[0]); } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java similarity index 57% rename from org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java rename to org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java index e10b13efb..d13eeb7e4 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ExternalToolTestCase.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java @@ -11,10 +11,14 @@ import static org.junit.Assert.assertEquals; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; -import org.eclipse.jgit.api.CherryPickResult; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.lib.CLIRepositoryTestCase; @@ -29,7 +33,7 @@ /** * Base test case for the {@code difftool} and {@code mergetool} commands. */ -public abstract class ExternalToolTestCase extends CLIRepositoryTestCase { +public abstract class ToolTestCase extends CLIRepositoryTestCase { public static class GitCliJGitWrapperParser { @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class) @@ -56,6 +60,12 @@ public void setUp() throws Exception { protected String[] runAndCaptureUsingInitRaw(String... args) throws Exception { + InputStream inputStream = null; // no input stream + return runAndCaptureUsingInitRaw(inputStream, args); + } + + protected String[] runAndCaptureUsingInitRaw(InputStream inputStream, + String... args) throws Exception { CLIGitCommand.Result result = new CLIGitCommand.Result(); GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); @@ -63,7 +73,7 @@ protected String[] runAndCaptureUsingInitRaw(String... args) clp.parseArgument(args); TextBuiltin cmd = bean.subcommand; - cmd.initRaw(db, null, null, result.out, result.err); + cmd.initRaw(db, null, inputStream, result.out, result.err); cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()])); if (cmd.getOutputWriter() != null) { cmd.getOutputWriter().flush(); @@ -71,28 +81,73 @@ protected String[] runAndCaptureUsingInitRaw(String... args) if (cmd.getErrorWriter() != null) { cmd.getErrorWriter().flush(); } + + List errLines = result.errLines().stream() + .filter(l -> !l.isBlank()) // we care only about error messages + .collect(Collectors.toList()); + assertEquals("Expected no standard error output from tool", + Collections.EMPTY_LIST.toString(), errLines.toString()); + return result.outLines().toArray(new String[0]); } - protected CherryPickResult createMergeConflict() throws Exception { + protected String[] createMergeConflict() throws Exception { + // create files on initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); writeTrashFile("a", "Hello world a"); writeTrashFile("b", "Hello world b"); git.add().addFilepattern(".").call(); git.commit().setMessage("files a & b added").call(); + // create another branch and change files + git.branchCreate().setName("branch_1").call(); + git.checkout().setName("branch_1").call(); writeTrashFile("a", "Hello world a 1"); writeTrashFile("b", "Hello world b 1"); git.add().addFilepattern(".").call(); - RevCommit commit1 = git.commit().setMessage("files a & b commit 1") - .call(); - git.branchCreate().setName("branch_1").call(); + RevCommit commit1 = git.commit() + .setMessage("files a & b modified commit 1").call(); + // checkout initial branch git.checkout().setName(TEST_BRANCH_NAME).call(); + // create another branch and change files + git.branchCreate().setName("branch_2").call(); + git.checkout().setName("branch_2").call(); writeTrashFile("a", "Hello world a 2"); writeTrashFile("b", "Hello world b 2"); git.add().addFilepattern(".").call(); - git.commit().setMessage("files a & b commit 2").call(); + git.commit().setMessage("files a & b modified commit 2").call(); + // cherry-pick conflicting changes + git.cherryPick().include(commit1).call(); + String[] conflictingFilenames = { "a", "b" }; + return conflictingFilenames; + } + + protected String[] createDeletedConflict() throws Exception { + // create files on initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); + writeTrashFile("a", "Hello world a"); + writeTrashFile("b", "Hello world b"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b added").call(); + // create another branch and change files + git.branchCreate().setName("branch_1").call(); + git.checkout().setName("branch_1").call(); + writeTrashFile("a", "Hello world a 1"); + writeTrashFile("b", "Hello world b 1"); + git.add().addFilepattern(".").call(); + RevCommit commit1 = git.commit() + .setMessage("files a & b modified commit 1").call(); + // checkout initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); + // create another branch and change files git.branchCreate().setName("branch_2").call(); - CherryPickResult result = git.cherryPick().include(commit1).call(); - return result; + git.checkout().setName("branch_2").call(); + git.rm().addFilepattern("a").call(); + git.rm().addFilepattern("b").call(); + git.commit().setMessage("files a & b deleted commit 2").call(); + // cherry-pick conflicting changes + git.cherryPick().include(commit1).call(); + String[] conflictingFilenames = { "a", "b" }; + return conflictingFilenames; } protected RevCommit createUnstagedChanges() throws Exception { @@ -121,6 +176,16 @@ protected List getRepositoryChanges(RevCommit commit) return changes; } + protected static InputStream createInputStream(String[] inputLines) { + return createInputStream(Arrays.asList(inputLines)); + } + + protected static InputStream createInputStream(List inputLines) { + String input = String.join(System.lineSeparator(), inputLines); + InputStream inputStream = new ByteArrayInputStream(input.getBytes()); + return inputStream; + } + protected static void assertArrayOfLinesEquals(String failMessage, String[] expected, String[] actual) { assertEquals(failMessage, toString(expected), toString(actual)); 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 8e2eef7eb..674185df2 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,8 +58,8 @@ 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]? +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 path ''{0}'' due to exception: {1} doesNotExist={0} does not exist dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge: @@ -91,6 +91,22 @@ listeningOn=Listening on {0} logNoSignatureVerifier="No signature verifier available" mergeConflict=CONFLICT(content): Merge conflict in {0} mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge: +mergeToolHelpSetToFollowing=''git mergetool --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. +mergeToolLaunch=Hit return to start merge resolution tool ({0}): +mergeToolDied=local or remote cannot be found in cache, stopping at {0} +mergeToolNoFiles=No files need merging +mergeToolMerging=Merging:\n{0} +mergeToolUnknownConflict=\nUnknown merge conflict for ''{0}'': +mergeToolNormalConflict=\nNormal merge conflict for ''{0}'':\n '{'local'}': modified file\n '{'remote'}': modified file +mergeToolMergeFailed=merge of {0} failed +mergeToolExecutionError=excution error +mergeToolFileUnchanged=\n{0} seems unchanged. +mergeToolDeletedConflict=\nDeleted merge conflict for ''{0}'': +mergeToolDeletedConflictByUs= {local}: deleted\n {remote}: modified file +mergeToolDeletedConflictByThem= {local}: modified file\n {remote}: deleted +mergeToolContinueUnresolvedPaths=\nContinue merging other unresolved paths [y/n]? +mergeToolWasMergeSuccessfull=Was the merge successful [y/n]? +mergeToolDeletedMergeDecision=Use (m)odified or (d)eleted file, or (a)bort? mergeFailed=Automatic merge failed; fix conflicts and then commit the result mergeCheckoutFailed=Please, commit your changes or stash them before you can merge. mergeMadeBy=Merge made by the ''{0}'' strategy. diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java index 2e90d52cb..ffba36fe2 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java @@ -113,11 +113,14 @@ void noTrustExitCode(@SuppressWarnings("unused") boolean on) { @Option(name = "--", metaVar = "metaVar_paths", handler = PathTreeFilterHandler.class) private TreeFilter pathFilter = TreeFilter.ALL; + private BufferedReader inputReader; + @Override protected void init(Repository repository, String gitDir) { super.init(repository, gitDir); diffFmt = new DiffFormatter(new BufferedOutputStream(outs)); diffTools = new DiffTools(repository); + inputReader = new BufferedReader(new InputStreamReader(ins, StandardCharsets.UTF_8)); } @Override @@ -208,10 +211,9 @@ 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)); + fileIndex, fileCount, fileName, toolNamePrompt) + " "); //$NON-NLS-1$ outw.flush(); - BufferedReader br = new BufferedReader( - new InputStreamReader(ins, StandardCharsets.UTF_8)); + BufferedReader br = inputReader; String line = null; if ((line = br.readLine()) != null) { if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$ @@ -224,17 +226,18 @@ private boolean isLaunchCompare(int fileIndex, int fileCount, private void showToolHelp() throws IOException { StringBuilder availableToolNames = new StringBuilder(); for (String name : diffTools.getAvailableTools().keySet()) { - availableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$ + availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ } StringBuilder notAvailableToolNames = new StringBuilder(); for (String name : diffTools.getNotAvailableTools().keySet()) { - notAvailableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$ + notAvailableToolNames + .append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ } StringBuilder userToolNames = new StringBuilder(); Map userTools = diffTools .getUserDefinedTools(); for (String name : userTools.keySet()) { - userToolNames.append(String.format("\t\t%s.cmd %s\n", //$NON-NLS-1$ + userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$ name, userTools.get(name).getCommand())); } outw.println(MessageFormat.format( diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java index 37afa54c7..dce5a7996 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java @@ -11,26 +11,35 @@ package org.eclipse.jgit.pgm; import java.io.BufferedReader; +import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.TreeMap; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Status; import org.eclipse.jgit.api.StatusCommand; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool; +import org.eclipse.jgit.diff.ContentSource; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool; +import org.eclipse.jgit.internal.diffmergetool.FileElement; import org.eclipse.jgit.internal.diffmergetool.MergeTools; +import org.eclipse.jgit.internal.diffmergetool.ToolException; import org.eclipse.jgit.lib.IndexDiff.StageState; -import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.pgm.internal.CLIText; +import org.eclipse.jgit.util.FS.ExecutionResult; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; import org.kohsuke.args4j.spi.RestOfArgumentsHandler; @@ -43,16 +52,16 @@ class MergeTool extends TextBuiltin { "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge") private String toolName; - private Optional prompt = Optional.empty(); + private BooleanTriState prompt = BooleanTriState.UNSET; @Option(name = "--prompt", usage = "usage_prompt") void setPrompt(@SuppressWarnings("unused") boolean on) { - prompt = Optional.of(Boolean.TRUE); + prompt = BooleanTriState.TRUE; } @Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt") void noPrompt(@SuppressWarnings("unused") boolean on) { - prompt = Optional.of(Boolean.FALSE); + prompt = BooleanTriState.FALSE; } @Option(name = "--tool-help", usage = "usage_toolHelp") @@ -74,10 +83,17 @@ void noGui(@SuppressWarnings("unused") boolean on) { @Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class) protected List filterPaths; + private BufferedReader inputReader; + @Override protected void init(Repository repository, String gitDir) { super.init(repository, gitDir); mergeTools = new MergeTools(repository); + inputReader = new BufferedReader(new InputStreamReader(ins)); + } + + enum MergeResult { + SUCCESSFUL, FAILED, ABORTED } @Override @@ -88,8 +104,8 @@ protected void run() { } else { // get prompt boolean showPrompt = mergeTools.isInteractive(); - if (prompt.isPresent()) { - showPrompt = prompt.get().booleanValue(); + if (prompt != BooleanTriState.UNSET) { + showPrompt = prompt == BooleanTriState.TRUE; } // get passed or default tool name String toolNameSelected = toolName; @@ -101,7 +117,7 @@ protected void run() { if (files.size() > 0) { merge(files, showPrompt, toolNameSelected); } else { - outw.println("No files need merging"); //$NON-NLS-1$ + outw.println(CLIText.get().mergeToolNoFiles); } } outw.flush(); @@ -113,88 +129,273 @@ protected void run() { private void merge(Map files, boolean showPrompt, String toolNamePrompt) throws Exception { // sort file names - List fileNames = new ArrayList<>(files.keySet()); - Collections.sort(fileNames); + List mergedFilePaths = new ArrayList<>(files.keySet()); + Collections.sort(mergedFilePaths); // show the files - outw.println("Merging:"); //$NON-NLS-1$ - for (String fileName : fileNames) { - outw.println(fileName); + StringBuilder mergedFiles = new StringBuilder(); + for (String mergedFilePath : mergedFilePaths) { + mergedFiles.append(MessageFormat.format("{0}\n", mergedFilePath)); //$NON-NLS-1$ } + outw.println(MessageFormat.format(CLIText.get().mergeToolMerging, + mergedFiles)); outw.flush(); - for (String fileName : fileNames) { - StageState fileState = files.get(fileName); - // only both-modified is valid for mergetool - if (fileState == StageState.BOTH_MODIFIED) { - outw.println("\nNormal merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ - outw.println(" {local}: modified file"); //$NON-NLS-1$ - outw.println(" {remote}: modified file"); //$NON-NLS-1$ - // check if user wants to launch merge resolution tool - boolean launch = true; - if (showPrompt) { - launch = isLaunch(toolNamePrompt); + // merge the files + MergeResult mergeResult = MergeResult.SUCCESSFUL; + for (String mergedFilePath : mergedFilePaths) { + // if last merge failed... + if (mergeResult == MergeResult.FAILED) { + // check if user wants to continue + if (showPrompt && !isContinueUnresolvedPaths()) { + mergeResult = MergeResult.ABORTED; } - if (launch) { - outw.println("TODO: Launch mergetool '" + toolNamePrompt //$NON-NLS-1$ - + "' for path '" + fileName + "'..."); //$NON-NLS-1$ //$NON-NLS-2$ - } else { - break; - } - } else if ((fileState == StageState.DELETED_BY_US) || (fileState == StageState.DELETED_BY_THEM)) { - outw.println("\nDeleted merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ - } else { - outw.println( - "\nUnknown merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ + } + // aborted ? + if (mergeResult == MergeResult.ABORTED) { break; } + // get file stage state and merge + StageState fileState = files.get(mergedFilePath); + if (fileState == StageState.BOTH_MODIFIED) { + mergeResult = mergeModified(mergedFilePath, showPrompt, + toolNamePrompt); + } else if ((fileState == StageState.DELETED_BY_US) + || (fileState == StageState.DELETED_BY_THEM)) { + mergeResult = mergeDeleted(mergedFilePath, + fileState == StageState.DELETED_BY_US); + } else { + outw.println(MessageFormat.format( + CLIText.get().mergeToolUnknownConflict, + mergedFilePath)); + mergeResult = MergeResult.ABORTED; + } } } - private boolean isLaunch(String toolNamePrompt) - throws IOException { - boolean launch = true; - outw.println("Hit return to start merge resolution tool (" //$NON-NLS-1$ - + toolNamePrompt + "): "); //$NON-NLS-1$ + private MergeResult mergeModified(String mergedFilePath, boolean showPrompt, + String toolNamePrompt) throws Exception { + outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict, + mergedFilePath)); outw.flush(); - BufferedReader br = new BufferedReader(new InputStreamReader(ins)); + // check if user wants to launch merge resolution tool + boolean launch = true; + if (showPrompt) { + launch = isLaunch(toolNamePrompt); + } + if (!launch) { + return MergeResult.ABORTED; // abort + } + boolean isMergeSuccessful = true; + ContentSource baseSource = ContentSource.create(db.newObjectReader()); + ContentSource localSource = ContentSource.create(db.newObjectReader()); + ContentSource remoteSource = ContentSource.create(db.newObjectReader()); + try { + FileElement base = null; + FileElement local = null; + FileElement remote = null; + DirCache cache = db.readDirCache(); + int firstIndex = cache.findEntry(mergedFilePath); + if (firstIndex >= 0) { + int nextIndex = cache.nextEntry(firstIndex); + for (; firstIndex < nextIndex; firstIndex++) { + DirCacheEntry entry = cache.getEntry(firstIndex); + ObjectId id = entry.getObjectId(); + switch (entry.getStage()) { + case DirCacheEntry.STAGE_1: + base = new FileElement(mergedFilePath, id.name(), + baseSource.open(mergedFilePath, id) + .openStream()); + break; + case DirCacheEntry.STAGE_2: + local = new FileElement(mergedFilePath, id.name(), + localSource.open(mergedFilePath, id) + .openStream()); + break; + case DirCacheEntry.STAGE_3: + remote = new FileElement(mergedFilePath, id.name(), + remoteSource.open(mergedFilePath, id) + .openStream()); + break; + } + } + } + if ((local == null) || (remote == null)) { + throw die(MessageFormat.format(CLIText.get().mergeToolDied, + mergedFilePath)); + } + File merged = new File(mergedFilePath); + long modifiedBefore = merged.lastModified(); + try { + // TODO: check how to return the exit-code of the + // tool to jgit / java runtime ? + // int rc =... + ExecutionResult executionResult = mergeTools.merge(db, local, + remote, base, mergedFilePath, toolName, prompt, gui); + outw.println( + new String(executionResult.getStdout().toByteArray())); + outw.flush(); + errw.println( + new String(executionResult.getStderr().toByteArray())); + errw.flush(); + } catch (ToolException e) { + isMergeSuccessful = false; + outw.println(e.getResultStdout()); + outw.flush(); + errw.println(MessageFormat.format( + CLIText.get().mergeToolMergeFailed, mergedFilePath)); + errw.flush(); + if (e.isCommandExecutionError()) { + errw.println(e.getMessage()); + throw die(CLIText.get().mergeToolExecutionError, e); + } + } + // if merge was successful check file modified + if (isMergeSuccessful) { + long modifiedAfter = merged.lastModified(); + if (modifiedBefore == modifiedAfter) { + outw.println(MessageFormat.format( + CLIText.get().mergeToolFileUnchanged, + mergedFilePath)); + isMergeSuccessful = !showPrompt || isMergeSuccessful(); + } + } + // if automatically or manually successful + // -> add the file to the index + if (isMergeSuccessful) { + addFile(mergedFilePath); + } + } finally { + baseSource.close(); + localSource.close(); + remoteSource.close(); + } + return isMergeSuccessful ? MergeResult.SUCCESSFUL : MergeResult.FAILED; + } + + private MergeResult mergeDeleted(String mergedFilePath, boolean deletedByUs) + throws Exception { + outw.println(MessageFormat.format(CLIText.get().mergeToolFileUnchanged, + mergedFilePath)); + if (deletedByUs) { + outw.println(CLIText.get().mergeToolDeletedConflictByUs); + } else { + outw.println(CLIText.get().mergeToolDeletedConflictByThem); + } + int mergeDecision = getDeletedMergeDecision(); + if (mergeDecision == 1) { + // add modified file + addFile(mergedFilePath); + } else if (mergeDecision == -1) { + // remove deleted file + rmFile(mergedFilePath); + } else { + return MergeResult.ABORTED; + } + return MergeResult.SUCCESSFUL; + } + + private void addFile(String fileName) throws Exception { + try (Git git = new Git(db)) { + git.add().addFilepattern(fileName).call(); + } + } + + private void rmFile(String fileName) throws Exception { + try (Git git = new Git(db)) { + git.rm().addFilepattern(fileName).call(); + } + } + + private boolean hasUserAccepted(String message) throws IOException { + boolean yes = true; + outw.print(message + " "); //$NON-NLS-1$ + outw.flush(); + BufferedReader br = inputReader; + String line = null; + while ((line = br.readLine()) != null) { + if (line.equalsIgnoreCase("y")) { //$NON-NLS-1$ + yes = true; + break; + } else if (line.equalsIgnoreCase("n")) { //$NON-NLS-1$ + yes = false; + break; + } + outw.print(message); + outw.flush(); + } + return yes; + } + + private boolean isContinueUnresolvedPaths() throws IOException { + return hasUserAccepted(CLIText.get().mergeToolContinueUnresolvedPaths); + } + + private boolean isMergeSuccessful() throws IOException { + return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull); + } + + private boolean isLaunch(String toolNamePrompt) throws IOException { + boolean launch = true; + outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch, + toolNamePrompt) + " "); //$NON-NLS-1$ + outw.flush(); + BufferedReader br = inputReader; String line = null; if ((line = br.readLine()) != null) { - if (!line.equalsIgnoreCase("Y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$ + if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$ launch = false; } } return launch; } - private void showToolHelp() throws IOException { - outw.println( - "'git mergetool --tool=' may be set to one of the following:"); //$NON-NLS-1$ - for (String name : mergeTools.getAvailableTools().keySet()) { - outw.println("\t\t" + name); //$NON-NLS-1$ + private int getDeletedMergeDecision() throws IOException { + int ret = 0; // abort + final String message = CLIText.get().mergeToolDeletedMergeDecision + + " "; //$NON-NLS-1$ + outw.print(message); + outw.flush(); + BufferedReader br = inputReader; + String line = null; + while ((line = br.readLine()) != null) { + if (line.equalsIgnoreCase("m")) { //$NON-NLS-1$ + ret = 1; // modified + break; + } else if (line.equalsIgnoreCase("d")) { //$NON-NLS-1$ + ret = -1; // deleted + break; + } else if (line.equalsIgnoreCase("a")) { //$NON-NLS-1$ + break; + } + outw.print(message); + outw.flush(); } - outw.println(""); //$NON-NLS-1$ - outw.println("\tuser-defined:"); //$NON-NLS-1$ + return ret; + } + + private void showToolHelp() throws IOException { + StringBuilder availableToolNames = new StringBuilder(); + for (String name : mergeTools.getAvailableTools().keySet()) { + availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ + } + StringBuilder notAvailableToolNames = new StringBuilder(); + for (String name : mergeTools.getNotAvailableTools().keySet()) { + notAvailableToolNames + .append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ + } + StringBuilder userToolNames = new StringBuilder(); Map userTools = mergeTools .getUserDefinedTools(); for (String name : userTools.keySet()) { - outw.println("\t\t" + name + ".cmd " //$NON-NLS-1$ //$NON-NLS-2$ - + userTools.get(name).getCommand()); + userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$ + name, userTools.get(name).getCommand())); } - outw.println(""); //$NON-NLS-1$ - outw.println( - "The following tools are valid, but not currently available:"); //$NON-NLS-1$ - for (String name : mergeTools.getNotAvailableTools().keySet()) { - outw.println("\t\t" + name); //$NON-NLS-1$ - } - outw.println(""); //$NON-NLS-1$ - outw.println("Some of the tools listed above only work in a windowed"); //$NON-NLS-1$ - outw.println( - "environment. If run in a terminal-only session, they will fail."); //$NON-NLS-1$ - return; + outw.println(MessageFormat.format( + CLIText.get().mergeToolHelpSetToFollowing, availableToolNames, + userToolNames, notAvailableToolNames)); } - private Map getFiles() - throws RevisionSyntaxException, NoWorkTreeException, - GitAPIException { + private Map getFiles() throws RevisionSyntaxException, + NoWorkTreeException, GitAPIException { Map files = new TreeMap<>(); try (Git git = new Git(db)) { StatusCommand statusCommand = git.status(); 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 7fe5b0fa4..989e649b7 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 @@ -169,6 +169,22 @@ public static String fatalError(String message) { /***/ public String logNoSignatureVerifier; /***/ public String mergeCheckoutConflict; /***/ public String mergeConflict; + /***/ public String mergeToolHelpSetToFollowing; + /***/ public String mergeToolLaunch; + /***/ public String mergeToolDied; + /***/ public String mergeToolNoFiles; + /***/ public String mergeToolMerging; + /***/ public String mergeToolUnknownConflict; + /***/ public String mergeToolNormalConflict; + /***/ public String mergeToolMergeFailed; + /***/ public String mergeToolExecutionError; + /***/ public String mergeToolFileUnchanged; + /***/ public String mergeToolDeletedConflict; + /***/ public String mergeToolDeletedConflictByUs; + /***/ public String mergeToolDeletedConflictByThem; + /***/ public String mergeToolContinueUnresolvedPaths; + /***/ public String mergeToolWasMergeSuccessfull; + /***/ public String mergeToolDeletedMergeDecision; /***/ public String mergeFailed; /***/ public String mergeCheckoutFailed; /***/ public String mergeMadeBy; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java index 0dde9b5f3..ad79fe8fc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java @@ -72,10 +72,18 @@ public ExecutionResult run(String command, File workingDir, } ExecutionResult result = fs.execute(pb, null); int rc = result.getRc(); - if ((rc != 0) && (checkExitCode - || isCommandExecutionError(rc))) { - throw new ToolException( - new String(result.getStderr().toByteArray()), result); + if (rc != 0) { + boolean execError = isCommandExecutionError(rc); + if (checkExitCode || execError) { + throw new ToolException( + "JGit: tool execution return code: " + rc + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + + "checkExitCode: " + checkExitCode + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + + "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + + "stderr: \n" //$NON-NLS-1$ + + new String( + result.getStderr().toByteArray()), + result, execError); + } } return result; } finally { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java index cdc8f015f..1ae87aaa6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java @@ -11,6 +11,7 @@ package org.eclipse.jgit.internal.diffmergetool; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -80,35 +81,27 @@ public void setStream(ObjectStream stream) { } /** - * @param workingDir the working directory used if file cannot be found (e.g. /dev/null) + * Returns a temporary file with in passed working directory and fills it + * with stream if valid. + * + * @param directory + * the working directory where the temporary file is created + * @param midName + * name added in the middle of generated temporary file name * @return the object stream * @throws IOException */ - public File getFile(File workingDir) throws IOException { + public File getFile(File directory, String midName) throws IOException { if (tempFile != null) { return tempFile; } - File file = new File(path); - String name = file.getName(); - if (path.equals(DiffEntry.DEV_NULL)) { - file = new File(workingDir, "nul"); //$NON-NLS-1$ - } - else if (stream != null) { - tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$ - try (OutputStream outStream = new FileOutputStream(tempFile)) { - int read = 0; - byte[] bytes = new byte[8 * 1024]; - while ((read = stream.read(bytes)) != -1) { - outStream.write(bytes, 0, read); - } - } finally { - // stream can only be consumed once --> close it - stream.close(); - stream = null; - } - return tempFile; - } - return file; + String[] fileNameAndExtension = splitBaseFileNameAndExtension( + new File(path)); + tempFile = File.createTempFile( + fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$ + fileNameAndExtension[1], directory); + copyFromStream(); + return tempFile; } /** @@ -130,19 +123,7 @@ public File getFile() throws IOException { // TODO: avoid long random file name (number generated by // createTempFile) tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$ - if (stream != null) { - try (OutputStream outStream = new FileOutputStream(tempFile)) { - int read = 0; - byte[] bytes = new byte[8 * 1024]; - while ((read = stream.read(bytes)) != -1) { - outStream.write(bytes, 0, read); - } - } finally { - // stream can only be consumed once --> close it - stream.close(); - stream = null; - } - } + copyFromStream(); return tempFile; } return file; @@ -157,4 +138,34 @@ public void cleanTemporaries() { tempFile = null; } + private void copyFromStream() throws IOException, FileNotFoundException { + if (stream != null) { + try (OutputStream outStream = new FileOutputStream(tempFile)) { + int read = 0; + byte[] bytes = new byte[8 * 1024]; + while ((read = stream.read(bytes)) != -1) { + outStream.write(bytes, 0, read); + } + } finally { + // stream can only be consumed once --> close it + stream.close(); + stream = null; + } + } + } + + private static String[] splitBaseFileNameAndExtension(File file) { + String[] result = new String[2]; + result[0] = file.getName(); + result[1] = ""; //$NON-NLS-1$ + if (!result[0].startsWith(".")) { //$NON-NLS-1$ + int idx = result[0].lastIndexOf("."); //$NON-NLS-1$ + if (idx != -1) { + result[1] = result[0].substring(idx, result[0].length()); + result[0] = result[0].substring(0, idx); + } + } + return result; + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java index cefefb8e7..c4c2ceccf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -10,6 +10,11 @@ package org.eclipse.jgit.internal.diffmergetool; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -48,7 +53,7 @@ public MergeTools(Repository repo) { * @param remoteFile * the remote file element * @param baseFile - * the base file element + * the base file element (can be null) * @param mergedFilePath * the path of 'merged' file * @param toolName @@ -65,35 +70,79 @@ public ExecutionResult merge(Repository repo, FileElement localFile, String toolName, BooleanTriState prompt, BooleanTriState gui) throws ToolException { ExternalMergeTool tool = guessTool(toolName, gui); + FileElement backup = null; + File tempDir = null; + ExecutionResult result = null; try { File workingDir = repo.getWorkTree(); - String localFilePath = localFile.getFile().getPath(); - String remoteFilePath = remoteFile.getFile().getPath(); - String baseFilePath = baseFile.getFile().getPath(); - String command = tool.getCommand(); - command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$ - command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$ - command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$ - command = command.replace("$BASE", baseFilePath); //$NON-NLS-1$ - Map env = new TreeMap<>(); - env.put(Constants.GIT_DIR_KEY, - repo.getDirectory().getAbsolutePath()); - env.put("LOCAL", localFilePath); //$NON-NLS-1$ - env.put("REMOTE", remoteFilePath); //$NON-NLS-1$ - env.put("MERGED", mergedFilePath); //$NON-NLS-1$ - env.put("BASE", baseFilePath); //$NON-NLS-1$ + // crate temp-directory or use working directory + tempDir = config.isWriteToTemp() + ? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$ + : workingDir; + // create additional backup file (copy worktree file) + backup = createBackupFile(mergedFilePath, tempDir); + // get local, remote and base file paths + String localFilePath = localFile.getFile(tempDir, "LOCAL") //$NON-NLS-1$ + .getPath(); + String remoteFilePath = remoteFile.getFile(tempDir, "REMOTE") //$NON-NLS-1$ + .getPath(); + String baseFilePath = ""; //$NON-NLS-1$ + if (baseFile != null) { + baseFilePath = baseFile.getFile(tempDir, "BASE").getPath(); //$NON-NLS-1$ + } + // prepare the command (replace the file paths) boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; + String command = prepareCommand(mergedFilePath, localFilePath, + remoteFilePath, baseFilePath, + tool.getCommand(baseFile != null)); + // prepare the environment + Map env = prepareEnvironment(repo, mergedFilePath, + localFilePath, remoteFilePath, baseFilePath); CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); - return cmdExec.run(command, workingDir, env); + result = cmdExec.run(command, workingDir, env); + // keep backup as .orig file + if (backup != null) { + keepBackupFile(mergedFilePath, backup); + } + return result; } catch (Exception e) { throw new ToolException(e); } finally { - localFile.cleanTemporaries(); - remoteFile.cleanTemporaries(); - baseFile.cleanTemporaries(); + // always delete backup file (ignore that it was may be already + // moved to keep-backup file) + if (backup != null) { + backup.cleanTemporaries(); + } + // if the tool returns an error and keepTemporaries is set to true, + // then these temporary files will be preserved + if (!((result == null) && config.isKeepTemporaries())) { + // delete the files + localFile.cleanTemporaries(); + remoteFile.cleanTemporaries(); + if (baseFile != null) { + baseFile.cleanTemporaries(); + } + // delete temporary directory if needed + if (config.isWriteToTemp() && (tempDir != null) + && tempDir.exists()) { + tempDir.delete(); + } + } } } + private static FileElement createBackupFile(String mergedFilePath, + File tempDir) throws IOException { + FileElement backup = null; + Path path = Paths.get(tempDir.getPath(), mergedFilePath); + if (Files.exists(path)) { + backup = new FileElement(mergedFilePath, "NOID", null); //$NON-NLS-1$ + Files.copy(path, backup.getFile(tempDir, "BACKUP").toPath(), //$NON-NLS-1$ + StandardCopyOption.REPLACE_EXISTING); + } + return backup; + } + /** * @return the tool names */ @@ -159,6 +208,38 @@ private ExternalMergeTool getTool(final String name) { return tool; } + private String prepareCommand(String mergedFilePath, String localFilePath, + String remoteFilePath, String baseFilePath, String command) { + command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$ + command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$ + command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$ + command = command.replace("$BASE", baseFilePath); //$NON-NLS-1$ + return command; + } + + private Map prepareEnvironment(Repository repo, + String mergedFilePath, String localFilePath, String remoteFilePath, + String baseFilePath) { + Map env = new TreeMap<>(); + env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath()); + env.put("LOCAL", localFilePath); //$NON-NLS-1$ + env.put("REMOTE", remoteFilePath); //$NON-NLS-1$ + env.put("MERGED", mergedFilePath); //$NON-NLS-1$ + env.put("BASE", baseFilePath); //$NON-NLS-1$ + return env; + } + + private void keepBackupFile(String mergedFilePath, FileElement backup) + throws IOException { + if (config.isKeepBackup()) { + Path backupPath = backup.getFile().toPath(); + Files.move(backupPath, + backupPath.resolveSibling( + Paths.get(mergedFilePath).getFileName() + ".orig"), //$NON-NLS-1$ + StandardCopyOption.REPLACE_EXISTING); + } + } + private Map setupPredefinedTools() { Map tools = new TreeMap<>(); for (CommandLineMergeTool tool : CommandLineMergeTool.values()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java index 7862cf596..1ae0780ac 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java @@ -26,6 +26,8 @@ public class ToolException extends Exception { private final ExecutionResult result; + private final boolean commandExecutionError; + /** * the serial version UID */ @@ -35,8 +37,7 @@ public class ToolException extends Exception { * */ public ToolException() { - super(); - result = null; + this(null, null, false); } /** @@ -44,8 +45,7 @@ public ToolException() { * the exception message */ public ToolException(String message) { - super(message); - result = null; + this(message, null, false); } /** @@ -53,10 +53,14 @@ public ToolException(String message) { * the exception message * @param result * the execution result + * @param commandExecutionError + * is command execution error happened ? */ - public ToolException(String message, ExecutionResult result) { + public ToolException(String message, ExecutionResult result, + boolean commandExecutionError) { super(message); this.result = result; + this.commandExecutionError = commandExecutionError; } /** @@ -68,6 +72,7 @@ public ToolException(String message, ExecutionResult result) { public ToolException(String message, Throwable cause) { super(message, cause); result = null; + commandExecutionError = false; } /** @@ -77,6 +82,7 @@ public ToolException(String message, Throwable cause) { public ToolException(Throwable cause) { super(cause); result = null; + commandExecutionError = false; } /** @@ -93,6 +99,13 @@ public ExecutionResult getResult() { return result; } + /** + * @return true if command execution error appears, false otherwise + */ + public boolean isCommandExecutionError() { + return commandExecutionError; + } + /** * @return the result Stderr */ From 814901bfce7828101a758f23681c7043d4c59053 Mon Sep 17 00:00:00 2001 From: Michael Keppler Date: Thu, 26 May 2022 23:17:44 +0200 Subject: [PATCH 5/7] Avoid warning "no explicit project encoding" Eclipse 2022-06 raises that warning for each project without explicit encoding. We can avoid the warning by adding explicit project settings for the otherwise implicit encoding. There is no functional change, neither for users of Eclipse nor for users of other IDEs or build tools. Signed-off-by: Michael Keppler Change-Id: I30a6f1369ef09bd445f5730628d01772db2ee7b3 --- .../.settings/org.eclipse.core.resources.prefs | 2 ++ .../.settings/org.eclipse.core.resources.prefs | 2 ++ .../.settings/org.eclipse.core.resources.prefs | 2 ++ .../.settings/org.eclipse.core.resources.prefs | 2 ++ 4 files changed, 8 insertions(+) create mode 100644 org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs create mode 100644 org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs create mode 100644 org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs create mode 100644 org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs diff --git a/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..99f26c020 --- /dev/null +++ b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..99f26c020 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..99f26c020 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..99f26c020 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 From e81085944f1a039566f2972c863d189724988b46 Mon Sep 17 00:00:00 2001 From: Andre Bossert Date: Sun, 19 Jan 2020 20:54:17 +0100 Subject: [PATCH 6/7] Add filtering with help of DirCacheCheckout.getContent() see: https://git-scm.com/docs/git-mergetool * refactoring of content (FileElement) handling * now the temporary files are already filled with filtered content in the calling classes (PGM), that can be used with EGit content too TODO: * keep the temporaries when no change detected and the user answers no to the question if the merge was successful Bug: 356832 Change-Id: I86a0a052d059957d4d152c1bb94c262902c377d2 Signed-off-by: Andre Bossert --- .../org/eclipse/jgit/pgm/DiffToolTest.java | 55 ++--- .../org/eclipse/jgit/pgm/ToolTestCase.java | 49 ++--- .../src/org/eclipse/jgit/pgm/DiffTool.java | 102 ++++++--- .../src/org/eclipse/jgit/pgm/MergeTool.java | 85 ++++++-- .../diffmergetool/ExternalDiffToolTest.java | 16 +- .../diffmergetool/ExternalMergeToolTest.java | 13 +- .../diffmergetool/ExternalToolTestCase.java | 12 +- .../internal/diffmergetool/DiffTools.java | 41 ++-- .../diffmergetool/ExternalToolUtils.java | 81 ++++++++ .../internal/diffmergetool/FileElement.java | 196 +++++++++++++----- .../internal/diffmergetool/MergeTools.java | 94 ++++----- .../internal/diffmergetool/ToolException.java | 4 +- 12 files changed, 487 insertions(+), 261 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java index dc34c0d67..8daaa6ad9 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java @@ -21,10 +21,8 @@ import java.util.Arrays; import java.util.List; -import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool; import org.eclipse.jgit.lib.StoredConfig; -import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Before; import org.junit.Test; @@ -49,9 +47,8 @@ public void testToolWithPrompt() throws Exception { "y", // accept launching diff tool }; - RevCommit commit = createUnstagedChanges(); - List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedCompareOutput(changes); + String[] conflictingFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedCompareOutput(conflictingFilenames); String option = "--tool"; @@ -68,10 +65,9 @@ public void testToolAbortLaunch() throws Exception { "n", // don't launch diff tool }; - RevCommit commit = createUnstagedChanges(); - List changes = getRepositoryChanges(commit); + String[] conflictingFilenames = createUnstagedChanges(); int abortIndex = 1; - String[] expectedOutput = getExpectedAbortOutput(changes, abortIndex); + String[] expectedOutput = getExpectedAbortOutput(conflictingFilenames, abortIndex); String option = "--tool"; @@ -92,9 +88,8 @@ public void testNotDefinedTool() throws Exception { @Test public void testTool() throws Exception { - RevCommit commit = createUnstagedChanges(); - List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedToolOutputNoPrompt(changes); + String[] conflictFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictFilenames); String[] options = { "--tool", @@ -111,9 +106,8 @@ public void testTool() throws Exception { @Test public void testToolTrustExitCode() throws Exception { - RevCommit commit = createUnstagedChanges(); - List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedToolOutputNoPrompt(changes); + String[] conflictingFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames); String[] options = { "--tool", "-t", }; @@ -126,9 +120,8 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, @Test public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception { - RevCommit commit = createUnstagedChanges(); - List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedToolOutputNoPrompt(changes); + String[] conflictingFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames); String[] options = { "--tool", "-t", }; @@ -142,9 +135,8 @@ expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, @Test public void testToolCached() throws Exception { - RevCommit commit = createStagedChanges(); - List changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedToolOutputNoPrompt(changes); + String[] conflictingFilenames = createStagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames); String[] options = { "--cached", "--staged", }; @@ -201,23 +193,21 @@ private void configureEchoTool(String toolName) { String.valueOf(false)); } - private static String[] getExpectedToolOutputNoPrompt(List changes) { - String[] expectedToolOutput = new String[changes.size()]; - for (int i = 0; i < changes.size(); ++i) { - DiffEntry change = changes.get(i); - String newPath = change.getNewPath(); + private static String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) { + String[] expectedToolOutput = new String[conflictingFilenames.length]; + for (int i = 0; i < conflictingFilenames.length; ++i) { + String newPath = conflictingFilenames[i]; String expectedLine = newPath; expectedToolOutput[i] = expectedLine; } return expectedToolOutput; } - private static String[] getExpectedCompareOutput(List changes) { + private static String[] getExpectedCompareOutput(String[] conflictingFilenames) { List expected = new ArrayList<>(); - int n = changes.size(); + int n = conflictingFilenames.length; for (int i = 0; i < n; ++i) { - DiffEntry change = changes.get(i); - String newPath = change.getNewPath(); + String newPath = conflictingFilenames[i]; expected.add( "Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'"); expected.add("Launch '" + TOOL_NAME + "' [Y/n]?"); @@ -226,13 +216,12 @@ private static String[] getExpectedCompareOutput(List changes) { return expected.toArray(new String[0]); } - private static String[] getExpectedAbortOutput(List changes, + private static String[] getExpectedAbortOutput(String[] conflictingFilenames, int abortIndex) { List expected = new ArrayList<>(); - int n = changes.size(); + int n = conflictingFilenames.length; for (int i = 0; i < n; ++i) { - DiffEntry change = changes.get(i); - String newPath = change.getNewPath(); + String newPath = conflictingFilenames[i]; expected.add( "Viewing (" + (i + 1) + "/" + n + "): '" + newPath + "'"); expected.add("Launch '" + TOOL_NAME + "' [Y/n]?"); diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java index d13eeb7e4..933f19bcc 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java @@ -94,15 +94,15 @@ protected String[] runAndCaptureUsingInitRaw(InputStream inputStream, protected String[] createMergeConflict() throws Exception { // create files on initial branch git.checkout().setName(TEST_BRANCH_NAME).call(); - writeTrashFile("a", "Hello world a"); - writeTrashFile("b", "Hello world b"); + writeTrashFile("dir1/a", "Hello world a"); + writeTrashFile("dir2/b", "Hello world b"); git.add().addFilepattern(".").call(); git.commit().setMessage("files a & b added").call(); // create another branch and change files git.branchCreate().setName("branch_1").call(); git.checkout().setName("branch_1").call(); - writeTrashFile("a", "Hello world a 1"); - writeTrashFile("b", "Hello world b 1"); + writeTrashFile("dir1/a", "Hello world a 1"); + writeTrashFile("dir2/b", "Hello world b 1"); git.add().addFilepattern(".").call(); RevCommit commit1 = git.commit() .setMessage("files a & b modified commit 1").call(); @@ -111,28 +111,28 @@ protected String[] createMergeConflict() throws Exception { // create another branch and change files git.branchCreate().setName("branch_2").call(); git.checkout().setName("branch_2").call(); - writeTrashFile("a", "Hello world a 2"); - writeTrashFile("b", "Hello world b 2"); + writeTrashFile("dir1/a", "Hello world a 2"); + writeTrashFile("dir2/b", "Hello world b 2"); git.add().addFilepattern(".").call(); git.commit().setMessage("files a & b modified commit 2").call(); // cherry-pick conflicting changes git.cherryPick().include(commit1).call(); - String[] conflictingFilenames = { "a", "b" }; + String[] conflictingFilenames = { "dir1/a", "dir2/b" }; return conflictingFilenames; } protected String[] createDeletedConflict() throws Exception { // create files on initial branch git.checkout().setName(TEST_BRANCH_NAME).call(); - writeTrashFile("a", "Hello world a"); - writeTrashFile("b", "Hello world b"); + writeTrashFile("dir1/a", "Hello world a"); + writeTrashFile("dir2/b", "Hello world b"); git.add().addFilepattern(".").call(); git.commit().setMessage("files a & b added").call(); // create another branch and change files git.branchCreate().setName("branch_1").call(); git.checkout().setName("branch_1").call(); - writeTrashFile("a", "Hello world a 1"); - writeTrashFile("b", "Hello world b 1"); + writeTrashFile("dir1/a", "Hello world a 1"); + writeTrashFile("dir2/b", "Hello world b 1"); git.add().addFilepattern(".").call(); RevCommit commit1 = git.commit() .setMessage("files a & b modified commit 1").call(); @@ -141,29 +141,30 @@ protected String[] createDeletedConflict() throws Exception { // create another branch and change files git.branchCreate().setName("branch_2").call(); git.checkout().setName("branch_2").call(); - git.rm().addFilepattern("a").call(); - git.rm().addFilepattern("b").call(); + git.rm().addFilepattern("dir1/a").call(); + git.rm().addFilepattern("dir2/b").call(); git.commit().setMessage("files a & b deleted commit 2").call(); // cherry-pick conflicting changes git.cherryPick().include(commit1).call(); - String[] conflictingFilenames = { "a", "b" }; + String[] conflictingFilenames = { "dir1/a", "dir2/b" }; return conflictingFilenames; } - protected RevCommit createUnstagedChanges() throws Exception { - writeTrashFile("a", "Hello world a"); - writeTrashFile("b", "Hello world b"); + protected String[] createUnstagedChanges() throws Exception { + writeTrashFile("dir1/a", "Hello world a"); + writeTrashFile("dir2/b", "Hello world b"); git.add().addFilepattern(".").call(); - RevCommit commit = git.commit().setMessage("files a & b").call(); - writeTrashFile("a", "New Hello world a"); - writeTrashFile("b", "New Hello world b"); - return commit; + git.commit().setMessage("files a & b").call(); + writeTrashFile("dir1/a", "New Hello world a"); + writeTrashFile("dir2/b", "New Hello world b"); + String[] conflictingFilenames = { "dir1/a", "dir2/b" }; + return conflictingFilenames; } - protected RevCommit createStagedChanges() throws Exception { - RevCommit commit = createUnstagedChanges(); + protected String[] createStagedChanges() throws Exception { + String[] conflictingFilenames = createUnstagedChanges(); git.add().addFilepattern(".").call(); - return commit; + return conflictingFilenames; } protected List getRepositoryChanges(RevCommit commit) diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java index ffba36fe2..74d91cd3d 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java @@ -11,9 +11,11 @@ package org.eclipse.jgit.pgm; import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP; import java.io.BufferedOutputStream; import java.io.BufferedReader; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; @@ -25,27 +27,36 @@ import org.eclipse.jgit.diff.ContentSource.Pair; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.Side; -import org.eclipse.jgit.diff.DiffFormatter; -import org.eclipse.jgit.dircache.DirCacheIterator; -import org.eclipse.jgit.errors.AmbiguousObjectException; -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.RevisionSyntaxException; -import org.eclipse.jgit.internal.diffmergetool.DiffTools; -import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool; -import org.eclipse.jgit.internal.diffmergetool.FileElement; import org.eclipse.jgit.internal.diffmergetool.ToolException; +import org.eclipse.jgit.internal.diffmergetool.DiffTools; +import org.eclipse.jgit.internal.diffmergetool.FileElement; +import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; -import org.eclipse.jgit.lib.ObjectStream; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.TextProgressMonitor; +import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.WorkingTreeIterator; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.FS.ExecutionResult; @@ -164,12 +175,6 @@ private void compare(List files, boolean showPrompt, if (mergedFilePath.equals(DiffEntry.DEV_NULL)) { mergedFilePath = ent.getOldPath(); } - FileElement local = new FileElement(ent.getOldPath(), - ent.getOldId().name(), - getObjectStream(sourcePair, Side.OLD, ent)); - FileElement remote = new FileElement(ent.getNewPath(), - ent.getNewId().name(), - getObjectStream(sourcePair, Side.NEW, ent)); // check if user wants to launch compare boolean launchCompare = true; if (showPrompt) { @@ -178,15 +183,20 @@ private void compare(List files, boolean showPrompt, } if (launchCompare) { try { - // TODO: check how to return the exit-code of - // the - // tool - // to - // jgit / java runtime ? + FileElement local = createFileElement( + FileElement.Type.LOCAL, sourcePair, Side.OLD, + ent); + FileElement remote = createFileElement( + FileElement.Type.REMOTE, sourcePair, Side.NEW, + ent); + FileElement merged = new FileElement(mergedFilePath, + FileElement.Type.MERGED); + // TODO: check how to return the exit-code of the tool + // to jgit / java runtime ? // int rc =... - ExecutionResult result = diffTools.compare(db, local, - remote, mergedFilePath, - toolName, prompt, gui, trustExitCode); + ExecutionResult result = diffTools.compare(local, + remote, merged, toolName, prompt, gui, + trustExitCode); outw.println(new String(result.getStdout().toByteArray())); errw.println( new String(result.getStderr().toByteArray())); @@ -278,16 +288,46 @@ private List getFiles() return files; } - private ObjectStream getObjectStream(Pair pair, Side side, DiffEntry ent) { - ObjectStream stream = null; - if (!pair.isWorkingTreeSource(side)) { - try { - stream = pair.open(side, ent).openStream(); - } catch (Exception e) { - stream = null; + private FileElement createFileElement(FileElement.Type elementType, + Pair pair, Side side, DiffEntry entry) + throws NoWorkTreeException, CorruptObjectException, IOException, + ToolException { + String entryPath = side == Side.NEW ? entry.getNewPath() + : entry.getOldPath(); + FileElement fileElement = new FileElement(entryPath, elementType); + if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) { + try (RevWalk revWalk = new RevWalk(db); + TreeWalk treeWalk = new TreeWalk(db, + revWalk.getObjectReader())) { + treeWalk.setFilter( + PathFilterGroup.createFromStrings(entryPath)); + if (side == Side.NEW) { + newTree.reset(); + treeWalk.addTree(newTree); + } else { + oldTree.reset(); + treeWalk.addTree(oldTree); + } + if (treeWalk.next()) { + final EolStreamType eolStreamType = treeWalk + .getEolStreamType(CHECKOUT_OP); + final String filterCommand = treeWalk.getFilterCommand( + Constants.ATTR_FILTER_TYPE_SMUDGE); + WorkingTreeOptions opt = db.getConfig() + .get(WorkingTreeOptions.KEY); + CheckoutMetadata checkoutMetadata = new CheckoutMetadata( + eolStreamType, filterCommand); + DirCacheCheckout.getContent(db, entryPath, + checkoutMetadata, pair.open(side, entry), opt, + new FileOutputStream( + fileElement.createTempFile(null))); + } else { + throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$ + + "' in staging area!", null); //$NON-NLS-1$ + } } } - return stream; + return fileElement; } private ContentSource source(AbstractTreeIterator iterator) { diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java index dce5a7996..971277075 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java @@ -10,8 +10,11 @@ package org.eclipse.jgit.pgm; +import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP; + import java.io.BufferedReader; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.text.MessageFormat; @@ -26,8 +29,12 @@ import org.eclipse.jgit.api.StatusCommand; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.diff.ContentSource; +import org.eclipse.jgit.internal.diffmergetool.FileElement.Type; import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool; @@ -35,9 +42,15 @@ import org.eclipse.jgit.internal.diffmergetool.MergeTools; import org.eclipse.jgit.internal.diffmergetool.ToolException; import org.eclipse.jgit.lib.IndexDiff.StageState; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.util.FS.ExecutionResult; import org.kohsuke.args4j.Argument; @@ -188,32 +201,67 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt, ContentSource baseSource = ContentSource.create(db.newObjectReader()); ContentSource localSource = ContentSource.create(db.newObjectReader()); ContentSource remoteSource = ContentSource.create(db.newObjectReader()); + // temporary directory if mergetool.writeToTemp == true + File tempDir = mergeTools.createTempDirectory(); + // the parent directory for temp files (can be same as tempDir or just + // the worktree dir) + File tempFilesParent = tempDir != null ? tempDir : db.getWorkTree(); try { FileElement base = null; FileElement local = null; FileElement remote = null; + FileElement merged = new FileElement(mergedFilePath, + Type.MERGED); DirCache cache = db.readDirCache(); - int firstIndex = cache.findEntry(mergedFilePath); - if (firstIndex >= 0) { - int nextIndex = cache.nextEntry(firstIndex); - for (; firstIndex < nextIndex; firstIndex++) { - DirCacheEntry entry = cache.getEntry(firstIndex); + try (RevWalk revWalk = new RevWalk(db); + TreeWalk treeWalk = new TreeWalk(db, + revWalk.getObjectReader())) { + treeWalk.setFilter( + PathFilterGroup.createFromStrings(mergedFilePath)); + DirCacheIterator cacheIter = new DirCacheIterator(cache); + treeWalk.addTree(cacheIter); + while (treeWalk.next()) { + if (treeWalk.isSubtree()) { + treeWalk.enterSubtree(); + continue; + } + final EolStreamType eolStreamType = treeWalk + .getEolStreamType(CHECKOUT_OP); + final String filterCommand = treeWalk.getFilterCommand( + Constants.ATTR_FILTER_TYPE_SMUDGE); + WorkingTreeOptions opt = db.getConfig() + .get(WorkingTreeOptions.KEY); + CheckoutMetadata checkoutMetadata = new CheckoutMetadata( + eolStreamType, filterCommand); + DirCacheEntry entry = treeWalk.getTree(DirCacheIterator.class).getDirCacheEntry(); + if (entry == null) { + continue; + } ObjectId id = entry.getObjectId(); switch (entry.getStage()) { case DirCacheEntry.STAGE_1: - base = new FileElement(mergedFilePath, id.name(), - baseSource.open(mergedFilePath, id) - .openStream()); + base = new FileElement(mergedFilePath, Type.BASE); + DirCacheCheckout.getContent(db, mergedFilePath, + checkoutMetadata, + baseSource.open(mergedFilePath, id), opt, + new FileOutputStream( + base.createTempFile(tempFilesParent))); break; case DirCacheEntry.STAGE_2: - local = new FileElement(mergedFilePath, id.name(), - localSource.open(mergedFilePath, id) - .openStream()); + local = new FileElement(mergedFilePath, Type.LOCAL); + DirCacheCheckout.getContent(db, mergedFilePath, + checkoutMetadata, + localSource.open(mergedFilePath, id), opt, + new FileOutputStream( + local.createTempFile(tempFilesParent))); break; case DirCacheEntry.STAGE_3: - remote = new FileElement(mergedFilePath, id.name(), - remoteSource.open(mergedFilePath, id) - .openStream()); + remote = new FileElement(mergedFilePath, Type.REMOTE); + DirCacheCheckout.getContent(db, mergedFilePath, + checkoutMetadata, + remoteSource.open(mergedFilePath, id), opt, + new FileOutputStream(remote + .createTempFile(tempFilesParent))); break; } } @@ -222,14 +270,13 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt, throw die(MessageFormat.format(CLIText.get().mergeToolDied, mergedFilePath)); } - File merged = new File(mergedFilePath); - long modifiedBefore = merged.lastModified(); + long modifiedBefore = merged.getFile().lastModified(); try { // TODO: check how to return the exit-code of the // tool to jgit / java runtime ? // int rc =... - ExecutionResult executionResult = mergeTools.merge(db, local, - remote, base, mergedFilePath, toolName, prompt, gui); + ExecutionResult executionResult = mergeTools.merge(local, + remote, merged, base, tempDir, toolName, prompt, gui); outw.println( new String(executionResult.getStdout().toByteArray())); outw.flush(); @@ -250,7 +297,7 @@ private MergeResult mergeModified(String mergedFilePath, boolean showPrompt, } // if merge was successful check file modified if (isMergeSuccessful) { - long modifiedAfter = merged.lastModified(); + long modifiedAfter = merged.getFile().lastModified(); if (modifiedBefore == modifiedAfter) { outw.println(MessageFormat.format( CLIText.get().mergeToolFileUnchanged, diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java index ebc67c81c..4fd55c6ca 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java @@ -54,8 +54,8 @@ public void testUserToolWithError() throws Exception { BooleanTriState gui = BooleanTriState.UNSET; BooleanTriState trustExitCode = BooleanTriState.TRUE; - manager.compare(db, local, remote, merged.getPath(), toolName, prompt, - gui, trustExitCode); + manager.compare(local, remote, merged, toolName, prompt, gui, + trustExitCode); fail("Expected exception to be thrown due to external tool exiting with error code: " + errorReturnCode); @@ -78,8 +78,8 @@ public void testUserToolWithCommandNotFoundError() throws Exception { BooleanTriState gui = BooleanTriState.UNSET; BooleanTriState trustExitCode = BooleanTriState.FALSE; - manager.compare(db, local, remote, merged.getPath(), toolName, prompt, - gui, trustExitCode); + manager.compare(local, remote, merged, toolName, prompt, gui, + trustExitCode); fail("Expected exception to be thrown due to external tool exiting with error code: " + errorReturnCode); @@ -183,8 +183,8 @@ public void testCompare() throws ToolException { DiffTools manager = new DiffTools(db); int expectedCompareResult = 0; - ExecutionResult compareResult = manager.compare(db, local, remote, - merged.getPath(), toolName, prompt, gui, trustExitCode); + ExecutionResult compareResult = manager.compare(local, remote, merged, + toolName, prompt, gui, trustExitCode); assertEquals("Incorrect compare result for external diff tool", expectedCompareResult, compareResult.getRc()); } @@ -263,8 +263,8 @@ public void testUndefinedTool() throws Exception { BooleanTriState gui = BooleanTriState.UNSET; BooleanTriState trustExitCode = BooleanTriState.UNSET; - manager.compare(db, local, remote, merged.getPath(), toolName, prompt, - gui, trustExitCode); + manager.compare(local, remote, merged, toolName, prompt, gui, + trustExitCode); fail("Expected exception to be thrown due to not defined external diff tool"); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java index 1dea44eaa..50576682e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java @@ -55,8 +55,7 @@ public void testUserToolWithError() throws Exception { BooleanTriState prompt = BooleanTriState.UNSET; BooleanTriState gui = BooleanTriState.UNSET; - manager.merge(db, local, remote, base, merged.getPath(), toolName, - prompt, gui); + manager.merge(local, remote, merged, base, null, toolName, prompt, gui); fail("Expected exception to be thrown due to external tool exiting with error code: " + errorReturnCode); @@ -78,8 +77,7 @@ public void testUserToolWithCommandNotFoundError() throws Exception { BooleanTriState prompt = BooleanTriState.UNSET; BooleanTriState gui = BooleanTriState.UNSET; - manager.merge(db, local, remote, base, merged.getPath(), toolName, - prompt, gui); + manager.merge(local, remote, merged, base, null, toolName, prompt, gui); fail("Expected exception to be thrown due to external tool exiting with error code: " + errorReturnCode); @@ -182,8 +180,8 @@ public void testCompare() throws ToolException { MergeTools manager = new MergeTools(db); int expectedCompareResult = 0; - ExecutionResult compareResult = manager.merge(db, local, remote, base, - merged.getPath(), toolName, prompt, gui); + ExecutionResult compareResult = manager.merge(local, remote, merged, + base, null, toolName, prompt, gui); assertEquals("Incorrect compare result for external merge tool", expectedCompareResult, compareResult.getRc()); } @@ -262,8 +260,7 @@ public void testUndefinedTool() throws Exception { BooleanTriState prompt = BooleanTriState.UNSET; BooleanTriState gui = BooleanTriState.UNSET; - manager.merge(db, local, remote, base, merged.getPath(), toolName, - prompt, gui); + manager.merge(local, remote, merged, base, null, toolName, prompt, gui); fail("Expected exception to be thrown due to not defined external merge tool"); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java index 6757eb463..0fd85cb45 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java @@ -60,10 +60,14 @@ public void setUp() throws Exception { commandResult = writeTrashFile("commandResult.txt", ""); commandResult.deleteOnExit(); - local = new FileElement(localFile.getAbsolutePath(), "LOCAL"); - remote = new FileElement(remoteFile.getAbsolutePath(), "REMOTE"); - merged = new FileElement(mergedFile.getAbsolutePath(), "MERGED"); - base = new FileElement(baseFile.getAbsolutePath(), "BASE"); + local = new FileElement(localFile.getAbsolutePath(), + FileElement.Type.LOCAL); + remote = new FileElement(remoteFile.getAbsolutePath(), + FileElement.Type.REMOTE); + merged = new FileElement(mergedFile.getAbsolutePath(), + FileElement.Type.MERGED); + base = new FileElement(baseFile.getAbsolutePath(), + FileElement.Type.BASE); } @After diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index 2f2b9de81..1dcc523bf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -12,12 +12,10 @@ import java.util.TreeMap; import java.util.Collections; -import java.io.File; import java.io.IOException; import java.util.Map; import java.util.Set; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.util.FS.ExecutionResult; @@ -28,6 +26,8 @@ */ public class DiffTools { + private final Repository repo; + private final DiffToolConfig config; private final Map predefinedTools; @@ -41,6 +41,7 @@ public class DiffTools { * the repository */ public DiffTools(Repository repo) { + this.repo = repo; config = repo.getConfig().get(DiffToolConfig.KEY); predefinedTools = setupPredefinedTools(); userDefinedTools = setupUserDefinedTools(config, predefinedTools); @@ -49,14 +50,13 @@ public DiffTools(Repository repo) { /** * Compare two versions of a file. * - * @param repo - * the repository * @param localFile * the local file element * @param remoteFile * the remote file element - * @param mergedFilePath - * the path of 'merged' file, it equals local or remote path + * @param mergedFile + * the merged file element, it's path equals local or remote + * element path * @param toolName * the selected tool name (can be null) * @param prompt @@ -68,36 +68,31 @@ public DiffTools(Repository repo) { * @return the execution result from tool * @throws ToolException */ - public ExecutionResult compare(Repository repo, FileElement localFile, - FileElement remoteFile, String mergedFilePath, String toolName, + public ExecutionResult compare(FileElement localFile, + FileElement remoteFile, FileElement mergedFile, String toolName, BooleanTriState prompt, BooleanTriState gui, BooleanTriState trustExitCode) throws ToolException { - ExternalDiffTool tool = guessTool(toolName, gui); try { - File workingDir = repo.getWorkTree(); - String localFilePath = localFile.getFile().getPath(); - String remoteFilePath = remoteFile.getFile().getPath(); - String command = tool.getCommand(); - command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$ - command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$ - command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$ - Map env = new TreeMap<>(); - env.put(Constants.GIT_DIR_KEY, - repo.getDirectory().getAbsolutePath()); - env.put("LOCAL", localFilePath); //$NON-NLS-1$ - env.put("REMOTE", remoteFilePath); //$NON-NLS-1$ - env.put("MERGED", mergedFilePath); //$NON-NLS-1$ + // prepare the command (replace the file paths) + String command = ExternalToolUtils.prepareCommand( + guessTool(toolName, gui).getCommand(), localFile, + remoteFile, mergedFile, null); + // prepare the environment + Map env = ExternalToolUtils.prepareEnvironment(repo, + localFile, remoteFile, mergedFile, null); boolean trust = config.isTrustExitCode(); if (trustExitCode != BooleanTriState.UNSET) { trust = trustExitCode == BooleanTriState.TRUE; } + // execute the tool CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); - return cmdExec.run(command, workingDir, env); + return cmdExec.run(command, repo.getWorkTree(), env); } catch (IOException | InterruptedException e) { throw new ToolException(e); } finally { localFile.cleanTemporaries(); remoteFile.cleanTemporaries(); + mergedFile.cleanTemporaries(); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java new file mode 100644 index 000000000..3efb90c49 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java @@ -0,0 +1,81 @@ +/* + * 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.io.IOException; +import java.util.Map; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Repository; + +/** + * Utilities for diff- and merge-tools. + */ +public class ExternalToolUtils { + + /** + * Prepare command for execution. + * + * @param command + * the input "command" string + * @param localFile + * the local file (ours) + * @param remoteFile + * the remote file (theirs) + * @param mergedFile + * the merged file (worktree) + * @param baseFile + * the base file (can be null) + * @return the prepared (with replaced variables) command string + * @throws IOException + */ + public static String prepareCommand(String command, FileElement localFile, + FileElement remoteFile, FileElement mergedFile, + FileElement baseFile) throws IOException { + command = localFile.replaceVariable(command); + command = remoteFile.replaceVariable(command); + command = mergedFile.replaceVariable(command); + if (baseFile != null) { + command = baseFile.replaceVariable(command); + } + return command; + } + + /** + * Prepare environment needed for execution. + * + * @param repo + * the repository + * @param localFile + * the local file (ours) + * @param remoteFile + * the remote file (theirs) + * @param mergedFile + * the merged file (worktree) + * @param baseFile + * the base file (can be null) + * @return the environment map with variables and values (file paths) + * @throws IOException + */ + public static Map prepareEnvironment(Repository repo, + FileElement localFile, FileElement remoteFile, + FileElement mergedFile, FileElement baseFile) throws IOException { + Map env = new TreeMap<>(); + env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath()); + localFile.addToEnv(env); + remoteFile.addToEnv(env); + mergedFile.addToEnv(env); + if (baseFile != null) { + baseFile.addToEnv(env); + } + return env; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java index 1ae87aaa6..5902c1e1b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java @@ -14,10 +14,11 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.util.Map; import org.eclipse.jgit.diff.DiffEntry; -import org.eclipse.jgit.lib.ObjectStream; /** * The element used as left or right file for compare. @@ -25,36 +26,71 @@ */ public class FileElement { + /** + * The file element type. + * + */ + public enum Type { + /** + * The local file element (ours). + */ + LOCAL, + /** + * The remote file element (theirs). + */ + REMOTE, + /** + * The merged file element (path in worktree). + */ + MERGED, + /** + * The base file element (of ours and theirs). + */ + BASE, + /** + * The backup file element (copy of merged / conflicted). + */ + BACKUP + } + private final String path; - private final String id; + private final Type type; - private ObjectStream stream; + private InputStream stream; private File tempFile; /** + * Creates file element for path. + * * @param path * the file path - * @param id - * the file id + * @param type + * the element type */ - public FileElement(final String path, final String id) { - this(path, id, null); + public FileElement(String path, Type type) { + this(path, type, null, null); } /** + * Creates file element for path. + * * @param path * the file path - * @param id - * the file id + * @param type + * the element type + * @param tempFile + * the temporary file to be used (can be null and will be created + * then) * @param stream * the object stream to load instead of file */ - public FileElement(final String path, final String id, - ObjectStream stream) { + public FileElement(String path, Type type, File tempFile, + InputStream stream) { this.path = path; - this.id = id; + this.type = type; + this.tempFile = tempFile; this.stream = stream; } @@ -66,71 +102,101 @@ public String getPath() { } /** - * @return the file id + * @return the element type */ - public String getId() { - return id; + public Type getType() { + return type; } /** - * @param stream - * the object stream - */ - public void setStream(ObjectStream stream) { - this.stream = stream; - } - - /** - * Returns a temporary file with in passed working directory and fills it - * with stream if valid. + * Return a temporary file within passed directory and fills it with stream + * if valid. * * @param directory - * the working directory where the temporary file is created + * the directory where the temporary file is created * @param midName * name added in the middle of generated temporary file name * @return the object stream * @throws IOException */ public File getFile(File directory, String midName) throws IOException { - if (tempFile != null) { + if ((tempFile != null) && (stream == null)) { return tempFile; } - String[] fileNameAndExtension = splitBaseFileNameAndExtension( - new File(path)); - tempFile = File.createTempFile( - fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$ - fileNameAndExtension[1], directory); - copyFromStream(); - return tempFile; + tempFile = getTempFile(path, directory, midName); + return copyFromStream(tempFile, stream); } /** - * Returns a real file from work tree or a temporary file with content if + * Return a real file from work tree or a temporary file with content if * stream is valid or if path is "/dev/null" * * @return the object stream * @throws IOException */ public File getFile() throws IOException { - if (tempFile != null) { + if ((tempFile != null) && (stream == null)) { return tempFile; } File file = new File(path); - String name = file.getName(); // if we have a stream or file is missing ("/dev/null") then create // temporary file - if ((stream != null) || path.equals(DiffEntry.DEV_NULL)) { - // TODO: avoid long random file name (number generated by - // createTempFile) - tempFile = File.createTempFile(".__", "__" + name); //$NON-NLS-1$ //$NON-NLS-2$ - copyFromStream(); - return tempFile; + if ((stream != null) || isNullPath()) { + tempFile = getTempFile(file); + return copyFromStream(tempFile, stream); } return file; } /** - * Deletes and invalidates temporary file if necessary. + * Check if path id "/dev/null" + * + * @return true if path is "/dev/null" + */ + public boolean isNullPath() { + return path.equals(DiffEntry.DEV_NULL); + } + + /** + * Create temporary file in given or system temporary directory + * + * @param directory + * the directory for the file (can be null); if null system + * temporary directory is used + * @return temporary file in directory or in the system temporary directory + * @throws IOException + */ + public File createTempFile(File directory) throws IOException { + if (tempFile == null) { + File file = new File(path); + if (directory != null) { + tempFile = getTempFile(file, directory, type.name()); + } else { + tempFile = getTempFile(file); + } + } + return tempFile; + } + + private static File getTempFile(File file) throws IOException { + return File.createTempFile(".__", "__" + file.getName()); //$NON-NLS-1$ //$NON-NLS-2$ + } + + private static File getTempFile(File file, File directory, String midName) + throws IOException { + String[] fileNameAndExtension = splitBaseFileNameAndExtension(file); + return File.createTempFile( + fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$ + fileNameAndExtension[1], directory); + } + + private static File getTempFile(String path, File directory, String midName) + throws IOException { + return getTempFile(new File(path), directory, midName); + } + + /** + * Delete and invalidate temporary file if necessary. */ public void cleanTemporaries() { if (tempFile != null && tempFile.exists()) @@ -138,9 +204,10 @@ public void cleanTemporaries() { tempFile = null; } - private void copyFromStream() throws IOException, FileNotFoundException { + private static File copyFromStream(File file, final InputStream stream) + throws IOException, FileNotFoundException { if (stream != null) { - try (OutputStream outStream = new FileOutputStream(tempFile)) { + try (OutputStream outStream = new FileOutputStream(file)) { int read = 0; byte[] bytes = new byte[8 * 1024]; while ((read = stream.read(bytes)) != -1) { @@ -149,23 +216,46 @@ private void copyFromStream() throws IOException, FileNotFoundException { } finally { // stream can only be consumed once --> close it stream.close(); - stream = null; } } + return file; } private static String[] splitBaseFileNameAndExtension(File file) { String[] result = new String[2]; result[0] = file.getName(); result[1] = ""; //$NON-NLS-1$ - if (!result[0].startsWith(".")) { //$NON-NLS-1$ - int idx = result[0].lastIndexOf("."); //$NON-NLS-1$ - if (idx != -1) { - result[1] = result[0].substring(idx, result[0].length()); - result[0] = result[0].substring(0, idx); - } + int idx = result[0].lastIndexOf("."); //$NON-NLS-1$ + // if "." was found (>-1) and last-index is not first char (>0), then + // split (same behavior like cgit) + if (idx > 0) { + result[1] = result[0].substring(idx, result[0].length()); + result[0] = result[0].substring(0, idx); } return result; } + /** + * Replace variable in input + * + * @param input + * the input string + * @return the replaced input string + * @throws IOException + */ + public String replaceVariable(String input) throws IOException { + return input.replace("$" + type.name(), getFile().getPath()); //$NON-NLS-1$ + } + + /** + * Add variable to environment map. + * + * @param env + * the environment where this element should be added + * @throws IOException + */ + public void addToEnv(Map env) throws IOException { + env.put(type.name(), getFile().getPath()); + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java index c4c2ceccf..9a2a8304e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -19,7 +19,7 @@ import java.util.Set; import java.util.TreeMap; -import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.internal.diffmergetool.FileElement.Type; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.util.FS.ExecutionResult; @@ -29,6 +29,8 @@ */ public class MergeTools { + Repository repo; + private final MergeToolConfig config; private final Map predefinedTools; @@ -37,25 +39,27 @@ public class MergeTools { /** * @param repo - * the repository database + * the repository */ public MergeTools(Repository repo) { + this.repo = repo; config = repo.getConfig().get(MergeToolConfig.KEY); predefinedTools = setupPredefinedTools(); userDefinedTools = setupUserDefinedTools(config, predefinedTools); } /** - * @param repo - * the repository * @param localFile * the local file element * @param remoteFile * the remote file element + * @param mergedFile + * the merged file element * @param baseFile * the base file element (can be null) - * @param mergedFilePath - * the path of 'merged' file + * @param tempDir + * the temporary directory (needed for backup and auto-remove, + * can be null) * @param toolName * the selected tool name (can be null) * @param prompt @@ -65,47 +69,35 @@ public MergeTools(Repository repo) { * @return the execution result from tool * @throws ToolException */ - public ExecutionResult merge(Repository repo, FileElement localFile, - FileElement remoteFile, FileElement baseFile, String mergedFilePath, + public ExecutionResult merge(FileElement localFile, FileElement remoteFile, + FileElement mergedFile, FileElement baseFile, File tempDir, String toolName, BooleanTriState prompt, BooleanTriState gui) throws ToolException { ExternalMergeTool tool = guessTool(toolName, gui); FileElement backup = null; - File tempDir = null; ExecutionResult result = null; try { File workingDir = repo.getWorkTree(); - // crate temp-directory or use working directory - tempDir = config.isWriteToTemp() - ? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$ - : workingDir; // create additional backup file (copy worktree file) - backup = createBackupFile(mergedFilePath, tempDir); - // get local, remote and base file paths - String localFilePath = localFile.getFile(tempDir, "LOCAL") //$NON-NLS-1$ - .getPath(); - String remoteFilePath = remoteFile.getFile(tempDir, "REMOTE") //$NON-NLS-1$ - .getPath(); - String baseFilePath = ""; //$NON-NLS-1$ - if (baseFile != null) { - baseFilePath = baseFile.getFile(tempDir, "BASE").getPath(); //$NON-NLS-1$ - } + backup = createBackupFile(mergedFile.getPath(), + tempDir != null ? tempDir : workingDir); // prepare the command (replace the file paths) boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; - String command = prepareCommand(mergedFilePath, localFilePath, - remoteFilePath, baseFilePath, - tool.getCommand(baseFile != null)); + String command = ExternalToolUtils.prepareCommand( + tool.getCommand(baseFile != null), localFile, remoteFile, + mergedFile, baseFile); // prepare the environment - Map env = prepareEnvironment(repo, mergedFilePath, - localFilePath, remoteFilePath, baseFilePath); + Map env = ExternalToolUtils.prepareEnvironment(repo, + localFile, remoteFile, mergedFile, baseFile); + // execute the tool CommandExecutor cmdExec = new CommandExecutor(repo.getFS(), trust); result = cmdExec.run(command, workingDir, env); // keep backup as .orig file if (backup != null) { - keepBackupFile(mergedFilePath, backup); + keepBackupFile(mergedFile.getPath(), backup); } return result; - } catch (Exception e) { + } catch (IOException | InterruptedException e) { throw new ToolException(e); } finally { // always delete backup file (ignore that it was may be already @@ -131,18 +123,29 @@ public ExecutionResult merge(Repository repo, FileElement localFile, } } - private static FileElement createBackupFile(String mergedFilePath, - File tempDir) throws IOException { + private FileElement createBackupFile(String filePath, File parentDir) + throws IOException { FileElement backup = null; - Path path = Paths.get(tempDir.getPath(), mergedFilePath); + Path path = Paths.get(filePath); if (Files.exists(path)) { - backup = new FileElement(mergedFilePath, "NOID", null); //$NON-NLS-1$ - Files.copy(path, backup.getFile(tempDir, "BACKUP").toPath(), //$NON-NLS-1$ + backup = new FileElement(filePath, Type.BACKUP); + Files.copy(path, backup.createTempFile(parentDir).toPath(), StandardCopyOption.REPLACE_EXISTING); } return backup; } + /** + * @return the created temporary directory if (mergetol.writeToTemp == true) + * or null if not configured or false. + * @throws IOException + */ + public File createTempDirectory() throws IOException { + return config.isWriteToTemp() + ? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$ + : null; + } + /** * @return the tool names */ @@ -208,27 +211,6 @@ private ExternalMergeTool getTool(final String name) { return tool; } - private String prepareCommand(String mergedFilePath, String localFilePath, - String remoteFilePath, String baseFilePath, String command) { - command = command.replace("$LOCAL", localFilePath); //$NON-NLS-1$ - command = command.replace("$REMOTE", remoteFilePath); //$NON-NLS-1$ - command = command.replace("$MERGED", mergedFilePath); //$NON-NLS-1$ - command = command.replace("$BASE", baseFilePath); //$NON-NLS-1$ - return command; - } - - private Map prepareEnvironment(Repository repo, - String mergedFilePath, String localFilePath, String remoteFilePath, - String baseFilePath) { - Map env = new TreeMap<>(); - env.put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath()); - env.put("LOCAL", localFilePath); //$NON-NLS-1$ - env.put("REMOTE", remoteFilePath); //$NON-NLS-1$ - env.put("MERGED", mergedFilePath); //$NON-NLS-1$ - env.put("BASE", baseFilePath); //$NON-NLS-1$ - return env; - } - private void keepBackupFile(String mergedFilePath, FileElement backup) throws IOException { if (config.isKeepBackup()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java index 1ae0780ac..27f7d12e6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java @@ -113,7 +113,7 @@ public String getResultStderr() { try { return new String(result.getStderr().toByteArray()); } catch (Exception e) { - LOG.warn(e.getMessage()); + LOG.warn("Failed to retrieve standard error output", e); //$NON-NLS-1$ } return ""; //$NON-NLS-1$ } @@ -125,7 +125,7 @@ public String getResultStdout() { try { return new String(result.getStdout().toByteArray()); } catch (Exception e) { - LOG.warn(e.getMessage()); + LOG.warn("Failed to retrieve standard output", e); //$NON-NLS-1$ } return ""; //$NON-NLS-1$ } From 8c681aac500d0755bd68f8b322f7fdb20cd2b8a1 Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Mon, 30 May 2022 16:34:07 +0200 Subject: [PATCH 7/7] Fixed since tags added for new merge constants Bug: 356832 Change-Id: I70197522675de0e9b53399ee3d9ed3f10312ff6b --- .../src/org/eclipse/jgit/lib/ConfigConstants.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 29c66f516..2342cad0d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -127,28 +127,28 @@ public final class ConfigConstants { /** * The "mergetool" section * - * @since 5.13 + * @since 6.2 */ public static final String CONFIG_MERGETOOL_SECTION = "mergetool"; /** * The "keepBackup" key within "mergetool" section * - * @since 5.13 + * @since 6.2 */ public static final String CONFIG_KEY_KEEP_BACKUP = "keepBackup"; /** * The "keepTemporaries" key within "mergetool" section * - * @since 5.13 + * @since 6.2 */ public static final String CONFIG_KEY_KEEP_TEMPORARIES = "keepTemporaries"; /** * The "writeToTemp" key within "mergetool" section * - * @since 5.13 + * @since 6.2 */ public static final String CONFIG_KEY_WRITE_TO_TEMP = "writeToTemp";