diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse')
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 */ |