From 9aa3a05cab141b72a93952cd2ac7ad3f55fd8f12 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 27 Feb 2013 11:49:49 +0100 Subject: [PATCH] fix wrong handling of git file hooks --- .../main/java/sonia/scm/web/GitFileHook.java | 328 ++++++++++++++++++ .../src/main/java/sonia/scm/web/GitHooks.java | 197 +++++++++++ .../java/sonia/scm/web/GitReceiveHook.java | 278 +-------------- 3 files changed, 534 insertions(+), 269 deletions(-) create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitFileHook.java create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitHooks.java diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitFileHook.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitFileHook.java new file mode 100644 index 0000000000..a0a7b8326e --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitFileHook.java @@ -0,0 +1,328 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of SCM-Manager; 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 REGENTS 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. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Stopwatch; +import com.google.common.io.Closeables; + +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.transport.ReceivePack; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.RepositoryHookType; +import sonia.scm.util.IOUtil; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; + + +/** + * + * @author Sebastian Sdorra + */ +public class GitFileHook +{ + + /** Field description */ + public static final String FILE_HOOKDIRECTORY = "hooks"; + + /** Field description */ + public static final String FILE_HOOK_POST_RECEIVE = "post-receive"; + + /** Field description */ + public static final String FILE_HOOK_PRE_RECEIVE = "pre-receive"; + + /** + * the logger for GitFileHook + */ + private static final Logger logger = + LoggerFactory.getLogger(GitFileHook.class); + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs ... + * + * + * + * + * @param executor + * @param type + * @param rpack + * @param commands + */ + private GitFileHook(RepositoryHookType type, + ReceivePack rpack, Iterable commands) + { + this.type = type; + this.rpack = rpack; + this.commands = commands; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * + * @param executor + * @param type + * @param rpack + * @param commands + */ + public static void execute(RepositoryHookType type, + ReceivePack rpack, Iterable commands) + { + new GitFileHook(type, rpack, commands).execute(); + } + + /** + * Method description + * + * + * @param hook + * + * @return + * + * @throws IOException + */ + private Process createProcess(File hook) throws IOException + { + ProcessBuilder pb = new ProcessBuilder(hook.getAbsolutePath()); + + // use repostitory directory as working directory for file hooks + // see issue #99 + pb.directory(rpack.getRepository().getDirectory()); + + // copy system environment for hook + pb.environment().putAll(System.getenv()); + + // start process + return pb.redirectErrorStream(true).start(); + } + + /** + * Method description + * + * + * @param rc + * + * @return + */ + private String createReceiveCommandOutput(ReceiveCommand rc) + { + StringBuilder sb = new StringBuilder(); + + sb.append(GitUtil.getId(rc.getOldId())); + sb.append(" "); + sb.append(GitUtil.getId(rc.getNewId())); + sb.append(" "); + sb.append(rc.getRefName()); + + return sb.toString(); + } + + /** + * Method description + * + * + * @param type + */ + private void execute() + { + File hook = getHookFile(); + + if ((hook == null) ||!hook.exists()) + { + logger.trace("no file hook found for {}", type); + } + else if (!hook.canExecute()) + { + logger.warn("hook file {} is not executeable", hook); + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("try to execute hook {}", hook); + + Stopwatch sw = new Stopwatch().start(); + + execute(hook); + logger.debug("file hook {} executed in {}", hook, sw.stop()); + } + else + { + execute(hook); + } + } + } + + /** + * Method description + * + * + * @param hook + */ + private void execute(File hook) + { + Process p; + PrintWriter writer = null; + BufferedReader stdReader = null; + + try + { + p = createProcess(hook); + writer = new PrintWriter(p.getOutputStream()); + stdReader = new BufferedReader(new InputStreamReader(p.getInputStream())); + + for (ReceiveCommand rc : commands) + { + String output = createReceiveCommandOutput(rc); + + logger.trace("write rc output \"{}\" to hook {}", output, hook); + writer.println(output); + } + + writer.close(); + + String line = stdReader.readLine(); + + while (line != null) + { + rpack.sendMessage(line); + line = stdReader.readLine(); + } + + // TODO handle timeout + int result = p.waitFor(); + + if (result == 0) + { + logger.debug("file hook {} executed successful", hook); + } + else + { + logger.warn("file hook {} returned with exit code {}", hook, result); + GitHooks.abortIfPossible(type, rpack, commands); + } + } + catch (Exception ex) + { + logger.error("failure during file hook execution", ex); + GitHooks.abortIfPossible(type, rpack, commands, + "failure during file hook execution"); + } + finally + { + Closeables.closeQuietly(writer); + Closeables.closeQuietly(stdReader); + } + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param type + * + * @return + */ + private File getHookFile() + { + File hook = null; + File directory = rpack.getRepository().getDirectory(); + String scriptName = null; + + if (type == RepositoryHookType.POST_RECEIVE) + { + scriptName = FILE_HOOK_POST_RECEIVE; + } + else if (type == RepositoryHookType.PRE_RECEIVE) + { + scriptName = FILE_HOOK_PRE_RECEIVE; + } + + if (scriptName != null) + { + hook = getHookFile(directory, scriptName); + } + + return hook; + } + + /** + * Method description + * + * + * @param directory + * @param name + * + * @return + */ + private File getHookFile(File directory, String name) + { + //J- + return IOUtil.getScript( + new File( + directory, + FILE_HOOKDIRECTORY.concat(File.separator).concat(name) + ) + ); + //J+ + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private Iterable commands; + + /** Field description */ + private ReceivePack rpack; + + /** Field description */ + private RepositoryHookType type; +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitHooks.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitHooks.java new file mode 100644 index 0000000000..31e8b0dcac --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitHooks.java @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of SCM-Manager; 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 REGENTS 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. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; + +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.transport.ReceivePack; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sonia.scm.repository.RepositoryHookType; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.List; + +/** + * + * @author Sebastian Sdorra + */ +public final class GitHooks +{ + + /** Field description */ + public static final String PREFIX_MSG = "[SCM] "; + + /** + * the logger for GitHooks + */ + private static final Logger logger = LoggerFactory.getLogger(GitHooks.class); + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param type + * @param rpack + * @param commands + */ + public static void abortIfPossible(RepositoryHookType type, + ReceivePack rpack, Iterable commands) + { + abortIfPossible(type, rpack, commands, null); + } + + /** + * Method description + * + * + * @param type + * @param rpack + * @param commands + * @param message + */ + public static void abortIfPossible(RepositoryHookType type, + ReceivePack rpack, Iterable commands, String message) + { + if (type == RepositoryHookType.PRE_RECEIVE) + { + for (ReceiveCommand rc : commands) + { + rc.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON); + } + } + + if (message != null) + { + sendPrefixedError(rpack, message); + } + } + + /** + * Method description + * + * + * @param type + * @param commands + * + * @return + */ + public static List filterReceiveable(RepositoryHookType type, + Iterable commands) + { + List receiveable = Lists.newArrayList(); + + for (ReceiveCommand command : commands) + { + if (isReceiveable(type, command)) + { + receiveable.add(command); + } + else + { + logger.debug("skip receive command, type={}, ref={}, result={}", + command.getType(), command.getRefName(), command.getResult()); + } + } + + return receiveable; + } + + /** + * Method description + * + * + * @param rpack + * @param message + */ + public static void sendPrefixedError(ReceivePack rpack, String message) + { + rpack.sendError(createPrefixedMessage(message)); + } + + /** + * Method description + * + * + * @param rpack + * @param message + */ + public static void sendPrefixedMessage(ReceivePack rpack, String message) + { + rpack.sendMessage(createPrefixedMessage(message)); + } + + /** + * Method description + * + * + * @param message + * + * @return + */ + private static String createPrefixedMessage(String message) + { + return PREFIX_MSG.concat(Strings.nullToEmpty(message)); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param rc + * @param type + * + * @return + */ + private static boolean isReceiveable(RepositoryHookType type, + ReceiveCommand rc) + { + //J- + return ((RepositoryHookType.PRE_RECEIVE == type) && + (rc.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED)) || + ((RepositoryHookType.POST_RECEIVE == type) && + (rc.getResult() == ReceiveCommand.Result.OK)); + //J+ + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java index 9c8d9e5f58..712f0d2cac 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitReceiveHook.java @@ -35,9 +35,6 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- -import com.google.common.collect.Lists; - -import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.PostReceiveHook; import org.eclipse.jgit.transport.PreReceiveHook; @@ -47,23 +44,14 @@ import org.eclipse.jgit.transport.ReceivePack; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.io.Command; -import sonia.scm.io.CommandResult; -import sonia.scm.io.SimpleCommand; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHookEvent; -import sonia.scm.repository.GitUtil; import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryUtil; -import sonia.scm.util.IOUtil; -import sonia.scm.util.Util; //~--- JDK imports ------------------------------------------------------------ -import java.io.File; -import java.io.IOException; - import java.util.Collection; import java.util.List; @@ -74,18 +62,6 @@ import java.util.List; public class GitReceiveHook implements PreReceiveHook, PostReceiveHook { - /** Field description */ - public static final String FILE_HOOKDIRECTORY = "hooks"; - - /** Field description */ - public static final String FILE_HOOK_POST_RECEIVE = "post-receive"; - - /** Field description */ - public static final String FILE_HOOK_PRE_RECEIVE = "pre-receive"; - - /** Field description */ - public static final String PREFIX_MSG = "[SCM] "; - /** the logger for GitReceiveHook */ private static final Logger logger = LoggerFactory.getLogger(GitReceiveHook.class); @@ -137,131 +113,6 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook onReceive(rpack, receiveCommands, RepositoryHookType.PRE_RECEIVE); } - /** - * Method description - * - * @param rpack - * @param rc - * @param repositoryDirectory - * @param hook - * @param oldId - * @param newId - * @param refName - */ - private void executeFileHook(ReceivePack rpack, ReceiveCommand rc, - File repositoryDirectory, File hook, ObjectId oldId, ObjectId newId, - String refName) - { - if (logger.isDebugEnabled()) - { - logger.debug("execute file hook '{}' in directoy '{}'"); - } - - final Command cmd = new SimpleCommand(hook.getAbsolutePath(), - GitUtil.getId(oldId), GitUtil.getId(newId), - Util.nonNull(refName)); - - // issue-99 - cmd.setWorkDirectory(repositoryDirectory); - - try - { - CommandResult result = cmd.execute(); - - if (result.isSuccessfull()) - { - if (logger.isDebugEnabled()) - { - logger.debug("executed file hook successfull"); - - if (logger.isTraceEnabled()) - { - String out = result.getOutput(); - - if (Util.isNotEmpty(out)) - { - logger.trace(out); - } - } - } - } - else - { - if (logger.isErrorEnabled()) - { - logger.error("failed to execute file hook"); - } - - String out = result.getOutput(); - - if (Util.isNotEmpty(out)) - { - logger.error(out); - } - - sendError(rpack, rc, out); - } - } - catch (IOException ex) - { - logger.error("could not execute file hook", ex); - } - } - - /** - * Method description - * - * - * @param rpack - * @param rc - * @param newId - * @param type - */ - private void handleFileHooks(ReceivePack rpack, ReceiveCommand rc, - RepositoryHookType type) - { - ObjectId newId = rc.getNewId(); - ObjectId oldId = null; - - if (isUpdateCommand(rc)) - { - oldId = rc.getOldId(); - - if (logger.isTraceEnabled()) - { - logger.trace("handle update receive command from commit '{}' to '{}'", - oldId.getName(), newId.getName()); - } - } - else if (logger.isTraceEnabled()) - { - logger.trace("handle receive command for commit '{}'", newId.getName()); - } - - File directory = rpack.getRepository().getDirectory(); - String scriptName = null; - - if (type == RepositoryHookType.POST_RECEIVE) - { - scriptName = FILE_HOOK_POST_RECEIVE; - } - else if (type == RepositoryHookType.PRE_RECEIVE) - { - scriptName = FILE_HOOK_PRE_RECEIVE; - } - - if (scriptName != null) - { - File hookScript = getHookScript(directory, scriptName); - - if (hookScript != null) - { - executeFileHook(rpack, rc, directory, hookScript, oldId, newId, - rc.getRefName()); - } - } - } - /** * Method description * @@ -287,10 +138,7 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook { logger.error("could not handle receive commands", ex); - if (type == RepositoryHookType.PRE_RECEIVE) - { - sendError(rpack, receiveCommands, ex.getMessage()); - } + GitHooks.abortIfPossible(type, rpack, receiveCommands, ex.getMessage()); } } @@ -300,140 +148,32 @@ public class GitReceiveHook implements PreReceiveHook, PostReceiveHook * * @param rpack * @param receiveCommands + * @param commands * @param type */ private void onReceive(ReceivePack rpack, - Collection receiveCommands, RepositoryHookType type) + Collection commands, RepositoryHookType type) { if (logger.isTraceEnabled()) { logger.trace("received git hook, type={}", type); } - List commands = Lists.newArrayList(); + List receiveCommands = GitHooks.filterReceiveable(type, + commands); - for (ReceiveCommand rc : receiveCommands) - { - if (isReceiveable(rc, type)) - { - commands.add(rc); - handleFileHooks(rpack, rc, type); - } - else if (logger.isTraceEnabled()) - { - //J- - logger.trace("skip receive command, type={}, ref={}, result={}", - new Object[] { - rc.getType(), - rc.getRefName(), - rc.getResult() - } - ); - //J+ - } - } + GitFileHook.execute(type, rpack, commands); - if (!commands.isEmpty()) + if (!receiveCommands.isEmpty()) { - handleReceiveCommands(rpack, commands, type); + handleReceiveCommands(rpack, receiveCommands, type); } else if (logger.isDebugEnabled()) { - logger.debug("no receive command found to process"); + logger.debug("no receive commands found to process"); } } - /** - * Method description - * - * - * @param rpack - * @param commands - * @param message - */ - private void sendError(ReceivePack rpack, Iterable commands, - String message) - { - if (logger.isWarnEnabled()) - { - logger.warn("abort git push request with msg: {}", message); - } - - for (ReceiveCommand rc : commands) - { - rc.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON); - } - - rpack.sendError(PREFIX_MSG.concat(Util.nonNull(message))); - } - - /** - * Method description - * - * - * - * @param rpack - * @param rc - * @param message - */ - private void sendError(ReceivePack rpack, ReceiveCommand rc, String message) - { - rc.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON); - rpack.sendError(PREFIX_MSG.concat(Util.nonNull(message))); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param directory - * @param name - * - * @return - */ - private File getHookScript(File directory, String name) - { - File baseFile = new File(directory, - FILE_HOOKDIRECTORY.concat(File.separator).concat(name)); - - return IOUtil.getScript(baseFile); - } - - /** - * Method description - * - * - * @param rc - * @param type - * - * @return - */ - private boolean isReceiveable(ReceiveCommand rc, RepositoryHookType type) - { - //J- - return ((RepositoryHookType.PRE_RECEIVE == type) && - (rc.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED)) || - ((RepositoryHookType.POST_RECEIVE == type) && - (rc.getResult() == ReceiveCommand.Result.OK)); - //J+ - } - - /** - * Method description - * - * - * @param rc - * - * @return - */ - private boolean isUpdateCommand(ReceiveCommand rc) - { - return (rc.getType() == ReceiveCommand.Type.UPDATE) - || (rc.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD); - } - //~--- fields --------------------------------------------------------------- /** Field description */