aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit/src/org/eclipse
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse')
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java40
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java20
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java83
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java77
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java25
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java239
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java327
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java323
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java33
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java242
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java262
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java28
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java147
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java452
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java91
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java27
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java139
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java19
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java69
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java108
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java104
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java48
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java49
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java44
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java69
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java72
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java15
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java8
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java41
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java74
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java50
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java23
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java44
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java8
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java120
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java36
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java94
54 files changed, 3394 insertions, 350 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
index f88179ac1a..ceba89d166 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
@@ -30,6 +30,7 @@ import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitConfig;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
@@ -183,9 +184,13 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> {
String message;
if (unmergedPaths != null) {
+ CommitConfig cfg = repo.getConfig()
+ .get(CommitConfig.KEY);
+ message = srcCommit.getFullMessage();
+ char commentChar = cfg.getCommentChar(message);
message = new MergeMessageFormatter()
- .formatWithConflicts(srcCommit.getFullMessage(),
- unmergedPaths, '#');
+ .formatWithConflicts(message, unmergedPaths,
+ commentChar);
} else {
message = srcCommit.getFullMessage();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
index 7a591aa3b5..3b3baf5a12 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -233,11 +233,25 @@ public class CommitCommand extends GitCommand<RevCommit> {
config = repo.getConfig().get(CommitConfig.KEY);
cleanupMode = config.resolve(cleanupMode, cleanDefaultIsStrip);
}
- char comments;
- if (commentChar == null) {
- comments = '#'; // TODO use git config core.commentChar
- } else {
- comments = commentChar.charValue();
+ char comments = (char) 0;
+ if (CleanupMode.STRIP.equals(cleanupMode)
+ || CleanupMode.SCISSORS.equals(cleanupMode)) {
+ if (commentChar == null) {
+ if (config == null) {
+ config = repo.getConfig().get(CommitConfig.KEY);
+ }
+ if (config.isAutoCommentChar()) {
+ // We're supposed to pick a character that isn't used,
+ // but then cleaning up won't remove any lines. So don't
+ // bother.
+ comments = (char) 0;
+ cleanupMode = CleanupMode.WHITESPACE;
+ } else {
+ comments = config.getCommentChar();
+ }
+ } else {
+ comments = commentChar.charValue();
+ }
}
message = CommitConfig.cleanText(message, cleanupMode, comments);
@@ -309,8 +323,14 @@ public class CommitCommand extends GitCommand<RevCommit> {
private void sign(CommitBuilder commit) throws ServiceUnavailableException,
CanceledException, UnsupportedSigningFormatException {
if (gpgSigner == null) {
- throw new ServiceUnavailableException(
- JGitText.get().signingServiceUnavailable);
+ gpgSigner = GpgSigner.getDefault();
+ if (gpgSigner == null) {
+ throw new ServiceUnavailableException(
+ JGitText.get().signingServiceUnavailable);
+ }
+ }
+ if (signingKey == null) {
+ signingKey = gpgConfig.getSigningKey();
}
if (gpgSigner instanceof GpgObjectSigner) {
((GpgObjectSigner) gpgSigner).signObject(commit,
@@ -645,12 +665,6 @@ public class CommitCommand extends GitCommand<RevCommit> {
signCommit = gpgConfig.isSignCommits() ? Boolean.TRUE
: Boolean.FALSE;
}
- if (signingKey == null) {
- signingKey = gpgConfig.getSigningKey();
- }
- if (gpgSigner == null) {
- gpgSigner = GpgSigner.getDefault();
- }
}
private boolean isMergeDuringRebase(RepositoryState state) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
index 0c691062f9..c3415581ef 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011, 2020 Christoph Brill <egore911@egore911.de> and others
+ * Copyright (C) 2011, 2022 Christoph Brill <egore911@egore911.de> 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
@@ -9,6 +9,7 @@
*/
package org.eclipse.jgit.api;
+import java.io.IOException;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.ArrayList;
@@ -20,8 +21,8 @@ import java.util.Map;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.NotSupportedException;
-import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
@@ -30,6 +31,8 @@ import org.eclipse.jgit.transport.FetchConnection;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.UrlConfig;
+import org.eclipse.jgit.util.SystemReader;
/**
* The ls-remote command
@@ -153,7 +156,7 @@ public class LsRemoteCommand extends
try (Transport transport = repo != null
? Transport.open(repo, remote)
- : Transport.open(new URIish(remote))) {
+ : Transport.open(new URIish(translate(remote)))) {
transport.setOptionUploadPack(uploadPack);
configure(transport);
Collection<RefSpec> refSpecs = new ArrayList<>(1);
@@ -185,11 +188,16 @@ public class LsRemoteCommand extends
throw new JGitInternalException(
JGitText.get().exceptionCaughtDuringExecutionOfLsRemoteCommand,
e);
- } catch (TransportException e) {
+ } catch (IOException | ConfigInvalidException e) {
throw new org.eclipse.jgit.api.errors.TransportException(
- e.getMessage(),
- e);
+ e.getMessage(), e);
}
}
+ private String translate(String uri)
+ throws IOException, ConfigInvalidException {
+ UrlConfig urls = new UrlConfig(
+ SystemReader.getInstance().getUserConfig());
+ return urls.replace(uri);
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
index ce068b6306..ed4a5342b3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
@@ -34,6 +34,7 @@ import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitConfig;
import org.eclipse.jgit.lib.Config.ConfigEnum;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -404,8 +405,11 @@ public class MergeCommand extends GitCommand<MergeResult> {
MergeStatus.FAILED, mergeStrategy, lowLevelResults,
failingPaths, null);
}
+ CommitConfig cfg = repo.getConfig().get(CommitConfig.KEY);
+ char commentChar = cfg.getCommentChar(message);
String mergeMessageWithConflicts = new MergeMessageFormatter()
- .formatWithConflicts(mergeMessage, unmergedPaths, '#');
+ .formatWithConflicts(mergeMessage, unmergedPaths,
+ commentChar);
repo.writeMergeCommitMsg(mergeMessageWithConflicts);
return new MergeResult(null, merger.getBaseCommitId(),
new ObjectId[] { headCommit.getId(),
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
index 2b0d8ce1c9..4e0d9d78c3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -449,7 +449,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
String oldMessage = commitToPick.getFullMessage();
CleanupMode mode = commitConfig.resolve(CleanupMode.DEFAULT, true);
boolean[] doChangeId = { false };
- String newMessage = editCommitMessage(doChangeId, oldMessage, mode);
+ String newMessage = editCommitMessage(doChangeId, oldMessage, mode,
+ commitConfig.getCommentChar(oldMessage));
try (Git git = new Git(repo)) {
newHead = git.commit()
.setMessage(newMessage)
@@ -494,12 +495,12 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
private String editCommitMessage(boolean[] doChangeId, String message,
- @NonNull CleanupMode mode) {
+ @NonNull CleanupMode mode, char commentChar) {
String newMessage;
CommitConfig.CleanupMode cleanup;
if (interactiveHandler instanceof InteractiveHandler2) {
InteractiveHandler2.ModifyResult modification = ((InteractiveHandler2) interactiveHandler)
- .editCommitMessage(message, mode, '#');
+ .editCommitMessage(message, mode, commentChar);
newMessage = modification.getMessage();
cleanup = modification.getCleanupMode();
if (CleanupMode.DEFAULT.equals(cleanup)) {
@@ -511,7 +512,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
cleanup = CommitConfig.CleanupMode.STRIP;
doChangeId[0] = false;
}
- return CommitConfig.cleanText(newMessage, cleanup, '#');
+ return CommitConfig.cleanText(newMessage, cleanup, commentChar);
}
private RebaseResult cherryPickCommit(RevCommit commitToPick)
@@ -808,8 +809,9 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
if (isLast) {
boolean[] doChangeId = { false };
if (sequenceContainsSquash) {
+ char commentChar = commitMessage.charAt(0);
commitMessage = editCommitMessage(doChangeId, commitMessage,
- CleanupMode.STRIP);
+ CleanupMode.STRIP, commentChar);
}
retNewHead = git.commit()
.setMessage(commitMessage)
@@ -829,30 +831,60 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
@SuppressWarnings("nls")
- private static String composeSquashMessage(boolean isSquash,
+ private String composeSquashMessage(boolean isSquash,
RevCommit commitToPick, String currSquashMessage, int count) {
StringBuilder sb = new StringBuilder();
String ordinal = getOrdinal(count);
- sb.setLength(0);
- sb.append("# This is a combination of ").append(count)
- .append(" commits.\n");
- // Add the previous message without header (i.e first line)
- sb.append(currSquashMessage
- .substring(currSquashMessage.indexOf('\n') + 1));
- sb.append("\n");
- if (isSquash) {
- sb.append("# This is the ").append(count).append(ordinal)
- .append(" commit message:\n");
- sb.append(commitToPick.getFullMessage());
+ // currSquashMessage is always non-empty here, and the first character
+ // is the comment character used so far.
+ char commentChar = currSquashMessage.charAt(0);
+ String newMessage = commitToPick.getFullMessage();
+ if (!isSquash) {
+ sb.append(commentChar).append(" This is a combination of ")
+ .append(count).append(" commits.\n");
+ // Add the previous message without header (i.e first line)
+ sb.append(currSquashMessage
+ .substring(currSquashMessage.indexOf('\n') + 1));
+ sb.append('\n');
+ sb.append(commentChar).append(" The ").append(count).append(ordinal)
+ .append(" commit message will be skipped:\n")
+ .append(commentChar).append(' ');
+ sb.append(newMessage.replaceAll("([\n\r])",
+ "$1" + commentChar + ' '));
} else {
- sb.append("# The ").append(count).append(ordinal)
- .append(" commit message will be skipped:\n# ");
- sb.append(commitToPick.getFullMessage().replaceAll("([\n\r])",
- "$1# "));
+ String currentMessage = currSquashMessage;
+ if (commitConfig.isAutoCommentChar()) {
+ // Figure out a new comment character taking into account the
+ // new message
+ String cleaned = CommitConfig.cleanText(currentMessage,
+ CommitConfig.CleanupMode.STRIP, commentChar) + '\n'
+ + newMessage;
+ char newCommentChar = commitConfig.getCommentChar(cleaned);
+ if (newCommentChar != commentChar) {
+ currentMessage = replaceCommentChar(currentMessage,
+ commentChar, newCommentChar);
+ commentChar = newCommentChar;
+ }
+ }
+ sb.append(commentChar).append(" This is a combination of ")
+ .append(count).append(" commits.\n");
+ // Add the previous message without header (i.e first line)
+ sb.append(
+ currentMessage.substring(currentMessage.indexOf('\n') + 1));
+ sb.append('\n');
+ sb.append(commentChar).append(" This is the ").append(count)
+ .append(ordinal).append(" commit message:\n");
+ sb.append(newMessage);
}
return sb.toString();
}
+ private String replaceCommentChar(String message, char oldChar,
+ char newChar) {
+ // (?m) - Switch on multi-line matching; \h - horizontal whitespace
+ return message.replaceAll("(?m)^(\\h*)" + oldChar, "$1" + newChar); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
private static String getOrdinal(int count) {
switch (count % 10) {
case 1:
@@ -886,10 +918,11 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
private void initializeSquashFixupFile(String messageFile,
String fullMessage) throws IOException {
- rebaseState
- .createFile(
- messageFile,
- "# This is a combination of 1 commits.\n# The first commit's message is:\n" + fullMessage); //$NON-NLS-1$);
+ char commentChar = commitConfig.getCommentChar(fullMessage);
+ rebaseState.createFile(messageFile,
+ commentChar + " This is a combination of 1 commits.\n" //$NON-NLS-1$
+ + commentChar + " The first commit's message is:\n" //$NON-NLS-1$
+ + fullMessage);
}
private String getOurCommitName() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
index db88ad8dc9..513f579b67 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
@@ -30,6 +30,7 @@ import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitConfig;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
@@ -185,9 +186,12 @@ public class RevertCommand extends GitCommand<RevCommit> {
MergeStatus.CONFLICTING, strategy,
merger.getMergeResults(), failingPaths, null);
if (!merger.failed() && !unmergedPaths.isEmpty()) {
+ CommitConfig config = repo.getConfig()
+ .get(CommitConfig.KEY);
+ char commentChar = config.getCommentChar(newMessage);
String message = new MergeMessageFormatter()
.formatWithConflicts(newMessage,
- merger.getUnmergedPaths(), '#');
+ merger.getUnmergedPaths(), commentChar);
repo.writeRevertHead(srcCommit.getId());
repo.writeMergeCommitMsg(message);
}
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 1a41df3d0a..64ff19c9c3 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 abstract class ContentSource {
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 abstract class ContentSource {
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 @@ public abstract class ContentSource {
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 abstract class ContentSource {
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/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index 3d50a82155..f6fc393c45 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -26,9 +26,9 @@ import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -113,7 +113,7 @@ public class DirCacheCheckout {
private Repository repo;
- private HashMap<String, CheckoutMetadata> updated = new HashMap<>();
+ private Map<String, CheckoutMetadata> updated = new LinkedHashMap<>();
private ArrayList<String> conflicts = new ArrayList<>();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java
index 535c6b9483..43dbc37f4f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 Obeo. and others
+ * Copyright (C) 2015, 2022 Obeo 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
@@ -38,6 +38,8 @@ public class PrePushHook extends GitHook<String> {
private String refs;
+ private boolean dryRun;
+
/**
* Constructor for PrePushHook
* <p>
@@ -145,6 +147,27 @@ public class PrePushHook extends GitHook<String> {
}
/**
+ * Sets whether the push is a dry run.
+ *
+ * @param dryRun
+ * {@code true} if the push is a dry run, {@code false} otherwise
+ * @since 6.2
+ */
+ public void setDryRun(boolean dryRun) {
+ this.dryRun = dryRun;
+ }
+
+ /**
+ * Tells whether the push is a dry run.
+ *
+ * @return {@code true} if the push is a dry run, {@code false} otherwise
+ * @since 6.2
+ */
+ protected boolean isDryRun() {
+ return dryRun;
+ }
+
+ /**
* Set Refs
*
* @param toRefs
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 2adde0a5d0..d4e3ccadfb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -268,6 +268,9 @@ public class JGitText extends TranslationBundle {
/***/ public String deletingNotSupported;
/***/ public String destinationIsNotAWildcard;
/***/ public String detachedHeadDetected;
+ /***/ public String diffToolNotGivenError;
+ /***/ public String diffToolNotSpecifiedInGitAttributesError;
+ /***/ public String diffToolNullError;
/***/ public String dirCacheDoesNotHaveABackingFile;
/***/ public String dirCacheFileIsNotLocked;
/***/ public String dirCacheIsNotLocked;
@@ -427,6 +430,7 @@ public class JGitText extends TranslationBundle {
/***/ public String invalidModeFor;
/***/ public String invalidModeForPath;
/***/ public String invalidNameContainsDotDot;
+ /***/ public String invalidNegativeAndForce;
/***/ public String invalidObject;
/***/ public String invalidOldIdSent;
/***/ public String invalidPacketLineHeader;
@@ -490,6 +494,8 @@ public class JGitText extends TranslationBundle {
/***/ public String mergeUsingStrategyResultedInDescription;
/***/ public String mergeRecursiveConflictsWhenMergingCommonAncestors;
/***/ public String mergeRecursiveTooManyMergeBasesFor;
+ /***/ public String mergeToolNotGivenError;
+ /***/ public String mergeToolNullError;
/***/ public String messageAndTaggerNotAllowedInUnannotatedTags;
/***/ public String minutesAgo;
/***/ public String mismatchOffset;
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 0000000000..668adeab65
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.eclipse.jgit.errors.NoWorkTreeException;
+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;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * 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<String, String> 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<String, String> envp = pb.environment();
+ if (env != null) {
+ envp.putAll(env);
+ }
+ ExecutionResult result = fs.execute(pb, null);
+ int rc = result.getRc();
+ 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 {
+ deleteCommandArray();
+ }
+ }
+
+ /**
+ * @param path
+ * the executable path
+ * @param workingDir
+ * the working directory
+ * @param env
+ * the environment
+ * @return the execution result
+ * @throws ToolException
+ * @throws InterruptedException
+ * @throws IOException
+ */
+ public boolean checkExecutable(String path, File workingDir,
+ Map<String, String> env)
+ throws ToolException, IOException, InterruptedException {
+ checkUseMsys2(path);
+ String command = null;
+ if (fs instanceof FS_Win32 && !useMsys2) {
+ Path p = Paths.get(path);
+ // Win32 (and not cygwin or MSYS2) where accepts only command / exe
+ // name as parameter
+ // so check if exists and executable in this case
+ if (p.isAbsolute() && Files.isExecutable(p)) {
+ return true;
+ }
+ // try where command for all other cases
+ command = "where " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
+ } else {
+ command = "which " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
+ }
+ boolean available = true;
+ try {
+ ExecutionResult rc = run(command, workingDir, env);
+ if (rc.getRc() != 0) {
+ available = false;
+ }
+ } catch (IOException | InterruptedException | NoWorkTreeException
+ | ToolException e) {
+ // no op: is true to not hide possible tools from user
+ }
+ return available;
+ }
+
+ 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 (!StringUtils.isEmptyOrNull(useMsys2Str)) {
+ 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/CommandLineDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java
index 509515c37a..00dec32718 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java
@@ -111,7 +111,7 @@ public enum CommandLineDiffTool {
* See: <a href=
* "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
*/
- gvimdiff("gviewdiff", "\"$LOCAL\" \"$REMOTE\""),
+ gvimdiff("gvimdiff", "\"$LOCAL\" \"$REMOTE\""),
/**
* See: <a href=
* "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
@@ -160,7 +160,7 @@ public enum CommandLineDiffTool {
* See: <a href=
* "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
*/
- vimdiff("viewdiff", gvimdiff),
+ vimdiff("vimdiff", gvimdiff),
/**
* See: <a href=
* "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
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 0000000000..3a22124328
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+/**
+ * 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
+ *
+ * <pre>
+ * 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
+ * </pre>
+ *
+ */
+@SuppressWarnings("nls")
+public enum CommandLineMergeTool {
+ /**
+ * See: <a href=
+ * "https://www.araxis.com/merge/documentation-windows/command-line.en">https://www.araxis.com/merge/documentation-windows/command-line.en</a>
+ */
+ araxis("compare",
+ "-wait -merge -3 -a1 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+ "-wait -2 \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+ false),
+ /**
+ * See: <a href=
+ * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a>
+ */
+ bc("bcomp", "\"$LOCAL\" \"$REMOTE\" \"$BASE\" --mergeoutput=\"$MERGED\"",
+ "\"$LOCAL\" \"$REMOTE\" --mergeoutput=\"$MERGED\"",
+ false),
+ /**
+ * See: <a href=
+ * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a>
+ */
+ bc3("bcompare", bc),
+ /**
+ * See: <a href=
+ * "https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm">https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm</a>
+ */
+ codecompare("CodeMerge",
+ "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -BF=\"$BASE\" -RF=\"$MERGED\"",
+ "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -RF=\"$MERGED\"",
+ false),
+ /**
+ * See: <a href=
+ * "https://www.deltawalker.com/integrate/command-line">https://www.deltawalker.com/integrate/command-line</a>
+ * <p>
+ * Hint: $(pwd) command must be defined
+ * </p>
+ */
+ deltawalker("DeltaWalker",
+ "\"$LOCAL\" \"$REMOTE\" \"$BASE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"",
+ "\"$LOCAL\" \"$REMOTE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"",
+ true),
+ /**
+ * See: <a href=
+ * "https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html">https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html</a>
+ */
+ diffmerge("diffmerge", //$NON-NLS-1$
+ "--merge --result=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"",
+ "--merge --result=\"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+ true),
+ /**
+ * See: <a href=
+ * "http://diffuse.sourceforge.net/manual.html#introduction-usage">http://diffuse.sourceforge.net/manual.html#introduction-usage</a>
+ * <p>
+ * Hint: check the ' | cat' for the call
+ * </p>
+ */
+ diffuse("diffuse", "\"$LOCAL\" \"$MERGED\" \"$REMOTE\" \"$BASE\"",
+ "\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", false),
+ /**
+ * See: <a href=
+ * "http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp">http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp</a>
+ */
+ ecmerge("ecmerge",
+ "--default --mode=merge3 \"$BASE\" \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"",
+ "--default --mode=merge2 \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"",
+ false),
+ /**
+ * See: <a href=
+ * "https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html">https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html</a>
+ * <p>
+ * Hint: $(basename) command must be defined
+ * </p>
+ */
+ emerge("emacs",
+ "-f emerge-files-with-ancestor-command \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$(basename \"$MERGED\")\"",
+ "-f emerge-files-command \"$LOCAL\" \"$REMOTE\" \"$(basename \"$MERGED\")\"",
+ true),
+ /**
+ * See: <a href=
+ * "https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options">https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options</a>
+ */
+ examdiff("ExamDiff",
+ "-merge \"$LOCAL\" \"$BASE\" \"$REMOTE\" -o:\"$MERGED\" -nh",
+ "-merge \"$LOCAL\" \"$REMOTE\" -o:\"$MERGED\" -nh",
+ false),
+ /**
+ * See: <a href=
+ * "https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html">https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html</a>
+ */
+ guiffy("guiffy", "-s \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"",
+ "-m \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true),
+ /**
+ * See: <a href=
+ * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+ */
+ gvimdiff("gvim",
+ "-f -d -c '4wincmd w | wincmd J' \"$LOCAL\" \"$BASE\" \"$REMOTE\" \"$MERGED\"",
+ "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"",
+ true),
+ /**
+ * See: <a href=
+ * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+ */
+ gvimdiff2("gvim", "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"",
+ "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", true),
+ /**
+ * See: <a href= "http://vimdoc.sourceforge.net/htmldoc/diff.html"></a>
+ */
+ gvimdiff3("gvim",
+ "-f -d -c 'hid | hid | hid' \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"",
+ "-f -d -c 'hid | hid' \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true),
+ /**
+ * See: <a href=
+ * "http://kdiff3.sourceforge.net/doc/documentation.html">http://kdiff3.sourceforge.net/doc/documentation.html</a>
+ */
+ 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: <a href=
+ * "http://meldmerge.org/help/file-mode.html">http://meldmerge.org/help/file-mode.html</a>
+ * <p>
+ * Hint: use meld with output option only (new versions)
+ * </p>
+ */
+ meld("meld", "--output=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"",
+ "\"$LOCAL\" \"$MERGED\" \"$REMOTE\"",
+ false),
+ /**
+ * See: <a href=
+ * "http://www.manpagez.com/man/1/opendiff/">http://www.manpagez.com/man/1/opendiff/</a>
+ * <p>
+ * Hint: check the ' | cat' for the call
+ * </p>
+ */
+ opendiff("opendiff",
+ "\"$LOCAL\" \"$REMOTE\" -ancestor \"$BASE\" -merge \"$MERGED\"",
+ "\"$LOCAL\" \"$REMOTE\" -merge \"$MERGED\"",
+ false),
+ /**
+ * See: <a href=
+ * "https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html">https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html</a>
+ * <p>
+ * Hint: check how to fix "no base present" / create_virtual_base problem
+ * </p>
+ */
+ p4merge("p4merge", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"",
+ "\"$REMOTE\" \"$LOCAL\" \"$MERGED\"", false),
+ /**
+ * See: <a href=
+ * "http://linux.math.tifr.res.in/manuals/man/tkdiff.html">http://linux.math.tifr.res.in/manuals/man/tkdiff.html</a>
+ */
+ tkdiff("tkdiff", "-a \"$BASE\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+ "-o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+ true),
+ /**
+ * See: <a href=
+ * "https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics">https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics</a>
+ * <p>
+ * Hint: merge without base is not supported
+ * </p>
+ * <p>
+ * Hint: cannot diff
+ * </p>
+ */
+ tortoisegitmerge("tortoisegitmerge",
+ "-base \"$BASE\" -mine \"$LOCAL\" -theirs \"$REMOTE\" -merged \"$MERGED\"",
+ null, false),
+ /**
+ * See: <a href=
+ * "https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics">https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics</a>
+ * <p>
+ * Hint: merge without base is not supported
+ * </p>
+ * <p>
+ * Hint: cannot diff
+ * </p>
+ */
+ tortoisemerge("tortoisemerge",
+ "-base:\"$BASE\" -mine:\"$LOCAL\" -theirs:\"$REMOTE\" -merged:\"$MERGED\"",
+ null, false),
+ /**
+ * See: <a href=
+ * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+ */
+ vimdiff("vim", gvimdiff),
+ /**
+ * See: <a href=
+ * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+ */
+ vimdiff2("vim", gvimdiff2),
+ /**
+ * See: <a href=
+ * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+ */
+ vimdiff3("vim", gvimdiff3),
+ /**
+ * See: <a href=
+ * "http://manual.winmerge.org/Command_line.html">http://manual.winmerge.org/Command_line.html</a>
+ * <p>
+ * Hint: check how 'mergetool_find_win32_cmd "WinMergeU.exe" "WinMerge"'
+ * works
+ * </p>
+ */
+ winmerge("WinMergeU",
+ "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+ "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+ false),
+ /**
+ * See: <a href=
+ * "http://furius.ca/xxdiff/doc/xxdiff-doc.html">http://furius.ca/xxdiff/doc/xxdiff-doc.html</a>
+ */
+ 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/DiffToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java
index 551f634f2d..c8b04f90f2 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 @@ public class DiffToolConfig {
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 39729a4eec..7cedd82995 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,6 @@
/*
- * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -10,24 +11,43 @@
package org.eclipse.jgit.internal.diffmergetool;
-import java.util.TreeMap;
+import java.io.File;
+import java.io.IOException;
import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
import java.util.Set;
+import java.util.TreeMap;
+import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.StringUtils;
/**
* Manages diff tools.
*/
public class DiffTools {
+ private final FS fs;
+
+ private final File gitDir;
+
+ private final File workTree;
+
private final DiffToolConfig config;
- private Map<String, ExternalDiffTool> predefinedTools;
+ private final Repository repo;
- private Map<String, ExternalDiffTool> userDefinedTools;
+ private final Map<String, ExternalDiffTool> predefinedTools;
+
+ private final Map<String, ExternalDiffTool> userDefinedTools;
/**
* Creates the external diff-tools manager for given repository.
@@ -36,46 +56,220 @@ public class DiffTools {
* the repository
*/
public DiffTools(Repository repo) {
- config = repo.getConfig().get(DiffToolConfig.KEY);
- setupPredefinedTools();
- setupUserDefinedTools();
+ this(repo, repo.getConfig());
+ }
+
+ /**
+ * Creates the external merge-tools manager for given configuration.
+ *
+ * @param config
+ * the git configuration
+ */
+ public DiffTools(StoredConfig config) {
+ this(null, config);
+ }
+
+ private DiffTools(Repository repo, StoredConfig config) {
+ this.repo = repo;
+ this.config = config.get(DiffToolConfig.KEY);
+ this.gitDir = repo == null ? null : repo.getDirectory();
+ this.fs = repo == null ? FS.DETECTED : repo.getFS();
+ this.workTree = repo == null ? null : repo.getWorkTree();
+ predefinedTools = setupPredefinedTools();
+ userDefinedTools = setupUserDefinedTools(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 localFile
+ * The local/left version of the file.
+ * @param remoteFile
+ * The remote/right version of the file.
* @param toolName
- * the selected tool name (can be null)
+ * Optionally the name of the tool to use. If not given the
+ * default tool will be used.
* @param prompt
- * the prompt option
+ * Optionally a flag whether to prompt the user before compare.
+ * If not given the default will be used.
* @param gui
- * the GUI option
+ * A flag whether to prefer a gui tool.
+ * @param trustExitCode
+ * Optionally a flag whether to trust the exit code of the tool.
+ * If not given the default will be used.
+ * @param promptHandler
+ * The handler to use when needing to prompt the user if he wants
+ * to continue.
+ * @param noToolHandler
+ * The handler to use when needing to inform the user, that no
+ * tool is configured.
+ * @return the optioanl result of executing the tool if it was executed
+ * @throws ToolException
+ * when the tool fails
+ */
+ public Optional<ExecutionResult> compare(FileElement localFile,
+ FileElement remoteFile, Optional<String> toolName,
+ BooleanTriState prompt, boolean gui, BooleanTriState trustExitCode,
+ PromptContinueHandler promptHandler,
+ InformNoToolHandler noToolHandler) throws ToolException {
+
+ String toolNameToUse;
+
+ if (toolName == null) {
+ throw new ToolException(JGitText.get().diffToolNullError);
+ }
+
+ if (toolName.isPresent()) {
+ toolNameToUse = toolName.get();
+ } else {
+ toolNameToUse = getDefaultToolName(gui);
+ }
+
+ if (StringUtils.isEmptyOrNull(toolNameToUse)) {
+ throw new ToolException(JGitText.get().diffToolNotGivenError);
+ }
+
+ boolean doPrompt;
+ if (prompt != BooleanTriState.UNSET) {
+ doPrompt = prompt == BooleanTriState.TRUE;
+ } else {
+ doPrompt = isInteractive();
+ }
+
+ if (doPrompt) {
+ if (!promptHandler.prompt(toolNameToUse)) {
+ return Optional.empty();
+ }
+ }
+
+ boolean trust;
+ if (trustExitCode != BooleanTriState.UNSET) {
+ trust = trustExitCode == BooleanTriState.TRUE;
+ } else {
+ trust = config.isTrustExitCode();
+ }
+
+ ExternalDiffTool tool = getTool(toolNameToUse);
+ if (tool == null) {
+ throw new ToolException(
+ "External diff tool is not defined: " + toolNameToUse); //$NON-NLS-1$
+ }
+
+ return Optional.of(
+ compare(localFile, remoteFile, tool, trust));
+ }
+
+ /**
+ * Compare two versions of a file.
+ *
+ * @param localFile
+ * the local file element
+ * @param remoteFile
+ * the remote file element
+ * @param tool
+ * the selected tool
* @param trustExitCode
* the "trust exit code" option
- * @return the return code from executed tool
+ * @return the execution result from 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(FileElement localFile,
+ FileElement remoteFile, ExternalDiffTool tool,
+ boolean trustExitCode) throws ToolException {
+ try {
+ if (tool == null) {
+ throw new ToolException(JGitText
+ .get().diffToolNotSpecifiedInGitAttributesError);
+ }
+ // prepare the command (replace the file paths)
+ String command = ExternalToolUtils.prepareCommand(tool.getCommand(),
+ localFile, remoteFile, null, null);
+ // prepare the environment
+ Map<String, String> env = ExternalToolUtils.prepareEnvironment(
+ gitDir, localFile, remoteFile, null, null);
+ // execute the tool
+ CommandExecutor cmdExec = new CommandExecutor(fs, trustExitCode);
+ return cmdExec.run(command, workTree, env);
+ } catch (IOException | InterruptedException e) {
+ throw new ToolException(e);
+ } finally {
+ localFile.cleanTemporaries();
+ remoteFile.cleanTemporaries();
+ }
}
/**
- * @return the tool names
+ * Get user defined tool names.
+ *
+ * @return the user defined tool names
*/
- public Set<String> getToolNames() {
- return config.getToolNames();
+ public Set<String> getUserDefinedToolNames() {
+ return userDefinedTools.keySet();
}
/**
+ * Get predefined tool names.
+ *
+ * @return the predefined tool names
+ */
+ public Set<String> getPredefinedToolNames() {
+ return predefinedTools.keySet();
+ }
+
+ /**
+ * Get all tool names.
+ *
+ * @return the all tool names (default or available tool name is the first
+ * in the set)
+ */
+ public Set<String> getAllToolNames() {
+ String defaultName = getDefaultToolName(false);
+ if (defaultName == null) {
+ defaultName = getFirstAvailableTool();
+ }
+ return ExternalToolUtils.createSortedToolSet(defaultName,
+ getUserDefinedToolNames(), getPredefinedToolNames());
+ }
+
+ /**
+ * Provides {@link Optional} with the name of an external diff tool if
+ * specified in git configuration for a path.
+ *
+ * The formed git configuration results from global rules as well as merged
+ * rules from info and worktree attributes.
+ *
+ * Triggers {@link TreeWalk} until specified path found in the tree.
+ *
+ * @param path
+ * path to the node in repository to parse git attributes for
+ * @return name of the difftool if set
+ * @throws ToolException
+ */
+ public Optional<String> getExternalToolFromAttributes(final String path)
+ throws ToolException {
+ return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
+ ExternalToolUtils.KEY_DIFF_TOOL);
+ }
+
+ /**
+ * Checks the availability of the predefined tools in the system.
+ *
+ * @return set of predefined available tools
+ */
+ public Set<String> getPredefinedAvailableTools() {
+ Map<String, ExternalDiffTool> defTools = getPredefinedTools(true);
+ Set<String> availableTools = new LinkedHashSet<>();
+ for (Entry<String, ExternalDiffTool> elem : defTools.entrySet()) {
+ if (elem.getValue().isAvailable()) {
+ availableTools.add(elem.getKey());
+ }
+ }
+ return availableTools;
+ }
+
+ /**
+ * Get user defined tools map.
+ *
* @return the user defined tools
*/
public Map<String, ExternalDiffTool> getUserDefinedTools() {
@@ -83,61 +277,106 @@ public class DiffTools {
}
/**
- * @return the available predefined tools
+ * Get predefined tools map.
+ *
+ * @param checkAvailability
+ * true: for checking if tools can be executed; ATTENTION: this
+ * check took some time, do not execute often (store the map for
+ * other actions); false: availability is NOT checked:
+ * isAvailable() returns default false is this case!
+ * @return the predefined tools with optionally checked availability (long
+ * running operation)
*/
- public Map<String, ExternalDiffTool> getAvailableTools() {
+ public Map<String, ExternalDiffTool> getPredefinedTools(
+ boolean checkAvailability) {
+ if (checkAvailability) {
+ for (ExternalDiffTool tool : predefinedTools.values()) {
+ PreDefinedDiffTool predefTool = (PreDefinedDiffTool) tool;
+ predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
+ gitDir, workTree, predefTool.getPath()));
+ }
+ }
return Collections.unmodifiableMap(predefinedTools);
}
/**
- * @return the NOT available predefined tools
+ * Get first available tool name.
+ *
+ * @return the name of first available predefined tool or null
*/
- public Map<String, ExternalDiffTool> getNotAvailableTools() {
- return Collections.unmodifiableMap(new TreeMap<>());
+ public String getFirstAvailableTool() {
+ for (ExternalDiffTool tool : predefinedTools.values()) {
+ if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
+ tool.getPath())) {
+ return tool.getName();
+ }
+ }
+ return null;
}
/**
+ * Get default (gui-)tool name.
+ *
* @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$
+ public String getDefaultToolName(boolean gui) {
+ String guiToolName;
+ if (gui) {
+ guiToolName = config.getDefaultGuiToolName();
+ if (guiToolName != null) {
+ return guiToolName;
+ }
+ }
+ return config.getDefaultToolName();
}
/**
+ * Is interactive diff (prompt enabled) ?
+ *
* @return is interactive (config prompt enabled) ?
*/
public boolean isInteractive() {
- return false;
+ return config.isPrompt();
+ }
+
+ private ExternalDiffTool getTool(final String name) {
+ ExternalDiffTool tool = userDefinedTools.get(name);
+ if (tool == null) {
+ tool = predefinedTools.get(name);
+ }
+ return tool;
}
- private void setupPredefinedTools() {
- predefinedTools = new TreeMap<>();
+ private static Map<String, ExternalDiffTool> setupPredefinedTools() {
+ Map<String, ExternalDiffTool> tools = new TreeMap<>();
for (CommandLineDiffTool tool : CommandLineDiffTool.values()) {
- predefinedTools.put(tool.name(), new PreDefinedDiffTool(tool));
+ tools.put(tool.name(), new PreDefinedDiffTool(tool));
}
+ return tools;
}
- private void setupUserDefinedTools() {
- userDefinedTools = new TreeMap<>();
+ private Map<String, ExternalDiffTool> setupUserDefinedTools(
+ Map<String, ExternalDiffTool> predefTools) {
+ Map<String, ExternalDiffTool> tools = new TreeMap<>();
Map<String, ExternalDiffTool> userTools = config.getTools();
for (String name : userTools.keySet()) {
ExternalDiffTool userTool = userTools.get(name);
// if difftool.<name>.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.<name>.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/ExternalDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java
index f2d7e828cb..e01b892a53 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java
@@ -30,4 +30,10 @@ public interface ExternalDiffTool {
*/
String getCommand();
+ /**
+ * @return availability of the tool: true if tool can be executed and false
+ * if not
+ */
+ boolean isAvailable();
+
}
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 0000000000..0c3ddf9afe
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+
+/**
+ * The merge tool interface.
+ */
+public interface ExternalMergeTool extends ExternalDiffTool {
+
+ /**
+ * @return the tool "trust exit code" option
+ */
+ 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/ExternalToolUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java
new file mode 100644
index 0000000000..b2dd846d70
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.util.TreeMap;
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.eclipse.jgit.attributes.Attributes;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
+import org.eclipse.jgit.util.FS;
+
+/**
+ * Utilities for diff- and merge-tools.
+ */
+public class ExternalToolUtils {
+
+ /**
+ * Key for merge tool git configuration section
+ */
+ public static final String KEY_MERGE_TOOL = "mergetool"; //$NON-NLS-1$
+
+ /**
+ * Key for diff tool git configuration section
+ */
+ public static final String KEY_DIFF_TOOL = "difftool"; //$NON-NLS-1$
+
+ /**
+ * 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 {
+ if (localFile != null) {
+ command = localFile.replaceVariable(command);
+ }
+ if (remoteFile != null) {
+ command = remoteFile.replaceVariable(command);
+ }
+ if (mergedFile != null) {
+ command = mergedFile.replaceVariable(command);
+ }
+ if (baseFile != null) {
+ command = baseFile.replaceVariable(command);
+ }
+ return command;
+ }
+
+ /**
+ * Prepare environment needed for execution.
+ *
+ * @param gitDir
+ * the .git directory
+ * @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<String, String> prepareEnvironment(File gitDir,
+ FileElement localFile, FileElement remoteFile,
+ FileElement mergedFile, FileElement baseFile) throws IOException {
+ Map<String, String> env = new TreeMap<>();
+ if (gitDir != null) {
+ env.put(Constants.GIT_DIR_KEY, gitDir.getAbsolutePath());
+ }
+ if (localFile != null) {
+ localFile.addToEnv(env);
+ }
+ if (remoteFile != null) {
+ remoteFile.addToEnv(env);
+ }
+ if (mergedFile != null) {
+ mergedFile.addToEnv(env);
+ }
+ if (baseFile != null) {
+ baseFile.addToEnv(env);
+ }
+ return env;
+ }
+
+ /**
+ * @param path
+ * the path to be quoted
+ * @return quoted path if it contains spaces
+ */
+ @SuppressWarnings("nls")
+ public static String quotePath(String path) {
+ // handling of spaces in path
+ if (path.contains(" ")) {
+ // add quotes before if needed
+ if (!path.startsWith("\"")) {
+ path = "\"" + path;
+ }
+ // add quotes after if needed
+ if (!path.endsWith("\"")) {
+ path = path + "\"";
+ }
+ }
+ return path;
+ }
+
+ /**
+ * @param fs
+ * the file system abstraction
+ * @param gitDir
+ * the .git directory
+ * @param directory
+ * the working directory
+ * @param path
+ * the tool path
+ * @return true if tool available and false otherwise
+ */
+ public static boolean isToolAvailable(FS fs, File gitDir, File directory,
+ String path) {
+ boolean available = true;
+ try {
+ CommandExecutor cmdExec = new CommandExecutor(fs, false);
+ available = cmdExec.checkExecutable(path, directory,
+ prepareEnvironment(gitDir, null, null, null, null));
+ } catch (Exception e) {
+ available = false;
+ }
+ return available;
+ }
+
+ /**
+ * @param defaultName
+ * the default tool name
+ * @param userDefinedNames
+ * the user defined tool names
+ * @param preDefinedNames
+ * the pre defined tool names
+ * @return the sorted tool names set: first element is default tool name if
+ * valid, then user defined tool names and then pre defined tool
+ * names
+ */
+ public static Set<String> createSortedToolSet(String defaultName,
+ Set<String> userDefinedNames, Set<String> preDefinedNames) {
+ Set<String> names = new LinkedHashSet<>();
+ if (defaultName != null) {
+ // remove defaultName from both sets
+ Set<String> namesPredef = new LinkedHashSet<>();
+ Set<String> namesUser = new LinkedHashSet<>();
+ namesUser.addAll(userDefinedNames);
+ namesUser.remove(defaultName);
+ namesPredef.addAll(preDefinedNames);
+ namesPredef.remove(defaultName);
+ // add defaultName as first in set
+ names.add(defaultName);
+ names.addAll(namesUser);
+ names.addAll(namesPredef);
+ } else {
+ names.addAll(userDefinedNames);
+ names.addAll(preDefinedNames);
+ }
+ return names;
+ }
+
+ /**
+ * Provides {@link Optional} with the name of an external tool if specified
+ * in git configuration for a path.
+ *
+ * The formed git configuration results from global rules as well as merged
+ * rules from info and worktree attributes.
+ *
+ * Triggers {@link TreeWalk} until specified path found in the tree.
+ *
+ * @param repository
+ * target repository to traverse into
+ * @param path
+ * path to the node in repository to parse git attributes for
+ * @param toolKey
+ * config key name for the tool
+ * @return attribute value for the given tool key if set
+ * @throws ToolException
+ */
+ public static Optional<String> getExternalToolFromAttributes(
+ final Repository repository, final String path,
+ final String toolKey) throws ToolException {
+ try {
+ WorkingTreeIterator treeIterator = new FileTreeIterator(repository);
+ try (TreeWalk walk = new TreeWalk(repository)) {
+ walk.addTree(treeIterator);
+ walk.setFilter(new NotIgnoredFilter(0));
+ while (walk.next()) {
+ String treePath = walk.getPathString();
+ if (treePath.equals(path)) {
+ Attributes attrs = walk.getAttributes();
+ if (attrs.containsKey(toolKey)) {
+ return Optional.of(attrs.getValue(toolKey));
+ }
+ }
+ if (walk.isSubtree()) {
+ walk.enterSubtree();
+ }
+ }
+ // no external tool specified
+ return Optional.empty();
+ }
+
+ } catch (RevisionSyntaxException | IOException e) {
+ throw new ToolException(e);
+ }
+ }
+
+}
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 0000000000..ba8ca54c58
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.io.File;
+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;
+
+/**
+ * The element used as left or right file for compare.
+ *
+ */
+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 Type type;
+
+ private final File workDir;
+
+ private InputStream stream;
+
+ private File tempFile;
+
+ /**
+ * Creates file element for path.
+ *
+ * @param path
+ * the file path
+ * @param type
+ * the element type
+ */
+ public FileElement(String path, Type type) {
+ this(path, type, null);
+ }
+
+ /**
+ * Creates file element for path.
+ *
+ * @param path
+ * the file path
+ * @param type
+ * the element type
+ * @param workDir
+ * the working directory of the path (can be null, then current
+ * working dir is used)
+ */
+ public FileElement(String path, Type type, File workDir) {
+ this(path, type, workDir, null);
+ }
+
+ /**
+ * @param path
+ * the file path
+ * @param type
+ * the element type
+ * @param workDir
+ * the working directory of the path (can be null, then current
+ * working dir is used)
+ * @param stream
+ * the object stream to load and write on demand, @see getFile(),
+ * to tempFile once (can be null)
+ */
+ public FileElement(String path, Type type, File workDir,
+ InputStream stream) {
+ this.path = path;
+ this.type = type;
+ this.workDir = workDir;
+ this.stream = stream;
+ }
+
+ /**
+ * @return the file path
+ */
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * @return the element type
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Return
+ * <ul>
+ * <li>a temporary file if already created and stream is not valid</li>
+ * <li>OR a real file from work tree: if no temp file was created (@see
+ * createTempFile()) and if no stream was set</li>
+ * <li>OR an empty temporary file if path is "/dev/null"</li>
+ * <li>OR a temporary file with stream content if stream is valid (not
+ * null); stream is closed and invalidated (set to null) after write to temp
+ * file, so stream is used only once during first call!</li>
+ * </ul>
+ *
+ * @return the object stream
+ * @throws IOException
+ */
+ public File getFile() throws IOException {
+ // if we have already temp file and no stream
+ // then just return this temp file (it was filled from outside)
+ if ((tempFile != null) && (stream == null)) {
+ return tempFile;
+ }
+ File file = new File(workDir, path);
+ // if we have a stream or file is missing (path is "/dev/null")
+ // then optionally create temporary file and fill it with stream content
+ if ((stream != null) || isNullPath()) {
+ if (tempFile == null) {
+ tempFile = getTempFile(file, type.name(), null);
+ }
+ if (stream != null) {
+ copyFromStream(tempFile, stream);
+ }
+ // invalidate the stream, because it is used once
+ stream = null;
+ return tempFile;
+ }
+ return file;
+ }
+
+ /**
+ * 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) {
+ tempFile = getTempFile(new File(path), type.name(), directory);
+ }
+ return tempFile;
+ }
+
+ /**
+ * Delete and invalidate temporary file if necessary.
+ */
+ public void cleanTemporaries() {
+ if (tempFile != null && tempFile.exists()) {
+ tempFile.delete();
+ }
+ tempFile = null;
+ }
+
+ /**
+ * 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<String, String> env) throws IOException {
+ env.put(type.name(), getFile().getPath());
+ }
+
+ private static File getTempFile(final File file, final String midName,
+ final File workingDir) throws IOException {
+ String[] fileNameAndExtension = splitBaseFileNameAndExtension(file);
+ // TODO: avoid long random file name (number generated by
+ // createTempFile)
+ return File.createTempFile(
+ fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
+ fileNameAndExtension[1], workingDir);
+ }
+
+ private static void copyFromStream(final File file,
+ final InputStream stream)
+ throws IOException, FileNotFoundException {
+ try (OutputStream outStream = new FileOutputStream(file)) {
+ 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 and invalidate
+ stream.close();
+ }
+ }
+
+ private static String[] splitBaseFileNameAndExtension(File file) {
+ String[] result = new String[2];
+ result[0] = file.getName();
+ result[1] = ""; //$NON-NLS-1$
+ 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;
+ }
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java
new file mode 100644
index 0000000000..36b290d37d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018-2019, Tim Neumann <Tim.Neumann@advantest.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.util.List;
+
+/**
+ * A handler for when the diff/merge tool manager wants to inform the user that
+ * no tool has been configured and one of the default tools will be used.
+ */
+public interface InformNoToolHandler {
+ /**
+ * Inform the user, that no tool is configured and that one of the given
+ * tools is used.
+ *
+ * @param toolNames
+ * The tools which are tried
+ */
+ void inform(List<String> toolNames);
+}
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 0000000000..9625d5f101
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import 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.internal.BooleanTriState;
+
+/**
+ * Keeps track of merge tool related configuration options.
+ */
+public class MergeToolConfig {
+
+ /** Key for {@link Config#get(SectionParser)}. */
+ public static final Config.SectionParser<MergeToolConfig> 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<String, ExternalMergeTool> tools;
+
+ private MergeToolConfig(Config rc) {
+ 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<String> subsections = rc.getSubsections(CONFIG_MERGETOOL_SECTION);
+ for (String name : subsections) {
+ 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(CONFIG_MERGETOOL_SECTION, name,
+ 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<String, ExternalMergeTool> getTools() {
+ return tools;
+ }
+
+ /**
+ * @return the tool names
+ */
+ public Set<String> 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 0000000000..b903201264
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.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.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+
+/**
+ * Manages merge tools.
+ */
+public class MergeTools {
+
+ private final FS fs;
+
+ private final File gitDir;
+
+ private final File workTree;
+
+ private final MergeToolConfig config;
+
+ private final Repository repo;
+
+ private final Map<String, ExternalMergeTool> predefinedTools;
+
+ private final Map<String, ExternalMergeTool> userDefinedTools;
+
+ /**
+ * Creates the external merge-tools manager for given repository.
+ *
+ * @param repo
+ * the repository
+ */
+ public MergeTools(Repository repo) {
+ this(repo, repo.getConfig());
+ }
+
+ /**
+ * Creates the external diff-tools manager for given configuration.
+ *
+ * @param config
+ * the git configuration
+ */
+ public MergeTools(StoredConfig config) {
+ this(null, config);
+ }
+
+ private MergeTools(Repository repo, StoredConfig config) {
+ this.repo = repo;
+ this.config = config.get(MergeToolConfig.KEY);
+ this.gitDir = repo == null ? null : repo.getDirectory();
+ this.fs = repo == null ? FS.DETECTED : repo.getFS();
+ this.workTree = repo == null ? null : repo.getWorkTree();
+ predefinedTools = setupPredefinedTools();
+ userDefinedTools = setupUserDefinedTools(predefinedTools);
+ }
+
+ /**
+ * Merge two versions of a file with optional base file.
+ *
+ * @param localFile
+ * The local/left version of the file.
+ * @param remoteFile
+ * The remote/right version of the file.
+ * @param mergedFile
+ * The file for the result.
+ * @param baseFile
+ * The base version of the file. May be null.
+ * @param tempDir
+ * The tmepDir used for the files. May be null.
+ * @param toolName
+ * Optionally the name of the tool to use. If not given the
+ * default tool will be used.
+ * @param prompt
+ * Optionally a flag whether to prompt the user before compare.
+ * If not given the default will be used.
+ * @param gui
+ * A flag whether to prefer a gui tool.
+ * @param promptHandler
+ * The handler to use when needing to prompt the user if he wants
+ * to continue.
+ * @param noToolHandler
+ * The handler to use when needing to inform the user, that no
+ * tool is configured.
+ * @return the optional result of executing the tool if it was executed
+ * @throws ToolException
+ * when the tool fails
+ */
+ public Optional<ExecutionResult> merge(FileElement localFile,
+ FileElement remoteFile, FileElement mergedFile,
+ FileElement baseFile, File tempDir, Optional<String> toolName,
+ BooleanTriState prompt, boolean gui,
+ PromptContinueHandler promptHandler,
+ InformNoToolHandler noToolHandler) throws ToolException {
+
+ String toolNameToUse;
+
+ if (toolName == null) {
+ throw new ToolException(JGitText.get().diffToolNullError);
+ }
+
+ if (toolName.isPresent()) {
+ toolNameToUse = toolName.get();
+ } else {
+ toolNameToUse = getDefaultToolName(gui);
+
+ if (StringUtils.isEmptyOrNull(toolNameToUse)) {
+ noToolHandler.inform(new ArrayList<>(predefinedTools.keySet()));
+ toolNameToUse = getFirstAvailableTool();
+ }
+ }
+
+ if (StringUtils.isEmptyOrNull(toolNameToUse)) {
+ throw new ToolException(JGitText.get().diffToolNotGivenError);
+ }
+
+ boolean doPrompt;
+ if (prompt != BooleanTriState.UNSET) {
+ doPrompt = prompt == BooleanTriState.TRUE;
+ } else {
+ doPrompt = isInteractive();
+ }
+
+ if (doPrompt) {
+ if (!promptHandler.prompt(toolNameToUse)) {
+ return Optional.empty();
+ }
+ }
+
+ ExternalMergeTool tool = getTool(toolNameToUse);
+ if (tool == null) {
+ throw new ToolException(
+ "External merge tool is not defined: " + toolNameToUse); //$NON-NLS-1$
+ }
+
+ return Optional.of(merge(localFile, remoteFile, mergedFile, baseFile,
+ tempDir, tool));
+ }
+
+ /**
+ * Merge two versions of a file with optional base file.
+ *
+ * @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 tempDir
+ * the temporary directory (needed for backup and auto-remove,
+ * can be null)
+ * @param tool
+ * the selected tool
+ * @return the execution result from tool
+ * @throws ToolException
+ */
+ public ExecutionResult merge(FileElement localFile, FileElement remoteFile,
+ FileElement mergedFile, FileElement baseFile, File tempDir,
+ ExternalMergeTool tool) throws ToolException {
+ FileElement backup = null;
+ ExecutionResult result = null;
+ try {
+ // create additional backup file (copy worktree file)
+ backup = createBackupFile(mergedFile,
+ tempDir != null ? tempDir : workTree);
+ // prepare the command (replace the file paths)
+ String command = ExternalToolUtils.prepareCommand(
+ tool.getCommand(baseFile != null), localFile, remoteFile,
+ mergedFile, baseFile);
+ // prepare the environment
+ Map<String, String> env = ExternalToolUtils.prepareEnvironment(
+ gitDir, localFile, remoteFile, mergedFile, baseFile);
+ boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
+ // execute the tool
+ CommandExecutor cmdExec = new CommandExecutor(fs, trust);
+ result = cmdExec.run(command, workTree, env);
+ // keep backup as .orig file
+ if (backup != null) {
+ keepBackupFile(mergedFile.getPath(), backup);
+ }
+ return result;
+ } catch (IOException | InterruptedException e) {
+ throw new ToolException(e);
+ } finally {
+ // 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 FileElement createBackupFile(FileElement from, File toParentDir)
+ throws IOException {
+ FileElement backup = null;
+ Path path = Paths.get(from.getPath());
+ if (Files.exists(path)) {
+ backup = new FileElement(from.getPath(), Type.BACKUP);
+ Files.copy(path, backup.createTempFile(toParentDir).toPath(),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+ return backup;
+ }
+
+ /**
+ * Create temporary directory.
+ *
+ * @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;
+ }
+
+ /**
+ * Get user defined tool names.
+ *
+ * @return the user defined tool names
+ */
+ public Set<String> getUserDefinedToolNames() {
+ return userDefinedTools.keySet();
+ }
+
+ /**
+ * @return the predefined tool names
+ */
+ public Set<String> getPredefinedToolNames() {
+ return predefinedTools.keySet();
+ }
+
+ /**
+ * Get all tool names.
+ *
+ * @return the all tool names (default or available tool name is the first
+ * in the set)
+ */
+ public Set<String> getAllToolNames() {
+ String defaultName = getDefaultToolName(false);
+ if (defaultName == null) {
+ defaultName = getFirstAvailableTool();
+ }
+ return ExternalToolUtils.createSortedToolSet(defaultName,
+ getUserDefinedToolNames(), getPredefinedToolNames());
+ }
+
+ /**
+ * Provides {@link Optional} with the name of an external merge tool if
+ * specified in git configuration for a path.
+ *
+ * The formed git configuration results from global rules as well as merged
+ * rules from info and worktree attributes.
+ *
+ * Triggers {@link TreeWalk} until specified path found in the tree.
+ *
+ * @param path
+ * path to the node in repository to parse git attributes for
+ * @return name of the difftool if set
+ * @throws ToolException
+ */
+ public Optional<String> getExternalToolFromAttributes(final String path)
+ throws ToolException {
+ return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
+ ExternalToolUtils.KEY_MERGE_TOOL);
+ }
+
+ /**
+ * Checks the availability of the predefined tools in the system.
+ *
+ * @return set of predefined available tools
+ */
+ public Set<String> getPredefinedAvailableTools() {
+ Map<String, ExternalMergeTool> defTools = getPredefinedTools(true);
+ Set<String> availableTools = new LinkedHashSet<>();
+ for (Entry<String, ExternalMergeTool> elem : defTools.entrySet()) {
+ if (elem.getValue().isAvailable()) {
+ availableTools.add(elem.getKey());
+ }
+ }
+ return availableTools;
+ }
+
+ /**
+ * @return the user defined tools
+ */
+ public Map<String, ExternalMergeTool> getUserDefinedTools() {
+ return Collections.unmodifiableMap(userDefinedTools);
+ }
+
+ /**
+ * Get predefined tools map.
+ *
+ * @param checkAvailability
+ * true: for checking if tools can be executed; ATTENTION: this
+ * check took some time, do not execute often (store the map for
+ * other actions); false: availability is NOT checked:
+ * isAvailable() returns default false is this case!
+ * @return the predefined tools with optionally checked availability (long
+ * running operation)
+ */
+ public Map<String, ExternalMergeTool> getPredefinedTools(
+ boolean checkAvailability) {
+ if (checkAvailability) {
+ for (ExternalMergeTool tool : predefinedTools.values()) {
+ PreDefinedMergeTool predefTool = (PreDefinedMergeTool) tool;
+ predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
+ gitDir, workTree, predefTool.getPath()));
+ }
+ }
+ return Collections.unmodifiableMap(predefinedTools);
+ }
+
+ /**
+ * Get first available tool name.
+ *
+ * @return the name of first available predefined tool or null
+ */
+ public String getFirstAvailableTool() {
+ String name = null;
+ for (ExternalMergeTool tool : predefinedTools.values()) {
+ if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
+ tool.getPath())) {
+ name = tool.getName();
+ break;
+ }
+ }
+ return name;
+ }
+
+ /**
+ * Is interactive merge (prompt enabled) ?
+ *
+ * @return is interactive (config prompt enabled) ?
+ */
+ public boolean isInteractive() {
+ return config.isPrompt();
+ }
+
+ /**
+ * Get the default (gui-)tool name.
+ *
+ * @param gui
+ * use the diff.guitool setting ?
+ * @return the default tool name
+ */
+ public String getDefaultToolName(boolean gui) {
+ return gui ? config.getDefaultGuiToolName()
+ : config.getDefaultToolName();
+ }
+
+ private ExternalMergeTool getTool(final String name) {
+ ExternalMergeTool tool = userDefinedTools.get(name);
+ if (tool == null) {
+ tool = predefinedTools.get(name);
+ }
+ return tool;
+ }
+
+ 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<String, ExternalMergeTool> setupPredefinedTools() {
+ Map<String, ExternalMergeTool> tools = new TreeMap<>();
+ for (CommandLineMergeTool tool : CommandLineMergeTool.values()) {
+ tools.put(tool.name(), new PreDefinedMergeTool(tool));
+ }
+ return tools;
+ }
+
+ private Map<String, ExternalMergeTool> setupUserDefinedTools(
+ Map<String, ExternalMergeTool> predefTools) {
+ Map<String, ExternalMergeTool> tools = new TreeMap<>();
+ Map<String, ExternalMergeTool> userTools = config.getTools();
+ for (String name : userTools.keySet()) {
+ ExternalMergeTool userTool = userTools.get(name);
+ // if mergetool.<name>.cmd is defined we have user defined tool
+ if (userTool.getCommand() != null) {
+ tools.put(name, userTool);
+ } else if (userTool.getPath() != null) {
+ // if mergetool.<name>.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/PreDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java
index 1c69fb4911..e1169a2d60 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 class PreDefinedDiffTool extends UserDefinedDiffTool {
*/
@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);
}
@@ -67,7 +56,7 @@ public class PreDefinedDiffTool extends UserDefinedDiffTool {
*/
@Override
public String getCommand() {
- return getPath() + " " + super.getCommand(); //$NON-NLS-1$
+ return ExternalToolUtils.quotePath(getPath()) + " " + super.getCommand(); //$NON-NLS-1$
}
}
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 0000000000..7b28d32820
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import 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 ExternalToolUtils.quotePath(getPath()) + " " //$NON-NLS-1$
+ + (withBase ? super.getCommand() : parametersWithoutBase);
+ }
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java
new file mode 100644
index 0000000000..6ad33df2a0
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018-2019, Tim Neumann <Tim.Neumann@advantest.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+/**
+ * A handler for when the diff/merge tool manager wants to prompt the user
+ * whether to continue
+ */
+public interface PromptContinueHandler {
+ /**
+ * Prompt the user whether to continue with the next file by opening a given
+ * tool.
+ *
+ * @param toolName
+ * The name of the tool to open
+ * @return Whether the user wants to continue
+ */
+ boolean prompt(String toolName);
+}
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 0000000000..7cc5bb50d9
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import 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;
+
+ private final boolean commandExecutionError;
+
+ /**
+ * the serial version UID
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ */
+ public ToolException() {
+ this(null, null, false);
+ }
+
+ /**
+ * @param message
+ * the exception message
+ */
+ public ToolException(String message) {
+ this(message, null, false);
+ }
+
+ /**
+ * @param message
+ * the exception message
+ * @param result
+ * the execution result
+ * @param commandExecutionError
+ * is command execution error happened ?
+ */
+ public ToolException(String message, ExecutionResult result,
+ boolean commandExecutionError) {
+ super(message);
+ this.result = result;
+ this.commandExecutionError = commandExecutionError;
+ }
+
+ /**
+ * @param message
+ * the exception message
+ * @param cause
+ * the cause for throw
+ */
+ public ToolException(String message, Throwable cause) {
+ super(message, cause);
+ result = null;
+ commandExecutionError = false;
+ }
+
+ /**
+ * @param cause
+ * the cause for throw
+ */
+ public ToolException(Throwable cause) {
+ super(cause);
+ result = null;
+ commandExecutionError = false;
+ }
+
+ /**
+ * @return true if result is valid, false else
+ */
+ public boolean isResult() {
+ return result != null;
+ }
+
+ /**
+ * @return the execution result
+ */
+ public ExecutionResult getResult() {
+ return result;
+ }
+
+ /**
+ * @return true if command execution error appears, false otherwise
+ */
+ public boolean isCommandExecutionError() {
+ return commandExecutionError;
+ }
+
+ /**
+ * @return the result Stderr
+ */
+ public String getResultStderr() {
+ if (result == null) {
+ return ""; //$NON-NLS-1$
+ }
+ try {
+ return new String(result.getStderr().toByteArray());
+ } catch (Exception e) {
+ LOG.warn("Failed to retrieve standard error output", e); //$NON-NLS-1$
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+ /**
+ * @return the result Stdout
+ */
+ public String getResultStdout() {
+ if (result == null) {
+ return ""; //$NON-NLS-1$
+ }
+ try {
+ return new String(result.getStdout().toByteArray());
+ } catch (Exception e) {
+ LOG.warn("Failed to retrieve standard output", e); //$NON-NLS-1$
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java
index 012296eb35..eb72d01cdb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java
@@ -15,6 +15,8 @@ package org.eclipse.jgit.internal.diffmergetool;
*/
public class UserDefinedDiffTool implements ExternalDiffTool {
+ private boolean available;
+
/**
* the diff tool name
*/
@@ -99,6 +101,23 @@ public class UserDefinedDiffTool implements ExternalDiffTool {
}
/**
+ * @return availability of the tool: true if tool can be executed and false
+ * if not
+ */
+ @Override
+ public boolean isAvailable() {
+ return available;
+ }
+
+ /**
+ * @param available
+ * true if tool can be found and false if not
+ */
+ public void setAvailable(boolean available) {
+ this.available = available;
+ }
+
+ /**
* Overrides the path for the given tool. Equivalent to setting
* {@code difftool.<tool>.path}.
*
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 0000000000..1dd2f0d793
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import 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 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 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/internal/storage/file/PackFileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java
index 17bd863528..a784af8c3f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java
@@ -15,6 +15,7 @@ import java.io.RandomAccessFile;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.Equality;
class PackFileSnapshot extends FileSnapshot {
@@ -61,7 +62,8 @@ class PackFileSnapshot extends FileSnapshot {
}
boolean isChecksumChanged(File packFile) {
- return wasChecksumChanged = checksum != MISSING_CHECKSUM
+ return wasChecksumChanged = !Equality.isSameInstance(checksum,
+ MISSING_CHECKSUM)
&& !checksum.equals(readChecksum(packFile));
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
index c7322b17bb..aa910d845c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
@@ -938,38 +938,27 @@ public class RefDirectory extends RefDatabase {
}
private PackedRefList readPackedRefs() throws IOException {
- int maxStaleRetries = 5;
- int retries = 0;
- while (true) {
- final FileSnapshot snapshot = FileSnapshot.save(packedRefsFile);
- final MessageDigest digest = Constants.newMessageDigest();
- try (BufferedReader br = new BufferedReader(new InputStreamReader(
- new DigestInputStream(new FileInputStream(packedRefsFile),
- digest),
- UTF_8))) {
- try {
- return new PackedRefList(parsePackedRefs(br), snapshot,
- ObjectId.fromRaw(digest.digest()));
- } catch (IOException e) {
- if (FileUtils.isStaleFileHandleInCausalChain(e)
- && retries < maxStaleRetries) {
- if (LOG.isDebugEnabled()) {
- LOG.debug(MessageFormat.format(
- JGitText.get().packedRefsHandleIsStale,
- Integer.valueOf(retries)), e);
+ try {
+ PackedRefList result = FileUtils.readWithRetries(packedRefsFile,
+ f -> {
+ FileSnapshot snapshot = FileSnapshot.save(f);
+ MessageDigest digest = Constants.newMessageDigest();
+ try (BufferedReader br = new BufferedReader(
+ new InputStreamReader(
+ new DigestInputStream(
+ new FileInputStream(f), digest),
+ UTF_8))) {
+ return new PackedRefList(parsePackedRefs(br),
+ snapshot,
+ ObjectId.fromRaw(digest.digest()));
}
- retries++;
- continue;
- }
- throw e;
- }
- } catch (FileNotFoundException noPackedRefs) {
- if (packedRefsFile.exists()) {
- throw noPackedRefs;
- }
- // Ignore it and leave the new list empty.
- return NO_PACKED_REFS;
- }
+ });
+ return result != null ? result : NO_PACKED_REFS;
+ } catch (IOException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IOException(MessageFormat
+ .format(JGitText.get().cannotReadFile, packedRefsFile), e);
}
}
@@ -1136,40 +1125,55 @@ public class RefDirectory extends RefDatabase {
}
final int limit = 4096;
- final byte[] buf;
- FileSnapshot otherSnapshot = FileSnapshot.save(path);
- try {
- buf = IO.readSome(path, limit);
- } catch (FileNotFoundException noFile) {
- if (path.isFile()) {
- throw noFile;
+
+ class LooseItems {
+ final FileSnapshot snapshot;
+
+ final byte[] buf;
+
+ LooseItems(FileSnapshot snapshot, byte[] buf) {
+ this.snapshot = snapshot;
+ this.buf = buf;
}
- return null; // doesn't exist or no file; not a reference.
}
-
- int n = buf.length;
+ LooseItems loose = null;
+ try {
+ loose = FileUtils.readWithRetries(path,
+ f -> new LooseItems(FileSnapshot.save(f),
+ IO.readSome(f, limit)));
+ } catch (IOException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IOException(
+ MessageFormat.format(JGitText.get().cannotReadFile, path),
+ e);
+ }
+ if (loose == null) {
+ return null;
+ }
+ int n = loose.buf.length;
if (n == 0)
return null; // empty file; not a reference.
- if (isSymRef(buf, n)) {
+ if (isSymRef(loose.buf, n)) {
if (n == limit)
return null; // possibly truncated ref
// trim trailing whitespace
- while (0 < n && Character.isWhitespace(buf[n - 1]))
+ while (0 < n && Character.isWhitespace(loose.buf[n - 1]))
n--;
if (n < 6) {
- String content = RawParseUtils.decode(buf, 0, n);
+ String content = RawParseUtils.decode(loose.buf, 0, n);
throw new IOException(MessageFormat.format(JGitText.get().notARef, name, content));
}
- final String target = RawParseUtils.decode(buf, 5, n);
+ final String target = RawParseUtils.decode(loose.buf, 5, n);
if (ref != null && ref.isSymbolic()
&& ref.getTarget().getName().equals(target)) {
assert(currentSnapshot != null);
- currentSnapshot.setClean(otherSnapshot);
+ currentSnapshot.setClean(loose.snapshot);
return ref;
}
- return newSymbolicRef(otherSnapshot, name, target);
+ return newSymbolicRef(loose.snapshot, name, target);
}
if (n < OBJECT_ID_STRING_LENGTH)
@@ -1177,23 +1181,23 @@ public class RefDirectory extends RefDatabase {
final ObjectId id;
try {
- id = ObjectId.fromString(buf, 0);
+ id = ObjectId.fromString(loose.buf, 0);
if (ref != null && !ref.isSymbolic()
&& id.equals(ref.getTarget().getObjectId())) {
assert(currentSnapshot != null);
- currentSnapshot.setClean(otherSnapshot);
+ currentSnapshot.setClean(loose.snapshot);
return ref;
}
} catch (IllegalArgumentException notRef) {
- while (0 < n && Character.isWhitespace(buf[n - 1]))
+ while (0 < n && Character.isWhitespace(loose.buf[n - 1]))
n--;
- String content = RawParseUtils.decode(buf, 0, n);
+ String content = RawParseUtils.decode(loose.buf, 0, n);
throw new IOException(MessageFormat.format(JGitText.get().notARef,
name, content), notRef);
}
- return new LooseUnpeeled(otherSnapshot, name, id);
+ return new LooseUnpeeled(loose.snapshot, name, id);
}
private static boolean isSymRef(byte[] buf, int n) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java
index 1c24aff12d..cda456c3cb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java
@@ -142,6 +142,7 @@ class BaseSearch {
return ptr;
}
+ @SuppressWarnings("ReferenceEquality")
private void add(AnyObjectId id, int objectType, int pathHash) {
ObjectToPack obj = new ObjectToPack(id, objectType);
obj.setEdge();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
index 55cc02683a..6a9b45b065 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020 Julian Ruppel <julian.ruppel@sap.com>
+ * Copyright (c) 2020, 2022 Julian Ruppel <julian.ruppel@sap.com> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -29,6 +29,7 @@ import org.eclipse.jgit.lib.Config.SectionParser;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
/**
* The standard "commit" configuration parameters.
@@ -44,6 +45,9 @@ public class CommitConfig {
private static final String CUT = " ------------------------ >8 ------------------------\n"; //$NON-NLS-1$
+ private static final char[] COMMENT_CHARS = { '#', ';', '@', '!', '$', '%',
+ '^', '&', '|', ':' };
+
/**
* How to clean up commit messages when committing.
*
@@ -99,6 +103,10 @@ public class CommitConfig {
private CleanupMode cleanupMode;
+ private char commentCharacter = '#';
+
+ private boolean autoCommentChar = false;
+
private CommitConfig(Config rc) {
commitTemplatePath = rc.getString(ConfigConstants.CONFIG_COMMIT_SECTION,
null, ConfigConstants.CONFIG_KEY_COMMIT_TEMPLATE);
@@ -106,6 +114,18 @@ public class CommitConfig {
null, ConfigConstants.CONFIG_KEY_COMMIT_ENCODING);
cleanupMode = rc.getEnum(ConfigConstants.CONFIG_COMMIT_SECTION, null,
ConfigConstants.CONFIG_KEY_CLEANUP, CleanupMode.DEFAULT);
+ String comment = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_COMMENT_CHAR);
+ if (!StringUtils.isEmptyOrNull(comment)) {
+ if ("auto".equalsIgnoreCase(comment)) { //$NON-NLS-1$
+ autoCommentChar = true;
+ } else {
+ char first = comment.charAt(0);
+ if (first > ' ' && first < 127) {
+ commentCharacter = first;
+ }
+ }
+ }
}
/**
@@ -131,6 +151,51 @@ public class CommitConfig {
}
/**
+ * Retrieves the comment character set by git config
+ * {@code core.commentChar}.
+ *
+ * @return the character to use for comments in commit messages
+ * @since 6.2
+ */
+ public char getCommentChar() {
+ return commentCharacter;
+ }
+
+ /**
+ * Determines the comment character to use for a particular text. If
+ * {@code core.commentChar} is "auto", tries to determine an unused
+ * character; if none is found, falls back to '#'. Otherwise returns the
+ * character given by {@code core.commentChar}.
+ *
+ * @param text
+ * existing text
+ *
+ * @return the character to use
+ * @since 6.2
+ */
+ public char getCommentChar(String text) {
+ if (isAutoCommentChar()) {
+ char toUse = determineCommentChar(text);
+ if (toUse > 0) {
+ return toUse;
+ }
+ return '#';
+ }
+ return getCommentChar();
+ }
+
+ /**
+ * Tells whether the comment character should be determined by choosing a
+ * character not occurring in a commit message.
+ *
+ * @return {@code true} if git config {@code core.commentChar} is "auto"
+ * @since 6.2
+ */
+ public boolean isAutoCommentChar() {
+ return autoCommentChar;
+ }
+
+ /**
* Retrieves the {@link CleanupMode} as given by git config
* {@code commit.cleanup}.
*
@@ -315,4 +380,41 @@ public class CommitConfig {
}
return false;
}
+
+ /**
+ * Determines a comment character by choosing one from a limited set of
+ * 7-bit ASCII characters that do not occur in the given text at the
+ * beginning of any line. If none can be determined, {@code (char) 0} is
+ * returned.
+ *
+ * @param text
+ * to get a comment character for
+ * @return the comment character, or {@code (char) 0} if none could be
+ * determined
+ * @since 6.2
+ */
+ public static char determineCommentChar(String text) {
+ if (StringUtils.isEmptyOrNull(text)) {
+ return '#';
+ }
+ final boolean[] inUse = new boolean[127];
+ for (String line : text.split("\n")) { //$NON-NLS-1$
+ int len = line.length();
+ for (int i = 0; i < len; i++) {
+ char ch = line.charAt(i);
+ if (!Character.isWhitespace(ch)) {
+ if (ch >= 0 && ch < inUse.length) {
+ inUse[ch] = true;
+ }
+ break;
+ }
+ }
+ }
+ for (char candidate : COMMENT_CHARS) {
+ if (!inUse[candidate]) {
+ return candidate;
+ }
+ }
+ return (char) 0;
+ }
} \ No newline at end of file
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 d4bd6c0e71..5c48c7a08e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -2,7 +2,7 @@
* Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
* Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com>
* Copyright (C) 2012-2013, Robin Rosenberg
- * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> and others
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -10,6 +10,7 @@
*
* SPDX-License-Identifier: BSD-3-Clause
*/
+
package org.eclipse.jgit.lib;
/**
@@ -31,14 +32,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,21 +53,21 @@ 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.<name>." section
*
* @since 6.1
*/
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
*/
@@ -124,6 +125,34 @@ public final class ConfigConstants {
public static final String CONFIG_MERGE_SECTION = "merge";
/**
+ * The "mergetool" section
+ *
+ * @since 6.2
+ */
+ public static final String CONFIG_MERGETOOL_SECTION = "mergetool";
+
+ /**
+ * The "keepBackup" key within "mergetool" section
+ *
+ * @since 6.2
+ */
+ public static final String CONFIG_KEY_KEEP_BACKUP = "keepBackup";
+
+ /**
+ * The "keepTemporaries" key within "mergetool" section
+ *
+ * @since 6.2
+ */
+ public static final String CONFIG_KEY_KEEP_TEMPORARIES = "keepTemporaries";
+
+ /**
+ * The "writeToTemp" key within "mergetool" section
+ *
+ * @since 6.2
+ */
+ public static final String CONFIG_KEY_WRITE_TO_TEMP = "writeToTemp";
+
+ /**
* The "filter" section
* @since 4.6
*/
@@ -203,6 +232,13 @@ public final class ConfigConstants {
public static final String CONFIG_KEY_FORCE_SIGN_ANNOTATED = "forceSignAnnotated";
/**
+ * The "commentChar" key.
+ *
+ * @since 6.2
+ */
+ public static final String CONFIG_KEY_COMMENT_CHAR = "commentChar";
+
+ /**
* The "hooksPath" key.
*
* @since 5.6
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
index 4b1dbedeb1..59775c475b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2021, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -26,20 +26,41 @@ public abstract class GpgSignatureVerifierFactory {
private static final Logger LOG = LoggerFactory
.getLogger(GpgSignatureVerifierFactory.class);
- private static volatile GpgSignatureVerifierFactory defaultFactory = loadDefault();
+ private static class DefaultFactory {
- private static GpgSignatureVerifierFactory loadDefault() {
- try {
- ServiceLoader<GpgSignatureVerifierFactory> loader = ServiceLoader
- .load(GpgSignatureVerifierFactory.class);
- Iterator<GpgSignatureVerifierFactory> iter = loader.iterator();
- if (iter.hasNext()) {
- return iter.next();
+ private static volatile GpgSignatureVerifierFactory defaultFactory = loadDefault();
+
+ private static GpgSignatureVerifierFactory loadDefault() {
+ try {
+ ServiceLoader<GpgSignatureVerifierFactory> loader = ServiceLoader
+ .load(GpgSignatureVerifierFactory.class);
+ Iterator<GpgSignatureVerifierFactory> iter = loader.iterator();
+ if (iter.hasNext()) {
+ return iter.next();
+ }
+ } catch (ServiceConfigurationError e) {
+ LOG.error(e.getMessage(), e);
}
- } catch (ServiceConfigurationError e) {
- LOG.error(e.getMessage(), e);
+ return null;
+ }
+
+ private DefaultFactory() {
+ // No instantiation
+ }
+
+ public static GpgSignatureVerifierFactory getDefault() {
+ return defaultFactory;
+ }
+
+ /**
+ * Sets the default factory.
+ *
+ * @param factory
+ * the new default factory
+ */
+ public static void setDefault(GpgSignatureVerifierFactory factory) {
+ defaultFactory = factory;
}
- return null;
}
/**
@@ -48,7 +69,7 @@ public abstract class GpgSignatureVerifierFactory {
* @return the default factory or {@code null} if none set
*/
public static GpgSignatureVerifierFactory getDefault() {
- return defaultFactory;
+ return DefaultFactory.getDefault();
}
/**
@@ -58,7 +79,7 @@ public abstract class GpgSignatureVerifierFactory {
* the new default factory
*/
public static void setDefault(GpgSignatureVerifierFactory factory) {
- defaultFactory = factory;
+ DefaultFactory.setDefault(factory);
}
/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
index 5b32cf0b5f..b25a61b506 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, Salesforce. and others
+ * Copyright (C) 2018, 2022 Salesforce 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
@@ -26,22 +26,38 @@ import org.slf4j.LoggerFactory;
* @since 5.3
*/
public abstract class GpgSigner {
+
private static final Logger LOG = LoggerFactory.getLogger(GpgSigner.class);
- private static GpgSigner defaultSigner = loadGpgSigner();
+ private static class DefaultSigner {
+
+ private static volatile GpgSigner defaultSigner = loadGpgSigner();
- private static GpgSigner loadGpgSigner() {
- try {
- ServiceLoader<GpgSigner> loader = ServiceLoader
- .load(GpgSigner.class);
- Iterator<GpgSigner> iter = loader.iterator();
- if (iter.hasNext()) {
- return iter.next();
+ private static GpgSigner loadGpgSigner() {
+ try {
+ ServiceLoader<GpgSigner> loader = ServiceLoader
+ .load(GpgSigner.class);
+ Iterator<GpgSigner> iter = loader.iterator();
+ if (iter.hasNext()) {
+ return iter.next();
+ }
+ } catch (ServiceConfigurationError e) {
+ LOG.error(e.getMessage(), e);
}
- } catch (ServiceConfigurationError e) {
- LOG.error(e.getMessage(), e);
+ return null;
+ }
+
+ private DefaultSigner() {
+ // No instantiation
+ }
+
+ public static GpgSigner getDefault() {
+ return defaultSigner;
+ }
+
+ public static void setDefault(GpgSigner signer) {
+ defaultSigner = signer;
}
- return null;
}
/**
@@ -50,7 +66,7 @@ public abstract class GpgSigner {
* @return the default signer, or <code>null</code>.
*/
public static GpgSigner getDefault() {
- return defaultSigner;
+ return DefaultSigner.getDefault();
}
/**
@@ -61,7 +77,7 @@ public abstract class GpgSigner {
* default.
*/
public static void setDefault(GpgSigner signer) {
- GpgSigner.defaultSigner = signer;
+ DefaultSigner.setDefault(signer);
}
/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java
index 94e7c53adb..c11fca13d8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java
@@ -138,6 +138,7 @@ public class PlotCommit<L extends PlotLane> extends RevCommit {
* the commit to test.
* @return true if the given commit built on top of this commit.
*/
+ @SuppressWarnings("ReferenceEquality")
public final boolean isChild(PlotCommit c) {
for (PlotCommit a : children)
if (a == c)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java
index 18ea7560fd..458f240982 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java
@@ -92,6 +92,7 @@ public class PlotCommitList<L extends PlotLane> extends
}
/** {@inheritDoc} */
+ @SuppressWarnings("ReferenceEquality")
@Override
protected void enter(int index, PlotCommit<L> currCommit) {
setupChildren(currCommit);
@@ -188,6 +189,7 @@ public class PlotCommitList<L extends PlotLane> extends
* may be null if <code>currCommit</code> is the first commit on
* the lane
*/
+ @SuppressWarnings("ReferenceEquality")
private void handleBlockedLanes(final int index, final PlotCommit currCommit,
final PlotCommit childOnLane) {
for (PlotCommit child : currCommit.children) {
@@ -214,6 +216,7 @@ public class PlotCommitList<L extends PlotLane> extends
}
// Handles the case where currCommit is a non-first parent of the child
+ @SuppressWarnings("ReferenceEquality")
private PlotLane handleMerge(final int index, final PlotCommit currCommit,
final PlotCommit childOnLane, PlotCommit child, PlotLane laneToUse) {
@@ -287,6 +290,7 @@ public class PlotCommitList<L extends PlotLane> extends
* @param child
* @param laneToContinue
*/
+ @SuppressWarnings("ReferenceEquality")
private void drawLaneToChild(final int commitIndex, PlotCommit child,
PlotLane laneToContinue) {
for (int r = commitIndex - 1; r >= 0; r--) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
index a50eaf1a8a..a25948e50b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
@@ -143,8 +143,19 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable {
*/
static final int TOPO_QUEUED = 1 << 6;
+ /**
+ * Set on a RevCommit when a {@link TreeRevFilter} has been applied.
+ * <p>
+ * This flag is processed by the {@link RewriteGenerator} to check if a
+ * {@link TreeRevFilter} has been applied.
+ *
+ * @see TreeRevFilter
+ * @see RewriteGenerator
+ */
+ static final int TREE_REV_FILTER_APPLIED = 1 << 7;
+
/** Number of flag bits we keep internal for our own use. See above flags. */
- static final int RESERVED_FLAGS = 7;
+ static final int RESERVED_FLAGS = 8;
private static final int APP_FLAGS = -1 & ~((1 << RESERVED_FLAGS) - 1);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java
index a928c2e79b..1adef07ad9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java
@@ -24,14 +24,7 @@ import org.eclipse.jgit.errors.MissingObjectException;
* commit that matched the revision walker's filters.
* <p>
* This generator is the second phase of a path limited revision walk and
- * assumes it is receiving RevCommits from {@link TreeRevFilter},
- * after they have been fully buffered by {@link AbstractRevQueue}. The full
- * buffering is necessary to allow the simple loop used within our own
- * {@link #rewrite(RevCommit)} to pull completely through a strand of
- * {@link RevWalk#REWRITE} colored commits and come up with a simplification
- * that makes the DAG dense. Not fully buffering the commits first would cause
- * this loop to abort early, due to commits not being parsed and colored
- * correctly.
+ * assumes it is receiving RevCommits from {@link TreeRevFilter}.
*
* @see TreeRevFilter
*/
@@ -43,9 +36,12 @@ class RewriteGenerator extends Generator {
private final Generator source;
+ private final FIFORevQueue pending;
+
RewriteGenerator(Generator s) {
super(s.firstParent);
source = s;
+ pending = new FIFORevQueue(s.firstParent);
}
@Override
@@ -58,13 +54,23 @@ class RewriteGenerator extends Generator {
return source.outputType() & ~NEEDS_REWRITE;
}
+ @SuppressWarnings("ReferenceEquality")
@Override
RevCommit next() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
- final RevCommit c = source.next();
+ RevCommit c = pending.next();
+
if (c == null) {
- return null;
+ c = source.next();
+ if (c == null) {
+ // We are done: Both the source generator and our internal list
+ // are completely exhausted.
+ return null;
+ }
}
+
+ applyFilterToParents(c);
+
boolean rewrote = false;
final RevCommit[] pList = c.parents;
final int nParents = pList.length;
@@ -90,10 +96,41 @@ class RewriteGenerator extends Generator {
return c;
}
- private RevCommit rewrite(RevCommit p) {
+ /**
+ * Makes sure that the {@link TreeRevFilter} has been applied to all parents
+ * of this commit by the previous {@link PendingGenerator}.
+ *
+ * @param c
+ * @throws MissingObjectException
+ * @throws IncorrectObjectTypeException
+ * @throws IOException
+ */
+ private void applyFilterToParents(RevCommit c)
+ throws MissingObjectException, IncorrectObjectTypeException,
+ IOException {
+ for (RevCommit parent : c.parents) {
+ while ((parent.flags & RevWalk.TREE_REV_FILTER_APPLIED) == 0) {
+
+ RevCommit n = source.next();
+
+ if (n != null) {
+ pending.add(n);
+ } else {
+ // Source generator is exhausted; filter has been applied to
+ // all commits
+ return;
+ }
+
+ }
+
+ }
+ }
+
+ private RevCommit rewrite(RevCommit p) throws MissingObjectException,
+ IncorrectObjectTypeException, IOException {
for (;;) {
- final RevCommit[] pList = p.parents;
- if (pList.length > 1) {
+
+ if (p.parents.length > 1) {
// This parent is a merge, so keep it.
//
return p;
@@ -113,14 +150,16 @@ class RewriteGenerator extends Generator {
return p;
}
- if (pList.length == 0) {
+ if (p.parents.length == 0) {
// We can't go back any further, other than to
// just delete the parent entirely.
//
return null;
}
- p = pList[0];
+ applyFilterToParents(p.parents[0]);
+ p = p.parents[0];
+
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java
index bfcea6ea8f..a79901ca10 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java
@@ -125,12 +125,6 @@ class StartGenerator extends Generator {
}
if ((g.outputType() & NEEDS_REWRITE) != 0) {
- // Correction for an upstream NEEDS_REWRITE is to buffer
- // fully and then apply a rewrite generator that can
- // pull through the rewrite chain and produce a dense
- // output graph.
- //
- g = new FIFORevQueue(g);
g = new RewriteGenerator(g);
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java
index 822fc5320c..92d72268d1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java
@@ -41,6 +41,8 @@ public class TreeRevFilter extends RevFilter {
private static final int UNINTERESTING = RevWalk.UNINTERESTING;
+ private static final int FILTER_APPLIED = RevWalk.TREE_REV_FILTER_APPLIED;
+
private final int rewriteFlag;
private final TreeWalk pathFilter;
@@ -101,6 +103,7 @@ public class TreeRevFilter extends RevFilter {
public boolean include(RevWalk walker, RevCommit c)
throws StopWalkException, MissingObjectException,
IncorrectObjectTypeException, IOException {
+ c.flags |= FILTER_APPLIED;
// Reset the tree filter to scan this commit and parents.
//
RevCommit[] pList = c.parents;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
index 2443c4e771..cba5e1697c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
@@ -20,7 +20,6 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.ByteArrayOutputStream;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.MessageFormat;
@@ -37,15 +36,11 @@ import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* The configuration file that is stored in the file of the file system.
*/
public class FileBasedConfig extends StoredConfig {
- private static final Logger LOG = LoggerFactory
- .getLogger(FileBasedConfig.class);
private final File configFile;
@@ -115,16 +110,15 @@ public class FileBasedConfig extends StoredConfig {
*/
@Override
public void load() throws IOException, ConfigInvalidException {
- final int maxRetries = 5;
- int retryDelayMillis = 20;
- int retries = 0;
- while (true) {
- final FileSnapshot oldSnapshot = snapshot;
- final FileSnapshot newSnapshot;
- // don't use config in this snapshot to avoid endless recursion
- newSnapshot = FileSnapshot.saveNoConfig(getFile());
- try {
- final byte[] in = IO.readFully(getFile());
+ try {
+ FileSnapshot[] lastSnapshot = { null };
+ Boolean wasRead = FileUtils.readWithRetries(getFile(), f -> {
+ final FileSnapshot oldSnapshot = snapshot;
+ final FileSnapshot newSnapshot;
+ // don't use config in this snapshot to avoid endless recursion
+ newSnapshot = FileSnapshot.saveNoConfig(f);
+ lastSnapshot[0] = newSnapshot;
+ final byte[] in = IO.readFully(f);
final ObjectId newHash = hash(in);
if (hash.equals(newHash)) {
if (oldSnapshot.equals(newSnapshot)) {
@@ -145,47 +139,17 @@ public class FileBasedConfig extends StoredConfig {
snapshot = newSnapshot;
hash = newHash;
}
- return;
- } catch (FileNotFoundException noFile) {
- // might be locked by another process (see exception Javadoc)
- if (retries < maxRetries && configFile.exists()) {
- if (LOG.isDebugEnabled()) {
- LOG.debug(MessageFormat.format(
- JGitText.get().configHandleMayBeLocked,
- Integer.valueOf(retries)), noFile);
- }
- try {
- Thread.sleep(retryDelayMillis);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- retries++;
- retryDelayMillis *= 2; // max wait 1260 ms
- continue;
- }
- if (configFile.exists()) {
- throw noFile;
- }
+ return Boolean.TRUE;
+ });
+ if (wasRead == null) {
clear();
- snapshot = newSnapshot;
- return;
- } catch (IOException e) {
- if (FileUtils.isStaleFileHandle(e)
- && retries < maxRetries) {
- if (LOG.isDebugEnabled()) {
- LOG.debug(MessageFormat.format(
- JGitText.get().configHandleIsStale,
- Integer.valueOf(retries)), e);
- }
- retries++;
- continue;
- }
- throw new IOException(MessageFormat
- .format(JGitText.get().cannotReadFile, getFile()), e);
- } catch (ConfigInvalidException e) {
- throw new ConfigInvalidException(MessageFormat
- .format(JGitText.get().cannotReadFile, getFile()), e);
+ snapshot = lastSnapshot[0];
}
+ } catch (IOException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ConfigInvalidException(MessageFormat
+ .format(JGitText.get().cannotReadFile, getFile()), e);
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
index f48e1e68cc..3f167ccce2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
@@ -1,7 +1,7 @@
/*
* Copyright (C) 2008, 2010 Google Inc.
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> 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
@@ -1004,9 +1004,12 @@ public abstract class BasePackFetchConnection extends BasePackConnection
OutputStream outputStream) throws IOException {
onReceivePack();
InputStream input = in;
- if (sideband)
- input = new SideBandInputStream(input, monitor, getMessageWriter(),
- outputStream);
+ SideBandInputStream sidebandIn = null;
+ if (sideband) {
+ sidebandIn = new SideBandInputStream(input, monitor,
+ getMessageWriter(), outputStream);
+ input = sidebandIn;
+ }
try (ObjectInserter ins = local.newObjectInserter()) {
PackParser parser = ins.newPackParser(input);
@@ -1015,6 +1018,10 @@ public abstract class BasePackFetchConnection extends BasePackConnection
parser.setLockMessage(lockMessage);
packLock = parser.parse(monitor);
ins.flush();
+ } finally {
+ if (sidebandIn != null) {
+ sidebandIn.drainMessages();
+ }
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
index b87a85d934..b7be59d6f8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> 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
@@ -194,10 +194,11 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen
// the other data channels.
//
int b = in.read();
- if (0 <= b)
+ if (0 <= b) {
throw new TransportException(uri, MessageFormat.format(
JGitText.get().expectedEOFReceived,
Character.valueOf((char) b)));
+ }
}
}
} catch (TransportException e) {
@@ -205,6 +206,9 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen
} catch (Exception e) {
throw new TransportException(uri, e.getMessage(), e);
} finally {
+ if (in instanceof SideBandInputStream) {
+ ((SideBandInputStream) in).drainMessages();
+ }
close();
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
index 1c1aa7b310..bb58a7e33f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
@@ -31,6 +31,7 @@ import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.NotSupportedException;
@@ -57,6 +58,12 @@ class FetchProcess {
/** List of things we want to fetch from the remote repository. */
private final Collection<RefSpec> toFetch;
+ /**
+ * List of things we don't want to fetch from the remote repository or to
+ * the local repository.
+ */
+ private final Collection<RefSpec> negativeRefSpecs;
+
/** Set of refs we will actually wind up asking to obtain. */
private final HashMap<ObjectId, Ref> askFor = new HashMap<>();
@@ -75,9 +82,12 @@ class FetchProcess {
private Map<String, Ref> localRefs;
- FetchProcess(Transport t, Collection<RefSpec> f) {
+ FetchProcess(Transport t, Collection<RefSpec> refSpecs) {
transport = t;
- toFetch = f;
+ toFetch = refSpecs.stream().filter(refSpec -> !refSpec.isNegative())
+ .collect(Collectors.toList());
+ negativeRefSpecs = refSpecs.stream().filter(RefSpec::isNegative)
+ .collect(Collectors.toList());
}
void execute(ProgressMonitor monitor, FetchResult result,
@@ -403,8 +413,13 @@ class FetchProcess {
private void expandWildcard(RefSpec spec, Set<Ref> matched)
throws TransportException {
for (Ref src : conn.getRefs()) {
- if (spec.matchSource(src) && matched.add(src))
- want(src, spec.expandFromSource(src));
+ if (spec.matchSource(src)) {
+ RefSpec expandedRefSpec = spec.expandFromSource(src);
+ if (!matchNegativeRefSpec(expandedRefSpec)
+ && matched.add(src)) {
+ want(src, expandedRefSpec);
+ }
+ }
}
}
@@ -420,11 +435,27 @@ class FetchProcess {
if (src == null) {
throw new TransportException(MessageFormat.format(JGitText.get().remoteDoesNotHaveSpec, want));
}
- if (matched.add(src)) {
+ if (!matchNegativeRefSpec(spec) && matched.add(src)) {
want(src, spec);
}
}
+ private boolean matchNegativeRefSpec(RefSpec spec) {
+ for (RefSpec negativeRefSpec : negativeRefSpecs) {
+ if (negativeRefSpec.getSource() != null && spec.getSource() != null
+ && negativeRefSpec.matchSource(spec.getSource())) {
+ return true;
+ }
+
+ if (negativeRefSpec.getDestination() != null
+ && spec.getDestination() != null && negativeRefSpec
+ .matchDestination(spec.getDestination())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private boolean localHasObject(ObjectId id) throws TransportException {
try {
return transport.local.getObjectDatabase().has(id);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
index 942dad46e0..b59ae0c450 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> and others
+ * Copyright (C) 2008, 2022 Marek Zawirski <marek.zawirski@gmail.com> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -166,6 +166,7 @@ class PushProcess {
if (prePush != null) {
try {
prePush.setRefs(willBeAttempted);
+ prePush.setDryRun(transport.isDryRun());
prePush.call();
} catch (AbortedByHookException | IOException e) {
throw new TransportException(e.getMessage(), e);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
index 56d0036a20..e9134a1a32 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
@@ -53,6 +53,9 @@ public class RefSpec implements Serializable {
/** Is this the special ":" RefSpec? */
private boolean matching;
+ /** Is this a negative refspec. */
+ private boolean negative;
+
/**
* How strict to be about wildcards.
*
@@ -96,12 +99,23 @@ public class RefSpec implements Serializable {
wildcard = false;
srcName = Constants.HEAD;
dstName = null;
+ negative =false;
allowMismatchedWildcards = WildcardMode.REQUIRE_MATCH;
}
/**
* Parse a ref specification for use during transport operations.
* <p>
+ * {@link RefSpec}s can be regular or negative, regular RefSpecs indicate
+ * what to include in transport operations while negative RefSpecs indicate
+ * what to exclude in fetch.
+ * <p>
+ * Negative {@link RefSpec}s can't be force, must have only source or
+ * destination. Wildcard patterns are also supported in negative RefSpecs
+ * but they can not go with {@code WildcardMode.REQUIRE_MATCH} because they
+ * are natually one to many mappings.
+ *
+ * <p>
* Specifications are typically one of the following forms:
* <ul>
* <li><code>refs/heads/master</code></li>
@@ -121,6 +135,12 @@ public class RefSpec implements Serializable {
* <li><code>refs/heads/*:refs/heads/master</code></li>
* </ul>
*
+ * Negative specifications are usually like:
+ * <ul>
+ * <li><code>^:refs/heads/master</code></li>
+ * <li><code>^refs/heads/*</code></li>
+ * </ul>
+ *
* @param spec
* string describing the specification.
* @param mode
@@ -133,11 +153,22 @@ public class RefSpec implements Serializable {
public RefSpec(String spec, WildcardMode mode) {
this.allowMismatchedWildcards = mode;
String s = spec;
+
+ if (s.startsWith("^+") || s.startsWith("+^")) { //$NON-NLS-1$ //$NON-NLS-2$
+ throw new IllegalArgumentException(
+ JGitText.get().invalidNegativeAndForce);
+ }
+
if (s.startsWith("+")) { //$NON-NLS-1$
force = true;
s = s.substring(1);
}
+ if (s.startsWith("^")) { //$NON-NLS-1$
+ negative = true;
+ s = s.substring(1);
+ }
+
boolean matchPushSpec = false;
final int c = s.lastIndexOf(':');
if (c == 0) {
@@ -181,6 +212,21 @@ public class RefSpec implements Serializable {
}
srcName = checkValid(s);
}
+
+ // Negative refspecs must only have dstName or srcName.
+ if (isNegative()) {
+ if (isNullOrEmpty(srcName) && isNullOrEmpty(dstName)) {
+ throw new IllegalArgumentException(MessageFormat
+ .format(JGitText.get().invalidRefSpec, spec));
+ }
+ if (!isNullOrEmpty(srcName) && !isNullOrEmpty(dstName)) {
+ throw new IllegalArgumentException(MessageFormat
+ .format(JGitText.get().invalidRefSpec, spec));
+ }
+ if(wildcard && mode == WildcardMode.REQUIRE_MATCH) {
+ throw new IllegalArgumentException(MessageFormat
+ .format(JGitText.get().invalidRefSpec, spec));}
+ }
matching = matchPushSpec;
}
@@ -205,13 +251,15 @@ public class RefSpec implements Serializable {
* the specification is invalid.
*/
public RefSpec(String spec) {
- this(spec, WildcardMode.REQUIRE_MATCH);
+ this(spec, spec.startsWith("^") ? WildcardMode.ALLOW_MISMATCH //$NON-NLS-1$
+ : WildcardMode.REQUIRE_MATCH);
}
private RefSpec(RefSpec p) {
matching = false;
force = p.isForceUpdate();
wildcard = p.isWildcard();
+ negative = p.isNegative();
srcName = p.getSource();
dstName = p.getDestination();
allowMismatchedWildcards = p.allowMismatchedWildcards;
@@ -246,6 +294,10 @@ public class RefSpec implements Serializable {
*/
public RefSpec setForceUpdate(boolean forceUpdate) {
final RefSpec r = new RefSpec(this);
+ if (forceUpdate && isNegative()) {
+ throw new IllegalArgumentException(
+ JGitText.get().invalidNegativeAndForce);
+ }
r.matching = matching;
r.force = forceUpdate;
return r;
@@ -265,6 +317,16 @@ public class RefSpec implements Serializable {
}
/**
+ * Check if this specification is a negative one.
+ *
+ * @return true if this specification is negative.
+ * @since 6.2
+ */
+ public boolean isNegative() {
+ return negative;
+ }
+
+ /**
* Get the source ref description.
* <p>
* During a fetch this is the name of the ref on the remote repository we
@@ -435,6 +497,10 @@ public class RefSpec implements Serializable {
return this;
}
+ private static boolean isNullOrEmpty(String refName) {
+ return refName == null || refName.isEmpty();
+ }
+
/**
* Expand this specification to exactly match a ref.
* <p>
@@ -570,6 +636,9 @@ public class RefSpec implements Serializable {
if (isForceUpdate() != b.isForceUpdate()) {
return false;
}
+ if(isNegative() != b.isNegative()) {
+ return false;
+ }
if (isMatching()) {
return b.isMatching();
} else if (b.isMatching()) {
@@ -587,6 +656,9 @@ public class RefSpec implements Serializable {
if (isForceUpdate()) {
r.append('+');
}
+ if(isNegative()) {
+ r.append('^');
+ }
if (isMatching()) {
r.append(':');
} else {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java
index 2f3160bb8e..c4e105ec4a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java
@@ -16,10 +16,7 @@ import java.io.Serializable;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
import org.eclipse.jgit.lib.Config;
@@ -54,10 +51,6 @@ public class RemoteConfig implements Serializable {
private static final String KEY_TIMEOUT = "timeout"; //$NON-NLS-1$
- private static final String KEY_INSTEADOF = "insteadof"; //$NON-NLS-1$
-
- private static final String KEY_PUSHINSTEADOF = "pushinsteadof"; //$NON-NLS-1$
-
private static final boolean DEFAULT_MIRROR = false;
/** Default value for {@link #getUploadPack()} if not specified. */
@@ -135,10 +128,10 @@ public class RemoteConfig implements Serializable {
String val;
vlst = rc.getStringList(SECTION, name, KEY_URL);
- Map<String, String> insteadOf = getReplacements(rc, KEY_INSTEADOF);
+ UrlConfig urls = new UrlConfig(rc);
uris = new ArrayList<>(vlst.length);
for (String s : vlst) {
- uris.add(new URIish(replaceUri(s, insteadOf)));
+ uris.add(new URIish(urls.replace(s)));
}
String[] plst = rc.getStringList(SECTION, name, KEY_PUSHURL);
pushURIs = new ArrayList<>(plst.length);
@@ -148,11 +141,9 @@ public class RemoteConfig implements Serializable {
if (pushURIs.isEmpty()) {
// Would default to the uris. If we have pushinsteadof, we must
// supply rewritten push uris.
- Map<String, String> pushInsteadOf = getReplacements(rc,
- KEY_PUSHINSTEADOF);
- if (!pushInsteadOf.isEmpty()) {
+ if (urls.hasPushReplacements()) {
for (String s : vlst) {
- String replaced = replaceUri(s, pushInsteadOf);
+ String replaced = urls.replacePush(s);
if (!s.equals(replaced)) {
pushURIs.add(new URIish(replaced));
}
@@ -248,39 +239,6 @@ public class RemoteConfig implements Serializable {
rc.unset(SECTION, getName(), key);
}
- private Map<String, String> getReplacements(final Config config,
- final String keyName) {
- final Map<String, String> replacements = new HashMap<>();
- for (String url : config.getSubsections(KEY_URL))
- for (String insteadOf : config.getStringList(KEY_URL, url, keyName))
- replacements.put(insteadOf, url);
- return replacements;
- }
-
- private String replaceUri(final String uri,
- final Map<String, String> replacements) {
- if (replacements.isEmpty()) {
- return uri;
- }
- Entry<String, String> match = null;
- for (Entry<String, String> replacement : replacements.entrySet()) {
- // Ignore current entry if not longer than previous match
- if (match != null
- && match.getKey().length() > replacement.getKey()
- .length()) {
- continue;
- }
- if (!uri.startsWith(replacement.getKey())) {
- continue;
- }
- match = replacement;
- }
- if (match != null) {
- return match.getValue() + uri.substring(match.getKey().length());
- }
- return uri;
- }
-
/**
* Get the local name this remote configuration is recognized as.
*
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
index 8a8d977ed3..96c7be5b97 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> 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
@@ -28,6 +28,8 @@ import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Unmultiplexes the data portion of a side-band channel.
@@ -46,6 +48,10 @@ import org.eclipse.jgit.util.RawParseUtils;
* @since 4.11
*/
public class SideBandInputStream extends InputStream {
+
+ private static final Logger LOG = LoggerFactory
+ .getLogger(SideBandInputStream.class);
+
static final int CH_DATA = 1;
static final int CH_PROGRESS = 2;
static final int CH_ERROR = 3;
@@ -210,6 +216,21 @@ public class SideBandInputStream extends InputStream {
monitor.beginTask(remote(currentTask), totalWorkUnits);
}
+ /**
+ * Forces any buffered progress messages to be written.
+ */
+ void drainMessages() {
+ if (!progressBuffer.isEmpty()) {
+ try {
+ progress("\n"); //$NON-NLS-1$
+ } catch (IOException e) {
+ // Just log; otherwise this IOException might hide a real
+ // TransportException
+ LOG.error(e.getMessage(), e);
+ }
+ }
+ }
+
private static String remote(String msg) {
String prefix = JGitText.get().prefixRemote;
StringBuilder r = new StringBuilder(prefix.length() + msg.length() + 1);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
index 1e98a56f79..a0194ea8b1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> 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
@@ -36,15 +36,35 @@ import org.eclipse.jgit.util.SystemReader;
*/
public abstract class SshSessionFactory {
- private static volatile SshSessionFactory INSTANCE = loadSshSessionFactory();
+ private static class DefaultFactory {
- private static SshSessionFactory loadSshSessionFactory() {
- ServiceLoader<SshSessionFactory> loader = ServiceLoader.load(SshSessionFactory.class);
- Iterator<SshSessionFactory> iter = loader.iterator();
- if(iter.hasNext()) {
- return iter.next();
+ private static volatile SshSessionFactory INSTANCE = loadSshSessionFactory();
+
+ private static SshSessionFactory loadSshSessionFactory() {
+ ServiceLoader<SshSessionFactory> loader = ServiceLoader
+ .load(SshSessionFactory.class);
+ Iterator<SshSessionFactory> iter = loader.iterator();
+ if (iter.hasNext()) {
+ return iter.next();
+ }
+ return null;
+ }
+
+ private DefaultFactory() {
+ // No instantiation
+ }
+
+ public static SshSessionFactory getInstance() {
+ return INSTANCE;
+ }
+
+ public static void setInstance(SshSessionFactory newFactory) {
+ if (newFactory != null) {
+ INSTANCE = newFactory;
+ } else {
+ INSTANCE = loadSshSessionFactory();
+ }
}
- return null;
}
/**
@@ -57,7 +77,7 @@ public abstract class SshSessionFactory {
* @return factory the current factory for this JVM.
*/
public static SshSessionFactory getInstance() {
- return INSTANCE;
+ return DefaultFactory.getInstance();
}
/**
@@ -68,11 +88,7 @@ public abstract class SshSessionFactory {
* {@code null} the default factory will be restored.
*/
public static void setInstance(SshSessionFactory newFactory) {
- if (newFactory != null) {
- INSTANCE = newFactory;
- } else {
- INSTANCE = loadSshSessionFactory();
- }
+ DefaultFactory.setInstance(newFactory);
}
/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
index 0eab4434ed..3222d6330c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
@@ -1230,7 +1230,9 @@ public abstract class Transport implements AutoCloseable {
* @param toFetch
* specification of refs to fetch locally. May be null or the
* empty collection to use the specifications from the
- * RemoteConfig. Source for each RefSpec can't be null.
+ * RemoteConfig. May contains regular and negative
+ * {@link RefSpec}s. Source for each regular RefSpec can't
+ * be null.
* @return information describing the tracking refs updated.
* @throws org.eclipse.jgit.errors.NotSupportedException
* this transport implementation does not support fetching
@@ -1264,7 +1266,9 @@ public abstract class Transport implements AutoCloseable {
* @param toFetch
* specification of refs to fetch locally. May be null or the
* empty collection to use the specifications from the
- * RemoteConfig. Source for each RefSpec can't be null.
+ * RemoteConfig. May contains regular and negative
+ * {@link RefSpec}s. Source for each regular RefSpec can't
+ * be null.
* @param branch
* the initial branch to check out when cloning the repository.
* Can be specified as ref name (<code>refs/heads/master</code>),
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java
new file mode 100644
index 0000000000..574fcf806d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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.transport;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Support for URL translations via git configs {@code url.<base>.insteadOf} and
+ * {@code url.<base>.pushInsteadOf}.
+ *
+ * @since 6.2
+ */
+public class UrlConfig {
+
+ private static final String KEY_INSTEADOF = "insteadof"; //$NON-NLS-1$
+
+ private static final String KEY_PUSHINSTEADOF = "pushinsteadof"; //$NON-NLS-1$
+
+ private static final String SECTION_URL = "url"; //$NON-NLS-1$
+
+ private final Config config;
+
+ private Map<String, String> insteadOf;
+
+ private Map<String, String> pushInsteadOf;
+
+ /**
+ * Creates a new {@link UrlConfig} instance.
+ *
+ * @param config
+ * {@link Config} to read values from
+ */
+ public UrlConfig(Config config) {
+ this.config = config;
+ }
+
+ /**
+ * Performs replacements as defined by git config
+ * {@code url.<base>.insteadOf}. If there is no match, the input is returned
+ * unchanged.
+ *
+ * @param url
+ * to substitute
+ * @return the {@code url} with substitution applied
+ */
+ public String replace(String url) {
+ if (insteadOf == null) {
+ insteadOf = load(KEY_INSTEADOF);
+ }
+ return replace(url, insteadOf);
+ }
+
+ /**
+ * Tells whether there are push replacements.
+ *
+ * @return {@code true} if there are push replacements, {@code false}
+ * otherwise
+ */
+ public boolean hasPushReplacements() {
+ if (pushInsteadOf == null) {
+ pushInsteadOf = load(KEY_PUSHINSTEADOF);
+ }
+ return !pushInsteadOf.isEmpty();
+ }
+
+ /**
+ * Performs replacements as defined by git config
+ * {@code url.<base>.pushInsteadOf}. If there is no match, the input is
+ * returned unchanged.
+ *
+ * @param url
+ * to substitute
+ * @return the {@code url} with substitution applied
+ */
+ public String replacePush(String url) {
+ if (pushInsteadOf == null) {
+ pushInsteadOf = load(KEY_PUSHINSTEADOF);
+ }
+ return replace(url, pushInsteadOf);
+ }
+
+ private Map<String, String> load(String key) {
+ Map<String, String> replacements = new HashMap<>();
+ for (String url : config.getSubsections(SECTION_URL)) {
+ for (String prefix : config.getStringList(SECTION_URL, url, key)) {
+ replacements.put(prefix, url);
+ }
+ }
+ return replacements;
+ }
+
+ private String replace(String uri, Map<String, String> replacements) {
+ Entry<String, String> match = null;
+ for (Entry<String, String> replacement : replacements.entrySet()) {
+ // Ignore current entry if not longer than previous match
+ if (match != null && match.getKey().length() > replacement.getKey()
+ .length()) {
+ continue;
+ }
+ if (uri.startsWith(replacement.getKey())) {
+ match = replacement;
+ }
+ }
+ if (match != null) {
+ return match.getValue() + uri.substring(match.getKey().length());
+ }
+ return uri;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java
new file mode 100644
index 0000000000..da1684630b
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022, Fabio Ponciroli <ponch78@gmail.com> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.util;
+
+/**
+ * Equality utilities.
+ *
+ * @since: 6.2
+ */
+public class Equality {
+
+ /**
+ * Compare by reference
+ *
+ * @param a
+ * First object to compare
+ * @param b
+ * Second object to compare
+ * @return {@code true} if the objects are identical, {@code false}
+ * otherwise
+ *
+ * @since 6.2
+ */
+ @SuppressWarnings("ReferenceEquality")
+ public static <T> boolean isSameInstance(T a, T b) {
+ return a == b;
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
index b9dd9baa61..f013e7e095 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
@@ -17,6 +17,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.InterruptedIOException;
import java.nio.channels.FileChannel;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.CopyOption;
@@ -655,6 +656,99 @@ public class FileUtils {
}
/**
+ * Like a {@link java.util.function.Function} but throwing an
+ * {@link Exception}.
+ *
+ * @param <A>
+ * input type
+ * @param <B>
+ * output type
+ * @since 6.2
+ */
+ @FunctionalInterface
+ public interface IOFunction<A, B> {
+
+ /**
+ * Performs the function.
+ *
+ * @param t
+ * input to operate on
+ * @return the output
+ * @throws Exception
+ * if a problem occurs
+ */
+ B apply(A t) throws Exception;
+ }
+
+ private static void backOff(long delay, IOException cause)
+ throws IOException {
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException e) {
+ IOException interruption = new InterruptedIOException();
+ interruption.initCause(e);
+ interruption.addSuppressed(cause);
+ Thread.currentThread().interrupt(); // Re-set flag
+ throw interruption;
+ }
+ }
+
+ /**
+ * Invokes the given {@link IOFunction}, performing a limited number of
+ * re-tries if exceptions occur that indicate either a stale NFS file handle
+ * or that indicate that the file may be written concurrently.
+ *
+ * @param <T>
+ * result type
+ * @param file
+ * to read
+ * @param reader
+ * for reading the file and creating an instance of {@code T}
+ * @return the result of the {@code reader}, or {@code null} if the file
+ * does not exist
+ * @throws Exception
+ * if a problem occurs
+ * @since 6.2
+ */
+ public static <T> T readWithRetries(File file,
+ IOFunction<File, ? extends T> reader)
+ throws Exception {
+ int maxStaleRetries = 5;
+ int retries = 0;
+ long backoff = 50;
+ while (true) {
+ try {
+ try {
+ return reader.apply(file);
+ } catch (IOException e) {
+ if (FileUtils.isStaleFileHandleInCausalChain(e)
+ && retries < maxStaleRetries) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(MessageFormat.format(
+ JGitText.get().packedRefsHandleIsStale,
+ Integer.valueOf(retries)), e);
+ }
+ retries++;
+ continue;
+ }
+ throw e;
+ }
+ } catch (FileNotFoundException noFile) {
+ if (!file.isFile()) {
+ return null;
+ }
+ // Probably Windows and some other thread is writing the file
+ // concurrently.
+ if (backoff > 1000) {
+ throw noFile;
+ }
+ backOff(backoff, noFile);
+ backoff *= 2; // 50, 100, 200, 400, 800 ms
+ }
+ }
+ }
+
+ /**
* @param file
* @return {@code true} if the passed file is a symbolic link
*/