Move hooks from core Gerrit to 'hooks' plugin

Change-Id: If45b32ffb0e9f52677886103212563d8af6c6099
This commit is contained in:
David Pursehouse 2016-06-20 21:11:33 +09:00
parent 041fce1dc5
commit cad96d6462
13 changed files with 10 additions and 1848 deletions

4
.gitmodules vendored
View File

@ -10,6 +10,10 @@
path = plugins/download-commands
url = ../plugins/download-commands
[submodule "plugins/hooks"]
path = plugins/hooks
url = ../plugins/hooks
[submodule "plugins/replication"]
path = plugins/replication
url = ../plugins/replication

View File

@ -2002,85 +2002,6 @@ all registered users.
+
By default, false.
[[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.syncHookTimeout]]hooks.syncHookTimeout::
+
Optional timeout value in seconds for synchronous hooks, if not specified
then 30 seconds will be used.
[[hooks.changeAbandonedHook]]hooks.changeAbandonedHook::
+
Optional filename for the change abandoned hook, if not specified then
`change-abandoned` will be used.
[[hooks.changeMergedHook]]hooks.changeMergedHook::
+
Optional filename for the change merged hook, if not specified then
`change-merged` will be used.
[[hooks.changeRestoredHook]]hooks.changeRestoredHook::
+
Optional filename for the change restored hook, if not specified then
`change-restored` will be used.
[[hooks.claSignedHook]]hooks.claSignedHook::
+
Optional filename for the CLA signed hook, if not specified then
`cla-signed` will be used.
[[hooks.commentAddedHook]]hooks.commentAddedHook::
+
Optional filename for the comment added hook, if not specified then
`comment-added` will be used.
[[hooks.draftPublishedHook]]hooks.draftPublishedHook::
+
Optional filename for the draft published hook, if not specified then
`draft-published` will be used.
[[hooks.hashtagsChangedHook]]hooks.hashtagsChangedHook::
+
Optional filename for the hashtags changed hook, if not specified then
`hashtags-changed` will be used.
[[hooks.projectCreatedHook]]hooks.projectCreatedHook::
+
Optional filename for the project created hook, if not specified then
`project-created` will be used.
[[hooks.patchsetCreatedHook]]hooks.patchsetCreatedHook::
+
Optional filename for the patchset created hook, if not specified then
`patchset-created` will be used.
[[hooks.refUpdateHook]]hooks.refUpdateHook::
+
Optional filename for the ref update hook, if not specified then
`ref-update` will be used.
[[hooks.refUpdatedHook]]hooks.refUpdatedHook::
+
Optional filename for the ref updated hook, if not specified then
`ref-updated` will be used.
[[hooks.reviewerAddedHook]]hooks.reviewerAddedHook::
+
Optional filename for the reviewer added hook, if not specified then
`reviewer-added` will be used.
[[hooks.topicChangedHook]]hooks.topicChangedHook::
+
Optional filename for the topic changed hook, if not specified then
`topic-changed` will be used.
[[http]]
=== Section http

View File

@ -1,192 +1,9 @@
= 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.
With the exception of the ref-update hook, hooks are run in the background
after the relevant 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
=== ref-update
This is called when a push request is received by Gerrit. It allows
a push to be rejected before it is committed to the Gerrit repository.
If the script exits with non-zero return code the push will be rejected.
Any output from the script will be returned to the user, regardless of the
return code.
This hook is called synchronously so it is recommended that
it not block. A default timeout on the hook is set to 30 seconds to avoid
"runaway" hooks using up server threads. See link:config-gerrit.html#hooks.syncHookTimeout[hooks.syncHookTimeout]
for configuration details.
====
ref-update --project <project name> --refname <refname> --uploader <uploader> --oldrev <sha1> --newrev <sha1>
====
=== patchset-created
This is called whenever a patchset is created (this includes new
changes and drafts).
====
patchset-created --change <change id> --is-draft <boolean> --kind <change kind> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
====
kind:: change kind represents the kind of change uploaded, also represented in link:json.html#patchSet[patchSet]
REWORK;; Nontrivial content changes.
TRIVIAL_REBASE;; Conflict-free merge between the new parent and the prior patch set.
MERGE_FIRST_PARENT_UPDATE;; Conflict-free change of first (left) parent of a merge commit.
NO_CODE_CHANGE;; No code changed; same tree and same parent tree.
NO_CHANGE;; No changes; same commit message, same tree and same parent tree.
=== draft-published
This is called whenever a draft change is published.
====
draft-published --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
====
=== comment-added
This is called whenever a comment is added to a change.
====
comment-added --change <change id> --is-draft <boolean> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> --<approval category id>-oldValue <score> ...]
====
=== change-merged
Called whenever a change has been merged.
====
change-merged --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1> --newrev <sha1>
====
=== change-abandoned
Called whenever a change has been abandoned.
====
change-abandoned --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --commit <sha1> --reason <reason>
====
=== change-restored
Called whenever a change has been restored.
====
change-restored --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --commit <sha1> --reason <reason>
====
=== ref-updated
Called whenever a ref has been updated.
====
ref-updated --oldrev <old rev> --newrev <new rev> --refname <ref name> --project <project name> --submitter <submitter>
====
=== project-created
Called whenever a project has been created.
====
project-created --project <project name> --head <head name>
====
=== reviewer-added
Called whenever a reviewer is added to a change.
====
reviewer-added --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --reviewer <reviewer>
====
=== reviewer-deleted
Called whenever a reviewer (with a vote) is removed from a change.
====
reviewer-deleted --change <change id> --change-url <change url> --change-owner <change owner> --project <project name> --branch <branch> --reviewer <reviewer> [--<approval category id> <score> --<approval category id> <score> ...]
====
=== topic-changed
Called whenever a change's topic is changed from the Web UI or via the REST API.
====
topic-changed --change <change id> --change-owner <change owner> --project <project name> --branch <branch> --changer <changer> --old-topic <old topic> --new-topic <new topic>
====
=== hashtags-changed
Called whenever hashtags are added to or removed from a change from the Web UI
or via the REST API.
====
hashtags-changed --change <change id> --change-owner <change owner> --project <project name> --branch <branch> --editor <editor> --added <hashtag> --removed <hashtag> --hashtag <hashtag>
====
The `--added` parameter may be passed multiple times, once for each
hashtag that was added to the change.
The `--removed` parameter may be passed multiple times, once for each
hashtag that was removed from the change.
The `--hashtag` parameter may be passed multiple times, once for each
hashtag remaining on the change after the add or remove operation has
been performed.
=== cla-signed
Called whenever a user signs a contributor license agreement.
====
cla-signed --submitter <submitter> --user-id <user_id> --cla-id <cla_id>
====
== Configuration Settings
It is possible to change where Gerrit looks for hooks, and what
filenames it looks for, by adding a [hooks] section in gerrit.config.
Gerrit will use the value of hooks.path for the hooks directory.
For the hook filenames, Gerrit will use the values of hooks.patchsetCreatedHook,
hooks.draftPublishedHook, hooks.commentAddedHook, hooks.changeMergedHook,
hooks.changeAbandonedHook, hooks.changeRestoredHook, hooks.refUpdatedHook,
hooks.refUpdateHook, hooks.reviewerAddedHook and hooks.claSignedHook.
== Missing Change URLs
If link:config-gerrit.html#gerrit.canonicalWebUrl[gerrit.canonicalWebUrl]
is not set in `gerrit.config` the `--change-url` flag may not be
passed to all hooks. Hooks started out of an SSH context (for example
the patchset-created hook) don't know the server's web URL, unless
this variable is configured.
== SEE ALSO
* link:config-gerrit.html#hooks[Section 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 via
the link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/hooks[
hooks plugin].
GERRIT
------

