Add event hook support
The following hooks are defined and implemented: * patchset-created --change <change id> --project <project name> --branch <branch> --commit <sha1> --patchset <patchset id> * comment-added --change <change id> --project <project name> --branch <branch> --author <comment author> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...] * change-merged --change <change id> --project <project name> --branch <branch> --submitter <submitter> --commit <sha1> * change-abandoned --change <change id> --project <project name> --branch <branch> --abandoner <abandoner> --reason <reason> Bug: issue 368 Bug: issue 383 Change-Id: Ic2f041a71c744d0938d79b1106c9119d6318731a Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
		
				
					committed by
					
						
						Shawn O. Pearce
					
				
			
			
				
	
			
			
			
						parent
						
							48bc519a98
						
					
				
				
					commit
					6c2b677980
				
			@@ -681,6 +681,35 @@ Valid replacements are `$\{project\}` for the project name in Gerrit
 | 
			
		||||
and `$\{branch\}` for the name of the branch.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[[hooks]]Section hooks
 | 
			
		||||
~~~~~~~~~~~~~~~~~~~~~~~~
 | 
			
		||||
 | 
			
		||||
See also link:config-hooks.html[Hooks].
 | 
			
		||||
 | 
			
		||||
[[hooks.path]]hooks.path::
 | 
			
		||||
+
 | 
			
		||||
Optional path to hooks, if not specified then `'$site_path'/hooks` will be used.
 | 
			
		||||
 | 
			
		||||
[[hooks.patchsetCreatedHook]]hooks.patchsetCreatedHook::
 | 
			
		||||
+
 | 
			
		||||
Optional filename for the patchset created hook, if not specified then
 | 
			
		||||
`patchset-created` will be used.
 | 
			
		||||
 | 
			
		||||
[[hooks.commentAddedHook]]hooks.commentAddedHook::
 | 
			
		||||
+
 | 
			
		||||
Optional filename for the comment added hook, if not specified then
 | 
			
		||||
`comment-added` will be used.
 | 
			
		||||
 | 
			
		||||
[[hooks.changeMergedHook]]hooks.changeMergedHook::
 | 
			
		||||
+
 | 
			
		||||
Optional filename for the change merged hook, if not specified then
 | 
			
		||||
`change-merged` will be used.
 | 
			
		||||
 | 
			
		||||
[[hooks.changeAbandonedHook]]hooks.changeAbandonedHook::
 | 
			
		||||
+
 | 
			
		||||
Optional filename for the change abandoned hook, if not specified then
 | 
			
		||||
`change-abandoned` will be used.
 | 
			
		||||
 | 
			
		||||
[[http]]Section http
 | 
			
		||||
~~~~~~~~~~~~~~~~~~~~
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										79
									
								
								Documentation/config-hooks.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								Documentation/config-hooks.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
Gerrit Code Review - Hooks
 | 
			
		||||
==========================
 | 
			
		||||
 | 
			
		||||
Gerrit does not run any of the standard git hooks in the
 | 
			
		||||
repositories it works with, but it does have its own hook mechanism
 | 
			
		||||
included. Gerrit looks in `'$site_path'/hooks` for executables with
 | 
			
		||||
names listed below.
 | 
			
		||||
 | 
			
		||||
The environment will have GIT_DIR set to the full path of the
 | 
			
		||||
affected git repository so that git commands can be easily run.
 | 
			
		||||
 | 
			
		||||
Make sure your hook scripts are executable if running on *nix.
 | 
			
		||||
 | 
			
		||||
Hooks are run in the background after the relevent change has
 | 
			
		||||
taken place so are unable to affect the outcome of any given
 | 
			
		||||
change. Because of the fact the hooks are run in the background
 | 
			
		||||
after the activity, a hook might not be notified about an event if
 | 
			
		||||
the server is shutdown before the hook can be invoked.
 | 
			
		||||
 | 
			
		||||
Supported Hooks
 | 
			
		||||
---------------
 | 
			
		||||
 | 
			
		||||
patchset-created
 | 
			
		||||
~~~~~~~~~~~~~~~~
 | 
			
		||||
 | 
			
		||||
This is called whenever a patchset is created (this includes new
 | 
			
		||||
changes)
 | 
			
		||||
 | 
			
		||||
