diff options
author | Matthias Sohn <matthias.sohn@sap.com> | 2015-02-04 14:21:52 +0100 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2015-02-04 14:21:52 +0100 |
commit | 21f667edbaa1727e08ea9356fcbfbefdee481b2f (patch) | |
tree | c845201e9a152f9828f2e2b265bd873f5c80c0eb | |
parent | 23ad3a37638f295e22a67ca16aefeeb53467e289 (diff) | |
parent | fc801dd79f7c6eb3765e4652136f219480478bf6 (diff) | |
download | jgit-21f667edbaa1727e08ea9356fcbfbefdee481b2f.tar.gz jgit-21f667edbaa1727e08ea9356fcbfbefdee481b2f.zip |
Merge branch 'stable-3.7'
* stable-3.7:
Add option --orphan for checkout
Prepare post 3.7.0.201502031740-rc1 builds
JGit v3.7.0.201502031740-rc1
Support for the pre-commit hook
Fix FileUtils.testRelativize_mixedCase which failed on Mac OS X
Add a hook test
Introduce hook support into the FS implementations
If a pack isn't found on disk remove it from pack list
Conflicts:
org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF
Change-Id: I936acd24d47b911fa30ab29856094e1b2c6ac3db
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
22 files changed, 1117 insertions, 11 deletions
diff --git a/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF index ad8800103b..a1b2782cb5 100644 --- a/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF @@ -6,6 +6,7 @@ Bundle-Version: 4.0.0.qualifier Bundle-Vendor: %provider_name Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Import-Package: org.eclipse.jgit.api;version="[4.0.0,4.1.0)", + org.eclipse.jgit.api.errors;version="[4.0.0,4.1.0)", org.eclipse.jgit.diff;version="[4.0.0,4.1.0)", org.eclipse.jgit.dircache;version="[4.0.0,4.1.0)", org.eclipse.jgit.internal.storage.file;version="4.0.0", diff --git a/org.eclipse.jgit.java7.test/src/org/eclipse/jgit/util/HookTest.java b/org.eclipse.jgit.java7.test/src/org/eclipse/jgit/util/HookTest.java new file mode 100644 index 0000000000..96550889c8 --- /dev/null +++ b/org.eclipse.jgit.java7.test/src/org/eclipse/jgit/util/HookTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2014 Matthias Sohn <matthias.sohn@sap.com> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.RejectCommitException; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.junit.Assume; +import org.junit.Test; + +public class HookTest extends RepositoryTestCase { + + @Test + public void testFindHook() throws Exception { + assumeSupportedPlatform(); + + Hook h = Hook.PRE_COMMIT; + assertNull("no hook should be installed", FS.DETECTED.findHook(db, h)); + File hookFile = writeHookFile(h.getName(), + "#!/bin/bash\necho \"test $1 $2\""); + assertEquals("exected to find pre-commit hook", hookFile, + FS.DETECTED.findHook(db, h)); + } + + @Test + public void testRunHook() throws Exception { + assumeSupportedPlatform(); + + Hook h = Hook.PRE_COMMIT; + writeHookFile( + h.getName(), + "#!/bin/sh\necho \"test $1 $2\"\nread INPUT\necho $INPUT\necho 1>&2 \"stderr\""); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + ProcessResult res = FS.DETECTED.runIfPresent(db, h, new String[] { + "arg1", "arg2" }, + new PrintStream(out), new PrintStream(err), "stdin"); + assertEquals("unexpected hook output", "test arg1 arg2\nstdin\n", + out.toString("UTF-8")); + assertEquals("unexpected output on stderr stream", "stderr\n", + err.toString("UTF-8")); + assertEquals("unexpected exit code", 0, res.getExitCode()); + assertEquals("unexpected process status", ProcessResult.Status.OK, + res.getStatus()); + } + + @Test + public void testPreCommitHook() throws Exception { + assumeSupportedPlatform(); + + Hook h = Hook.PRE_COMMIT; + writeHookFile(h.getName(), + "#!/bin/sh\necho \"test\"\n\necho 1>&2 \"stderr\"\nexit 1"); + Git git = Git.wrap(db); + String path = "a.txt"; + writeTrashFile(path, "content"); + git.add().addFilepattern(path).call(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + git.commit().setMessage("commit") + .setHookOutputStream(new PrintStream(out)).call(); + fail("expected pre-commit hook to abort commit"); + } catch (RejectCommitException e) { + assertEquals("unexpected error message from pre-commit hook", + "Commit rejected by \"pre-commit\" hook.\nstderr\n", + e.getMessage()); + assertEquals("unexpected output from pre-commit hook", "test\n", + out.toString()); + } catch (Throwable e) { + fail("unexpected exception thrown by pre-commit hook: " + e); + } + } + + private File writeHookFile(final String name, final String data) + throws IOException { + File path = new File(db.getWorkTree() + "/.git/hooks/", name); + JGitTestUtil.write(path, data); + FS.DETECTED.setExecute(path, true); + return path; + } + + private void assumeSupportedPlatform() { + Assume.assumeTrue(FS.DETECTED instanceof FS_POSIX + || FS.DETECTED instanceof FS_Win32_Java7Cygwin); + } +} diff --git a/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_POSIX_Java7.java b/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_POSIX_Java7.java index 4a73a9bcf5..300cf93bc8 100644 --- a/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_POSIX_Java7.java +++ b/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_POSIX_Java7.java @@ -53,6 +53,9 @@ import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; import java.util.Set; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Repository; + /** * FS implementation for Java7 on unix like systems */ @@ -344,4 +347,17 @@ public class FS_POSIX_Java7 extends FS_POSIX { public String normalize(String name) { return FileUtil.normalize(name); } + + /** + * @since 3.7 + */ + @Override + public File findHook(Repository repository, Hook hook) { + final File gitdir = repository.getDirectory(); + final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS) + .resolve(hook.getName()); + if (Files.isExecutable(hookPath)) + return hookPath.toFile(); + return null; + } } diff --git a/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_Win32_Java7Cygwin.java b/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_Win32_Java7Cygwin.java index e40d7cf0b5..b6e5d93885 100644 --- a/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_Win32_Java7Cygwin.java +++ b/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_Win32_Java7Cygwin.java @@ -45,6 +45,11 @@ package org.eclipse.jgit.util; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Repository; /** * FS for Java7 on Windows with Cygwin @@ -135,4 +140,17 @@ public class FS_Win32_Java7Cygwin extends FS_Win32_Cygwin { public Attributes getAttributes(File path) { return FileUtil.getFileAttributesBasic(this, path); } + + /** + * @since 3.7 + */ + @Override + public File findHook(Repository repository, Hook hook) { + final File gitdir = repository.getDirectory(); + final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS) + .resolve(hook.getName()); + if (Files.isExecutable(hookPath)) + return hookPath.toFile(); + return null; + } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java index 90284736cf..cef9b9e1a6 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java @@ -188,6 +188,18 @@ public class CheckoutTest extends CLIRepositoryTestCase { assertEquals("Hello world a", read(fileA)); } + @Test + public void testCheckoutOrphan() throws Exception { + Git git = new Git(db); + git.commit().setMessage("initial commit").call(); + + assertEquals("Switched to a new branch 'new_branch'", + execute("git checkout --orphan new_branch")); + assertEquals("refs/heads/new_branch", db.getRef("HEAD").getTarget().getName()); + RevCommit commit = git.commit().setMessage("orphan commit").call(); + assertEquals(0, commit.getParentCount()); + } + /** * Steps: * <ol> diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties index 8a77bf928a..2806f91463 100644 --- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties +++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties @@ -345,3 +345,4 @@ usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repo usage_useNameInsteadOfOriginToTrackUpstream=use <name> instead of 'origin' to track upstream usage_checkoutBranchAfterClone=checkout named branch instead of remotes's HEAD usage_viewCommitHistory=View commit history +usage_orphan=Create a new orphan branch. The first commit made on this new branch will have no parents amd it will be the root of a new history totally disconnected from other branches and commits.
\ No newline at end of file diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java index 8f911fd924..56d4fcff02 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java @@ -71,6 +71,9 @@ class Checkout extends TextBuiltin { @Option(name = "--force", aliases = { "-f" }, usage = "usage_forceCheckout") private boolean force = false; + @Option(name = "--orphan", usage = "usage_orphan") + private boolean orphan = false; + @Argument(required = true, index = 0, metaVar = "metaVar_name", usage = "usage_checkout") private String name; @@ -95,6 +98,7 @@ class Checkout extends TextBuiltin { command.setCreateBranch(createBranch); command.setName(name); command.setForce(force); + command.setOrphan(orphan); } try { String oldBranch = db.getBranch(); @@ -107,10 +111,9 @@ class Checkout extends TextBuiltin { name)); return; } - if (createBranch) + if (createBranch || orphan) outw.println(MessageFormat.format( - CLIText.get().switchedToNewBranch, - Repository.shortenRefName(ref.getName()))); + CLIText.get().switchedToNewBranch, name)); else outw.println(MessageFormat.format( CLIText.get().switchedToBranch, diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java index 7b1627854f..0d7d31b3ad 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java @@ -50,6 +50,7 @@ import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; +import java.util.regex.Matcher; import org.eclipse.jgit.junit.JGitTestUtil; import org.junit.After; @@ -434,4 +435,71 @@ public class FileUtilTest { String target = fs.readSymLink(new File(trash, "x")); assertEquals("y", target); } + + @Test + public void testRelativize_doc() { + // This is the javadoc example + String base = toOSPathString("c:\\Users\\jdoe\\eclipse\\git\\project"); + String other = toOSPathString("c:\\Users\\jdoe\\eclipse\\git\\another_project\\pom.xml"); + String expected = toOSPathString("..\\another_project\\pom.xml"); + + String actual = FileUtils.relativize(base, other); + assertEquals(expected, actual); + } + + @Test + public void testRelativize_mixedCase() { + SystemReader systemReader = SystemReader.getInstance(); + String base = toOSPathString("C:\\git\\jgit"); + String other = toOSPathString("C:\\Git\\test\\d\\f.txt"); + String expectedCaseInsensitive = toOSPathString("..\\test\\d\\f.txt"); + String expectedCaseSensitive = toOSPathString("..\\..\\Git\\test\\d\\f.txt"); + + if (systemReader.isWindows()) { + String actual = FileUtils.relativize(base, other); + assertEquals(expectedCaseInsensitive, actual); + } else if (systemReader.isMacOS()) { + String actual = FileUtils.relativize(base, other); + assertEquals(expectedCaseInsensitive, actual); + } else { + String actual = FileUtils.relativize(base, other); + assertEquals(expectedCaseSensitive, actual); + } + } + + @Test + public void testRelativize_scheme() { + String base = toOSPathString("file:/home/eclipse/runtime-New_configuration/project_1/file.java"); + String other = toOSPathString("file:/home/eclipse/runtime-New_configuration/project"); + // 'file.java' is treated as a folder + String expected = toOSPathString("../../project"); + + String actual = FileUtils.relativize(base, other); + assertEquals(expected, actual); + } + + @Test + public void testRelativize_equalPaths() { + String base = toOSPathString("file:/home/eclipse/runtime-New_configuration/project_1"); + String other = toOSPathString("file:/home/eclipse/runtime-New_configuration/project_1"); + String expected = ""; + + String actual = FileUtils.relativize(base, other); + assertEquals(expected, actual); + } + + @Test + public void testRelativize_whitespaces() { + String base = toOSPathString("/home/eclipse 3.4/runtime New_configuration/project_1"); + String other = toOSPathString("/home/eclipse 3.4/runtime New_configuration/project_1/file"); + String expected = "file"; + + String actual = FileUtils.relativize(base, other); + assertEquals(expected, actual); + } + + private String toOSPathString(String path) { + return path.replaceAll("/|\\\\", + Matcher.quoteReplacement(File.separator)); + } } diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 55cf3ea71e..efe61052b0 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -104,6 +104,7 @@ commitAlreadyExists=exists {0} commitMessageNotSpecified=commit message not specified commitOnRepoWithoutHEADCurrentlyNotSupported=Commit on repo without HEAD currently not supported commitAmendOnInitialNotPossible=Amending is not possible on initial commit. +commitRejectedByHook=Commit rejected by "{0}" hook.\n{1} compressingObjects=Compressing objects connectionFailed=connection failed connectionTimeOut=Connection time out: {0} @@ -193,6 +194,7 @@ errorListing=Error listing {0} errorOccurredDuringUnpackingOnTheRemoteEnd=error occurred during unpacking on the remote end: {0} errorReadingInfoRefs=error reading info/refs errorSymlinksNotSupported=Symlinks are not supported with this OS/JRE +exceptionCaughtDuringExecutionOfHook=Exception caught during execution of "{0}" hook. exceptionCaughtDuringExecutionOfAddCommand=Exception caught during execution of add command exceptionCaughtDuringExecutionOfArchiveCommand=Exception caught during execution of archive command exceptionCaughtDuringExecutionOfCherryPickCommand=Exception caught during execution of cherry-pick command. {0} @@ -206,6 +208,7 @@ exceptionCaughtDuringExecutionOfResetCommand=Exception caught during execution o exceptionCaughtDuringExecutionOfRevertCommand=Exception caught during execution of revert command. {0} exceptionCaughtDuringExecutionOfRmCommand=Exception caught during execution of rm command exceptionCaughtDuringExecutionOfTagCommand=Exception caught during execution of tag command +exceptionHookExecutionInterrupted=Execution of "{0}" hook interrupted. exceptionOccurredDuringAddingOfOptionToALogCommand=Exception occurred during adding of {0} as option to a Log command exceptionOccurredDuringReadingOfGIT_DIR=Exception occurred during reading of $GIT_DIR/{0}. {1} exceptionWhileReadingPack=ERROR: Exception caught while accessing pack file {0}, the pack file might be corrupt @@ -392,6 +395,7 @@ packObjectCountMismatch=Pack object count mismatch: pack {0} index {1}: {2} packRefs=Pack refs packSizeNotSetYet=Pack size not yet set since it has not yet been received packTooLargeForIndexVersion1=Pack too large for index version 1 +packWasDeleted=Pack file {0} was deleted, removing it from pack list packWriterStatistics=Total {0,number,#0} (delta {1,number,#0}), reused {2,number,#0} (delta {3,number,#0}) panicCantRenameIndexFile=Panic: index file {0} must be renamed to replace {1}; until then repository is corrupt patchApplyException=Cannot apply: {0} 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 30dcaa5b6f..d4eb0b3b21 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java @@ -169,7 +169,8 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { .setMessage(srcCommit.getFullMessage()) .setReflogComment(reflogPrefix + " " //$NON-NLS-1$ + srcCommit.getShortMessage()) - .setAuthor(srcCommit.getAuthorIdent()).call(); + .setAuthor(srcCommit.getAuthorIdent()) + .setNoVerify(true).call(); cherryPickedRefs.add(src); } else { if (merger.failed()) 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 f8406e0231..93400aa4d1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -42,8 +42,10 @@ */ package org.eclipse.jgit.api; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.PrintStream; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; @@ -56,6 +58,7 @@ import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoFilepatternException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.NoMessageException; +import org.eclipse.jgit.api.errors.RejectCommitException; import org.eclipse.jgit.api.errors.UnmergedPathsException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.dircache.DirCache; @@ -84,6 +87,9 @@ import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.ChangeIdUtil; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.Hook; +import org.eclipse.jgit.util.ProcessResult; /** * A class used to execute a {@code Commit} command. It has setters for all @@ -120,10 +126,19 @@ public class CommitCommand extends GitCommand<RevCommit> { private String reflogComment; /** + * Setting this option bypasses the {@link Hook#PRE_COMMIT pre-commit} and + * {@link Hook#COMMIT_MSG commit-msg} hooks. + */ + private boolean noVerify; + + private PrintStream hookOutRedirect; + + /** * @param repo */ protected CommitCommand(Repository repo) { super(repo); + hookOutRedirect = System.out; } /** @@ -144,11 +159,14 @@ public class CommitCommand extends GitCommand<RevCommit> { * else * @throws WrongRepositoryStateException * when repository is not in the right state for committing + * @throws RejectCommitException + * if there are either pre-commit or commit-msg hooks present in + * the repository and at least one of them rejects the commit. */ public RevCommit call() throws GitAPIException, NoHeadException, NoMessageException, UnmergedPathsException, - ConcurrentRefUpdateException, - WrongRepositoryStateException { + ConcurrentRefUpdateException, WrongRepositoryStateException, + RejectCommitException { checkCallable(); Collections.sort(only); @@ -160,6 +178,23 @@ public class CommitCommand extends GitCommand<RevCommit> { throw new WrongRepositoryStateException(MessageFormat.format( JGitText.get().cannotCommitOnARepoWithState, state.name())); + + if (!noVerify) { + final ByteArrayOutputStream errorByteArray = new ByteArrayOutputStream(); + final PrintStream hookErrRedirect = new PrintStream( + errorByteArray); + ProcessResult preCommitHookResult = FS.DETECTED.runIfPresent( + repo, Hook.PRE_COMMIT, new String[0], hookOutRedirect, + hookErrRedirect, null); + if (preCommitHookResult.getStatus() == ProcessResult.Status.OK + && preCommitHookResult.getExitCode() != 0) { + String errorMessage = MessageFormat.format( + JGitText.get().commitRejectedByHook, Hook.PRE_COMMIT.getName(), + errorByteArray.toString()); + throw new RejectCommitException(errorMessage); + } + } + processOptions(state, rw); if (all && !repo.isBare() && repo.getWorkTree() != null) { @@ -733,4 +768,36 @@ public class CommitCommand extends GitCommand<RevCommit> { return this; } + /** + * Sets the {@link #noVerify} option on this commit command. + * <p> + * Both the {@link Hook#PRE_COMMIT pre-commit} and {@link Hook#COMMIT_MSG + * commit-msg} hooks can block a commit by their return value; setting this + * option to <code>true</code> will bypass these two hooks. + * </p> + * + * @param noVerify + * Whether this commit should be verified by the pre-commit and + * commit-msg hooks. + * @return {@code this} + * @since 3.7 + */ + public CommitCommand setNoVerify(boolean noVerify) { + this.noVerify = noVerify; + return this; + } + + /** + * Set the output stream for hook scripts executed by this command. If not + * set it defaults to {@code System.out}. + * + * @param hookStdOut + * the output stream for hook scripts executed by this command + * @return {@code this} + * @since 3.7 + */ + public CommitCommand setHookOutputStream(PrintStream hookStdOut) { + this.hookOutRedirect = hookStdOut; + return this; + } } 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 47424a9074..7d3e823187 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -462,7 +462,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { String newMessage = interactiveHandler .modifyCommitMessage(oldMessage); newHead = new Git(repo).commit().setMessage(newMessage) - .setAmend(true).call(); + .setAmend(true).setNoVerify(true).call(); return null; case EDIT: rebaseState.createFile(AMEND, commitToPick.name()); @@ -768,15 +768,14 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } retNewHead = new Git(repo).commit() .setMessage(stripCommentLines(commitMessage)) - .setAmend(true).call(); + .setAmend(true).setNoVerify(true).call(); rebaseState.getFile(MESSAGE_SQUASH).delete(); rebaseState.getFile(MESSAGE_FIXUP).delete(); } else { // Next step is either Squash or Fixup - retNewHead = new Git(repo).commit() - .setMessage(commitMessage).setAmend(true) - .call(); + retNewHead = new Git(repo).commit().setMessage(commitMessage) + .setAmend(true).setNoVerify(true).call(); } return retNewHead; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java new file mode 100644 index 0000000000..6036a2731b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 Obeo. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api.errors; + +/** + * Exception thrown when a commit is rejected by a hook (either + * {@link org.eclipse.jgit.util.Hook#PRE_COMMIT pre-commit} or + * {@link org.eclipse.jgit.util.Hook#COMMIT_MSG commit-msg}). + * + * @since 3.7 + */ +public class RejectCommitException extends GitAPIException { + private static final long serialVersionUID = 1L; + + /** + * @param message + */ + public RejectCommitException(String message) { + super(message); + } +} 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 fd38dc1491..8c81c95134 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -163,6 +163,7 @@ public class JGitText extends TranslationBundle { /***/ public String commitMessageNotSpecified; /***/ public String commitOnRepoWithoutHEADCurrentlyNotSupported; /***/ public String commitAmendOnInitialNotPossible; + /***/ public String commitRejectedByHook; /***/ public String compressingObjects; /***/ public String connectionFailed; /***/ public String connectionTimeOut; @@ -252,6 +253,7 @@ public class JGitText extends TranslationBundle { /***/ public String errorOccurredDuringUnpackingOnTheRemoteEnd; /***/ public String errorReadingInfoRefs; /***/ public String errorSymlinksNotSupported; + /***/ public String exceptionCaughtDuringExecutionOfHook; /***/ public String exceptionCaughtDuringExecutionOfAddCommand; /***/ public String exceptionCaughtDuringExecutionOfArchiveCommand; /***/ public String exceptionCaughtDuringExecutionOfCherryPickCommand; @@ -265,6 +267,7 @@ public class JGitText extends TranslationBundle { /***/ public String exceptionCaughtDuringExecutionOfRevertCommand; /***/ public String exceptionCaughtDuringExecutionOfRmCommand; /***/ public String exceptionCaughtDuringExecutionOfTagCommand; + /***/ public String exceptionHookExecutionInterrupted; /***/ public String exceptionOccurredDuringAddingOfOptionToALogCommand; /***/ public String exceptionOccurredDuringReadingOfGIT_DIR; /***/ public String exceptionWhileReadingPack; @@ -451,6 +454,7 @@ public class JGitText extends TranslationBundle { /***/ public String packRefs; /***/ public String packSizeNotSetYet; /***/ public String packTooLargeForIndexVersion1; + /***/ public String packWasDeleted; /***/ public String packWriterStatistics; /***/ public String panicCantRenameIndexFile; /***/ public String patchApplyException; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java index 76fadefe53..adbe1f8652 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java @@ -557,6 +557,9 @@ public class ObjectDirectory extends FileObjectDatabase { tmpl = JGitText.get().corruptPack; // Assume the pack is corrupted, and remove it from the list. removePack(p); + } else if (e instanceof FileNotFoundException) { + tmpl = JGitText.get().packWasDeleted; + removePack(p); } else { tmpl = JGitText.get().exceptionWhileReadingPack; // Don't remove the pack from the list, as the error may be diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java index 705d54cfa3..ed0ed04d94 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -387,6 +387,13 @@ public final class Constants { public static final String MODULES = "modules"; /** + * Name of the folder (inside gitDir) where the hooks are stored. + * + * @since 3.7 + */ + public static final String HOOKS = "hooks"; + + /** * Create a new digest function for objects. * * @return a new digest object. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java index 0c63b190f2..081bf87c54 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -44,18 +44,31 @@ package org.eclipse.jgit.util; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.PrintWriter; import java.security.AccessController; import java.security.PrivilegedAction; import java.text.MessageFormat; import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.errors.SymlinksNotSupportedException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.ProcessResult.Status; /** Abstraction to support various file system operations not in Java. */ public abstract class FS { @@ -614,6 +627,288 @@ public abstract class FS { } /** + * See {@link FileUtils#relativize(String, String)}. + * + * @param base + * The path against which <code>other</code> should be + * relativized. + * @param other + * The path that will be made relative to <code>base</code>. + * @return A relative path that, when resolved against <code>base</code>, + * will yield the original <code>other</code>. + * @see FileUtils#relativize(String, String) + * @since 3.7 + */ + public String relativize(String base, String other) { + return FileUtils.relativize(base, other); + } + + /** + * Checks whether the given hook is defined for the given repository, then + * runs it with the given arguments. + * <p> + * The hook's standard output and error streams will be redirected to + * <code>System.out</code> and <code>System.err</code> respectively. The + * hook will have no stdin. + * </p> + * + * @param repository + * The repository for which a hook should be run. + * @param hook + * The hook to be executed. + * @param args + * Arguments to pass to this hook. Cannot be <code>null</code>, + * but can be an empty array. + * @return The ProcessResult describing this hook's execution. + * @throws JGitInternalException + * if we fail to run the hook somehow. Causes may include an + * interrupted process or I/O errors. + * @since 3.7 + */ + public ProcessResult runIfPresent(Repository repository, final Hook hook, + String[] args) throws JGitInternalException { + return runIfPresent(repository, hook, args, System.out, System.err, + null); + } + + /** + * Checks whether the given hook is defined for the given repository, then + * runs it with the given arguments. + * + * @param repository + * The repository for which a hook should be run. + * @param hook + * The hook to be executed. + * @param args + * Arguments to pass to this hook. Cannot be <code>null</code>, + * but can be an empty array. + * @param outRedirect + * A print stream on which to redirect the hook's stdout. Can be + * <code>null</code>, in which case the hook's standard output + * will be lost. + * @param errRedirect + * A print stream on which to redirect the hook's stderr. Can be + * <code>null</code>, in which case the hook's standard error + * will be lost. + * @param stdinArgs + * A string to pass on to the standard input of the hook. May be + * <code>null</code>. + * @return The ProcessResult describing this hook's execution. + * @throws JGitInternalException + * if we fail to run the hook somehow. Causes may include an + * interrupted process or I/O errors. + * @since 3.7 + */ + public ProcessResult runIfPresent(Repository repository, final Hook hook, + String[] args, PrintStream outRedirect, PrintStream errRedirect, + String stdinArgs) throws JGitInternalException { + return new ProcessResult(Status.NOT_SUPPORTED); + } + + /** + * See + * {@link #runIfPresent(Repository, Hook, String[], PrintStream, PrintStream, String)} + * . Should only be called by FS supporting shell scripts execution. + * + * @param repository + * The repository for which a hook should be run. + * @param hook + * The hook to be executed. + * @param args + * Arguments to pass to this hook. Cannot be <code>null</code>, + * but can be an empty array. + * @param outRedirect + * A print stream on which to redirect the hook's stdout. Can be + * <code>null</code>, in which case the hook's standard output + * will be lost. + * @param errRedirect + * A print stream on which to redirect the hook's stderr. Can be + * <code>null</code>, in which case the hook's standard error + * will be lost. + * @param stdinArgs + * A string to pass on to the standard input of the hook. May be + * <code>null</code>. + * @return The ProcessResult describing this hook's execution. + * @throws JGitInternalException + * if we fail to run the hook somehow. Causes may include an + * interrupted process or I/O errors. + * @since 3.7 + */ + protected ProcessResult internalRunIfPresent(Repository repository, + final Hook hook, String[] args, PrintStream outRedirect, + PrintStream errRedirect, String stdinArgs) + throws JGitInternalException { + final File hookFile = findHook(repository, hook); + if (hookFile == null) + return new ProcessResult(Status.NOT_PRESENT); + + final String hookPath = hookFile.getAbsolutePath(); + final File runDirectory; + if (repository.isBare()) + runDirectory = repository.getDirectory(); + else + runDirectory = repository.getWorkTree(); + final String cmd = relativize(runDirectory.getAbsolutePath(), + hookPath); + ProcessBuilder hookProcess = runInShell(cmd, args); + hookProcess.directory(runDirectory); + try { + return new ProcessResult(runProcess(hookProcess, outRedirect, + errRedirect, stdinArgs), Status.OK); + } catch (IOException e) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().exceptionCaughtDuringExecutionOfHook, + hook.getName()), e); + } catch (InterruptedException e) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().exceptionHookExecutionInterrupted, + hook.getName()), e); + } + } + + + /** + * Tries to find a hook matching the given one in the given repository. + * + * @param repository + * The repository within which to find a hook. + * @param hook + * The hook we're trying to find. + * @return The {@link File} containing this particular hook if it exists in + * the given repository, <code>null</code> otherwise. + * @since 3.7 + */ + public File findHook(Repository repository, final Hook hook) { + final File hookFile = new File(new File(repository.getDirectory(), + Constants.HOOKS), hook.getName()); + return hookFile.isFile() ? hookFile : null; + } + + /** + * Runs the given process until termination, clearing its stdout and stderr + * streams on-the-fly. + * + * @param hookProcessBuilder + * The process builder configured for this hook. + * @param outRedirect + * A print stream on which to redirect the hook's stdout. Can be + * <code>null</code>, in which case the hook's standard output + * will be lost. + * @param errRedirect + * A print stream on which to redirect the hook's stderr. Can be + * <code>null</code>, in which case the hook's standard error + * will be lost. + * @param stdinArgs + * A string to pass on to the standard input of the hook. Can be + * <code>null</code>. + * @return the exit value of this hook. + * @throws IOException + * if an I/O error occurs while executing this hook. + * @throws InterruptedException + * if the current thread is interrupted while waiting for the + * process to end. + * @since 3.7 + */ + protected int runProcess(ProcessBuilder hookProcessBuilder, + OutputStream outRedirect, OutputStream errRedirect, String stdinArgs) + throws IOException, InterruptedException { + final ExecutorService executor = Executors.newFixedThreadPool(2); + Process process = null; + // We'll record the first I/O exception that occurs, but keep on trying + // to dispose of our open streams and file handles + IOException ioException = null; + try { + process = hookProcessBuilder.start(); + final Callable<Void> errorGobbler = new StreamGobbler( + process.getErrorStream(), errRedirect); + final Callable<Void> outputGobbler = new StreamGobbler( + process.getInputStream(), outRedirect); + executor.submit(errorGobbler); + executor.submit(outputGobbler); + if (stdinArgs != null) { + final PrintWriter stdinWriter = new PrintWriter( + process.getOutputStream()); + stdinWriter.print(stdinArgs); + stdinWriter.flush(); + // We are done with this hook's input. Explicitly close its + // stdin now to kick off any blocking read the hook might have. + stdinWriter.close(); + } + return process.waitFor(); + } catch (IOException e) { + ioException = e; + } finally { + shutdownAndAwaitTermination(executor); + if (process != null) { + try { + process.waitFor(); + } catch (InterruptedException e) { + // Thrown by the outer try. + // Swallow this one to carry on our cleanup, and clear the + // interrupted flag (processes throw the exception without + // clearing the flag). + Thread.interrupted(); + } + // A process doesn't clean its own resources even when destroyed + // Explicitly try and close all three streams, preserving the + // outer I/O exception if any. + try { + process.getErrorStream().close(); + } catch (IOException e) { + ioException = ioException != null ? ioException : e; + } + try { + process.getInputStream().close(); + } catch (IOException e) { + ioException = ioException != null ? ioException : e; + } + try { + process.getOutputStream().close(); + } catch (IOException e) { + ioException = ioException != null ? ioException : e; + } + process.destroy(); + } + } + // We can only be here if the outer try threw an IOException. + throw ioException; + } + + /** + * Shuts down an {@link ExecutorService} in two phases, first by calling + * {@link ExecutorService#shutdown() shutdown} to reject incoming tasks, and + * then calling {@link ExecutorService#shutdownNow() shutdownNow}, if + * necessary, to cancel any lingering tasks. Returns true if the pool has + * been properly shutdown, false otherwise. + * <p> + * + * @param pool + * the pool to shutdown + * @return <code>true</code> if the pool has been properly shutdown, + * <code>false</code> otherwise. + */ + private static boolean shutdownAndAwaitTermination(ExecutorService pool) { + boolean hasShutdown = true; + pool.shutdown(); // Disable new tasks from being submitted + try { + // Wait a while for existing tasks to terminate + if (!pool.awaitTermination(5, TimeUnit.SECONDS)) { + pool.shutdownNow(); // Cancel currently executing tasks + // Wait a while for tasks to respond to being canceled + if (!pool.awaitTermination(5, TimeUnit.SECONDS)) + hasShutdown = false; + } + } catch (InterruptedException ie) { + // (Re-)Cancel if current thread also interrupted + pool.shutdownNow(); + // Preserve interrupt status + Thread.currentThread().interrupt(); + hasShutdown = false; + } + return hasShutdown; + } + + /** * Initialize a ProcesssBuilder to run a command using the system shell. * * @param cmd @@ -802,4 +1097,50 @@ public abstract class FS { public String normalize(String name) { return name; } + + /** + * This runnable will consume an input stream's content into an output + * stream as soon as it gets available. + * <p> + * Typically used to empty processes' standard output and error, preventing + * them to choke. + * </p> + * <p> + * <b>Note</b> that a {@link StreamGobbler} will never close either of its + * streams. + * </p> + */ + private static class StreamGobbler implements Callable<Void> { + private final BufferedReader reader; + + private final BufferedWriter writer; + + public StreamGobbler(InputStream stream, OutputStream output) { + this.reader = new BufferedReader(new InputStreamReader(stream)); + if (output == null) + this.writer = null; + else + this.writer = new BufferedWriter(new OutputStreamWriter(output)); + } + + public Void call() throws IOException { + boolean writeFailure = false; + + String line = null; + while ((line = reader.readLine()) != null) { + // Do not try to write again after a failure, but keep reading + // as long as possible to prevent the input stream from choking. + if (!writeFailure && writer != null) { + try { + writer.write(line); + writer.newLine(); + writer.flush(); + } catch (IOException e) { + writeFailure = true; + } + } + } + return null; + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java index b7de056ee4..ee29584239 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java @@ -44,11 +44,14 @@ package org.eclipse.jgit.util; import java.io.File; import java.io.IOException; +import java.io.PrintStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.lib.Repository; /** * Base FS for POSIX based systems @@ -121,4 +124,15 @@ public abstract class FS_POSIX extends FS { proc.command(argv); return proc; } + + /** + * @since 3.7 + */ + @Override + public ProcessResult runIfPresent(Repository repository, Hook hook, + String[] args, PrintStream outRedirect, PrintStream errRedirect, + String stdinArgs) throws JGitInternalException { + return internalRunIfPresent(repository, hook, args, outRedirect, + errRedirect, stdinArgs); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java index 81f2001125..d0abd33275 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java @@ -44,12 +44,15 @@ package org.eclipse.jgit.util; import java.io.File; +import java.io.PrintStream; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.lib.Repository; /** * FS implementation for Cygwin on Windows @@ -135,4 +138,24 @@ public class FS_Win32_Cygwin extends FS_Win32 { proc.command(argv); return proc; } + + /** + * @since 3.7 + */ + @Override + public String relativize(String base, String other) { + final String relativized = super.relativize(base, other); + return relativized.replace(File.separatorChar, '/'); + } + + /** + * @since 3.7 + */ + @Override + public ProcessResult runIfPresent(Repository repository, Hook hook, + String[] args, PrintStream outRedirect, PrintStream errRedirect, + String stdinArgs) throws JGitInternalException { + return internalRunIfPresent(repository, hook, args, outRedirect, + errRedirect, stdinArgs); + } } 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 9e5b22180b..1e58245ea7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java @@ -51,6 +51,7 @@ import java.nio.channels.FileLock; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; import org.eclipse.jgit.internal.JGitText; @@ -387,4 +388,69 @@ public class FileUtils { } throw new IOException(JGitText.get().cannotCreateTempDir); } + + /** + * This will try and make a given path relative to another. + * <p> + * For example, if this is called with the two following paths : + * + * <pre> + * <code>base = "c:\\Users\\jdoe\\eclipse\\git\\project"</code> + * <code>other = "c:\\Users\\jdoe\\eclipse\\git\\another_project\\pom.xml"</code> + * </pre> + * + * This will return "..\\another_project\\pom.xml". + * </p> + * <p> + * This method uses {@link File#separator} to split the paths into segments. + * </p> + * <p> + * <b>Note</b> that this will return the empty String if <code>base</code> + * and <code>other</code> are equal. + * </p> + * + * @param base + * The path against which <code>other</code> should be + * relativized. This will be assumed to denote the path to a + * folder and not a file. + * @param other + * The path that will be made relative to <code>base</code>. + * @return A relative path that, when resolved against <code>base</code>, + * will yield the original <code>other</code>. + * @since 3.7 + */ + public static String relativize(String base, String other) { + if (base.equals(other)) + return ""; //$NON-NLS-1$ + + final boolean ignoreCase = !FS.DETECTED.isCaseSensitive(); + final String[] baseSegments = base.split(Pattern.quote(File.separator)); + final String[] otherSegments = other.split(Pattern + .quote(File.separator)); + + int commonPrefix = 0; + while (commonPrefix < baseSegments.length + && commonPrefix < otherSegments.length) { + if (ignoreCase + && baseSegments[commonPrefix] + .equalsIgnoreCase(otherSegments[commonPrefix])) + commonPrefix++; + else if (!ignoreCase + && baseSegments[commonPrefix] + .equals(otherSegments[commonPrefix])) + commonPrefix++; + else + break; + } + + final StringBuilder builder = new StringBuilder(); + for (int i = commonPrefix; i < baseSegments.length; i++) + builder.append("..").append(File.separator); //$NON-NLS-1$ + for (int i = commonPrefix; i < otherSegments.length; i++) { + builder.append(otherSegments[i]); + if (i < otherSegments.length - 1) + builder.append(File.separator); + } + return builder.toString(); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java new file mode 100644 index 0000000000..c24c9a3d0a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2014 Obeo. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +/** + * An enum describing the different hooks a user can implement to customize his + * repositories. + * + * @since 3.7 + */ +public enum Hook { + /** + * Literal for the "pre-commit" git hook. + * <p> + * This hook is invoked by git commit, and can be bypassed with the + * "no-verify" option. It takes no parameter, and is invoked before + * obtaining the proposed commit log message and making a commit. + * </p> + * <p> + * A non-zero exit code from the called hook means that the commit should be + * aborted. + * </p> + */ + PRE_COMMIT("pre-commit"), //$NON-NLS-1$ + + /** + * Literal for the "prepare-commit-msg" git hook. + * <p> + * This hook is invoked by git commit right after preparing the default + * message, and before any editing possibility is displayed to the user. + * </p> + * <p> + * A non-zero exit code from the called hook means that the commit should be + * aborted. + * </p> + */ + PREPARE_COMMIT_MSG("prepare-commit-msg"), //$NON-NLS-1$ + + /** + * Literal for the "commit-msg" git hook. + * <p> + * This hook is invoked by git commit, and can be bypassed with the + * "no-verify" option. Its single parameter is the path to the file + * containing the prepared commit message (typically + * "<gitdir>/COMMIT-EDITMSG"). + * </p> + * <p> + * A non-zero exit code from the called hook means that the commit should be + * aborted. + * </p> + */ + COMMIT_MSG("commit-msg"), //$NON-NLS-1$ + + /** + * Literal for the "post-commit" git hook. + * <p> + * This hook is invoked by git commit. It takes no parameter and is invoked + * after a commit has been made. + * </p> + * <p> + * The exit code of this hook has no significance. + * </p> + */ + POST_COMMIT("post-commit"), //$NON-NLS-1$ + + /** + * Literal for the "post-rewrite" git hook. + * <p> + * This hook is invoked after commands that rewrite commits (currently, only + * "git rebase" and "git commit --amend"). It a single argument denoting the + * source of the call (one of <code>rebase</code> or <code>amend</code>). It + * then accepts a list of rewritten commits through stdin, in the form + * <code><old SHA-1> <new SHA-1>LF</code>. + * </p> + * <p> + * The exit code of this hook has no significance. + * </p> + */ + POST_REWRITE("post-rewrite"), //$NON-NLS-1$ + + /** + * Literal for the "pre-rebase" git hook. + * <p> + * </p> + * This hook is invoked right before the rebase operation runs. It accepts + * up to two parameters, the first being the upstream from which the branch + * to rebase has been forked. If the tip of the series of commits to rebase + * is HEAD, the other parameter is unset. Otherwise, that tip is passed as + * the second parameter of the script. + * <p> + * A non-zero exit code from the called hook means that the rebase should be + * aborted. + * </p> + */ + PRE_REBASE("pre-rebase"); //$NON-NLS-1$ + + private final String name; + + private Hook(String name) { + this.name = name; + } + + /** + * @return The name of this hook. + */ + public String getName() { + return name; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java new file mode 100644 index 0000000000..f56bb1577e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2014 Obeo. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +/** + * Describes the result of running an external process. + * + * @since 3.7 + */ +public class ProcessResult { + /** + * Status of a process' execution. + */ + public static enum Status { + /** + * The script was found and launched properly. It may still have exited + * with a non-zero {@link #exitCode}. + */ + OK, + + /** The script was not found on disk and thus could not be launched. */ + NOT_PRESENT, + + /** + * The script was found but could not be launched since it was not + * supported by the current {@link FS}. + */ + NOT_SUPPORTED; + } + + /** The exit code of the process. */ + private final int exitCode; + + /** Status of the process' execution. */ + private final Status status; + + /** + * Instantiates a process result with the given status and an exit code of + * <code>-1</code>. + * + * @param status + * Status describing the execution of the external process. + */ + public ProcessResult(Status status) { + this(-1, status); + } + + /** + * @param exitCode + * Exit code of the process. + * @param status + * Status describing the execution of the external process. + */ + public ProcessResult(int exitCode, Status status) { + this.exitCode = exitCode; + this.status = status; + } + + /** + * @return The exit code of the process. + */ + public int getExitCode() { + return exitCode; + } + + /** + * @return The status of the process' execution. + */ + public Status getStatus() { + return status; + } +} |