]> source.dussan.org Git - jgit.git/commitdiff
Support for the pre-commit hook 06/40506/3
authorLaurent Goubet <laurent.goubet@obeo.fr>
Fri, 31 Oct 2014 14:20:14 +0000 (15:20 +0100)
committerMatthias Sohn <matthias.sohn@sap.com>
Mon, 2 Feb 2015 20:23:32 +0000 (21:23 +0100)
Introduce support for the pre-commit hook into JGit, along with the
--no-verify commit command option to bypass it when rebasing /
cherry-picking.

Change-Id: If86df98577fa56c5c03d783579c895a38bee9d18
Signed-off-by: Laurent Goubet <laurent.goubet@obeo.fr>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF
org.eclipse.jgit.java7.test/src/org/eclipse/jgit/util/HookTest.java
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java

index a6e8f67dd57f29edefeadb4aa539a4787670fa53..182c2679e3f68fe42ac4145ed2fca00f137a045e 100644 (file)
@@ -6,6 +6,7 @@ Bundle-Version: 3.7.0.qualifier
 Bundle-Vendor: %provider_name
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
 Import-Package: org.eclipse.jgit.api;version="[3.7.0,3.8.0)",
+ org.eclipse.jgit.api.errors;version="[3.7.0,3.8.0)",
  org.eclipse.jgit.diff;version="[3.7.0,3.8.0)",
  org.eclipse.jgit.dircache;version="[3.7.0,3.8.0)",
  org.eclipse.jgit.internal.storage.file;version="3.7.0",
index 73086ab4af5ef1cf816b4dfb115b05e71d6cafc7..96550889c8856816db7f1a360536a05f1ad8ecf3 100644 (file)
@@ -44,12 +44,15 @@ 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;
@@ -91,6 +94,33 @@ public class HookTest extends RepositoryTestCase {
                                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);
index 71ec6b2bcd3f5a799d56c0c8f6470bc1078fbf54..efe61052b0134269e714e1388a2cf93a24ee1f37 100644 (file)
@@ -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}
index 30dcaa5b6f0b97a57ee2c4f72f5c632741f767d5..d4eb0b3b21ca2e9b29bbfd3f2850738301c9da7b 100644 (file)
@@ -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())
index f8406e0231f0d594fea6318c0a04e71f78ed3ba9..93400aa4d1ef259d20d37e05ad004966104cf294 100644 (file)
  */
 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
@@ -119,11 +125,20 @@ 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;
+       }
 }
index 47424a90749d67fedfc23806c22d03481c69b9b3..7d3e82318707cc131b180752fa152fc3bed52a26 100644 (file)
@@ -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 (file)
index 0000000..6036a27
--- /dev/null
@@ -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);
+       }
+}
index 138f40f7ce7977848324e6aa865b89d3afbae4a8..8c81c95134b71f6ada0eabd16aa1fc808fbfc4e1 100644 (file)
@@ -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;