From eaf4d500b886a7e776f50bf53497fe463e714b25 Mon Sep 17 00:00:00 2001 From: Andre Bossert Date: Fri, 8 Mar 2019 22:31:34 +0100 Subject: 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 --- .../src/org/eclipse/jgit/pgm/DiffTool.java | 15 +- .../src/org/eclipse/jgit/pgm/MergeTool.java | 323 +++++++++++++++++---- .../src/org/eclipse/jgit/pgm/internal/CLIText.java | 16 + 3 files changed, 287 insertions(+), 67 deletions(-) (limited to 'org.eclipse.jgit.pgm/src/org/eclipse') 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 2e90d52cba..ffba36fe20 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 @@ class DiffTool extends TextBuiltin { @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 @@ class DiffTool extends TextBuiltin { 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 @@ class DiffTool extends TextBuiltin { 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 37afa54c78..dce5a7996d 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 @@ class MergeTool extends TextBuiltin { @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 @@ class MergeTool extends TextBuiltin { } 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 @@ class MergeTool extends TextBuiltin { 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 @@ class MergeTool extends TextBuiltin { 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); - } - if (launch) { - outw.println("TODO: Launch mergetool '" + toolNamePrompt //$NON-NLS-1$ - + "' for path '" + fileName + "'..."); //$NON-NLS-1$ //$NON-NLS-2$ - } else { - break; + // 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; } - } 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$ + } + // 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 MergeResult mergeModified(String mergedFilePath, boolean showPrompt, + String toolNamePrompt) throws Exception { + outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict, + mergedFilePath)); + outw.flush(); + // 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( - "\nUnknown merge conflict for '" + fileName + "':"); //$NON-NLS-1$ //$NON-NLS-2$ + 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 { + 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.print(MessageFormat.format(CLIText.get().mergeToolLaunch, + toolNamePrompt) + " "); //$NON-NLS-1$ outw.flush(); - BufferedReader br = new BufferedReader(new InputStreamReader(ins)); + 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 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(); + } + return ret; + } + private void showToolHelp() throws IOException { - outw.println( - "'git mergetool --tool=' may be set to one of the following:"); //$NON-NLS-1$ + StringBuilder availableToolNames = new StringBuilder(); for (String name : mergeTools.getAvailableTools().keySet()) { - outw.println("\t\t" + name); //$NON-NLS-1$ + availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ } - outw.println(""); //$NON-NLS-1$ - outw.println("\tuser-defined:"); //$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()); - } - 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$ + 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("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 7fe5b0fa45..989e649b72 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 class CLIText extends TranslationBundle { /***/ 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; -- cgit v1.2.3