diff options
author | Laurent Delaigue <laurent.delaigue@obeo.fr> | 2015-02-23 11:19:45 +0100 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2015-03-02 17:45:15 +0100 |
commit | efeb02bf2bed32ef94d5c4891404f551cdc6957f (patch) | |
tree | 58b7bfac45098f0ab6a7fce1dccb14d6cc093887 | |
parent | 26fd56f167e6377777e6d46c14779183e4bcb55a (diff) | |
download | jgit-efeb02bf2bed32ef94d5c4891404f551cdc6957f.tar.gz jgit-efeb02bf2bed32ef94d5c4891404f551cdc6957f.zip |
Support for the commit-msg hook.
This hook uses the file .git/COMMIT_EDITMSG to receive and potentially
modify the commit message.
Change-Id: Ibe2faadfb5d3932a5a3da2252d8156c4c04856c7
Signed-off-by: Laurent Delaigue <laurent.delaigue@obeo.fr>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
6 files changed, 262 insertions, 0 deletions
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 0324efca6a..d3cc0ce0d6 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 @@ -53,9 +53,11 @@ import java.io.PrintStream; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.AbortedByHookException; +import org.eclipse.jgit.hooks.CommitMsgHook; import org.eclipse.jgit.hooks.PreCommitHook; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Assume; import org.junit.Test; @@ -74,6 +76,62 @@ public class HookTest extends RepositoryTestCase { } @Test + public void testFailedCommitMsgHookBlocksCommit() throws Exception { + assumeSupportedPlatform(); + + writeHookFile(CommitMsgHook.NAME, + "#!/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 commit-msg hook to abort commit"); + } catch (AbortedByHookException e) { + assertEquals("unexpected error message from commit-msg hook", + "Rejected by \"commit-msg\" hook.\nstderr\n", + e.getMessage()); + assertEquals("unexpected output from commit-msg hook", "test\n", + out.toString()); + } + } + + @Test + public void testCommitMsgHookReceivesCorrectParameter() throws Exception { + assumeSupportedPlatform(); + + writeHookFile(CommitMsgHook.NAME, + "#!/bin/sh\necho $1\n\necho 1>&2 \"stderr\"\nexit 0"); + Git git = Git.wrap(db); + String path = "a.txt"; + writeTrashFile(path, "content"); + git.add().addFilepattern(path).call(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + git.commit().setMessage("commit") + .setHookOutputStream(new PrintStream(out)).call(); + assertEquals(".git/COMMIT_EDITMSG\n", out.toString("UTF-8")); + } + + @Test + public void testCommitMsgHookCanModifyCommitMessage() throws Exception { + assumeSupportedPlatform(); + + writeHookFile(CommitMsgHook.NAME, + "#!/bin/sh\necho \"new message\" > $1\nexit 0"); + Git git = Git.wrap(db); + String path = "a.txt"; + writeTrashFile(path, "content"); + git.add().addFilepattern(path).call(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + RevCommit revCommit = git.commit().setMessage("commit") + .setHookOutputStream(new PrintStream(out)).call(); + assertEquals("new message\n", revCommit.getFullMessage()); + } + + @Test public void testRunHook() throws Exception { assumeSupportedPlatform(); 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 b5c726eed0..b57cff7ea3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -214,6 +214,11 @@ public class CommitCommand extends GitCommand<RevCommit> { parents.add(0, headId); } + if (!noVerify) { + message = Hooks.commitMsg(repo, hookOutRedirect) + .setCommitMessage(message).call(); + } + // lock the index DirCache index = repo.lockDirCache(); try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java new file mode 100644 index 0000000000..fa1707575a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java @@ -0,0 +1,146 @@ +/* + * 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.File; +import java.io.IOException; +import java.io.PrintStream; + +import org.eclipse.jgit.api.errors.AbortedByHookException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Repository; + +/** + * The <code>commit-msg</code> hook implementation. This hook is run before the + * commit and can reject the commit. It passes one argument to the hook script, + * which is the path to the COMMIT_MSG file, relative to the repository + * workTree. + * + * @since 4.0 + */ +public class CommitMsgHook extends GitHook<String> { + + /** + * Constant indicating the name of the commit-smg hook. + */ + public static final String NAME = "commit-msg"; //$NON-NLS-1$ + + /** + * The commit message. + */ + private String commitMessage; + + /** + * @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 CommitMsgHook(Repository repo, PrintStream outputStream) { + super(repo, outputStream); + } + + @Override + public String call() throws IOException, AbortedByHookException { + if (commitMessage == null) { + throw new IllegalStateException(); + } + if (canRun()) { + getRepository().writeCommitEditMsg(commitMessage); + doRun(); + commitMessage = getRepository().readCommitEditMsg(); + } + return commitMessage; + } + + /** + * @return {@code true} if and only if the path to the message commit file + * is not null (which would happen in a bare repository) and the + * commit message is also not null. + */ + private boolean canRun() { + return getCommitEditMessageFilePath() != null && commitMessage != null; + } + + @Override + public String getHookName() { + return NAME; + } + + /** + * This hook receives one parameter, which is the path to the file holding + * the current commit-msg, relative to the repository's work tree. + */ + @Override + protected String[] getParameters() { + return new String[] { getCommitEditMessageFilePath() }; + } + + /** + * @return The path to the commit edit message file relative to the + * repository's work tree, or null if the repository is bare. + */ + private String getCommitEditMessageFilePath() { + File gitDir = getRepository().getDirectory(); + if (gitDir == null) { + return null; + } + return Repository.stripWorkDir(getRepository().getWorkTree(), new File( + gitDir, Constants.COMMIT_EDITMSG)); + } + + /** + * It is mandatory to call this method with a non-null value before actually + * calling the hook. + * + * @param commitMessage + * The commit message before the hook has run. + * @return {@code this} for convenience. + */ + public CommitMsgHook setCommitMessage(String commitMessage) { + this.commitMessage = commitMessage; + return this; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java index a2e4fa5609..1494576ab8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java @@ -63,4 +63,15 @@ public class Hooks { PrintStream outputStream) { return new PreCommitHook(repo, outputStream); } + + /** + * @param repo + * @param outputStream + * The output stream, or {@code null} to use {@code System.out} + * @return The commit-msg hook for the given repository. + */ + public static CommitMsgHook commitMsg(Repository repo, + PrintStream outputStream) { + return new CommitMsgHook(repo, outputStream); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java index ed0ed04d94..535a6ee175 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -618,6 +618,14 @@ public final class Constants { */ public static final String ORIG_HEAD = "ORIG_HEAD"; + /** + * Name of the file in which git commands and hooks store and read the + * message prepared for the upcoming commit. + * + * @since 4.0 + */ + public static final String COMMIT_EDITMSG = "COMMIT_EDITMSG"; + /** objectid for the empty blob */ public static final ObjectId EMPTY_BLOB_ID = ObjectId .fromString("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index 4ef0051503..b0fe331676 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -1352,6 +1352,40 @@ public abstract class Repository implements AutoCloseable { } /** + * Return the information stored in the file $GIT_DIR/COMMIT_EDITMSG. In + * this file hooks triggered by an operation may read or modify the current + * commit message. + * + * @return a String containing the content of the COMMIT_EDITMSG file or + * {@code null} if this file doesn't exist + * @throws IOException + * @throws NoWorkTreeException + * if this is bare, which implies it has no working directory. + * See {@link #isBare()}. + * @since 4.0 + */ + public String readCommitEditMsg() throws IOException, NoWorkTreeException { + return readCommitMsgFile(Constants.COMMIT_EDITMSG); + } + + /** + * Write new content to the file $GIT_DIR/COMMIT_EDITMSG. In this file hooks + * triggered by an operation may read or modify the current commit message. + * If {@code null} is specified as message the file will be deleted. + * + * @param msg + * the message which should be written or {@code null} to delete + * the file + * + * @throws IOException + * @since 4.0 + */ + public void writeCommitEditMsg(String msg) throws IOException { + File commiEditMsgFile = new File(gitDir, Constants.COMMIT_EDITMSG); + writeCommitMsg(commiEditMsgFile, msg); + } + + /** * Return the information stored in the file $GIT_DIR/MERGE_HEAD. In this * file operations triggering a merge will store the IDs of all heads which * should be merged together with HEAD. |