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 <laurent.goubet@obeo.fr>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
Laurent Goubet 2014-10-31 15:20:14 +01:00 committed by Matthias Sohn
parent d2e0bfa568
commit 494e893c54
8 changed files with 169 additions and 8 deletions

View 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",

View File

@ -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);

View 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}

View File

@ -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())

View File

@ -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<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 @@ 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.
* <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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;