aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.pgm/src/org
diff options
context:
space:
mode:
authorAndre Bossert <andre.bossert@siemens.com>2019-03-08 22:31:34 +0100
committerAndrey Loskutov <loskutov@gmx.de>2022-05-25 13:52:04 +0200
commiteaf4d500b886a7e776f50bf53497fe463e714b25 (patch)
tree740ac70c01372243cdbc8c902113ff144c8f3c2b /org.eclipse.jgit.pgm/src/org
parent85734356351ec2df4067b2472a37f6d9bcbb7350 (diff)
downloadjgit-eaf4d500b886a7e776f50bf53497fe463e714b25.tar.gz
jgit-eaf4d500b886a7e776f50bf53497fe463e714b25.zip
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 <andre.bossert@siemens.com>
Diffstat (limited to 'org.eclipse.jgit.pgm/src/org')
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java15
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java323
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java16
3 files changed, 287 insertions, 67 deletions
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<String, ExternalDiffTool> 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<Boolean> 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<String> 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<String, StageState> files, boolean showPrompt,
String toolNamePrompt) throws Exception {
// sort file names
- List<String> fileNames = new ArrayList<>(files.keySet());
- Collections.sort(fileNames);
+ List<String> 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=<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<String, ExternalMergeTool> 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<String, StageState> getFiles()
- throws RevisionSyntaxException, NoWorkTreeException,
- GitAPIException {
+ private Map<String, StageState> getFiles() throws RevisionSyntaxException,
+ NoWorkTreeException, GitAPIException {
Map<String, StageState> 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;