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:
parent
327048b612
commit
f736d6cd9f
@ -1330,6 +1330,15 @@ Optional filename for the reviewer added hook, if not specified then
|
||||
Optional filename for the CLA signed hook, if not specified then
|
||||
`cla-signed` will be used.
|
||||
|
||||
[[hooks.refUpdateHook]]hooks.refUpdateHook::
|
||||
+
|
||||
Optional filename for the ref update hook, if not specified then
|
||||
`ref-update` will be used.
|
||||
|
||||
[[hooks.syncHookTimeout]]hooks.syncHookTimeout::
|
||||
+ Optional timeout value in seconds for synchronous hooks, if not specified
|
||||
then 30 seconds will be used.
|
||||
|
||||
[[http]]Section http
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -11,15 +11,33 @@ affected git repository so that git commands can be easily run.
|
||||
|
||||
Make sure your hook scripts are executable if running on *nix.
|
||||
|
||||
Hooks are run in the background after the 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.
|
||||
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
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -123,7 +141,7 @@ 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.reviewerAddedHook and hooks.claSignedHook.
|
||||
hooks.refUpdateHook, hooks.reviewerAddedHook and hooks.claSignedHook.
|
||||
|
||||
Missing Change URLs
|
||||
-------------------
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import com.google.common.util.concurrent.CheckedFuture;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.gerrit.common.ChangeHookRunner.HookResult;
|
||||
import com.google.gerrit.common.ChangeHooks;
|
||||
import com.google.gerrit.common.PageLinks;
|
||||
import com.google.gerrit.common.data.Capable;
|
||||
@ -760,6 +761,19 @@ public class ReceiveCommits {
|
||||
continue;
|
||||
}
|
||||
|
||||
HookResult result = hooks.doRefUpdateHook(project, cmd.getRefName(),
|
||||
currentUser.getAccount(), cmd.getOldId(),
|
||||
cmd.getNewId());
|
||||
|
||||
if (result != null) {
|
||||
final String message = result.toString().trim();
|
||||
if (result.getExitValue() != 0) {
|
||||
reject(cmd, message);
|
||||
continue;
|
||||
}
|
||||
rp.sendMessage(message);
|
||||
}
|
||||
|
||||
if (MagicBranch.isMagicBranch(cmd.getRefName())) {
|
||||
parseNewChangeCommand(cmd);
|
||||
continue;
|
||||
|
Loading…
x
Reference in New Issue
Block a user