From 494e893c541b5cf465b362c69354c08d7a81c249 Mon Sep 17 00:00:00 2001 From: Laurent Goubet Date: Fri, 31 Oct 2014 15:20:14 +0100 Subject: [PATCH] Support for the pre-commit hook 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 Signed-off-by: Matthias Sohn --- .../META-INF/MANIFEST.MF | 1 + .../src/org/eclipse/jgit/util/HookTest.java | 30 ++++++++ .../eclipse/jgit/internal/JGitText.properties | 1 + .../eclipse/jgit/api/CherryPickCommand.java | 3 +- .../org/eclipse/jgit/api/CommitCommand.java | 71 ++++++++++++++++++- .../org/eclipse/jgit/api/RebaseCommand.java | 9 ++- .../api/errors/RejectCommitException.java | 61 ++++++++++++++++ .../org/eclipse/jgit/internal/JGitText.java | 1 + 8 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java diff --git a/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF index a6e8f67dd..182c2679e 100644 --- a/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.java7.test/META-INF/MANIFEST.MF @@ -6,6 +6,7 @@ Bundle-Version: 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", 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 73086ab4a..96550889c 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 @@ -44,12 +44,15 @@ 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 void testRunHook() throws Exception { 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); 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 71ec6b2bc..efe61052b 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -104,6 +104,7 @@ commitAlreadyExists=exists {0} commitMessageNotSpecified=commit message not specified commitOnRepoWithoutHEADCurrentlyNotSupported=Commit on repo without HEAD currently not supported commitAmendOnInitialNotPossible=Amending is not possible on initial commit. +commitRejectedByHook=Commit rejected by "{0}" hook.\n{1} compressingObjects=Compressing objects connectionFailed=connection failed connectionTimeOut=Connection time out: {0} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java index 30dcaa5b6..d4eb0b3b2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java @@ -169,7 +169,8 @@ public CherryPickResult call() throws GitAPIException, NoMessageException, .setMessage(srcCommit.getFullMessage()) .setReflogComment(reflogPrefix + " " //$NON-NLS-1$ + srcCommit.getShortMessage()) - .setAuthor(srcCommit.getAuthorIdent()).call(); + .setAuthor(srcCommit.getAuthorIdent()) + .setNoVerify(true).call(); cherryPickedRefs.add(src); } else { if (merger.failed()) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index f8406e023..93400aa4d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -42,8 +42,10 @@ */ package org.eclipse.jgit.api; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.PrintStream; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; @@ -56,6 +58,7 @@ import org.eclipse.jgit.api.errors.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.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 { 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 @@ protected CommitCommand(Repository repo) { * 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 RevCommit call() throws GitAPIException, NoHeadException, 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 CommitCommand setReflogComment(String reflogComment) { return this; } + /** + * 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. + *

+ * + * @param noVerify + * Whether this commit should be verified by the pre-commit and + * commit-msg hooks. + * @return {@code this} + * @since 3.7 + */ + public CommitCommand setNoVerify(boolean noVerify) { + this.noVerify = noVerify; + return this; + } + + /** + * Set the output stream for hook scripts executed by this command. If not + * set it defaults to {@code System.out}. + * + * @param hookStdOut + * the output stream for hook scripts executed by this command + * @return {@code this} + * @since 3.7 + */ + public CommitCommand setHookOutputStream(PrintStream hookStdOut) { + this.hookOutRedirect = hookStdOut; + return this; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java index 47424a907..7d3e82318 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -462,7 +462,7 @@ private RebaseResult processStep(RebaseTodoLine step, boolean shouldPick) 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 @@ private RevCommit squashIntoPrevious(boolean sequenceContainsSquash, } retNewHead = new Git(repo).commit() .setMessage(stripCommentLines(commitMessage)) - .setAmend(true).call(); + .setAmend(true).setNoVerify(true).call(); rebaseState.getFile(MESSAGE_SQUASH).delete(); rebaseState.getFile(MESSAGE_FIXUP).delete(); } else { // Next step is either Squash or Fixup - retNewHead = new Git(repo).commit() - .setMessage(commitMessage).setAmend(true) - .call(); + retNewHead = new Git(repo).commit().setMessage(commitMessage) + .setAmend(true).setNoVerify(true).call(); } return retNewHead; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java new file mode 100644 index 000000000..6036a2731 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RejectCommitException.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 Obeo. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api.errors; + +/** + * Exception thrown when a commit is rejected by a hook (either + * {@link org.eclipse.jgit.util.Hook#PRE_COMMIT pre-commit} or + * {@link org.eclipse.jgit.util.Hook#COMMIT_MSG commit-msg}). + * + * @since 3.7 + */ +public class RejectCommitException extends GitAPIException { + private static final long serialVersionUID = 1L; + + /** + * @param message + */ + public RejectCommitException(String message) { + super(message); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 138f40f7c..8c81c9513 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -163,6 +163,7 @@ public static JGitText get() { /***/ public String commitMessageNotSpecified; /***/ public String commitOnRepoWithoutHEADCurrentlyNotSupported; /***/ public String commitAmendOnInitialNotPossible; + /***/ public String commitRejectedByHook; /***/ public String compressingObjects; /***/ public String connectionFailed; /***/ public String connectionTimeOut;