Extension for commit validation plugins

CommitValidationListener is informed on new commits
coming through ReceiveCommits with the ability to
reject or provide warning with and send messages
back to the Git client output console.

Plugins can contribute additional commit validation
by offering an implementation of
CommitValidationListener and returning a
CommitValidationResult.

Change-Id: Ie14400617cfb2d9ab4591522be1b3b492d3846f2
Signed-off-by: Luca Milanesio <luca.milanesio@gmail.com>
This commit is contained in:
Luca Milanesio 2012-05-05 14:28:23 -07:00
parent 7669e7813c
commit 3025cb15b8
8 changed files with 231 additions and 3 deletions

View File

@ -35,6 +35,7 @@ Error Messages
* link:error-squash-commits-first.html[squash commits first] * link:error-squash-commits-first.html[squash commits first]
* link:error-upload-denied.html[Upload denied for project \'...'] * link:error-upload-denied.html[Upload denied for project \'...']
* link:error-not-allowed-to-upload-merges.html[you are not allowed to upload merges] * link:error-not-allowed-to-upload-merges.html[you are not allowed to upload merges]
* link:error-prohibited-by-gerrit-reject-by-plugin.html[Prohibited by Gerrit (rejected by plugin X)]
General Hints General Hints

View File

@ -0,0 +1,14 @@
Prohibited by Gerrit (rejected by plugin X)
===========================================
This is the default error message that is returned by Gerrit if a push
has been rejected because of failed validation by plugin X.
Bear in mind that the (rejected by plugin X) message is always present
and X indicates the plugin name as it appears in /#/admin/plugins/ list.
Please refer to the plugin author documentation for knowing the
validation rule applied.
GERRIT
------
Part of link:error-messages.html[Gerrit Error Messages]

View File

@ -59,6 +59,7 @@ import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.git.ReloadSubmitQueueOp; import com.google.gerrit.server.git.ReloadSubmitQueueOp;
import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.mail.FromAddressGenerator; import com.google.gerrit.server.mail.FromAddressGenerator;
import com.google.gerrit.server.mail.FromAddressGeneratorProvider; import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
import com.google.gerrit.server.mail.VelocityRuntimeProvider; import com.google.gerrit.server.mail.VelocityRuntimeProvider;
@ -180,6 +181,7 @@ public class GerritGlobalModule extends FactoryModule {
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class); DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
DynamicSet.setOf(binder(), NewProjectCreatedListener.class); DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ChangeCache.class); DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ChangeCache.class);
DynamicSet.setOf(binder(), CommitValidationListener.class);
bind(AnonymousUser.class); bind(AnonymousUser.class);
} }

View File

@ -0,0 +1,38 @@
// 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.server.events;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.ReceiveCommand;
public class CommitReceivedEvent extends ChangeEvent {
public final ReceiveCommand command;
public final Project project;
public final String refName;
public final RevCommit commit;
public final IdentifiedUser user;
public CommitReceivedEvent(ReceiveCommand command, Project project,
String refName, RevCommit commit, IdentifiedUser user) {
this.command = command;
this.project = project;
this.refName = refName;
this.commit = commit;
this.user = user;
}
}

View File

@ -24,6 +24,7 @@ import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_RE
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
@ -39,6 +40,7 @@ import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.Capable; import com.google.gerrit.common.data.Capable;
import com.google.gerrit.common.data.PermissionRule; import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.errors.NoSuchAccountException; import com.google.gerrit.common.errors.NoSuchAccountException;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
@ -57,12 +59,16 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.TrackingFooters; import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.MultiProgressMonitor.Task; import com.google.gerrit.server.git.MultiProgressMonitor.Task;
import com.google.gerrit.server.git.validators.CommitValidationResult;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.mail.CreateChangeSender; import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.mail.MergedSender; import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender; import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.plugins.PluginLoader;
import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectControl;
@ -288,6 +294,8 @@ public class ReceiveCommits {
private Task commandProgress; private Task commandProgress;
private MessageSender messageSender; private MessageSender messageSender;
private BatchRefUpdate batch; private BatchRefUpdate batch;
private final DynamicSet<CommitValidationListener> commitValidators;
private final PluginLoader pluginLoader;
@Inject @Inject
ReceiveCommits(final ReviewDb db, ReceiveCommits(final ReviewDb db,
@ -311,10 +319,11 @@ public class ReceiveCommits {
@ChangeUpdateExecutor ListeningExecutorService changeUpdateExector, @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
final RequestScopePropagator requestScopePropagator, final RequestScopePropagator requestScopePropagator,
final SshInfo sshInfo, final SshInfo sshInfo,
final DynamicSet<CommitValidationListener> commitValidationListeners,
@Assisted final ProjectControl projectControl, @Assisted final ProjectControl projectControl,
@Assisted final Repository repo, @Assisted final Repository repo,
final SubmoduleOp.Factory subOpFactory) throws IOException { final SubmoduleOp.Factory subOpFactory,
final PluginLoader pluginLoader) throws IOException {
this.currentUser = (IdentifiedUser) projectControl.getCurrentUser(); this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
this.db = db; this.db = db;
this.schemaFactory = schemaFactory; this.schemaFactory = schemaFactory;
@ -340,10 +349,12 @@ public class ReceiveCommits {
this.projectControl = projectControl; this.projectControl = projectControl;
this.project = projectControl.getProject(); this.project = projectControl.getProject();
this.repo = repo; this.repo = repo;
this.pluginLoader = pluginLoader;
this.rp = new ReceivePack(repo); this.rp = new ReceivePack(repo);
this.rejectCommits = loadRejectCommitsMap(); this.rejectCommits = loadRejectCommitsMap();
this.subOpFactory = subOpFactory; this.subOpFactory = subOpFactory;
this.commitValidators = commitValidationListeners;
this.messageSender = new ReceivePackMessageSender(); this.messageSender = new ReceivePackMessageSender();
@ -2029,6 +2040,20 @@ public class ReceiveCommits {
} }
} }
for (CommitValidationListener validator : commitValidators) {
CommitValidationResult validationResult =
validator.onCommitReceived(new CommitReceivedEvent(cmd, project, ctl
.getRefName(), c, currentUser));
String pluginName = pluginLoader.getPluginName(validator);
if (!validationResult.validated) {
reject(cmd, String.format("%s (rejected by plugin %s)",
validationResult.message, pluginName));
return false;
} else if(!Strings.isNullOrEmpty(validationResult.message)) {
addMessage(String.format("%s (from plugin %s)", pluginName));
}
}
return true; return true;
} }

