Browse Source

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 <laurent.delaigue@obeo.fr>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
stable-4.0
Laurent Delaigue 10 years ago committed by Christian Halstrick
parent
commit
26fd56f167
  1. 1
      org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF
  2. 32
      org.eclipse.jgit.java7.test/src/org/eclipse/jgit/util/HookTest.java
  3. 4
      org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_POSIX_Java7.java
  4. 4
      org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_Win32_Java7Cygwin.java
  5. 1
      org.eclipse.jgit/META-INF/MANIFEST.MF
  6. 2
      org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
  7. 37
      org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
  8. 104
      org.eclipse.jgit/src/org/eclipse/jgit/api/errors/AbortedByHookException.java
  9. 159
      org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java
  10. 25
      org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java
  11. 84
      org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java
  12. 2
      org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
  13. 48
      org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
  14. 6
      org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
  15. 6
      org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
  16. 149
      org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java
  17. 9
      org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java

1
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.api.errors;version="[4.0.0,4.1.0)",
org.eclipse.jgit.diff;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.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.internal.storage.file;version="4.0.0",
org.eclipse.jgit.junit;version="[4.0.0,4.1.0)", org.eclipse.jgit.junit;version="[4.0.0,4.1.0)",
org.eclipse.jgit.lib;version="[4.0.0,4.1.0)", org.eclipse.jgit.lib;version="[4.0.0,4.1.0)",

32
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 java.io.PrintStream;
import org.eclipse.jgit.api.Git; 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.JGitTestUtil;
import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.junit.RepositoryTestCase;
import org.junit.Assume; import org.junit.Assume;
@ -64,25 +65,25 @@ public class HookTest extends RepositoryTestCase {
public void testFindHook() throws Exception { public void testFindHook() throws Exception {
assumeSupportedPlatform(); assumeSupportedPlatform();
Hook h = Hook.PRE_COMMIT; assertNull("no hook should be installed",
assertNull("no hook should be installed", FS.DETECTED.findHook(db, h)); FS.DETECTED.findHook(db, PreCommitHook.NAME));
File hookFile = writeHookFile(h.getName(), File hookFile = writeHookFile(PreCommitHook.NAME,
"#!/bin/bash\necho \"test $1 $2\""); "#!/bin/bash\necho \"test $1 $2\"");
assertEquals("exected to find pre-commit hook", hookFile, assertEquals("expected to find pre-commit hook", hookFile,
FS.DETECTED.findHook(db, h)); FS.DETECTED.findHook(db, PreCommitHook.NAME));
} }
@Test @Test
public void testRunHook() throws Exception { public void testRunHook() throws Exception {
assumeSupportedPlatform(); assumeSupportedPlatform();
Hook h = Hook.PRE_COMMIT; writeHookFile(PreCommitHook.NAME,
writeHookFile(
h.getName(),
"#!/bin/sh\necho \"test $1 $2\"\nread INPUT\necho $INPUT\necho 1>&2 \"stderr\""); "#!/bin/sh\necho \"test $1 $2\"\nread INPUT\necho $INPUT\necho 1>&2 \"stderr\"");
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = 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" }, "arg1", "arg2" },
new PrintStream(out), new PrintStream(err), "stdin"); new PrintStream(out), new PrintStream(err), "stdin");
assertEquals("unexpected hook output", "test arg1 arg2\nstdin\n", assertEquals("unexpected hook output", "test arg1 arg2\nstdin\n",
@ -95,11 +96,10 @@ public class HookTest extends RepositoryTestCase {
} }
@Test @Test
public void testPreCommitHook() throws Exception { public void testFailedPreCommitHookBlockCommit() throws Exception {
assumeSupportedPlatform(); assumeSupportedPlatform();
Hook h = Hook.PRE_COMMIT; writeHookFile(PreCommitHook.NAME,
writeHookFile(h.getName(),
"#!/bin/sh\necho \"test\"\n\necho 1>&2 \"stderr\"\nexit 1"); "#!/bin/sh\necho \"test\"\n\necho 1>&2 \"stderr\"\nexit 1");
Git git = Git.wrap(db); Git git = Git.wrap(db);
String path = "a.txt"; String path = "a.txt";
@ -110,14 +110,12 @@ public class HookTest extends RepositoryTestCase {
git.commit().setMessage("commit") git.commit().setMessage("commit")
.setHookOutputStream(new PrintStream(out)).call(); .setHookOutputStream(new PrintStream(out)).call();
fail("expected pre-commit hook to abort commit"); fail("expected pre-commit hook to abort commit");
} catch (RejectCommitException e) { } catch (AbortedByHookException e) {
assertEquals("unexpected error message from pre-commit hook", 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()); e.getMessage());
assertEquals("unexpected output from pre-commit hook", "test\n", assertEquals("unexpected output from pre-commit hook", "test\n",
out.toString()); out.toString());
} catch (Throwable e) {
fail("unexpected exception thrown by pre-commit hook: " + e);
} }
} }