====
 | 
			
		||||
  patchset-created --change <change id> --project <project name> --branch <branch> --commit <sha1> --patchset <patchset id>
 | 
			
		||||
====
 | 
			
		||||
 | 
			
		||||
comment-added
 | 
			
		||||
~~~~~~~~~~~~~
 | 
			
		||||
 | 
			
		||||
This is called whenever a comment is added to a change.
 | 
			
		||||
 | 
			
		||||
====
 | 
			
		||||
  comment-added --change <change id> --project <project name> --branch <branch> --author <comment author> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...]
 | 
			
		||||
====
 | 
			
		||||
 | 
			
		||||
change-merged
 | 
			
		||||
~~~~~~~~~~~~~
 | 
			
		||||
 | 
			
		||||
Called whenever a change has been merged.
 | 
			
		||||
 | 
			
		||||
====
 | 
			
		||||
  change-merged --change <change id> --project <project name> --branch <branch> --submitter <submitter> --commit <sha1>
 | 
			
		||||
====
 | 
			
		||||
 | 
			
		||||
change-abandoned
 | 
			
		||||
~~~~~~~~~~~~~~~~
 | 
			
		||||
 | 
			
		||||
Called whenever a change has been abandoned.
 | 
			
		||||
 | 
			
		||||
====
 | 
			
		||||
  change-abandoned --change <change id> --project <project name> --branch <branch> --abandoner <abandoner> --reason <reason>
 | 
			
		||||
====
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Configuration Settings
 | 
			
		||||
----------------------
 | 
			
		||||
 | 
			
		||||
It is possible to change where gerrit looks for hooks, and what
 | 
			
		||||
filenames it looks for by adding a [hooks] section to gerrit.config.
 | 
			
		||||
 | 
			
		||||
Gerrit will use the value of hooks.path for the hooks directory, and
 | 
			
		||||
the values of hooks.patchsetCreatedHook, hooks.commentAddedHook,
 | 
			
		||||
hooks.changeMergedHook and hooks.changeAbandonedHook for the
 | 
			
		||||
filenames for the hooks.
 | 
			
		||||
 | 
			
		||||
See Also
 | 
			
		||||
--------
 | 
			
		||||
 | 
			
		||||
* link:config-gerrit.html#hooks[Section hooks]
 | 
			
		||||
 | 
			
		||||
GERRIT
 | 
			
		||||
------
 | 
			
		||||
Part of link:index.html[Gerrit Code Review]
 | 
			
		||||
@@ -29,6 +29,7 @@ Configuration
 | 
			
		||||
* link:config-headerfooter.html[Site Header/Footer]
 | 
			
		||||
* link:config-sso.html[Single Sign-On Systems]
 | 
			
		||||
* link:config-apache2.html[Apache 2 Reverse Proxy]
 | 
			
		||||
* link:config-hooks.html[Hooks]
 | 
			
		||||
 | 
			
		||||
Developer Documentation
 | 
			
		||||
-----------------------
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@
 | 
			
		||||
 | 
			
		||||
package com.google.gerrit.httpd.rpc.changedetail;
 | 
			
		||||
 | 
			
		||||
import com.google.gerrit.common.ChangeHookRunner;
 | 
			
		||||
import com.google.gerrit.common.data.ChangeDetail;
 | 
			
		||||
import com.google.gerrit.common.errors.NoSuchEntityException;
 | 
			
		||||
import com.google.gerrit.httpd.rpc.Handler;
 | 
			
		||||