View File

@ -0,0 +1,36 @@
// 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.server.git.validators;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.server.events.CommitReceivedEvent;
/**
* Listener to provide validation on received commits.
*
* Invoked by Gerrit when a new commit is received, has passed basic Gerrit
* validation and can be then subject to extra validation checks.
*
*/
@ExtensionPoint
public interface CommitValidationListener {
/**
* Commit validation.
*
* @param received commit event details
* @return validation result
*/
public CommitValidationResult onCommitReceived(CommitReceivedEvent receiveEvent);
}

View File

@ -0,0 +1,85 @@
// 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.server.git.validators;
/**
* Result of a commit validation from a CommitValidationListener.
*
* Commit validators should return CommitValidationResult.SUCCESS
* in case of valid commit and CommitValidationResult.FAILURE in
* case of rejected commits.
*
* When reason of the failure needs to be displayed on the remote
* client, {@link #newFailure(String)} can be used to return additional
* textual description.
*/
public class CommitValidationResult {
public final boolean validated;
public final String message;
/**
* Successful commit validation.
*/
public static final CommitValidationResult SUCCESS =
new CommitValidationResult(true, "");
/**
* Commit validation failed.
*/
public static final CommitValidationResult FAILURE =
new CommitValidationResult(false, "Prohibited by Gerrit, invalid commit");
/**
* Commit validation failed with a reason.
*
* @param message reason of the commit validation failure.
*
* @return validation failure with reason.
*/
public static CommitValidationResult newFailure(String message) {
return new CommitValidationResult(false, message);
}
/**
* Commit validation result and reason.
*
* @param validated true if commit is valid or false if has to be rejected.
* @param message reason of the commit validation failure or warning message when
* commit has been validated.
*/
protected CommitValidationResult(boolean validated, String message) {
this.validated = validated;
this.message = message;
}
/**
* Gets validation status.
*
* @return validation status.
*/
public boolean isValidated() {
return validated;
}
/**
* Gets additional textual description for the validation.
*
* @return textual validation reason.
*/
public String getValidationReason() {
return message;
}
}

View File

@ -60,6 +60,7 @@ import java.util.jar.Manifest;
@Singleton @Singleton
public class PluginLoader implements LifecycleListener { public class PluginLoader implements LifecycleListener {
static final String PLUGIN_TMP_PREFIX = "plugin_";
static final Logger log = LoggerFactory.getLogger(PluginLoader.class); static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
private final File pluginsDir; private final File pluginsDir;
@ -471,7 +472,7 @@ public class PluginLoader implements LifecycleListener {
private static String tempNameFor(String name) { private static String tempNameFor(String name) {
SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm"); SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
return "plugin_" + name + "_" + fmt.format(new Date()) + "_"; return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(new Date()) + "_";
} }
private Class<? extends Module> load(String name, ClassLoader pluginLoader) private Class<? extends Module> load(String name, ClassLoader pluginLoader)
@ -509,4 +510,30 @@ public class PluginLoader implements LifecycleListener {
} }
return Arrays.asList(matches); return Arrays.asList(matches);
} }
public String getPluginName(Object pluginObject) {
ClassLoader pluginClassLoader = pluginObject.getClass().getClassLoader();
if (!(pluginClassLoader instanceof URLClassLoader)) {
throw new IllegalArgumentException("Object does not belong to a plugin");
}
String loaderFileName =
((URLClassLoader) pluginClassLoader).getURLs()[0].getFile();
int pluginPrefixPos = loaderFileName.indexOf(PLUGIN_TMP_PREFIX);
if (pluginPrefixPos < 0) {
throw new IllegalArgumentException(
"Object does not belong to a plugin (loaded from " + loaderFileName
+ ")");
}
loaderFileName =
removeSuffix(
loaderFileName.substring(pluginPrefixPos + PLUGIN_TMP_PREFIX.length()), '_');
loaderFileName = removeSuffix(loaderFileName, '_');
return removeSuffix(loaderFileName, '_');
}
private String removeSuffix(String loaderFileName, char suffixTerm) {
return loaderFileName.substring(0, loaderFileName.lastIndexOf(suffixTerm));
}
} }