View File

@ -19,8 +19,6 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.ChangeHookApiListener;
import com.google.gerrit.common.ChangeHookRunner;
import com.google.gerrit.common.EventBroker;
import com.google.gerrit.common.StreamEventsApiListener;
import com.google.gerrit.gpg.GpgModule;
@ -343,9 +341,7 @@ public class Daemon extends SiteProgram {
modules.add(createIndexModule());
modules.add(new WorkQueue.Module());
modules.add(new ChangeHookApiListener.Module());
modules.add(new StreamEventsApiListener.Module());
modules.add(new ChangeHookRunner.Module());
modules.add(new EventBroker.Module());
modules.add(test
? new H2AccountPatchReviewStore.InMemoryModule()

View File

@ -14,8 +14,6 @@
package com.google.gerrit.pgm.util;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.DisabledChangeHooks;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
@ -27,7 +25,6 @@ import com.google.gerrit.server.git.validators.CommitValidators;
public class BatchGitModule extends FactoryModule {
@Override
protected void configure() {
bind(ChangeHooks.class).to(DisabledChangeHooks.class);
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
DynamicSet.setOf(binder(), CommitValidationListener.class);
factory(CommitValidators.Factory.class);

View File

@ -1,376 +0,0 @@
// Copyright (C) 2015 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 static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
import static org.eclipse.jgit.lib.Constants.R_HEADS;
import com.google.gerrit.common.ChangeHookRunner.HookResult;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.events.AgreementSignupListener;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeMergedListener;
import com.google.gerrit.extensions.events.ChangeRestoredListener;
import com.google.gerrit.extensions.events.CommentAddedListener;
import com.google.gerrit.extensions.events.DraftPublishedListener;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.HashtagsEditedListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
import com.google.gerrit.extensions.events.ReviewerAddedListener;
import com.google.gerrit.extensions.events.ReviewerDeletedListener;
import com.google.gerrit.extensions.events.RevisionCreatedListener;
import com.google.gerrit.extensions.events.TopicEditedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@Singleton
public class ChangeHookApiListener implements
AgreementSignupListener,
ChangeAbandonedListener,
ChangeMergedListener,
ChangeRestoredListener,
CommentAddedListener,
DraftPublishedListener,
GitReferenceUpdatedListener,
HashtagsEditedListener,
NewProjectCreatedListener,
ReviewerAddedListener,
ReviewerDeletedListener,
RevisionCreatedListener,
TopicEditedListener {
/** A logger for this class. */
private static final Logger log =
LoggerFactory.getLogger(ChangeHookApiListener.class);
public static class Module extends LifecycleModule {
@Override
protected void configure() {
DynamicSet.bind(binder(), AgreementSignupListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), ChangeAbandonedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), ChangeMergedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), ChangeRestoredListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), CommentAddedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), DraftPublishedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), HashtagsEditedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), NewProjectCreatedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), ReviewerAddedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), ReviewerDeletedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), RevisionCreatedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), TopicEditedListener.class)
.to(ChangeHookApiListener.class);
DynamicSet.bind(binder(), CommitValidationListener.class)
.to(ChangeHookValidator.class);
}
}
/** Reject commits that don't pass user-supplied ref-update hook. */
public static class ChangeHookValidator implements
CommitValidationListener {
private final ChangeHooks hooks;
@Inject
public ChangeHookValidator(ChangeHooks hooks) {
this.hooks = hooks;
}
@Override
public List<CommitValidationMessage> onCommitReceived(
CommitReceivedEvent receiveEvent) throws CommitValidationException {
IdentifiedUser user = receiveEvent.user;
String refname = receiveEvent.refName;
ObjectId old = ObjectId.zeroId();
if (receiveEvent.commit.getParentCount() > 0) {
old = receiveEvent.commit.getParent(0);
}
if (receiveEvent.command.getRefName().startsWith(REFS_CHANGES)) {
/*
* If the ref-update hook tries to distinguish behavior between pushes to
* refs/heads/... and refs/for/..., make sure we send it the correct
* refname.
* Also, if this is targetting refs/for/, make sure we behave the same as
* what a push to refs/for/ would behave; in particular, setting oldrev
* to 0000000000000000000000000000000000000000.
*/
refname = refname.replace(R_HEADS, "refs/for/refs/heads/");
old = ObjectId.zeroId();
}
HookResult result = hooks.doRefUpdateHook(receiveEvent.project, refname,
user.getAccount(), old, receiveEvent.commit);
if (result != null && result.getExitValue() != 0) {
throw new CommitValidationException(result.toString().trim());
}
return Collections.emptyList();
}
}
private final Provider<ReviewDb> db;
private final AccountCache accounts;
private final ChangeHooks hooks;
private final PatchSetUtil psUtil;
private final ChangeNotes.Factory changeNotesFactory;
@Inject
public ChangeHookApiListener(
Provider<ReviewDb> db,
AccountCache accounts,
ChangeHooks hooks,
PatchSetUtil psUtil,
ChangeNotes.Factory changeNotesFactory) {
this.db = db;
this.accounts = accounts;
this.hooks = hooks;
this.psUtil = psUtil;
this.changeNotesFactory = changeNotesFactory;
}
@Override
public void onNewProjectCreated(NewProjectCreatedListener.Event ev) {
hooks.doProjectCreatedHook(new Project.NameKey(ev.getProjectName()),
ev.getHeadName());
}
@Override
public void onRevisionCreated(RevisionCreatedListener.Event ev) {
try {
ChangeNotes notes = getNotes(ev.getChange());
hooks.doPatchsetCreatedHook(notes.getChange(),
getPatchSet(notes, ev.getRevision()), db.get());
} catch (OrmException e) {
log.error("PatchsetCreated hook failed to run "
+ ev.getChange()._number, e);
}
}
@Override
public void onDraftPublished(DraftPublishedListener.Event ev) {
try {
ChangeNotes notes = getNotes(ev.getChange());
hooks.doDraftPublishedHook(notes.getChange(),
getPatchSet(notes, ev.getRevision()), db.get());
} catch (OrmException e) {
log.error("DraftPublished hook failed to run "
+ ev.getChange()._number, e);
}
}
@Override
public void onCommentAdded(CommentAddedListener.Event ev) {
Map<String, Short> approvals = convertApprovalsMap(ev.getApprovals());
Map<String, Short> oldApprovals = convertApprovalsMap(ev.getOldApprovals());
try {
ChangeNotes notes = getNotes(ev.getChange());
hooks.doCommentAddedHook(notes.getChange(),
getAccount(ev.getAuthor()),
getPatchSet(notes, ev.getRevision()),
ev.getComment(), approvals, oldApprovals, db.get());
} catch (OrmException e) {
log.error("CommentAdded hook failed to fun" + ev.getChange()._number, e);
}
}
@Override
public void onChangeMerged(ChangeMergedListener.Event ev) {
try {
ChangeNotes notes = getNotes(ev.getChange());
hooks.doChangeMergedHook(notes.getChange(),
getAccount(ev.getMerger()),
getPatchSet(notes, ev.getRevision()),
db.get(), ev.getNewRevisionId());
} catch (OrmException e) {
log.error("ChangeMerged hook failed to run " + ev.getChange()._number, e);
}
}
@Override
public void onChangeAbandoned(ChangeAbandonedListener.Event ev) {
try {
ChangeNotes notes = getNotes(ev.getChange());
hooks.doChangeAbandonedHook(notes.getChange(),
getAccount(ev.getAbandoner()),
getPatchSet(notes, ev.getRevision()),
ev.getReason(), db.get());
} catch (OrmException e) {
log.error("ChangeAbandoned hook failed to run "
+ ev.getChange()._number, e);
}
}
@Override
public void onChangeRestored(ChangeRestoredListener.Event ev) {
try {
ChangeNotes notes = getNotes(ev.getChange());
hooks.doChangeRestoredHook(notes.getChange(),
getAccount(ev.getRestorer()),
getPatchSet(notes, ev.getRevision()),
ev.getReason(), db.get());
} catch (OrmException e) {
log.error("ChangeRestored hook failed to run "
+ ev.getChange()._number, e);
}
}
@Override
public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event ev) {
hooks.doRefUpdatedHook(
new Branch.NameKey(ev.getProjectName(), ev.getRefName()),
ObjectId.fromString(ev.getOldObjectId()),
ObjectId.fromString(ev.getNewObjectId()),
getAccount(ev.getUpdater()));
}
@Override
public void onReviewerAdded(ReviewerAddedListener.Event ev) {
try {
ChangeNotes notes = getNotes(ev.getChange());
hooks.doReviewerAddedHook(notes.getChange(),
getAccount(ev.getReviewer()),
psUtil.current(db.get(), notes),
db.get());
} catch (OrmException e) {
log.error("ReviewerAdded hook failed to run "
+ ev.getChange()._number, e);
}
}
@Override
public void onReviewerDeleted(ReviewerDeletedListener.Event ev) {
try {
ChangeNotes notes = getNotes(ev.getChange());
hooks.doReviewerDeletedHook(notes.getChange(),
getAccount(ev.getReviewer()),
psUtil.current(db.get(), notes),
ev.getComment(),
convertApprovalsMap(ev.getNewApprovals()),
convertApprovalsMap(ev.getOldApprovals()),
db.get());
} catch (OrmException e) {
log.error("ReviewerDeleted hook failed to run "
+ ev.getChange()._number, e);
}
}
@Override
public void onTopicEdited(TopicEditedListener.Event ev) {
try {
hooks.doTopicChangedHook(getNotes(ev.getChange()).getChange(),
getAccount(ev.getEditor()), ev.getOldTopic(), db.get());
} catch (OrmException e) {
log.error("TopicChanged hook failed to run "
+ ev.getChange()._number, e);
}
}
@Override
public void onHashtagsEdited(HashtagsEditedListener.Event ev) {
try {
hooks.doHashtagsChangedHook(getNotes(ev.getChange()).getChange(),
getAccount(ev.getEditor()),
new HashSet<>(ev.getAddedHashtags()),
new HashSet<>(ev.getRemovedHashtags()),
new HashSet<>(ev.getHashtags()),
db.get());
} catch (OrmException e) {
log.error("HashtagsChanged hook failed to run "
+ ev.getChange()._number, e);
}
}
@Override
public void onAgreementSignup(AgreementSignupListener.Event ev) {
hooks.doClaSignupHook(getAccount(ev.getAccount()), ev.getAgreementName());
}
private ChangeNotes getNotes(ChangeInfo info) throws OrmException {
try {
return changeNotesFactory.createChecked(new Change.Id(info._number));
} catch (NoSuchChangeException e) {
throw new OrmException(e);
}
}
private PatchSet getPatchSet(ChangeNotes notes, RevisionInfo info)
throws OrmException {
return psUtil.get(db.get(), notes, PatchSet.Id.fromRef(info.ref));
}
private Account getAccount(AccountInfo info) {
if (info != null) {
AccountState account = accounts.get(new Account.Id(info._accountId));
if (account != null) {
return account.getAccount();
}
}
return null;
}
private static Map<String, Short> convertApprovalsMap(
Map<String, ApprovalInfo> approvals) {
Map<String, Short> result = new HashMap<>();
for (Entry<String, ApprovalInfo> e : approvals.entrySet()) {
Short value =
e.getValue().value == null ? null : e.getValue().value.shortValue();
result.put(e.getKey(), value);
}
return result;
}
}