4
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 * @since 3.7
*/ */
@Override @Override
public File findHook(Repository repository, Hook hook) { public File findHook(Repository repository, String hookName) {
final File gitdir = repository.getDirectory(); final File gitdir = repository.getDirectory();
final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS) final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS)
.resolve(hook.getName()); .resolve(hookName);
if (Files.isExecutable(hookPath)) if (Files.isExecutable(hookPath))
return hookPath.toFile(); return hookPath.toFile();
return null; return null;

4
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 * @since 3.7
*/ */
@Override @Override
public File findHook(Repository repository, Hook hook) { public File findHook(Repository repository, String hookName) {
final File gitdir = repository.getDirectory(); final File gitdir = repository.getDirectory();
final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS) final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS)
.resolve(hook.getName()); .resolve(hookName);
if (Files.isExecutable(hookPath)) if (Files.isExecutable(hookPath))
return hookPath.toFile(); return hookPath.toFile();
return null; return null;

1
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.lib,
org.eclipse.jgit.revwalk", org.eclipse.jgit.revwalk",
org.eclipse.jgit.gitrepo.internal;version="4.0.0";x-internal:=true, 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;version="4.0.0",
org.eclipse.jgit.ignore.internal;version="4.0.0";x-friends:="org.eclipse.jgit.test", 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", org.eclipse.jgit.internal;version="4.0.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test",

2
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties

@ -99,12 +99,12 @@ checkoutUnexpectedResult=Checkout returned unexpected result {0}
classCastNotA=Not a {0} classCastNotA=Not a {0}
cloneNonEmptyDirectory=Destination path "{0}" already exists and is not an empty directory cloneNonEmptyDirectory=Destination path "{0}" already exists and is not an empty directory
collisionOn=Collision on {0} collisionOn=Collision on {0}
commandRejectedByHook=Rejected by "{0}" hook.\n{1}
commandWasCalledInTheWrongState=Command {0} was called in the wrong state commandWasCalledInTheWrongState=Command {0} was called in the wrong state
commitAlreadyExists=exists {0} commitAlreadyExists=exists {0}
commitMessageNotSpecified=commit message not specified commitMessageNotSpecified=commit message not specified
commitOnRepoWithoutHEADCurrentlyNotSupported=Commit on repo without HEAD currently not supported commitOnRepoWithoutHEADCurrentlyNotSupported=Commit on repo without HEAD currently not supported
commitAmendOnInitialNotPossible=Amending is not possible on initial commit. commitAmendOnInitialNotPossible=Amending is not possible on initial commit.
commitRejectedByHook=Commit rejected by "{0}" hook.\n{1}
compressingObjects=Compressing objects compressingObjects=Compressing objects
connectionFailed=connection failed connectionFailed=connection failed
connectionTimeOut=Connection time out: {0} connectionTimeOut=Connection time out: {0}

37
org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java