@@ -55,13 +56,15 @@ class AbandonChange extends Handler<ChangeDetail> {
 | 
			
		||||
  @Nullable
 | 
			
		||||
  private final String message;
 | 
			
		||||
 | 
			
		||||
  private final ChangeHookRunner hooks;
 | 
			
		||||
 | 
			
		||||
  @Inject
 | 
			
		||||
  AbandonChange(final ChangeControl.Factory changeControlFactory,
 | 
			
		||||
      final ReviewDb db, final IdentifiedUser currentUser,
 | 
			
		||||
      final AbandonedSender.Factory abandonedSenderFactory,
 | 
			
		||||
      final ChangeDetailFactory.Factory changeDetailFactory,
 | 
			
		||||
      @Assisted final PatchSet.Id patchSetId,
 | 
			
		||||
      @Assisted @Nullable final String message) {
 | 
			
		||||
      @Assisted @Nullable final String message, final ChangeHookRunner hooks) {
 | 
			
		||||
    this.changeControlFactory = changeControlFactory;
 | 
			
		||||
    this.db = db;
 | 
			
		||||
    this.currentUser = currentUser;
 | 
			
		||||
@@ -70,6 +73,7 @@ class AbandonChange extends Handler<ChangeDetail> {
 | 
			
		||||
 | 
			
		||||
    this.patchSetId = patchSetId;
 | 
			
		||||
    this.message = message;
 | 
			
		||||
    this.hooks = hooks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
@@ -129,6 +133,8 @@ class AbandonChange extends Handler<ChangeDetail> {
 | 
			
		||||
      cm.send();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hooks.doChangeAbandonedHook(change, currentUser.getAccount(), message);
 | 
			
		||||
 | 
			
		||||
    return changeDetailFactory.create(changeId).call();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,288 @@
 | 
			
		||||
// Copyright (C) 2010 The Android Open Source Project
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
// http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
package com.google.gerrit.common;
 | 
			
		||||
 | 
			
		||||
import com.google.gerrit.reviewdb.Account;
 | 
			
		||||
import com.google.gerrit.reviewdb.ApprovalCategory;
 | 
			
		||||
import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 | 
			
		||||
import com.google.gerrit.reviewdb.Change;
 | 
			
		||||
import com.google.gerrit.reviewdb.PatchSet;
 | 
			
		||||
import com.google.gerrit.server.config.GerritServerConfig;
 | 
			
		||||
import com.google.gerrit.server.config.SitePaths;
 | 
			
		||||
import com.google.gerrit.server.git.GitRepositoryManager;
 | 
			
		||||
import com.google.gerrit.server.git.WorkQueue;
 | 
			
		||||
import com.google.inject.Inject;
 | 
			
		||||
import com.google.inject.Singleton;
 | 
			
		||||
import java.io.BufferedReader;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStreamReader;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import org.eclipse.jgit.lib.Config;
 | 
			
		||||
import org.eclipse.jgit.lib.Repository;
 | 
			
		||||
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class implements hooks for certain gerrit events.
 | 
			
		||||
 */
 | 
			
		||||
@Singleton
 | 
			
		||||
public class ChangeHookRunner {
 | 
			
		||||
    /** A logger for this class. */
 | 
			
		||||
    private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
 | 
			
		||||
 | 
			
		||||
    /** Filename of the new patchset hook. */
 | 
			
		||||
    private final File patchsetCreatedHook;
 | 
			
		||||
 | 
			
		||||
    /** Filename of the new comments hook. */
 | 
			
		||||
    private final File commentAddedHook;
 | 
			
		||||
 | 
			
		||||
    /** Filename of the change merged hook. */
 | 
			
		||||
    private final File changeMergedHook;
 | 
			
		||||
 | 
			
		||||
    /** Filename of the change abandoned hook. */
 | 
			
		||||
    private final File changeAbandonedHook;
 | 
			
		||||
 | 
			
		||||
    /** Repository Manager. */
 | 
			
		||||
    private final GitRepositoryManager repoManager;
 | 
			
		||||
 | 
			
		||||
    /** Queue of hooks that need to run. */
 | 
			
		||||
    private final WorkQueue.Executor hookQueue;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new ChangeHookRunner.
 | 
			
		||||
     *
 | 
			
		||||
     * @param queue Queue to use when processing hooks.
 | 
			
		||||
     * @param repoManager The repository manager.
 | 
			
		||||
     * @param config Config file to use.
 | 
			
		||||
     * @param sitePath The sitepath of this gerrit install.
 | 
			
		||||
     */
 | 
			
		||||
    @Inject
 | 
			
		||||
    public ChangeHookRunner(final WorkQueue queue, final GitRepositoryManager repoManager, @GerritServerConfig final Config config, final SitePaths sitePath) {
 | 
			
		||||
        this.repoManager = repoManager;
 | 
			
		||||
        this.hookQueue = queue.createQueue(1, "hook");
 | 
			
		||||
 | 
			
		||||
        final File hooksPath = sitePath.resolve(getValue(config, "hooks", "path", sitePath.hooks_dir.getAbsolutePath()));
 | 
			
		||||
 | 
			
		||||
        patchsetCreatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "patchsetCreatedHook", "patchset-created")).getPath());
 | 
			
		||||
        commentAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "commentAddedHook", "comment-added")).getPath());
 | 
			
		||||
        changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath());
 | 
			
		||||
        changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Helper Method for getting values from the config.
 | 
			
		||||
     *
 | 
			
		||||
     * @param config Config file to get value from.
 | 
			
		||||
     * @param section Section to look in.
 | 
			
		||||
     * @param setting Setting to get.
 | 
			
		||||
     * @param fallback Fallback value.
 | 
			
		||||
     * @return Setting value if found, else fallback.
 | 
			
		||||
     */
 | 
			
		||||
    private String getValue(final Config config, final String section, final String setting, final String fallback) {
 | 
			
		||||
        final String result = config.getString(section, null, setting);
 | 
			
		||||
        return (result == null) ? fallback : result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the Repository for the given change, or null on error.
 | 
			
		||||
     *
 | 
			
		||||
     * @param change Change to get repo for,
 | 
			
		||||
     * @return Repository or null.
 | 
			
		||||
     */
 | 
			
		||||
    private Repository getRepo(final Change change) {
 | 
			
		||||
        try {
 | 
			
		||||
            return repoManager.openRepository(change.getProject().get());
 | 
			
		||||
        } catch (Exception ex) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fire the Patchset Created Hook.
 | 
			
		||||
     *
 | 
			
		||||
     * @param change The change itself.
 | 
			
		||||
     * @param patchSet The Patchset that was created.
 | 
			
		||||
     */
 | 
			
		||||
    public void doPatchsetCreatedHook(final Change change, final PatchSet patchSet) {
 | 
			
		||||
        final List<String> args = new ArrayList<String>();
 | 
			
		||||
        args.add(patchsetCreatedHook.getAbsolutePath());
 | 
			
		||||
 | 
			
		||||
        args.add("--change");
 | 
			
		||||
        args.add(change.getKey().get());
 | 
			
		||||
        args.add("--project");
 | 
			
		||||
        args.add(change.getProject().get());
 | 
			
		||||
        args.add("--branch");
 | 
			
		||||
        args.add(change.getDest().getShortName());
 | 
			
		||||
        args.add("--commit");
 | 
			
		||||
        args.add(patchSet.getRevision().get());
 | 
			
		||||
        args.add("--patchset");
 | 
			
		||||
        args.add(Integer.toString(patchSet.getPatchSetId()));
 | 
			
		||||
 | 
			
		||||
        runHook(getRepo(change), args);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fire the Comment Added Hook.
 | 
			
		||||
     *
 | 
			
		||||
     * @param change The change itself.
 | 
			
		||||
     * @param account The gerrit user who commited the change.
 | 
			
		||||
     * @param comment The comment given.
 | 
			
		||||
     * @param approvals Map of Approval Categories and Scores
 | 
			
		||||
     */
 | 
			
		||||
    public void doCommentAddedHook(final Change change, final Account account, final String comment, final Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> approvals) {
 | 
			
		||||
        final List<String> args = new ArrayList<String>();
 | 
			
		||||
        args.add(commentAddedHook.getAbsolutePath());
 | 
			
		||||
 | 
			
		||||
        args.add("--change");
 | 
			
		||||
        args.add(change.getKey().get());
 | 
			
		||||
        args.add("--project");
 | 
			
		||||
        args.add(change.getProject().get());
 | 
			
		||||
        args.add("--branch");
 | 
			
		||||
        args.add(change.getDest().getShortName());
 | 
			
		||||
        args.add("--author");
 | 
			
		||||
        args.add(getDisplayName(account));
 | 
			
		||||
        args.add("--comment");
 | 
			
		||||
        args.add(comment == null ? "" : comment);
 | 
			
		||||
        for (Map.Entry<ApprovalCategory.Id, ApprovalCategoryValue.Id> approval : approvals.entrySet()) {
 | 
			
		||||
            args.add("--" + approval.getKey().get());
 | 
			
		||||
            args.add(Short.toString(approval.getValue().get()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        runHook(getRepo(change), args);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fire the Change Merged Hook.
 | 
			
		||||
     *
 | 
			
		||||
     * @param change The change itself.
 | 
			
		||||
     * @param account The gerrit user who commited the change.
 | 
			
		||||
     * @param patchSet The patchset that was merged.
 | 
			
		||||
     */
 | 
			
		||||
    public void doChangeMergedHook(final Change change, final Account account, final PatchSet patchSet) {
 | 
			
		||||
        final List<String> args = new ArrayList<String>();
 | 
			
		||||
        args.add(changeMergedHook.getAbsolutePath());
 | 
			
		||||
 | 
			
		||||
        args.add("--change");
 | 
			
		||||
        args.add(change.getKey().get());
 | 
			
		||||
        args.add("--project");
 | 
			
		||||
        args.add(change.getProject().get());
 | 
			
		||||
        args.add("--branch");
 | 
			
		||||
        args.add(change.getDest().getShortName());
 | 
			
		||||
        args.add("--submitter");
 | 
			
		||||
        args.add(getDisplayName(account));
 | 
			
		||||
        args.add("--commit");
 | 
			
		||||
        args.add(patchSet.getRevision().get());
 | 
			
		||||
 | 
			
		||||
        runHook(getRepo(change), args);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fire the Change Abandoned Hook.
 | 
			
		||||
     *
 | 
			
		||||
     * @param change The change itself.
 | 
			
		||||
     * @param account The gerrit user who abandoned the change.
 | 
			
		||||
     * @param reason Reason for abandoning the change.
 | 
			
		||||
     */
 | 
			
		||||
    public void doChangeAbandonedHook(final Change change, final Account account, final String reason) {
 | 
			
		||||
        final List<String> args = new ArrayList<String>();
 | 
			
		||||
        args.add(changeAbandonedHook.getAbsolutePath());
 | 
			
		||||
 | 
			
		||||
        args.add("--change");
 | 
			
		||||
        args.add(change.getKey().get());
 | 
			
		||||
        args.add("--project");
 | 
			
		||||
        args.add(change.getProject().get());
 | 
			
		||||
        args.add("--branch");
 | 
			
		||||
        args.add(change.getDest().getShortName());
 | 
			
		||||
        args.add("--abandoner");
 | 
			
		||||
        args.add(getDisplayName(account));
 | 
			
		||||
        args.add("--reason");
 | 
			
		||||
        args.add(reason == null ? "" : reason);
 | 
			
		||||
 | 
			
		||||
        runHook(getRepo(change), args);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the display name for the given account.
 | 
			
		||||
     *
 | 
			
		||||
     * @param account Account to get name for.
 | 
			
		||||
     * @return Name for this account.
 | 
			
		||||
     */
 | 
			
		||||
    private String getDisplayName(final Account account) {
 | 
			
		||||
        if (account != null) {
 | 
			
		||||
            String result = (account.getFullName() == null) ? "Anonymous Coward" : account.getFullName();
 | 
			
		||||
            if (account.getPreferredEmail() != null) {
 | 
			
		||||
                result += " (" + account.getPreferredEmail() + ")";
 | 
			
		||||
            }
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return "Anonymous Coward";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Run a hook.
 | 
			
		||||
     *
 | 
			
		||||
     * @param repo Repo to run the hook for.
 | 
			
		||||
     * @param args Arguments to use to run the hook.
 | 
			
		||||
     */
 | 
			
		||||
    private synchronized void runHook(final Repository repo, final List<String> args) {
 | 
			
		||||
        if (repo == null) {
 | 
			
		||||
            log.error("No repo found for hook.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        hookQueue.execute(new Runnable() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void run() {
 | 
			
		||||
                try {
 | 
			
		||||
                    if (new File(args.get(0)).exists()) {
 | 
			
		||||
                        final ProcessBuilder pb = new ProcessBuilder(args);
 | 
			
		||||
                        pb.redirectErrorStream(true);
 | 
			
		||||
                        pb.directory(repo.getDirectory());
 | 
			
		||||
                        final Map<String, String> env = pb.environment();
 | 
			
		||||
                        env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
 | 
			
		||||
 | 
			
		||||
                        Process ps = pb.start();
 | 
			
		||||
                        ps.getOutputStream().close();
 | 
			
		||||
 | 
			
		||||
                        BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
 | 
			
		||||
                        try {
 | 
			
		||||
                            String line;
 | 
			
		||||
                            while ((line = br.readLine()) != null) {
 | 
			
		||||
                                log.info("hook output: " + line);
 | 
			
		||||
                            }
 | 
			
		||||
                        } finally {
 | 
			
		||||
                            try {
 | 
			
		||||
                                br.close();
 | 
			
		||||
                            } catch (IOException e2) {
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            ps.waitFor();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (Throwable e) {
 | 
			
		||||
                    log.error("Unexpected error during hook execution", e);
 | 
			
		||||
                } finally {
 | 
			
		||||
                    repo.close();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -28,6 +28,7 @@ public final class SitePaths {
 | 
			
		||||
  public final File etc_dir;
 | 
			
		||||
  public final File lib_dir;
 | 
			
		||||
  public final File logs_dir;
 | 
			
		||||
  public final File hooks_dir;
 | 
			
		||||
  public final File static_dir;
 | 
			
		||||
 | 
			
		||||
  public final File gerrit_sh;
 | 
			
		||||
@@ -59,6 +60,7 @@ public final class SitePaths {
 | 
			
		||||
    etc_dir = new File(site_path, "etc");
 | 
			
		||||
    lib_dir = new File(site_path, "lib");
 | 
			
		||||
    logs_dir = new File(site_path, "logs");
 | 
			
		||||
    hooks_dir = new File(site_path, "hooks");
 | 
			
		||||
    static_dir = new File(site_path, "static");
 | 
			
		||||
 | 
			
		||||
    gerrit_sh = new File(bin_dir, "gerrit.sh");
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,8 @@
 | 
			
		||||
 | 
			
		||||
package com.google.gerrit.server.git;
 | 
			
		||||
 | 
			
		||||
import com.google.gerrit.common.ChangeHookRunner;
 | 
			
		||||
import com.google.gerrit.common.data.AccountInfoCache;
 | 
			
		||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
 | 
			
		||||
import static java.util.concurrent.TimeUnit.MINUTES;
 | 
			
		||||
 | 
			
		||||
@@ -32,6 +34,7 @@ import com.google.gerrit.reviewdb.ReviewDb;
 | 
			
		||||
import com.google.gerrit.server.ChangeUtil;
 | 
			
		||||
import com.google.gerrit.server.GerritPersonIdent;
 | 
			
		||||
import com.google.gerrit.server.IdentifiedUser;
 | 
			
		||||
import com.google.gerrit.server.account.AccountCache;
 | 
			
		||||
import com.google.gerrit.server.config.CanonicalWebUrl;
 | 
			
		||||
import com.google.gerrit.server.config.Nullable;
 | 
			
		||||
import com.google.gerrit.server.mail.EmailException;
 | 
			
		||||
@@ -149,6 +152,9 @@ public class MergeOp {
 | 
			
		||||
  private Set<RevCommit> alreadyAccepted;
 | 
			
		||||
  private RefUpdate branchUpdate;
 | 
			
		||||
 | 
			
		||||
  private final ChangeHookRunner hooks;
 | 
			
		||||
  private final AccountCache accountCache;
 | 
			
		||||
 | 
			
		||||
  @Inject
 | 
			
		||||
  MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
 | 
			
		||||
      final ProjectCache pc, final FunctionState.Factory fs,
 | 
			
		||||
@@ -158,7 +164,8 @@ public class MergeOp {
 | 
			
		||||
      final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif,
 | 
			
		||||
      final IdentifiedUser.GenericFactory iuf,
 | 
			
		||||
      @GerritPersonIdent final PersonIdent myIdent,
 | 
			
		||||
      final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch) {
 | 
			
		||||
      final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
 | 
			
		||||
      final ChangeHookRunner hooks, final AccountCache accountCache) {
 | 
			
		||||
    repoManager = grm;
 | 
			
		||||
    schemaFactory = sf;
 | 
			
		||||
    functionState = fs;
 | 
			
		||||
@@ -171,6 +178,8 @@ public class MergeOp {
 | 
			
		||||
    patchSetInfoFactory = psif;
 | 
			
		||||
    identifiedUserFactory = iuf;
 | 
			
		||||
    this.mergeQueue = mergeQueue;
 | 
			
		||||
    this.hooks = hooks;
 | 
			
		||||
    this.accountCache = accountCache;
 | 
			
		||||
 | 
			
		||||
    this.myIdent = myIdent;
 | 
			
		||||
    destBranch = branch;
 | 
			
		||||
@@ -1151,6 +1160,12 @@ public class MergeOp {
 | 
			
		||||
    } catch (EmailException e) {
 | 
			
		||||
      log.error("Cannot send email for submitted patch set " + c.getId(), e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        hooks.doChangeMergedHook(c, accountCache.get(submitter.getAccountId()).getAccount(), schema.patchSets().get(c.currentPatchSetId()));
 | 
			
		||||
    } catch (OrmException ex) {
 | 
			
		||||
        log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void setNew(Change c, ChangeMessage msg) {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@
 | 
			
		||||
 | 
			
		||||
package com.google.gerrit.server.patch;
 | 
			
		||||
 | 
			
		||||
import com.google.gerrit.common.ChangeHookRunner;
 | 
			
		||||
import com.google.gerrit.common.data.ApprovalType;
 | 
			
		||||
import com.google.gerrit.common.data.ApprovalTypes;
 | 
			
		||||
import com.google.gerrit.reviewdb.ApprovalCategory;
 | 
			
		||||
@@ -65,6 +66,7 @@ public class PublishComments implements Callable<VoidResult> {
 | 
			
		||||
  private final PatchSetInfoFactory patchSetInfoFactory;
 | 
			
		||||
  private final ChangeControl.Factory changeControlFactory;
 | 
			
		||||
  private final FunctionState.Factory functionStateFactory;
 | 
			
		||||
  private final ChangeHookRunner hooks;
 | 
			
		||||
 | 
			
		||||
  private final PatchSet.Id patchSetId;
 | 
			
		||||
  private final String messageText;
 | 
			
		||||
@@ -82,6 +84,7 @@ public class PublishComments implements Callable<VoidResult> {
 | 
			
		||||
      final PatchSetInfoFactory patchSetInfoFactory,
 | 
			
		||||
      final ChangeControl.Factory changeControlFactory,
 | 
			
		||||
      final FunctionState.Factory functionStateFactory,
 | 
			
		||||
      final ChangeHookRunner hooks,
 | 
			
		||||
 | 
			
		||||
      @Assisted final PatchSet.Id patchSetId,
 | 
			
		||||
      @Assisted final String messageText,
 | 
			
		||||
@@ -93,6 +96,7 @@ public class PublishComments implements Callable<VoidResult> {
 | 
			
		||||
    this.commentSenderFactory = commentSenderFactory;
 | 
			
		||||
    this.changeControlFactory = changeControlFactory;
 | 
			
		||||
    this.functionStateFactory = functionStateFactory;
 | 
			
		||||
    this.hooks = hooks;
 | 
			
		||||
 | 
			
		||||
    this.patchSetId = patchSetId;
 | 
			
		||||
    this.messageText = messageText;
 | 
			
		||||
@@ -121,6 +125,7 @@ public class PublishComments implements Callable<VoidResult> {
 | 
			
		||||
 | 
			
		||||
    touchChange();
 | 
			
		||||
    email();
 | 
			
		||||
    fireHook();
 | 
			
		||||
    return VoidResult.INSTANCE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -278,4 +283,14 @@ public class PublishComments implements Callable<VoidResult> {
 | 
			
		||||
      log.error("Failed to obtain PatchSetInfo for patch set " + patchSetId, e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void fireHook() {
 | 
			
		||||
    final Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> changed =
 | 
			
		||||
        new HashMap<ApprovalCategory.Id, ApprovalCategoryValue.Id>();
 | 
			
		||||
    for (ApprovalCategoryValue.Id v : approvals) {
 | 
			
		||||
      changed.put(v.getParentKey(), v);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hooks.doCommentAddedHook(change, user.getAccount(), messageText, changed);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@
 | 
			
		||||
 | 
			
		||||
package com.google.gerrit.sshd.commands;
 | 
			
		||||
 | 
			
		||||
import com.google.gerrit.common.ChangeHookRunner;
 | 
			
		||||
import com.google.gerrit.common.data.ApprovalType;
 | 
			
		||||
import com.google.gerrit.common.data.ApprovalTypes;
 | 
			
		||||
import com.google.gerrit.reviewdb.ApprovalCategory;
 | 
			
		||||
@@ -49,8 +50,10 @@ import org.slf4j.LoggerFactory;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
public class ApproveCommand extends BaseCommand {
 | 
			
		||||
@@ -106,6 +109,9 @@ public class ApproveCommand extends BaseCommand {
 | 
			
		||||
  @Inject
 | 
			
		||||
  private FunctionState.Factory functionStateFactory;
 | 
			
		||||
 | 
			
		||||
  @Inject
 | 
			
		||||
  private ChangeHookRunner hooks;
 | 
			
		||||
 | 
			
		||||
  private List<ApproveOption> optionList;
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
@@ -154,6 +160,9 @@ public class ApproveCommand extends BaseCommand {
 | 
			
		||||
    msgBuf.append(patchSetId.get());
 | 
			
		||||
    msgBuf.append(": ");
 | 
			
		||||
 | 
			
		||||
    final Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> approvalsMap =
 | 
			
		||||
        new HashMap<ApprovalCategory.Id, ApprovalCategoryValue.Id>();
 | 
			
		||||
 | 
			
		||||
    for (ApproveOption co : optionList) {
 | 
			
		||||
      final ApprovalCategory.Id category = co.getCategoryId();
 | 
			
		||||
      PatchSetApproval.Key psaKey =
 | 
			
		||||
@@ -174,10 +183,12 @@ public class ApproveCommand extends BaseCommand {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      String message =
 | 
			
		||||
          db.approvalCategoryValues().get(
 | 
			
		||||
              new ApprovalCategoryValue.Id(category, score)).getName();
 | 
			
		||||
      final ApprovalCategoryValue.Id val =
 | 
			
		||||
          new ApprovalCategoryValue.Id(category, score);
 | 
			
		||||
 | 
			
		||||
      String message = db.approvalCategoryValues().get(val).getName();
 | 
			
		||||
      msgBuf.append(" " + message + ";");
 | 
			
		||||
      approvalsMap.put(category, val);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    msgBuf.deleteCharAt(msgBuf.length() - 1);
 | 
			
		||||
@@ -196,6 +207,9 @@ public class ApproveCommand extends BaseCommand {
 | 
			
		||||
 | 
			
		||||
    ChangeUtil.touch(change, db);
 | 
			
		||||
    sendMail(change, change.currentPatchSetId(), cm);
 | 
			
		||||
 | 
			
		||||
    hooks.doCommentAddedHook(change, currentUser.getAccount(), changeComment,
 | 
			
		||||
        approvalsMap);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private Set<PatchSet.Id> parsePatchSetId(final String patchIdentity)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@
 | 
			
		||||
 | 
			
		||||
package com.google.gerrit.sshd.commands;
 | 
			
		||||
 | 
			
		||||
import com.google.gerrit.common.ChangeHookRunner;
 | 
			
		||||
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD;
 | 
			
		||||
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_REPLACE;
 | 
			
		||||
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_UPDATE;
 | 
			
		||||
@@ -153,6 +154,9 @@ final class Receive extends AbstractGitCommand {
 | 
			
		||||
  @Inject
 | 
			
		||||
  private PatchSetInfoFactory patchSetInfoFactory;
 | 
			
		||||
 | 
			
		||||
  @Inject
 | 
			
		||||
  private ChangeHookRunner hooks;
 | 
			
		||||
 | 
			
		||||
  @Inject
 | 
			
		||||
  @CanonicalWebUrl
 | 
			
		||||
  @Nullable
 | 
			
		||||
@@ -870,6 +874,8 @@ final class Receive extends AbstractGitCommand {
 | 
			
		||||
    } catch (EmailException e) {
 | 
			
		||||
      log.error("Cannot send email for new change " + change.getId(), e);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hooks.doPatchsetCreatedHook(change, ps);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static boolean isReviewer(final FooterLine candidateFooterLine) {
 | 
			
		||||
@@ -1132,6 +1138,8 @@ final class Receive extends AbstractGitCommand {
 | 
			
		||||
          insertDummyApproval(result, reviewer, catId, db);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      hooks.doPatchsetCreatedHook(result.change, ps);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final RefUpdate ru = repo.updateRef(ps.getRefName());
 | 
			
		||||
@@ -1447,6 +1455,8 @@ final class Receive extends AbstractGitCommand {
 | 
			
		||||
        final PatchSet.Id psi = result.patchSet.getId();
 | 
			
		||||
        log.error("Cannot send email for submitted patch set " + psi, e);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      hooks.doChangeMergedHook(result.change, currentUser.getAccount(), result.patchSet);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user