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:
parent
7669e7813c
commit
3025cb15b8
@ -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
|
||||||
|
@ -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]
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user