@ -42,7 +42,6 @@
*/ */
package org.eclipse.jgit.api; package org.eclipse.jgit.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.PrintStream; import java.io.PrintStream;
@ -52,13 +51,13 @@ import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import org.eclipse.jgit.api.errors.AbortedByHookException;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.NoFilepatternException; import org.eclipse.jgit.api.errors.NoFilepatternException;
import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.api.errors.NoMessageException; 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.UnmergedPathsException;
import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
import org.eclipse.jgit.dircache.DirCache; 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.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.errors.UnmergedPathException; import org.eclipse.jgit.errors.UnmergedPathException;
import org.eclipse.jgit.hooks.Hooks;
import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants; 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.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.ChangeIdUtil; 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 * A class used to execute a {@code Commit} command. It has setters for all
@ -126,8 +123,7 @@ public class CommitCommand extends GitCommand<RevCommit> {
private String reflogComment; private String reflogComment;
/** /**
* Setting this option bypasses the {@link Hook#PRE_COMMIT pre-commit} and * Setting this option bypasses the pre-commit and commit-msg hooks.
* {@link Hook#COMMIT_MSG commit-msg} hooks.
*/ */
private boolean noVerify; private boolean noVerify;
@ -138,7 +134,6 @@ public class CommitCommand extends GitCommand<RevCommit> {
*/ */
protected CommitCommand(Repository repo) { protected CommitCommand(Repository repo) {
super(repo); super(repo);
hookOutRedirect = System.out;
} }
/** /**
@ -159,14 +154,14 @@ public class CommitCommand extends GitCommand<RevCommit> {
* else * else
* @throws WrongRepositoryStateException * @throws WrongRepositoryStateException
* when repository is not in the right state for committing * 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 * 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, public RevCommit call() throws GitAPIException, NoHeadException,
NoMessageException, UnmergedPathsException, NoMessageException, UnmergedPathsException,
ConcurrentRefUpdateException, WrongRepositoryStateException, ConcurrentRefUpdateException, WrongRepositoryStateException,
RejectCommitException { AbortedByHookException {
checkCallable(); checkCallable();
Collections.sort(only); Collections.sort(only);
@ -180,19 +175,7 @@ public class CommitCommand extends GitCommand<RevCommit> {
state.name())); state.name()));
if (!noVerify) { if (!noVerify) {
final ByteArrayOutputStream errorByteArray = new ByteArrayOutputStream(); Hooks.preCommit(repo, hookOutRedirect).call();
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); processOptions(state, rw);
@ -771,9 +754,9 @@ public class CommitCommand extends GitCommand<RevCommit> {
/** /**
* Sets the {@link #noVerify} option on this commit command. * Sets the {@link #noVerify} option on this commit command.
* <p> * <p>
* Both the {@link Hook#PRE_COMMIT pre-commit} and {@link Hook#COMMIT_MSG * Both the pre-commit and commit-msg hooks can block a commit by their
* commit-msg} hooks can block a commit by their return value; setting this * return value; setting this option to <code>true</code> will bypass these
* option to <code>true</code> will bypass these two hooks. * two hooks.
* </p> * </p>
* *
* @param noVerify * @param noVerify

104
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());
}
}

159
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 <T>
* the return type which is expected from {@link #call()}
* @see <a href="http://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks">Git
* Hooks on the git-scm official site</a>
* @since 4.0
*/
abstract class GitHook<T> implements Callable<T> {
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());
}
}
}

