Add the ability to run a ref update hook
Allow the user to provide an update (similar to git's update hook) hook run before a push is accepted by Gerrit. This allows the exclusion of certain commits or any other update checks. The hook takes the following form: ref-update --project <project name> --refname <refname> --uploader <uploader> --oldrev <sha1> --newrev <sha1> If the script exits with non zero return code the push will be rejected. The output of the script will be returned to the user as the reason for the rejection. This hook is called synchronously so shouldn't block or wait. A timeout on the hook is set to 30 seconds to avoid "runaway" hooks using up server threads. This value can be configured using "syncHookTimeout" in the "hooks" stanza. It is an integer value in seconds. Change-Id: Ibed5dc5c18e59db465511520f76fac93acc561e0
This commit is contained in:
@@ -17,6 +17,8 @@ package com.google.gerrit.common;
|
||||
import com.google.gerrit.common.data.ApprovalType;
|
||||
import com.google.gerrit.common.data.ApprovalTypes;
|
||||
import com.google.gerrit.common.data.ContributorAgreement;
|
||||
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.ApprovalCategory;
|
||||
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
|
||||
@@ -49,7 +51,6 @@ 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.AbstractModule;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
@@ -63,24 +64,34 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
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 {
|
||||
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 AbstractModule {
|
||||
public static class Module extends LifecycleModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(ChangeHookRunner.class);
|
||||
bind(ChangeHooks.class).to(ChangeHookRunner.class);
|
||||
listener().to(ChangeHookRunner.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +105,53 @@ public class ChangeHookRunner implements ChangeHooks {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
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. */
|
||||
private final Map<ChangeListener, ChangeListenerHolder> listeners =
|
||||
new ConcurrentHashMap<ChangeListener, ChangeListenerHolder>();
|
||||
@@ -128,6 +186,9 @@ public class ChangeHookRunner implements ChangeHooks {
|
||||
/** Filename of the cla signed hook. */
|
||||
private final File claSignedHook;
|
||||
|
||||
/** Filename of the update hook. */
|
||||
private final File refUpdateHook;
|
||||
|
||||
private final String anonymousCowardName;
|
||||
|
||||
/** Repository Manager. */
|
||||
@@ -146,6 +207,12 @@ public class ChangeHookRunner implements ChangeHooks {
|
||||
|
||||
private final SitePaths sitePaths;
|
||||
|
||||
/** Thread pool used to monitor sync hooks */
|
||||
private final ExecutorService syncHookThreadPool = Executors.newCachedThreadPool();
|
||||
|
||||
/** Timeout value for synchronous hooks */
|
||||
private final int syncHookTimeout;
|
||||
|
||||
/**
|
||||
* Create a new ChangeHookRunner.
|
||||
*
|
||||
@@ -184,6 +251,8 @@ public class ChangeHookRunner implements ChangeHooks {
|
||||
refUpdatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdatedHook", "ref-updated")).getPath());
|
||||
reviewerAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "reviewerAddedHook", "reviewer-added")).getPath());
|
||||
claSignedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "claSignedHook", "cla-signed")).getPath());
|
||||
refUpdateHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdateHook", "ref-update")).getPath());
|
||||
syncHookTimeout = config.getInt("hooks", "syncHookTimeout", 30);
|
||||
}
|
||||
|
||||
public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
|
||||
@@ -230,6 +299,38 @@ public class ChangeHookRunner implements ChangeHooks {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the update hook
|
||||
*
|
||||
*/
|
||||
public HookResult doRefUpdateHook(final Project project, final String refname,
|
||||
final Account uploader, final ObjectId oldId, final ObjectId newId) {
|
||||
|
||||
final List<String> args = new ArrayList<String>();
|
||||
addArg(args, "--project", project.getName());
|
||||
addArg(args, "--refname", refname);
|
||||
addArg(args, "--uploader", getDisplayName(uploader));
|
||||
addArg(args, "--oldrev", oldId.getName());
|
||||
addArg(args, "--newrev", newId.getName());
|
||||
|
||||
HookResult hookResult;
|
||||
|
||||
try {
|
||||
hookResult = runSyncHook(project.getNameKey(), refUpdateHook, args);
|
||||
} catch (TimeoutException e) {
|
||||
hookResult = new HookResult(-1, "Synchronous hook timed out");
|
||||
}
|
||||
|
||||
return hookResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the Patchset Created Hook.
|
||||
*
|
||||
* @param change The change itself.
|
||||
* @param patchSet The Patchset that was created.
|
||||
* @throws OrmException
|
||||
*/
|
||||
public void doPatchsetCreatedHook(final Change change, final PatchSet patchSet,
|
||||
final ReviewDb db) throws OrmException {
|
||||
final PatchSetCreatedEvent event = new PatchSetCreatedEvent();
|
||||
@@ -534,31 +635,84 @@ public class ChangeHookRunner implements ChangeHooks {
|
||||
private synchronized void runHook(Project.NameKey project, File hook,
|
||||
List<String> args) {
|
||||
if (project != null && hook.exists()) {
|
||||
hookQueue.execute(new HookTask(project, hook, args));
|
||||
hookQueue.execute(new AsyncHookTask(project, hook, args));
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void runHook(File hook, List<String> args) {
|
||||
if (hook.exists()) {
|
||||
hookQueue.execute(new HookTask(null, hook, args));
|
||||
hookQueue.execute(new AsyncHookTask(null, hook, args));
|
||||
}
|
||||
}
|
||||
|
||||
private final class HookTask implements Runnable {
|
||||
private HookResult runSyncHook(Project.NameKey project,
|
||||
File hook, List<String> args) throws TimeoutException {
|
||||
|
||||
if (!hook.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SyncHookTask syncHook = new SyncHookTask(project, hook, args);
|
||||
FutureTask<HookResult> task = new FutureTask<HookResult>(syncHook);
|
||||
|
||||
syncHookThreadPool.execute(task);
|
||||
|
||||
String message;
|
||||
|
||||
try {
|
||||
return task.get(syncHookTimeout, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
message = "Synchronous hook timed out " + hook.getAbsolutePath();
|
||||
log.error(message);
|
||||
} catch (Exception e) {
|
||||
message = "Error running hook " + hook.getAbsolutePath();
|
||||
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 File hook;
|
||||
private final List<String> args;
|
||||
private StringWriter output;
|
||||
private Process ps;
|
||||
|
||||
private HookTask(Project.NameKey project, File hook, List<String> args) {
|
||||
protected HookTask(Project.NameKey project, File hook, List<String> args) {
|
||||
this.project = project;
|
||||
this.hook = hook;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
public String getOutput() {
|
||||
return output != null ? output.toString() : null;
|
||||
}
|
||||
|
||||
protected HookResult runHook() {
|
||||
Repository repo = null;
|
||||
HookResult result = null;
|
||||
try {
|
||||
|
||||
final List<String> argv = new ArrayList<String>(1 + args.size());
|
||||
argv.add(hook.getAbsolutePath());
|
||||
argv.addAll(args);
|
||||
@@ -579,23 +733,22 @@ public class ChangeHookRunner implements ChangeHooks {
|
||||
env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
|
||||
}
|
||||
|
||||
Process ps = pb.start();
|
||||
ps = pb.start();
|
||||
ps.getOutputStream().close();
|
||||
|
||||
BufferedReader br =
|
||||
new BufferedReader(new InputStreamReader(ps.getInputStream()));
|
||||
InputStream is = ps.getInputStream();
|
||||
String output = null;
|
||||
try {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
log.info("hook[" + hook.getName() + "] output: " + line);
|
||||
}
|
||||
output = readOutput(is);
|
||||
} finally {
|
||||
try {
|
||||
br.close();
|
||||
is.close();
|
||||
} catch (IOException closeErr) {
|
||||
}
|
||||
ps.waitFor();
|
||||
result = new HookResult(ps.exitValue(), output);
|
||||
}
|
||||
} catch (InterruptedException iex) {
|
||||
// InterruptedExeception - timeout or cancel
|
||||
} catch (Throwable err) {
|
||||
log.error("Error running hook " + hook.getAbsolutePath(), err);
|
||||
} finally {
|
||||
@@ -603,11 +756,74 @@ public class ChangeHookRunner implements ChangeHooks {
|
||||
repo.close();
|
||||
}
|
||||
}
|
||||
|
||||
log.info("hook[" + getName() + "] exitValue:" + result.getExitValue());
|
||||
|
||||
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 = 0;
|
||||
while ((n = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, n);
|
||||
}
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
protected String getName() {
|
||||
return hook.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "hook " + hook.getName();
|
||||
}
|
||||
|
||||
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, File hook, List<String> args) {
|
||||
super(project, hook, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HookResult call() throws Exception {
|
||||
return super.runHook();
|
||||
}
|
||||
}
|
||||
|
||||
/** Runable type used to run async hooks */
|
||||
private final class AsyncHookTask extends HookTask implements Runnable {
|
||||
|
||||
private AsyncHookTask(Project.NameKey project, File hook, List<String> args) {
|
||||
super(project, hook, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
super.runHook();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common;
|
||||
|
||||
import com.google.gerrit.common.ChangeHookRunner.HookResult;
|
||||
import com.google.gerrit.common.data.ContributorAgreement;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.ApprovalCategory;
|
||||
@@ -22,6 +23,7 @@ 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.server.ReviewDb;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
|
||||
@@ -148,4 +150,17 @@ public interface ChangeHooks {
|
||||
PatchSet patchSet, ReviewDb db) throws OrmException;
|
||||
|
||||
public void doClaSignupHook(Account account, ContributorAgreement cla);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param account The gerrit user who moved the ref
|
||||
*/
|
||||
public HookResult doRefUpdateHook(Project project, String refName,
|
||||
Account uploader, ObjectId oldId, ObjectId newId);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.google.gerrit.common;
|
||||
|
||||
import com.google.gerrit.common.ChangeHookRunner.HookResult;
|
||||
import com.google.gerrit.common.data.ContributorAgreement;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.ApprovalCategory;
|
||||
@@ -21,6 +22,7 @@ import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
import com.google.gerrit.reviewdb.client.Branch.NameKey;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
|
||||
@@ -93,4 +95,10 @@ public final class DisabledChangeHooks implements ChangeHooks {
|
||||
@Override
|
||||
public void removeChangeListener(ChangeListener listener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public HookResult doRefUpdateHook(Project project, String refName,
|
||||
Account uploader, ObjectId oldId, ObjectId newId) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user