From 26fd56f167e6377777e6d46c14779183e4bcb55a Mon Sep 17 00:00:00 2001 From: Laurent Delaigue Date: Mon, 23 Feb 2015 11:18:50 +0100 Subject: [PATCH] Refactored pre-commit hook to make it less invasive. Hooks are now obtained via a convenient API like git commands, and callers don't have to check for their existence. The pre-commit hook has been updated accordingly. Change-Id: I3383ffb10e2f3b588d7367b9139b606ec7f62758 Signed-off-by: Laurent Delaigue Signed-off-by: Matthias Sohn --- .../META-INF/MANIFEST.MF | 1 + .../src/org/eclipse/jgit/util/HookTest.java | 32 ++-- .../org/eclipse/jgit/util/FS_POSIX_Java7.java | 4 +- .../jgit/util/FS_Win32_Java7Cygwin.java | 4 +- org.eclipse.jgit/META-INF/MANIFEST.MF | 1 + .../eclipse/jgit/internal/JGitText.properties | 2 +- .../org/eclipse/jgit/api/CommitCommand.java | 37 ++-- .../api/errors/AbortedByHookException.java | 104 ++++++++++++ .../src/org/eclipse/jgit/hooks/GitHook.java | 159 ++++++++++++++++++ .../Hooks.java} | 25 +-- .../org/eclipse/jgit/hooks/PreCommitHook.java | 84 +++++++++ .../org/eclipse/jgit/internal/JGitText.java | 2 +- .../src/org/eclipse/jgit/util/FS.java | 48 +++--- .../src/org/eclipse/jgit/util/FS_POSIX.java | 6 +- .../eclipse/jgit/util/FS_Win32_Cygwin.java | 6 +- .../src/org/eclipse/jgit/util/Hook.java | 149 ---------------- .../org/eclipse/jgit/util/ProcessResult.java | 9 + 17 files changed, 435 insertions(+), 238 deletions(-) create mode 100755 org.eclipse.jgit/src/org/eclipse/jgit/api/errors/AbortedByHookException.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java rename org.eclipse.jgit/src/org/eclipse/jgit/{api/errors/RejectCommitException.java => hooks/Hooks.java} (79%) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java delete mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java diff --git a/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF index a1b2782cb..a160c8c49 100644 --- a/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF @@ -9,6 +9,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.hooks;version="[4.0.0,4.1.0)", org.eclipse.jgit.internal.storage.file;version="4.0.0", org.eclipse.jgit.junit;version="[4.0.0,4.1.0)", org.eclipse.jgit.lib;version="[4.0.0,4.1.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 index 96550889c..0324efca6 100644 --- 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 @@ -52,7 +52,8 @@ 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.api.errors.AbortedByHookException; +import org.eclipse.jgit.hooks.PreCommitHook; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; import org.junit.Assume; @@ -64,25 +65,25 @@ public class HookTest extends RepositoryTestCase { 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(), + assertNull("no hook should be installed", + FS.DETECTED.findHook(db, PreCommitHook.NAME)); + File hookFile = writeHookFile(PreCommitHook.NAME, "#!/bin/bash\necho \"test $1 $2\""); - assertEquals("exected to find pre-commit hook", hookFile, - FS.DETECTED.findHook(db, h)); + assertEquals("expected to find pre-commit hook", hookFile, + FS.DETECTED.findHook(db, PreCommitHook.NAME)); } @Test public void testRunHook() throws Exception { assumeSupportedPlatform(); - Hook h = Hook.PRE_COMMIT; - writeHookFile( - h.getName(), + writeHookFile(PreCommitHook.NAME, "#!/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[] { + ProcessResult res = FS.DETECTED.runHookIfPresent(db, + PreCommitHook.NAME, + new String[] { "arg1", "arg2" }, new PrintStream(out), new PrintStream(err), "stdin"); assertEquals("unexpected hook output", "test arg1 arg2\nstdin\n", @@ -95,11 +96,10 @@ public class HookTest extends RepositoryTestCase { } @Test - public void testPreCommitHook() throws Exception { + public void testFailedPreCommitHookBlockCommit() throws Exception { assumeSupportedPlatform(); - Hook h = Hook.PRE_COMMIT; - writeHookFile(h.getName(), + writeHookFile(PreCommitHook.NAME, "#!/bin/sh\necho \"test\"\n\necho 1>&2 \"stderr\"\nexit 1"); Git git = Git.wrap(db); String path = "a.txt"; @@ -110,14 +110,12 @@ public class HookTest extends RepositoryTestCase { git.commit().setMessage("commit") .setHookOutputStream(new PrintStream(out)).call(); fail("expected pre-commit hook to abort commit"); - } catch (RejectCommitException e) { + } catch (AbortedByHookException e) { assertEquals("unexpected error message from pre-commit hook", - "Commit rejected by \"pre-commit\" hook.\nstderr\n", + "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); } } 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 300cf93bc..118e166a2 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 @@ -352,10 +352,10 @@ public class FS_POSIX_Java7 extends FS_POSIX { * @since 3.7 */ @Override - public File findHook(Repository repository, Hook hook) { + public File findHook(Repository repository, String hookName) { final File gitdir = repository.getDirectory(); final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS) - .resolve(hook.getName()); + .resolve(hookName); 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 b6e5d9388..4c371de9d 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 @@ -145,10 +145,10 @@ public class FS_Win32_Java7Cygwin extends FS_Win32_Cygwin { * @since 3.7 */ @Override - public File findHook(Repository repository, Hook hook) { + public File findHook(Repository repository, String hookName) { final File gitdir = repository.getDirectory(); final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS) - .resolve(hook.getName()); + .resolve(hookName); if (Files.isExecutable(hookPath)) return hookPath.toFile(); return null; diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index b403052a5..42c48a17b 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -53,6 +53,7 @@ Export-Package: org.eclipse.jgit.api;version="4.0.0"; org.eclipse.jgit.lib, org.eclipse.jgit.revwalk", org.eclipse.jgit.gitrepo.internal;version="4.0.0";x-internal:=true, + org.eclipse.jgit.hooks;version="4.0.0", org.eclipse.jgit.ignore;version="4.0.0", org.eclipse.jgit.ignore.internal;version="4.0.0";x-friends:="org.eclipse.jgit.test", org.eclipse.jgit.internal;version="4.0.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test", 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 109027d93..a003b02cc 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -99,12 +99,12 @@ checkoutUnexpectedResult=Checkout returned unexpected result {0} classCastNotA=Not a {0} cloneNonEmptyDirectory=Destination path "{0}" already exists and is not an empty directory collisionOn=Collision on {0} +commandRejectedByHook=Rejected by "{0}" hook.\n{1} commandWasCalledInTheWrongState=Command {0} was called in the wrong state 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} 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 93400aa4d..b5c726eed 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -42,7 +42,6 @@ */ package org.eclipse.jgit.api; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; @@ -52,13 +51,13 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; 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; @@ -67,6 +66,7 @@ import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.UnmergedPathException; +import org.eclipse.jgit.hooks.Hooks; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; @@ -87,9 +87,6 @@ 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 @@ -126,8 +123,7 @@ public class CommitCommand extends GitCommand { private String reflogComment; /** - * Setting this option bypasses the {@link Hook#PRE_COMMIT pre-commit} and - * {@link Hook#COMMIT_MSG commit-msg} hooks. + * Setting this option bypasses the pre-commit and commit-msg hooks. */ private boolean noVerify; @@ -138,7 +134,6 @@ public class CommitCommand extends GitCommand { */ protected CommitCommand(Repository repo) { super(repo); - hookOutRedirect = System.out; } /** @@ -159,14 +154,14 @@ public class CommitCommand extends GitCommand { * else * @throws WrongRepositoryStateException * when repository is not in the right state for committing - * @throws RejectCommitException + * @throws AbortedByHookException * if there are either pre-commit or commit-msg hooks present in - * the repository and at least one of them rejects the commit. + * the repository and one of them rejects the commit. */ public RevCommit call() throws GitAPIException, NoHeadException, NoMessageException, UnmergedPathsException, ConcurrentRefUpdateException, WrongRepositoryStateException, - RejectCommitException { + AbortedByHookException { checkCallable(); Collections.sort(only); @@ -180,19 +175,7 @@ public class CommitCommand extends GitCommand { 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); - } + Hooks.preCommit(repo, hookOutRedirect).call(); } processOptions(state, rw); @@ -771,9 +754,9 @@ public class CommitCommand extends GitCommand { /** * Sets the {@link #noVerify} option on this commit command. *