25
org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java → 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 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * 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 * Factory class for instantiating supported hooks.
* {@link org.eclipse.jgit.util.Hook#PRE_COMMIT pre-commit} or
* {@link org.eclipse.jgit.util.Hook#COMMIT_MSG commit-msg}).
* *
* @since 3.7 * @since 4.0
*/ */
public class RejectCommitException extends GitAPIException { public class Hooks {
private static final long serialVersionUID = 1L;
/** /**
* @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) { public static PreCommitHook preCommit(Repository repo,
super(message); PrintStream outputStream) {
return new PreCommitHook(repo, outputStream);
} }
} }

84
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 <code>pre-commit</code> hook implementation. This hook is run before the
* commit and can reject the commit.
*
* @since 4.0
*/
public class PreCommitHook extends GitHook<Void> {
/** 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;
}
}

2
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java

@ -158,12 +158,12 @@ public class JGitText extends TranslationBundle {
/***/ public String classCastNotA; /***/ public String classCastNotA;
/***/ public String cloneNonEmptyDirectory; /***/ public String cloneNonEmptyDirectory;
/***/ public String collisionOn; /***/ public String collisionOn;
/***/ public String commandRejectedByHook;
/***/ public String commandWasCalledInTheWrongState; /***/ public String commandWasCalledInTheWrongState;
/***/ public String commitAlreadyExists; /***/ public String commitAlreadyExists;
/***/ public String commitMessageNotSpecified; /***/ public String commitMessageNotSpecified;
/***/ public String commitOnRepoWithoutHEADCurrentlyNotSupported; /***/ public String commitOnRepoWithoutHEADCurrentlyNotSupported;
/***/ public String commitAmendOnInitialNotPossible; /***/ public String commitAmendOnInitialNotPossible;
/***/ public String commitRejectedByHook;
/***/ public String compressingObjects; /***/ public String compressingObjects;
/***/ public String connectionFailed; /***/ public String connectionFailed;
/***/ public String connectionTimeOut; /***/ public String connectionTimeOut;

48
org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java

@ -660,8 +660,8 @@ public abstract class FS {
* *
* @param repository * @param repository
* The repository for which a hook should be run. * The repository for which a hook should be run.
* @param hook * @param hookName
* The hook to be executed. * The name of the hook to be executed.
* @param args * @param args
* Arguments to pass to this hook. Cannot be <code>null</code>, * Arguments to pass to this hook. Cannot be <code>null</code>,
* but can be an empty array. * but can be an empty array.
@ -669,11 +669,12 @@ public abstract class FS {
* @throws JGitInternalException * @throws JGitInternalException
* if we fail to run the hook somehow. Causes may include an * if we fail to run the hook somehow. Causes may include an
* interrupted process or I/O errors. * 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 { String[] args) throws JGitInternalException {
return runIfPresent(repository, hook, args, System.out, System.err, return runHookIfPresent(repository, hookName, args, System.out, System.err,
null); null);
} }
@ -683,8 +684,8 @@ public abstract class FS {
* *
* @param repository * @param repository
* The repository for which a hook should be run. * The repository for which a hook should be run.
* @param hook * @param hookName
* The hook to be executed. * The name of the hook to be executed.
* @param args * @param args
* Arguments to pass to this hook. Cannot be <code>null</code>, * Arguments to pass to this hook. Cannot be <code>null</code>,
* but can be an empty array. * but can be an empty array.
@ -703,9 +704,10 @@ public abstract class FS {
* @throws JGitInternalException * @throws JGitInternalException
* if we fail to run the hook somehow. Causes may include an * if we fail to run the hook somehow. Causes may include an
* interrupted process or I/O errors. * 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[] args, PrintStream outRedirect, PrintStream errRedirect,
String stdinArgs) throws JGitInternalException { String stdinArgs) throws JGitInternalException {
return new ProcessResult(Status.NOT_SUPPORTED); return new ProcessResult(Status.NOT_SUPPORTED);
@ -713,13 +715,13 @@ public abstract class FS {
/** /**
* See * 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. * . Should only be called by FS supporting shell scripts execution.
* *
* @param repository * @param repository
* The repository for which a hook should be run. * The repository for which a hook should be run.
* @param hook * @param hookName
* The hook to be executed. * The name of the hook to be executed.
* @param args * @param args
* Arguments to pass to this hook. Cannot be <code>null</code>, * Arguments to pass to this hook. Cannot be <code>null</code>,
* but can be an empty array. * but can be an empty array.
@ -738,13 +740,13 @@ public abstract class FS {
* @throws JGitInternalException * @throws JGitInternalException
* if we fail to run the hook somehow. Causes may include an * if we fail to run the hook somehow. Causes may include an
* interrupted process or I/O errors. * interrupted process or I/O errors.
* @since 3.7 * @since 4.0
*/ */
protected ProcessResult internalRunIfPresent(Repository repository, protected ProcessResult internalRunHookIfPresent(Repository repository,
final Hook hook, String[] args, PrintStream outRedirect, final String hookName, String[] args, PrintStream outRedirect,
PrintStream errRedirect, String stdinArgs) PrintStream errRedirect, String stdinArgs)
throws JGitInternalException { throws JGitInternalException {
final File hookFile = findHook(repository, hook); final File hookFile = findHook(repository, hookName);
if (hookFile == null) if (hookFile == null)
return new ProcessResult(Status.NOT_PRESENT); return new ProcessResult(Status.NOT_PRESENT);
@ -764,11 +766,11 @@ public abstract class FS {
} catch (IOException e) { } catch (IOException e) {
throw new JGitInternalException(MessageFormat.format( throw new JGitInternalException(MessageFormat.format(
JGitText.get().exceptionCaughtDuringExecutionOfHook, JGitText.get().exceptionCaughtDuringExecutionOfHook,
hook.getName()), e); hookName), e);
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new JGitInternalException(MessageFormat.format( throw new JGitInternalException(MessageFormat.format(
JGitText.get().exceptionHookExecutionInterrupted, JGitText.get().exceptionHookExecutionInterrupted,
hook.getName()), e); hookName), e);
} }
} }
@ -778,15 +780,15 @@ public abstract class FS {
* *
* @param repository * @param repository
* The repository within which to find a hook. * The repository within which to find a hook.
* @param hook * @param hookName
* The hook we're trying to find. * The name of the hook we're trying to find.
* @return The {@link File} containing this particular hook if it exists in * @return The {@link File} containing this particular hook if it exists in
* the given repository, <code>null</code> otherwise. * the given repository, <code>null</code> 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(), final File hookFile = new File(new File(repository.getDirectory(),
Constants.HOOKS), hook.getName()); Constants.HOOKS), hookName);
return hookFile.isFile() ? hookFile : null; return hookFile.isFile() ? hookFile : null;
} }

6
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 @Override
public ProcessResult runIfPresent(Repository repository, Hook hook, public ProcessResult runHookIfPresent(Repository repository, String hookName,
String[] args, PrintStream outRedirect, PrintStream errRedirect, String[] args, PrintStream outRedirect, PrintStream errRedirect,
String stdinArgs) throws JGitInternalException { String stdinArgs) throws JGitInternalException {
return internalRunIfPresent(repository, hook, args, outRedirect, return internalRunHookIfPresent(repository, hookName, args, outRedirect,
errRedirect, stdinArgs); errRedirect, stdinArgs);
} }
} }

6
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 @Override
public ProcessResult runIfPresent(Repository repository, Hook hook, public ProcessResult runHookIfPresent(Repository repository, String hookName,
String[] args, PrintStream outRedirect, PrintStream errRedirect, String[] args, PrintStream outRedirect, PrintStream errRedirect,
String stdinArgs) throws JGitInternalException { String stdinArgs) throws JGitInternalException {
return internalRunIfPresent(repository, hook, args, outRedirect, return internalRunHookIfPresent(repository, hookName, args, outRedirect,
errRedirect, stdinArgs); errRedirect, stdinArgs);
} }
} }

149
org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java

@ -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.
* <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
* "&lt;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>&lt;old SHA-1> &lt;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;
}
}

9
org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java

@ -109,4 +109,13 @@ public class ProcessResult {
public Status getStatus() { public Status getStatus() {
return status; return status;
} }
/**
* @return <code>true</code> if the execution occurred and resulted in a
* return code different from 0, <code>false</code> otherwise.
* @since 4.0
*/
public boolean isExecutedWithError() {
return getStatus() == ProcessResult.Status.OK && getExitCode() != 0;
}
} }

Loading…
Cancel
Save