Java tutorial
// 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.collect.Sets; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.gerrit.common.data.ContributorAgreement; 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.extensions.events.NewProjectCreatedListener; 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.CurrentUser; 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.ApprovalAttribute; import com.google.gerrit.server.events.ChangeAbandonedEvent; import com.google.gerrit.server.events.ChangeMergedEvent; import com.google.gerrit.server.events.ChangeRestoredEvent; import com.google.gerrit.server.events.CommentAddedEvent; import com.google.gerrit.server.events.DraftPublishedEvent; import com.google.gerrit.server.events.EventFactory; import com.google.gerrit.server.events.HashtagsChangedEvent; import com.google.gerrit.server.events.MergeFailedEvent; import com.google.gerrit.server.events.PatchSetCreatedEvent; import com.google.gerrit.server.events.ProjectCreatedEvent; import com.google.gerrit.server.events.RefUpdatedEvent; import com.google.gerrit.server.events.ReviewerAddedEvent; import com.google.gerrit.server.events.TopicChangedEvent; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectState; 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.RefUpdate; import org.eclipse.jgit.lib.Repository; 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.Map.Entry; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; 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, EventDispatcher, EventSource, LifecycleListener, NewProjectCreatedListener { /** 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); bind(EventDispatcher.class).to(ChangeHookRunner.class); bind(EventSource.class).to(ChangeHookRunner.class); DynamicSet.bind(binder(), NewProjectCreatedListener.class).to(ChangeHookRunner.class); listener().to(ChangeHookRunner.class); } } private static class EventListenerHolder { final EventListener listener; final CurrentUser user; EventListenerHolder(EventListener l, CurrentUser u) { listener = l; user = u; } } /** 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(); } } /** Listeners to receive changes as they happen (limited by visibility * of holder's user). */ private final Map<EventListener, EventListenerHolder> listeners = new ConcurrentHashMap<>(); /** Listeners to receive all changes as they happen. */ private final DynamicSet<EventListener> unrestrictedListeners; /** Path of the new patchset hook. */ private final Path patchsetCreatedHook; /** Path of the draft published hook. */ private final Path draftPublishedHook; /** Path of the new comments hook. */ private final Path commentAddedHook; /** Path of the change merged hook. */ private final Path changeMergedHook; /** Path of the merge failed hook. */ private final Path mergeFailedHook; /** Path of the change abandoned hook. */ private final Path changeAbandonedHook; /** Path of the change restored hook. */ private final Path changeRestoredHook; /** Path of the ref updated hook. */ private final Path refUpdatedHook; /** Path of the reviewer added hook. */ private final Path reviewerAddedHook; /** Path of the topic changed hook. */ private final Path topicChangedHook; /** Path of the cla signed hook. */ private final Path claSignedHook; /** Path of the update hook. */ private final Path refUpdateHook; /** Path of the hashtags changed hook */ private final Path hashtagsChangedHook; /** Path of the project created hook. */ private final 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, DynamicSet<EventListener> unrestrictedListeners) { this.anonymousCowardName = anonymousCowardName; this.repoManager = repoManager; this.hookQueue = queue.createQueue(1, "hook"); this.projectCache = projectCache; this.accountCache = accountCache; this.eventFactory = eventFactory; this.sitePaths = sitePath; this.unrestrictedListeners = unrestrictedListeners; 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"); mergeFailedHook = hook(config, hooksPath, "merge-failed"); changeAbandonedHook = hook(config, hooksPath, "change-abandoned"); changeRestoredHook = hook(config, hooksPath, "change-restored"); refUpdatedHook = hook(config, hooksPath, "ref-updated"); reviewerAddedHook = hook(config, hooksPath, "reviewer-added"); 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 Path hook(Config config, Path path, String name) { String setting = name.replace("-", "") + "hook"; String value = config.getString("hooks", null, setting); return path.resolve(value != null ? value : name); } @Override public void addEventListener(EventListener listener, CurrentUser user) { listeners.put(listener, new EventListenerHolder(listener, user)); } @Override public void removeEventListener(EventListener listener) { listeners.remove(listener); } /** * 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); } } /** * Fire the update hook * */ @Override public HookResult doRefUpdateHook(Project project, String refname, Account uploader, ObjectId oldId, ObjectId newId) { 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) { ProjectCreatedEvent event = new ProjectCreatedEvent(); event.projectName = project.get(); event.headName = headName; fireEvent(project, event); List<String> args = new ArrayList<>(); addArg(args, "--project", project.get()); addArg(args, "--head", headName); runHook(project, projectCreatedHook, args); } /** * Fire the Patchset Created Hook. * * @param change The change itself. * @param patchSet The Patchset that was created. * @throws OrmException */ @Override public void doPatchsetCreatedHook(Change change, PatchSet patchSet, ReviewDb db) throws OrmException { PatchSetCreatedEvent event = new PatchSetCreatedEvent(); AccountState uploader = accountCache.get(patchSet.getUploader()); AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.uploader = eventFactory.asAccountAttribute(uploader.getAccount()); fireEvent(change, event, db); List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--is-draft", String.valueOf(patchSet.isDraft())); addArg(args, "--kind", String.valueOf(event.patchSet.kind)); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--uploader", getDisplayName(uploader.getAccount())); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--patchset", event.patchSet.number); runHook(change.getProject(), patchsetCreatedHook, args); } @Override public void doDraftPublishedHook(Change change, PatchSet patchSet, ReviewDb db) throws OrmException { DraftPublishedEvent event = new DraftPublishedEvent(); AccountState uploader = accountCache.get(patchSet.getUploader()); AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.uploader = eventFactory.asAccountAttribute(uploader.getAccount()); fireEvent(change, event, db); List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--uploader", getDisplayName(uploader.getAccount())); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--patchset", event.patchSet.number); runHook(change.getProject(), draftPublishedHook, args); } @Override public void doCommentAddedHook(Change change, Account account, PatchSet patchSet, String comment, Map<String, Short> approvals, ReviewDb db) throws OrmException { CommentAddedEvent event = new CommentAddedEvent(); AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.author = eventFactory.asAccountAttribute(account); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.comment = comment; LabelTypes labelTypes = projectCache.get(change.getProject()).getLabelTypes(); if (approvals.size() > 0) { event.approvals = new ApprovalAttribute[approvals.size()]; int i = 0; for (Map.Entry<String, Short> approval : approvals.entrySet()) { event.approvals[i++] = getApprovalAttribute(labelTypes, approval); } } fireEvent(change, event, db); List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false"); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--author", getDisplayName(account)); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--comment", comment == null ? "" : comment); for (Map.Entry<String, Short> approval : approvals.entrySet()) { LabelType lt = labelTypes.byLabel(approval.getKey()); if (lt != null) { addArg(args, "--" + lt.getName(), Short.toString(approval.getValue())); } } runHook(change.getProject(), commentAddedHook, args); } @Override public void doChangeMergedHook(Change change, Account account, PatchSet patchSet, ReviewDb db, String mergeResultRev) throws OrmException { ChangeMergedEvent event = new ChangeMergedEvent(); AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.submitter = eventFactory.asAccountAttribute(account); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.newRev = mergeResultRev; fireEvent(change, event, db); List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--submitter", getDisplayName(account)); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--newrev", mergeResultRev); runHook(change.getProject(), changeMergedHook, args); } @Override public void doMergeFailedHook(Change change, Account account, PatchSet patchSet, String reason, ReviewDb db) throws OrmException { MergeFailedEvent event = new MergeFailedEvent(); AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.submitter = eventFactory.asAccountAttribute(account); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.reason = reason; fireEvent(change, event, db); List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--submitter", getDisplayName(account)); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--reason", reason == null ? "" : reason); runHook(change.getProject(), mergeFailedHook, args); } @Override public void doChangeAbandonedHook(Change change, Account account, PatchSet patchSet, String reason, ReviewDb db) throws OrmException { ChangeAbandonedEvent event = new ChangeAbandonedEvent(); AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.abandoner = eventFactory.asAccountAttribute(account); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.reason = reason; fireEvent(change, event, db); List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--abandoner", getDisplayName(account)); addArg(args, "--commit", event.patchSet.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 { ChangeRestoredEvent event = new ChangeRestoredEvent(); AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.restorer = eventFactory.asAccountAttribute(account); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.reason = reason; fireEvent(change, event, db); List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--restorer", getDisplayName(account)); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--reason", reason == null ? "" : reason); runHook(change.getProject(), changeRestoredHook, args); } @Override public void doRefUpdatedHook(Branch.NameKey refName, RefUpdate refUpdate, Account account) { doRefUpdatedHook(refName, refUpdate.getOldObjectId(), refUpdate.getNewObjectId(), account); } @Override public void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId, ObjectId newId, Account account) { RefUpdatedEvent event = new RefUpdatedEvent(); if (account != null) { event.submitter = eventFactory.asAccountAttribute(account); } event.refUpdate = eventFactory.asRefUpdateAttribute(oldId, newId, refName); fireEvent(refName, event); List<String> args = new ArrayList<>(); addArg(args, "--oldrev", event.refUpdate.oldRev); addArg(args, "--newrev", event.refUpdate.newRev); addArg(args, "--refname", event.refUpdate.refName); addArg(args, "--project", event.refUpdate.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 { ReviewerAddedEvent event = new ReviewerAddedEvent(); AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.reviewer = eventFactory.asAccountAttribute(account); fireEvent(change, event, db); List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--reviewer", getDisplayName(account)); runHook(change.getProject(), reviewerAddedHook, args); } @Override public void doTopicChangedHook(Change change, Account account, String oldTopic, ReviewDb db) throws OrmException { TopicChangedEvent event = new TopicChangedEvent(); AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.changer = eventFactory.asAccountAttribute(account); event.oldTopic = oldTopic; fireEvent(change, event, db); List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--changer", getDisplayName(account)); addArg(args, "--old-topic", oldTopic); addArg(args, "--new-topic", event.change.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 { HashtagsChangedEvent event = new HashtagsChangedEvent(); AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.editor = eventFactory.asAccountAttribute(account); event.hashtags = hashtagArray(hashtags); event.added = hashtagArray(added); event.removed = hashtagArray(removed); fireEvent(change, event, db); List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.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, ContributorAgreement cla) { if (account != null) { List<String> args = new ArrayList<>(); addArg(args, "--submitter", getDisplayName(account)); addArg(args, "--user-id", account.getId().toString()); addArg(args, "--cla-name", cla.getName()); runHook(claSignedHook, args); } } @Override public void postEvent(Change change, com.google.gerrit.server.events.Event event, ReviewDb db) throws OrmException { fireEvent(change, event, db); } @Override public void postEvent(Branch.NameKey branchName, com.google.gerrit.server.events.Event event) { fireEvent(branchName, event); } private void fireEventForUnrestrictedListeners(com.google.gerrit.server.events.Event event) { for (EventListener listener : unrestrictedListeners) { listener.onEvent(event); } } private void fireEvent(Change change, com.google.gerrit.server.events.Event event, ReviewDb db) throws OrmException { for (EventListenerHolder holder : listeners.values()) { if (isVisibleTo(change, holder.user, db)) { holder.listener.onEvent(event); } } fireEventForUnrestrictedListeners(event); } private void fireEvent(Project.NameKey project, ProjectCreatedEvent event) { for (EventListenerHolder holder : listeners.values()) { if (isVisibleTo(project, event, holder.user)) { holder.listener.onEvent(event); } } fireEventForUnrestrictedListeners(event); } private void fireEventForUnrestrictedListeners(ProjectCreatedEvent event) { for (EventListener listener : unrestrictedListeners) { listener.onEvent(event); } } private boolean isVisibleTo(Project.NameKey project, ProjectCreatedEvent event, CurrentUser user) { ProjectState pe = projectCache.get(project); if (pe == null) { return false; } ProjectControl pc = pe.controlFor(user); return pc.controlForRef(event.getHeadName()).isVisible(); } private void fireEvent(Branch.NameKey branchName, com.google.gerrit.server.events.Event event) { for (EventListenerHolder holder : listeners.values()) { if (isVisibleTo(branchName, holder.user)) { holder.listener.onEvent(event); } } fireEventForUnrestrictedListeners(event); } private boolean isVisibleTo(Change change, CurrentUser user, ReviewDb db) throws OrmException { ProjectState pe = projectCache.get(change.getProject()); if (pe == null) { return false; } ProjectControl pc = pe.controlFor(user); return pc.controlFor(change).isVisible(db); } private boolean isVisibleTo(Branch.NameKey branchName, CurrentUser user) { ProjectState pe = projectCache.get(branchName.getParentKey()); if (pe == null) { return false; } ProjectControl pc = pe.controlFor(user); return pc.controlForRef(branchName).isVisible(); } /** * Create an ApprovalAttribute for the given approval suitable for serialization to JSON. * @param approval * @return object suitable for serialization to JSON */ private ApprovalAttribute getApprovalAttribute(LabelTypes labelTypes, Entry<String, Short> approval) { ApprovalAttribute a = new ApprovalAttribute(); a.type = approval.getKey(); LabelType lt = labelTypes.byLabel(approval.getKey()); if (lt != null) { a.description = lt.getName(); } a.value = Short.toString(approval.getValue()); return a; } /** * 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, Path hook, List<String> args) { if (project != null && Files.exists(hook)) { hookQueue.execute(new AsyncHookTask(project, hook, args)); } } private synchronized void runHook(Path hook, List<String> args) { if (Files.exists(hook)) { hookQueue.execute(new AsyncHookTask(null, hook, args)); } } private HookResult runSyncHook(Project.NameKey project, Path hook, List<String> args) { if (!Files.exists(hook)) { return null; } SyncHookTask syncHook = new SyncHookTask(project, hook, 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.toAbsolutePath(); log.error(message); } catch (Exception e) { message = "Error running hook " + hook.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(); } } @Override public void onNewProjectCreated(NewProjectCreatedListener.Event event) { Project.NameKey project = new Project.NameKey(event.getProjectName()); doProjectCreatedHook(project, event.getHeadName()); } }