- * 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 true will bypass these two hooks. + * Both the pre-commit and commit-msg hooks can block a commit by their + * return value; setting this option to true will bypass these + * two hooks. *

* * @param noVerify diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/AbortedByHookException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/AbortedByHookException.java new file mode 100755 index 000000000..995611ef0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/AbortedByHookException.java @@ -0,0 +1,104 @@ +/* + * 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; + +import java.text.MessageFormat; + +import org.eclipse.jgit.internal.JGitText; + +/** + * Exception thrown when a hook returns a process result with a value different + * from 0. It is up to the caller to decide whether this should block execution + * or not. + * + * @since 4.0 + */ +public class AbortedByHookException extends GitAPIException { + private static final long serialVersionUID = 1L; + + /** + * The hook that caused this exception. + */ + private final String hookName; + + /** + * The process result. + */ + private final int returnCode; + + /** + * @param message + * The error details. + * @param hookName + * The name of the hook that interrupted the command, must not be + * null. + * @param returnCode + * The return code of the hook process that has been run. + */ + public AbortedByHookException(String message, String hookName, + int returnCode) { + super(message); + this.hookName = hookName; + this.returnCode = returnCode; + } + + /** + * @return the type of the hook that interrupted the git command. + */ + public String getHookName() { + return hookName; + } + + /** + * @return the hook process result. + */ + public int getReturnCode() { + return returnCode; + } + + @Override + public String getMessage() { + return MessageFormat.format(JGitText.get().commandRejectedByHook, + hookName, super.getMessage()); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java new file mode 100644 index 000000000..ad2eeb020 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java @@ -0,0 +1,159 @@ +/* + * 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.hooks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.concurrent.Callable; + +import org.eclipse.jgit.api.errors.AbortedByHookException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.ProcessResult; + +/** + * Git can fire off custom scripts when certain important actions occur. These + * custom scripts are called "hooks". There are two groups of hooks: client-side + * (that run on local operations such as committing and merging), and + * server-side (that run on network operations such as receiving pushed + * commits). This is the abstract super-class of the different hook + * implementations in JGit. + * + * @param + * the return type which is expected from {@link #call()} + * @see Git + * Hooks on the git-scm official site + * @since 4.0 + */ +abstract class GitHook implements Callable { + + private final Repository repo; + + /** + * The output stream to be used by the hook. + */ + protected final PrintStream outputStream; + + /** + * @param repo + * @param outputStream + * The output stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.out}. + */ + protected GitHook(Repository repo, PrintStream outputStream) { + this.repo = repo; + this.outputStream = outputStream; + } + + /** + * Run the hook. + * + * @throws IOException + * if IO goes wrong. + * @throws AbortedByHookException + * If the hook has been run and a returned an exit code + * different from zero. + */ + public abstract T call() throws IOException, AbortedByHookException; + + /** + * @return The name of the hook, which must not be {@code null}. + */ + public abstract String getHookName(); + + /** + * @return The repository. + */ + protected Repository getRepository() { + return repo; + } + + /** + * Override this method when needed to provide relevant parameters to the + * underlying hook script. The default implementation returns an empty + * array. + * + * @return The parameters the hook receives. + */ + protected String[] getParameters() { + return new String[0]; + } + + /** + * Override to provide relevant arguments via stdin to the underlying hook + * script. The default implementation returns {@code null}. + * + * @return The parameters the hook receives. + */ + protected String getStdinArgs() { + return null; + } + + /** + * @return The output stream the hook must use. Never {@code null}, + * {@code System.out} is returned by default. + */ + protected PrintStream getOutputStream() { + return outputStream == null ? System.out : outputStream; + } + + /** + * Runs the hook, without performing any validity checks. + * + * @throws AbortedByHookException + * If the underlying hook script exited with non-zero. + */ + protected void doRun() throws AbortedByHookException { + final ByteArrayOutputStream errorByteArray = new ByteArrayOutputStream(); + final PrintStream hookErrRedirect = new PrintStream(errorByteArray); + ProcessResult result = FS.DETECTED.runHookIfPresent(getRepository(), + getHookName(), getParameters(), getOutputStream(), + hookErrRedirect, getStdinArgs()); + if (result.isExecutedWithError()) { + throw new AbortedByHookException(errorByteArray.toString(), + getHookName(), result.getExitCode()); + } + } + +} \ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java similarity index 79% rename from org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java rename to org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java index 6036a2731..a2e4fa560 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java @@ -40,22 +40,27 @@ * 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; +package org.eclipse.jgit.hooks; + +import java.io.PrintStream; + +import org.eclipse.jgit.lib.Repository; /** - * 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}). + * Factory class for instantiating supported hooks. * - * @since 3.7 + * @since 4.0 */ -public class RejectCommitException extends GitAPIException { - private static final long serialVersionUID = 1L; +public class Hooks { /** - * @param message + * @param repo + * @param outputStream + * The output stream, or {@code null} to use {@code System.out} + * @return The pre-commit hook for the given repository. */ - public RejectCommitException(String message) { - super(message); + public static PreCommitHook preCommit(Repository repo, + PrintStream outputStream) { + return new PreCommitHook(repo, outputStream); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java new file mode 100644 index 000000000..1ab32e0c2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java @@ -0,0 +1,84 @@ +/* + * 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.hooks; + +import java.io.IOException; +import java.io.PrintStream; + +import org.eclipse.jgit.api.errors.AbortedByHookException; +import org.eclipse.jgit.lib.Repository; + +/** + * The pre-commit hook implementation. This hook is run before the + * commit and can reject the commit. + * + * @since 4.0 + */ +public class PreCommitHook extends GitHook { + + /** The pre-commit hook name. */ + public static final String NAME = "pre-commit"; //$NON-NLS-1$ + + /** + * @param repo + * The repository + * @param outputStream + * The output stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.out}. + */ + protected PreCommitHook(Repository repo, PrintStream outputStream) { + super(repo, outputStream); + } + + @Override + public Void call() throws IOException, AbortedByHookException { + doRun(); + return null; + } + + @Override + public String getHookName() { + return NAME; + } + +} 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 d567a2474..63b788f6b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -158,12 +158,12 @@ public class JGitText extends TranslationBundle { /***/ public String classCastNotA; /***/ public String cloneNonEmptyDirectory; /***/ public String collisionOn; + /***/ public String commandRejectedByHook; /***/ public String commandWasCalledInTheWrongState; /***/ public String commitAlreadyExists; /***/ public String commitMessageNotSpecified; /***/ public String commitOnRepoWithoutHEADCurrentlyNotSupported; /***/ public String commitAmendOnInitialNotPossible; - /***/ public String commitRejectedByHook; /***/ public String compressingObjects; /***/ public String connectionFailed; /***/ public String connectionTimeOut; 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 875e12f57..de09ad4f7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -660,8 +660,8 @@ public abstract class FS { * * @param repository * The repository for which a hook should be run. - * @param hook - * The hook to be executed. + * @param hookName + * The name of the hook to be executed. * @param args * Arguments to pass to this hook. Cannot be null, * but can be an empty array. @@ -669,11 +669,12 @@ public abstract class FS { * @throws JGitInternalException * if we fail to run the hook somehow. Causes may include an * interrupted process or I/O errors. - * @since 3.7 + * @since 4.0 */ - public ProcessResult runIfPresent(Repository repository, final Hook hook, + public ProcessResult runHookIfPresent(Repository repository, + final String hookName, String[] args) throws JGitInternalException { - return runIfPresent(repository, hook, args, System.out, System.err, + return runHookIfPresent(repository, hookName, args, System.out, System.err, null); } @@ -683,8 +684,8 @@ public abstract class FS { * * @param repository * The repository for which a hook should be run. - * @param hook - * The hook to be executed. + * @param hookName + * The name of the hook to be executed. * @param args * Arguments to pass to this hook. Cannot be null, * but can be an empty array. @@ -703,9 +704,10 @@ public abstract class FS { * @throws JGitInternalException * if we fail to run the hook somehow. Causes may include an * interrupted process or I/O errors. - * @since 3.7 + * @since 4.0 */ - public ProcessResult runIfPresent(Repository repository, final Hook hook, + public ProcessResult runHookIfPresent(Repository repository, + final String hookName, String[] args, PrintStream outRedirect, PrintStream errRedirect, String stdinArgs) throws JGitInternalException { return new ProcessResult(Status.NOT_SUPPORTED); @@ -713,13 +715,13 @@ public abstract class FS { /** * See - * {@link #runIfPresent(Repository, Hook, String[], PrintStream, PrintStream, String)} + * {@link #runHookIfPresent(Repository, String, 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 hookName + * The name of the hook to be executed. * @param args * Arguments to pass to this hook. Cannot be null, * but can be an empty array. @@ -738,13 +740,13 @@ public abstract class FS { * @throws JGitInternalException * if we fail to run the hook somehow. Causes may include an * interrupted process or I/O errors. - * @since 3.7 + * @since 4.0 */ - protected ProcessResult internalRunIfPresent(Repository repository, - final Hook hook, String[] args, PrintStream outRedirect, + protected ProcessResult internalRunHookIfPresent(Repository repository, + final String hookName, String[] args, PrintStream outRedirect, PrintStream errRedirect, String stdinArgs) throws JGitInternalException { - final File hookFile = findHook(repository, hook); + final File hookFile = findHook(repository, hookName); if (hookFile == null) return new ProcessResult(Status.NOT_PRESENT); @@ -764,11 +766,11 @@ public abstract class FS { } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().exceptionCaughtDuringExecutionOfHook, - hook.getName()), e); + hookName), e); } catch (InterruptedException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().exceptionHookExecutionInterrupted, - hook.getName()), e); + hookName), e); } } @@ -778,15 +780,15 @@ public abstract class FS { * * @param repository * The repository within which to find a hook. - * @param hook - * The hook we're trying to find. + * @param hookName + * The name of the hook we're trying to find. * @return The {@link File} containing this particular hook if it exists in * the given repository, null otherwise. - * @since 3.7 + * @since 4.0 */ - public File findHook(Repository repository, final Hook hook) { + public File findHook(Repository repository, final String hookName) { final File hookFile = new File(new File(repository.getDirectory(), - Constants.HOOKS), hook.getName()); + Constants.HOOKS), hookName); return hookFile.isFile() ? hookFile : 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 ee2958423..a6984baa3 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 @@ -126,13 +126,13 @@ public abstract class FS_POSIX extends FS { } /** - * @since 3.7 + * @since 4.0 */ @Override - public ProcessResult runIfPresent(Repository repository, Hook hook, + public ProcessResult runHookIfPresent(Repository repository, String hookName, String[] args, PrintStream outRedirect, PrintStream errRedirect, String stdinArgs) throws JGitInternalException { - return internalRunIfPresent(repository, hook, args, outRedirect, + return internalRunHookIfPresent(repository, hookName, 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 d0abd3327..000bdcef5 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 @@ -149,13 +149,13 @@ public class FS_Win32_Cygwin extends FS_Win32 { } /** - * @since 3.7 + * @since 4.0 */ @Override - public ProcessResult runIfPresent(Repository repository, Hook hook, + public ProcessResult runHookIfPresent(Repository repository, String hookName, String[] args, PrintStream outRedirect, PrintStream errRedirect, String stdinArgs) throws JGitInternalException { - return internalRunIfPresent(repository, hook, args, outRedirect, + return internalRunHookIfPresent(repository, hookName, args, outRedirect, errRedirect, stdinArgs); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java deleted file mode 100644 index c24c9a3d0..000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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. - *

- * 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. - *

- *

- * A non-zero exit code from the called hook means that the commit should be - * aborted. - *

- */ - PRE_COMMIT("pre-commit"), //$NON-NLS-1$ - - /** - * Literal for the "prepare-commit-msg" git hook. - *

- * This hook is invoked by git commit right after preparing the default - * message, and before any editing possibility is displayed to the user. - *

- *

- * A non-zero exit code from the called hook means that the commit should be - * aborted. - *

- */ - PREPARE_COMMIT_MSG("prepare-commit-msg"), //$NON-NLS-1$ - - /** - * Literal for the "commit-msg" git hook. - *

- * 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"). - *

- *

- * A non-zero exit code from the called hook means that the commit should be - * aborted. - *

- */ - COMMIT_MSG("commit-msg"), //$NON-NLS-1$ - - /** - * Literal for the "post-commit" git hook. - *

- * This hook is invoked by git commit. It takes no parameter and is invoked - * after a commit has been made. - *

- *

- * The exit code of this hook has no significance. - *

- */ - POST_COMMIT("post-commit"), //$NON-NLS-1$ - - /** - * Literal for the "post-rewrite" git hook. - *

- * 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 rebase or amend). It - * then accepts a list of rewritten commits through stdin, in the form - * <old SHA-1> <new SHA-1>LF. - *

- *

- * The exit code of this hook has no significance. - *

- */ - POST_REWRITE("post-rewrite"), //$NON-NLS-1$ - - /** - * Literal for the "pre-rebase" git hook. - *

- *

- * 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. - *

- * A non-zero exit code from the called hook means that the rebase should be - * aborted. - *

- */ - 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 index f56bb1577..77c9608c7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java @@ -109,4 +109,13 @@ public class ProcessResult { public Status getStatus() { return status; } + + /** + * @return true if the execution occurred and resulted in a + * return code different from 0, false otherwise. + * @since 4.0 + */ + public boolean isExecutedWithError() { + return getStatus() == ProcessResult.Status.OK && getExitCode() != 0; + } }