View File

@ -1,886 +0,0 @@
// 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.common.base.Optional;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.data.PatchSetAttribute;
import com.google.gerrit.server.data.RefUpdateAttribute;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/** Spawns local executables when a hook action occurs. */
@Singleton
public class ChangeHookRunner implements ChangeHooks, LifecycleListener {
/** A logger for this class. */
private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
public static class Module extends LifecycleModule {
@Override
protected void configure() {
bind(ChangeHookRunner.class);
bind(ChangeHooks.class).to(ChangeHookRunner.class);
listener().to(ChangeHookRunner.class);
}
}
/** Container class used to hold the return code and output of script hook execution */
public static class HookResult {
private int exitValue = -1;
private String output;
private String executionError;
private HookResult(int exitValue, String output) {
this.exitValue = exitValue;
this.output = output;
}
private HookResult(String output, String executionError) {
this.output = output;
this.executionError = executionError;
}
public int getExitValue() {
return exitValue;
}
public void setExitValue(int exitValue) {
this.exitValue = exitValue;
}
public String getOutput() {
return output;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (output != null && output.length() != 0) {
sb.append(output);
if (executionError != null) {
sb.append(" - ");
}
}
if (executionError != null ) {
sb.append(executionError);
}
return sb.toString();
}
}
/** Path of the new patchset hook. */
private final Optional<Path> patchsetCreatedHook;
/** Path of the draft published hook. */
private final Optional<Path> draftPublishedHook;
/** Path of the new comments hook. */
private final Optional<Path> commentAddedHook;
/** Path of the change merged hook. */
private final Optional<Path> changeMergedHook;
/** Path of the change abandoned hook. */
private final Optional<Path> changeAbandonedHook;
/** Path of the change restored hook. */
private final Optional<Path> changeRestoredHook;
/** Path of the ref updated hook. */
private final Optional<Path> refUpdatedHook;
/** Path of the reviewer added hook. */
private final Optional<Path> reviewerAddedHook;
/** Path of the reviewer deleted hook. */
private final Optional<Path> reviewerDeletedHook;
/** Path of the topic changed hook. */
private final Optional<Path> topicChangedHook;
/** Path of the cla signed hook. */
private final Optional<Path> claSignedHook;
/** Path of the update hook. */
private final Optional<Path> refUpdateHook;
/** Path of the hashtags changed hook */
private final Optional<Path> hashtagsChangedHook;
/** Path of the project created hook. */
private final Optional<Path> projectCreatedHook;
private final String anonymousCowardName;
/** Repository Manager. */
private final GitRepositoryManager repoManager;
/** Queue of hooks that need to run. */
private final WorkQueue.Executor hookQueue;
private final ProjectCache projectCache;
private final AccountCache accountCache;
private final EventFactory eventFactory;
private final SitePaths sitePaths;
/** Thread pool used to monitor sync hooks */
private final ExecutorService syncHookThreadPool;
/** Timeout value for synchronous hooks */
private final int syncHookTimeout;
/**
* 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.
* @param projectCache the project cache instance for the server.
*/
@Inject
public ChangeHookRunner(WorkQueue queue,
GitRepositoryManager repoManager,
@GerritServerConfig Config config,
@AnonymousCowardName String anonymousCowardName,
SitePaths sitePath,
ProjectCache projectCache,
AccountCache accountCache,
EventFactory eventFactory) {
this.anonymousCowardName = anonymousCowardName;
this.repoManager = repoManager;
this.hookQueue = queue.createQueue(1, "hook");
this.projectCache = projectCache;
this.accountCache = accountCache;
this.eventFactory = eventFactory;
this.sitePaths = sitePath;
Path hooksPath;
String hooksPathConfig = config.getString("hooks", null, "path");
if (hooksPathConfig != null) {
hooksPath = Paths.get(hooksPathConfig);
} else {
hooksPath = sitePath.hooks_dir;
}
// When adding a new hook, make sure to check that the setting name
// canonicalizes correctly in hook() below.
patchsetCreatedHook = hook(config, hooksPath, "patchset-created");
draftPublishedHook = hook(config, hooksPath, "draft-published");
commentAddedHook = hook(config, hooksPath, "comment-added");
changeMergedHook = hook(config, hooksPath, "change-merged");
changeAbandonedHook = hook(config, hooksPath, "change-abandoned");
changeRestoredHook = hook(config, hooksPath, "change-restored");
refUpdatedHook = hook(config, hooksPath, "ref-updated");
reviewerAddedHook = hook(config, hooksPath, "reviewer-added");
reviewerDeletedHook = hook(config, hooksPath, "reviewer-deleted");
topicChangedHook = hook(config, hooksPath, "topic-changed");
claSignedHook = hook(config, hooksPath, "cla-signed");
refUpdateHook = hook(config, hooksPath, "ref-update");
hashtagsChangedHook = hook(config, hooksPath, "hashtags-changed");
projectCreatedHook = hook(config, hooksPath, "project-created");
syncHookTimeout = config.getInt("hooks", "syncHookTimeout", 30);
syncHookThreadPool = Executors.newCachedThreadPool(
new ThreadFactoryBuilder()
.setNameFormat("SyncHook-%d")
.build());
}
private static Optional<Path> hook(Config config, Path path, String name) {
String setting = name.replace("-", "") + "hook";
String value = config.getString("hooks", null, setting);
Path p = path.resolve(value != null ? value : name);
return Files.exists(p) ? Optional.of(p) : Optional.<Path>absent();
}
/**
* Get the Repository for the given project name, or null on error.
*
* @param name Project to get repo for,
* @return Repository or null.
*/
private Repository openRepository(Project.NameKey name) {
try {
return repoManager.openRepository(name);
} catch (IOException err) {
log.warn("Cannot open repository " + name.get(), err);
return null;
}
}
private void addArg(List<String> args, String name, String value) {
if (value != null) {
args.add(name);
args.add(value);
}
}
@Override
public HookResult doRefUpdateHook(Project project, String refname,
Account uploader, ObjectId oldId, ObjectId newId) {
if (!refUpdateHook.isPresent()) {
return null;
}
List<String> args = new ArrayList<>();
addArg(args, "--project", project.getName());
addArg(args, "--refname", refname);
addArg(args, "--uploader", getDisplayName(uploader));
addArg(args, "--oldrev", oldId.getName());
addArg(args, "--newrev", newId.getName());
return runSyncHook(project.getNameKey(), refUpdateHook, args);
}
@Override
public void doProjectCreatedHook(Project.NameKey project, String headName) {
if (!projectCreatedHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
addArg(args, "--project", project.get());
addArg(args, "--head", headName);
runHook(project, projectCreatedHook, args);
}
@Override
public void doPatchsetCreatedHook(Change change,
PatchSet patchSet, ReviewDb db) throws OrmException {
if (!patchsetCreatedHook.isPresent()) {
return;
}
AccountState owner = accountCache.get(change.getOwner());
AccountState uploader = accountCache.get(patchSet.getUploader());
List<String> args = new ArrayList<>();
ChangeAttribute c = eventFactory.asChangeAttribute(change);
PatchSetAttribute ps = patchSetAttribute(change, patchSet);
addArg(args, "--change", c.id);
addArg(args, "--is-draft", String.valueOf(patchSet.isDraft()));
addArg(args, "--kind", String.valueOf(ps.kind));
addArg(args, "--change-url", c.url);
addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
addArg(args, "--project", c.project);
addArg(args, "--branch", c.branch);
addArg(args, "--topic", c.topic);
addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
addArg(args, "--commit", ps.revision);
addArg(args, "--patchset", ps.number);
runHook(change.getProject(), patchsetCreatedHook, args);
}
@Override
public void doDraftPublishedHook(Change change, PatchSet patchSet,
ReviewDb db) throws OrmException {
if (!draftPublishedHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
ChangeAttribute c = eventFactory.asChangeAttribute(change);
PatchSetAttribute ps = patchSetAttribute(change, patchSet);
AccountState owner = accountCache.get(change.getOwner());
AccountState uploader = accountCache.get(patchSet.getUploader());
addArg(args, "--change", c.id);
addArg(args, "--change-url", c.url);
addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
addArg(args, "--project", c.project);
addArg(args, "--branch", c.branch);
addArg(args, "--topic", c.topic);
addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
addArg(args, "--commit", ps.revision);
addArg(args, "--patchset", ps.number);
runHook(change.getProject(), draftPublishedHook, args);
}
@Override
public void doCommentAddedHook(final Change change, Account account,
PatchSet patchSet, String comment, final Map<String, Short> approvals,
final Map<String, Short> oldApprovals, ReviewDb db)
throws OrmException {
if (!commentAddedHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
ChangeAttribute c = eventFactory.asChangeAttribute(change);
PatchSetAttribute ps = patchSetAttribute(change, patchSet);
AccountState owner = accountCache.get(change.getOwner());
addArg(args, "--change", c.id);
addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false");
addArg(args, "--change-url", c.url);
addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
addArg(args, "--project", c.project);
addArg(args, "--branch", c.branch);
addArg(args, "--topic", c.topic);
addArg(args, "--author", getDisplayName(account));
addArg(args, "--commit", ps.revision);
addArg(args, "--comment", comment == null ? "" : comment);
LabelTypes labelTypes = projectCache.get(
change.getProject()).getLabelTypes();
for (Map.Entry<String, Short> approval : approvals.entrySet()) {
LabelType lt = labelTypes.byLabel(approval.getKey());
if (lt != null && approval.getValue() != null) {
addArg(args, "--" + lt.getName(), Short.toString(approval.getValue()));
if (oldApprovals != null && !oldApprovals.isEmpty()) {
Short oldValue = oldApprovals.get(approval.getKey());
if (oldValue != null) {
addArg(args, "--" + lt.getName() + "-oldValue",
Short.toString(oldValue));
}
}
}
}
runHook(change.getProject(), commentAddedHook, args);
}
@Override
public void doChangeMergedHook(Change change, Account account,
PatchSet patchSet, ReviewDb db, String mergeResultRev)
throws OrmException {
if (!changeMergedHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
ChangeAttribute c = eventFactory.asChangeAttribute(change);
PatchSetAttribute ps = patchSetAttribute(change, patchSet);
AccountState owner = accountCache.get(change.getOwner());
addArg(args, "--change", c.id);
addArg(args, "--change-url", c.url);
addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
addArg(args, "--project", c.project);
addArg(args, "--branch", c.branch);
addArg(args, "--topic", c.topic);
addArg(args, "--submitter", getDisplayName(account));
addArg(args, "--commit", ps.revision);
addArg(args, "--newrev", mergeResultRev);
runHook(change.getProject(), changeMergedHook, args);
}
@Override
public void doChangeAbandonedHook(Change change, Account account,
PatchSet patchSet, String reason, ReviewDb db)
throws OrmException {
if (!changeAbandonedHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
ChangeAttribute c = eventFactory.asChangeAttribute(change);
PatchSetAttribute ps = patchSetAttribute(change, patchSet);
AccountState owner = accountCache.get(change.getOwner());
addArg(args, "--change", c.id);
addArg(args, "--change-url", c.url);
addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
addArg(args, "--project", c.project);
addArg(args, "--branch", c.branch);
addArg(args, "--topic", c.topic);
addArg(args, "--abandoner", getDisplayName(account));
addArg(args, "--commit", ps.revision);
addArg(args, "--reason", reason == null ? "" : reason);
runHook(change.getProject(), changeAbandonedHook, args);
}
@Override
public void doChangeRestoredHook(Change change, Account account,
PatchSet patchSet, String reason, ReviewDb db)
throws OrmException {
if (!changeRestoredHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
ChangeAttribute c = eventFactory.asChangeAttribute(change);
PatchSetAttribute ps = patchSetAttribute(change, patchSet);
AccountState owner = accountCache.get(change.getOwner());
addArg(args, "--change", c.id);
addArg(args, "--change-url", c.url);
addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
addArg(args, "--project", c.project);
addArg(args, "--branch", c.branch);
addArg(args, "--topic", c.topic);
addArg(args, "--restorer", getDisplayName(account));
addArg(args, "--commit", ps.revision);
addArg(args, "--reason", reason == null ? "" : reason);
runHook(change.getProject(), changeRestoredHook, args);
}
@Override
public void doRefUpdatedHook(final Branch.NameKey refName,
final ObjectId oldId, final ObjectId newId, Account account) {
if (!refUpdatedHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
RefUpdateAttribute r =
eventFactory.asRefUpdateAttribute(oldId, newId, refName);
addArg(args, "--oldrev", r.oldRev);
addArg(args, "--newrev", r.newRev);
addArg(args, "--refname", r.refName);
addArg(args, "--project", r.project);
if (account != null) {
addArg(args, "--submitter", getDisplayName(account));
}
runHook(refName.getParentKey(), refUpdatedHook, args);
}
@Override
public void doReviewerAddedHook(Change change, Account account,
PatchSet patchSet, ReviewDb db) throws OrmException {
if (!reviewerAddedHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
ChangeAttribute c = eventFactory.asChangeAttribute(change);
AccountState owner = accountCache.get(change.getOwner());
addArg(args, "--change", c.id);
addArg(args, "--change-url", c.url);
addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
addArg(args, "--project", c.project);
addArg(args, "--branch", c.branch);
addArg(args, "--reviewer", getDisplayName(account));
runHook(change.getProject(), reviewerAddedHook, args);
}
@Override
public void doReviewerDeletedHook(final Change change, Account account,
PatchSet patchSet, String comment, final Map<String, Short> approvals,
final Map<String, Short> oldApprovals, ReviewDb db) throws OrmException {
if (!reviewerDeletedHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
ChangeAttribute c = eventFactory.asChangeAttribute(change);
AccountState owner = accountCache.get(change.getOwner());
addArg(args, "--change", c.id);
addArg(args, "--change-url", c.url);
addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
addArg(args, "--project", c.project);
addArg(args, "--branch", c.branch);
addArg(args, "--reviewer", getDisplayName(account));
LabelTypes labelTypes = projectCache.get(
change.getProject()).getLabelTypes();
// append votes that were removed
for (Map.Entry<String, Short> approval : approvals.entrySet()) {
LabelType lt = labelTypes.byLabel(approval.getKey());
if (lt != null && approval.getValue() != null) {
addArg(args, "--" + lt.getName(), Short.toString(approval.getValue()));
if (oldApprovals != null && !oldApprovals.isEmpty()) {
Short oldValue = oldApprovals.get(approval.getKey());
if (oldValue != null) {
addArg(args, "--" + lt.getName() + "-oldValue",
Short.toString(oldValue));
}
}
}
}
runHook(change.getProject(), reviewerDeletedHook, args);
}
@Override
public void doTopicChangedHook(Change change, Account account,
String oldTopic, ReviewDb db)
throws OrmException {
if (!topicChangedHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
ChangeAttribute c = eventFactory.asChangeAttribute(change);
AccountState owner = accountCache.get(change.getOwner());
addArg(args, "--change", c.id);
addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
addArg(args, "--project", c.project);
addArg(args, "--branch", c.branch);
addArg(args, "--changer", getDisplayName(account));
addArg(args, "--old-topic", oldTopic);
addArg(args, "--new-topic", c.topic);
runHook(change.getProject(), topicChangedHook, args);
}
String[] hashtagArray(Set<String> hashtags) {
if (hashtags != null && hashtags.size() > 0) {
return Sets.newHashSet(hashtags).toArray(
new String[hashtags.size()]);
}
return null;
}
@Override
public void doHashtagsChangedHook(Change change, Account account,
Set<String> added, Set<String> removed, Set<String> hashtags, ReviewDb db)
throws OrmException {
if (!hashtagsChangedHook.isPresent()) {
return;
}
List<String> args = new ArrayList<>();
ChangeAttribute c = eventFactory.asChangeAttribute(change);
AccountState owner = accountCache.get(change.getOwner());
addArg(args, "--change", c.id);
addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
addArg(args, "--project", c.project);
addArg(args, "--branch", c.branch);
addArg(args, "--editor", getDisplayName(account));
if (hashtags != null) {
for (String hashtag : hashtags) {
addArg(args, "--hashtag", hashtag);
}
}
if (added != null) {
for (String hashtag : added) {
addArg(args, "--added", hashtag);
}
}
if (removed != null) {
for (String hashtag : removed) {
addArg(args, "--removed", hashtag);
}
}
runHook(change.getProject(), hashtagsChangedHook, args);
}
@Override
public void doClaSignupHook(Account account, String claName) {
if (!claSignedHook.isPresent()) {
return;
}
if (account != null) {
List<String> args = new ArrayList<>();
addArg(args, "--submitter", getDisplayName(account));
addArg(args, "--user-id", account.getId().toString());
addArg(args, "--cla-name", claName);
runHook(claSignedHook, args);
}
}
private PatchSetAttribute patchSetAttribute(Change change,
PatchSet patchSet) {
try (Repository repo =
repoManager.openRepository(change.getProject());
RevWalk revWalk = new RevWalk(repo)) {
return eventFactory.asPatchSetAttribute(
revWalk, change, patchSet);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Get the display name for the given account.
*
* @param account Account to get name for.
* @return Name for this account.
*/
private String getDisplayName(Account account) {
if (account != null) {
String result = (account.getFullName() == null)
? anonymousCowardName
: account.getFullName();
if (account.getPreferredEmail() != null) {
result += " (" + account.getPreferredEmail() + ")";
}
return result;
}
return anonymousCowardName;
}
/**
* Run a hook.
*
* @param project used to open repository to run the hook for.
* @param hook the hook to execute.
* @param args Arguments to use to run the hook.
*/
private synchronized void runHook(Project.NameKey project, Optional<Path> hook,
List<String> args) {
if (project != null && hook.isPresent()) {
hookQueue.execute(new AsyncHookTask(project, hook.get(), args));
}
}
private synchronized void runHook(Optional<Path> hook, List<String> args) {
if (hook.isPresent()) {
hookQueue.execute(new AsyncHookTask(null, hook.get(), args));
}
}
private HookResult runSyncHook(Project.NameKey project,
Optional<Path> hook, List<String> args) {
if (!hook.isPresent()) {
return null;
}
SyncHookTask syncHook = new SyncHookTask(project, hook.get(), args);
FutureTask<HookResult> task = new FutureTask<>(syncHook);
syncHookThreadPool.execute(task);
String message;
try {
return task.get(syncHookTimeout, TimeUnit.SECONDS);
} catch (TimeoutException e) {
message = "Synchronous hook timed out " + hook.get().toAbsolutePath();
log.error(message);
} catch (Exception e) {
message = "Error running hook " + hook.get().toAbsolutePath();
log.error(message, e);
}
task.cancel(true);
syncHook.cancel();
return new HookResult(syncHook.getOutput(), message);
}
@Override
public void start() {
}
@Override
public void stop() {
syncHookThreadPool.shutdown();
boolean isTerminated;
do {
try {
isTerminated = syncHookThreadPool.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException ie) {
isTerminated = false;
}
} while (!isTerminated);
}
private class HookTask {
private final Project.NameKey project;
private final Path hook;
private final List<String> args;
private StringWriter output;
private Process ps;
protected HookTask(Project.NameKey project, Path hook, List<String> args) {
this.project = project;
this.hook = hook;
this.args = args;
}
public String getOutput() {
return output != null ? output.toString() : null;
}
protected HookResult runHook() {
Repository repo = null;
HookResult result = null;
try {
List<String> argv = new ArrayList<>(1 + args.size());
argv.add(hook.toAbsolutePath().toString());
argv.addAll(args);
ProcessBuilder pb = new ProcessBuilder(argv);
pb.redirectErrorStream(true);
if (project != null) {
repo = openRepository(project);
}
Map<String, String> env = pb.environment();
env.put("GERRIT_SITE", sitePaths.site_path.toAbsolutePath().toString());
if (repo != null) {
pb.directory(repo.getDirectory());
env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
}
ps = pb.start();
ps.getOutputStream().close();
String output = null;
try (InputStream is = ps.getInputStream()) {
output = readOutput(is);
} finally {
ps.waitFor();
result = new HookResult(ps.exitValue(), output);
}
} catch (InterruptedException iex) {
// InterruptedExeception - timeout or cancel
} catch (Throwable err) {
log.error("Error running hook " + hook.toAbsolutePath(), err);
} finally {
if (repo != null) {
repo.close();
}
}
if (result != null) {
int exitValue = result.getExitValue();
if (exitValue == 0) {
log.debug("hook[" + getName() + "] exitValue:" + exitValue);
} else {
log.info("hook[" + getName() + "] exitValue:" + exitValue);
}
BufferedReader br =
new BufferedReader(new StringReader(result.getOutput()));
try {
String line;
while ((line = br.readLine()) != null) {
log.info("hook[" + getName() + "] output: " + line);
}
} catch (IOException iox) {
log.error("Error writing hook output", iox);
}
}
return result;
}
private String readOutput(InputStream is) throws IOException {
output = new StringWriter();
InputStreamReader input = new InputStreamReader(is);
char[] buffer = new char[4096];
int n;
while ((n = input.read(buffer)) != -1) {
output.write(buffer, 0, n);
}
return output.toString();
}
protected String getName() {
return hook.getFileName().toString();
}
@Override
public String toString() {
return "hook " + hook.getFileName();
}
public void cancel() {
ps.destroy();
}
}
/** Callable type used to run synchronous hooks */
private final class SyncHookTask extends HookTask
implements Callable<HookResult> {
private SyncHookTask(Project.NameKey project, Path hook, List<String> args) {
super(project, hook, args);
}
@Override
public HookResult call() throws Exception {
return super.runHook();
}
}
/** Runnable type used to run asynchronous hooks */
private final class AsyncHookTask extends HookTask implements Runnable {
private AsyncHookTask(Project.NameKey project, Path hook, List<String> args) {
super(project, hook, args);
}
@Override
public void run() {
super.runHook();
}
}
}

View File

@ -1,203 +0,0 @@
// Copyright (C) 2012 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.common.ChangeHookRunner.HookResult;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.server.OrmException;
import org.eclipse.jgit.lib.ObjectId;
import java.util.Map;
import java.util.Set;
/** Invokes hooks on server actions. */
public interface ChangeHooks {
/**
* Fire the Patchset Created Hook.
*
* @param change The change itself.
* @param patchSet The Patchset that was created.
* @param db The review database.
* @throws OrmException
*/
void doPatchsetCreatedHook(Change change, PatchSet patchSet,
ReviewDb db) throws OrmException;
/**
* Fire the Draft Published Hook.
*
* @param change The change itself.
* @param patchSet The Patchset that was published.
* @param db The review database.
* @throws OrmException
*/
void doDraftPublishedHook(Change change, PatchSet patchSet,
ReviewDb db) throws OrmException;
/**
* Fire the Comment Added Hook.
*
* @param change The change itself.
* @param account The gerrit user who added the comment.
* @param patchSet The patchset this comment is related to.
* @param comment The comment given.
* @param approvals Map of label IDs to scores
* @param oldApprovals Map of label IDs to old approval scores
* @param db The review database.
* @throws OrmException
*/
void doCommentAddedHook(Change change, Account account,
PatchSet patchSet, String comment,
Map<String, Short> approvals, Map<String, Short> oldApprovals,
ReviewDb db)
throws OrmException;
/**
* Fire the Change Merged Hook.
*
* @param change The change itself.
* @param account The gerrit user who submitted the change.
* @param patchSet The patchset that was merged.
* @param db The review database.
* @param mergeResultRev The SHA-1 of the merge result revision.
* @throws OrmException
*/
void doChangeMergedHook(Change change, Account account,
PatchSet patchSet, ReviewDb db, String mergeResultRev) throws OrmException;
/**
* 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.
* @param db The review database.
* @throws OrmException
*/
void doChangeAbandonedHook(Change change, Account account,
PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
/**
* Fire the Change Restored Hook.
*
* @param change The change itself.
* @param account The gerrit user who restored the change.
* @param patchSet The patchset that was restored.
* @param reason Reason for restoring the change.
* @param db The review database.
* @throws OrmException
*/
void doChangeRestoredHook(Change change, Account account,
PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
/**
* Fire the Ref Updated Hook.
*
* @param refName The Branch.NameKey of the ref that was updated.
* @param oldId The ref's old id.
* @param newId The ref's new id.
* @param account The gerrit user who moved the ref.
*/
void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId,
ObjectId newId, Account account);
/**
* Fire the Reviewer Added Hook.
*
* @param change The change itself.
* @param patchSet The patchset that the reviewer was added on.
* @param account The gerrit user who was added as reviewer.
* @param db The review database.
*/
void doReviewerAddedHook(Change change, Account account,
PatchSet patchSet, ReviewDb db) throws OrmException;
/**
* Fire the Reviewer Deleted Hook
*
* @param change The change itself.
* @param account The reviewer that was removed.
* @param patchSet The patchset that the reviewer was removed from.
* @param comment The comment given.
* @param approvals Map of label IDs to scores.
* @param oldApprovals Map of label IDs to old approval scores
* @param db The review database.
* @throws OrmException
*/
void doReviewerDeletedHook(Change change, Account account, PatchSet patchSet,
String comment, Map<String, Short> approvals,
Map<String, Short> oldApprovals, ReviewDb db) throws OrmException;
/**
* Fire the Topic Changed Hook
*
* @param change The change itself.
* @param account The gerrit user who changed the topic.
* @param oldTopic The old topic name.
* @param db The review database.
*/
void doTopicChangedHook(Change change, Account account,
String oldTopic, ReviewDb db) throws OrmException;
/**
* Fire the contributor license agreement signup hook.
*
* @param account The gerrit user who signed the contributor license
* agreement.
* @param claName The name of the contributor license agreement.
*/
void doClaSignupHook(Account account, String claName);
/**
* Fire the Ref update Hook.
*
* @param project The target project.
* @param refName The Branch.NameKey of the ref provided by client.
* @param uploader The gerrit user running the command.
* @param oldId The ref's old id.
* @param newId The ref's new id.
*/
HookResult doRefUpdateHook(Project project, String refName,
Account uploader, ObjectId oldId, ObjectId newId);
/**
* Fire the hashtags changed Hook.
*
* @param change The change itself.
* @param account The gerrit user changing the hashtags.
* @param added List of hashtags that were added to the change.
* @param removed List of hashtags that were removed from the change.
* @param hashtags List of hashtags on the change after adding or removing.
* @param db The review database.
* @throws OrmException
*/
void doHashtagsChangedHook(Change change, Account account,
Set<String>added, Set<String> removed, Set<String> hashtags,
ReviewDb db) throws OrmException;
/**
* Fire the project created hook.
*
* @param project The project that was created.
* @param headName The head name of the created project.
*/
void doProjectCreatedHook(Project.NameKey project, String headName);
}

View File

@ -1,103 +0,0 @@
// Copyright (C) 2012 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.common.ChangeHookRunner.HookResult;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import org.eclipse.jgit.lib.ObjectId;
import java.util.Map;
import java.util.Set;
/** Does not invoke hooks. */
public final class DisabledChangeHooks implements ChangeHooks {
@Override
public void doChangeAbandonedHook(Change change, Account account,
PatchSet patchSet, String reason, ReviewDb db) {
}
@Override
public void doChangeMergedHook(Change change, Account account,
PatchSet patchSet, ReviewDb db, String mergeResultRev) {
}
@Override
public void doChangeRestoredHook(Change change, Account account,
PatchSet patchSet, String reason, ReviewDb db) {
}
@Override
public void doClaSignupHook(Account account, String claName) {
}
@Override
public void doCommentAddedHook(Change change, Account account,
PatchSet patchSet, String comment,
Map<String, Short> approvals, Map<String, Short> oldApprovals,
ReviewDb db) {
}
@Override
public void doPatchsetCreatedHook(Change change, PatchSet patchSet,
ReviewDb db) {
}
@Override
public void doDraftPublishedHook(Change change, PatchSet patchSet,
ReviewDb db) {
}
@Override
public void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId,
ObjectId newId, Account account) {
}
@Override
public void doReviewerAddedHook(Change change, Account account, PatchSet patchSet,
ReviewDb db) {
}
@Override
public void doReviewerDeletedHook(Change change, Account account,
PatchSet patchSet, String comment, Map<String, Short> approvals,
Map<String, Short> oldApprovals, ReviewDb db) {
}
@Override
public void doTopicChangedHook(Change change, Account account, String oldTopic,
ReviewDb db) {
}
@Override
public void doHashtagsChangedHook(Change change, Account account, Set<String> added,
Set<String> removed, Set<String> hashtags, ReviewDb db) {
}
@Override
public HookResult doRefUpdateHook(Project project, String refName,
Account uploader, ObjectId oldId, ObjectId newId) {
return null;
}
@Override
public void doProjectCreatedHook(Project.NameKey project, String headName) {
}
}

View File

@ -18,8 +18,6 @@ import static com.google.inject.Scopes.SINGLETON;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.DisabledChangeHooks;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.gpg.GpgModule;
import com.google.gerrit.metrics.DisabledMetricMaker;
@ -180,7 +178,6 @@ public class InMemoryModule extends FactoryModule {
bind(SecureStore.class).to(DefaultSecureStore.class);
bind(ChangeHooks.class).to(DisabledChangeHooks.class);
install(NoSshKeyCache.module());
install(new CanonicalWebUrlModule() {
@Override

View File

@ -18,8 +18,6 @@ import static com.google.inject.Scopes.SINGLETON;
import static com.google.inject.Stage.PRODUCTION;
import com.google.common.base.Splitter;
import com.google.gerrit.common.ChangeHookApiListener;
import com.google.gerrit.common.ChangeHookRunner;
import com.google.gerrit.common.EventBroker;
import com.google.gerrit.common.StreamEventsApiListener;
import com.google.gerrit.gpg.GpgModule;
@ -300,9 +298,7 @@ public class WebAppInitializer extends GuiceServletContextListener
modules.add(new EventBroker.Module());
modules.add(new H2AccountPatchReviewStore.Module());
modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class));
modules.add(new ChangeHookApiListener.Module());
modules.add(new StreamEventsApiListener.Module());
modules.add(new ChangeHookRunner.Module());
modules.add(new ReceiveCommitsExecutorModule());
modules.add(new DiffExecutorModule());
modules.add(new MimeUtil2Module());

View File

@ -2,6 +2,7 @@ BASE = get_base_path()
CORE = [
'commit-message-length-validator',
'download-commands',
'hooks',
'replication',
'reviewnotes',
'singleusergroup'

1
plugins/hooks Submodule

@ -0,0 +1 @@
Subproject commit ac06f404363a29d52ef06d607e29f4a553d1c455