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 0324efca6..d3cc0ce0d 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; @@ -73,6 +75,62 @@ public class HookTest extends RepositoryTestCase { FS.DETECTED.findHook(db, PreCommitHook.NAME)); } + @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 b5c726eed..b57cff7ea 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 { 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 000000000..fa1707575 --- /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 commit-msg 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 { + + /** + * 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 a2e4fa560..1494576ab 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 ed0ed04d9..535a6ee17 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 4ef005150..b0fe33167 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -1351,6 +1351,40 @@ public abstract class Repository implements AutoCloseable { writeCommitMsg(mergeMsgFile, msg); } + /** + * 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