Introduce ref operation validation
Create extension point that is responsible for ref operations validation.
Change-Id: Ia6386ce6480ae63b75ec3b2836267c2f8d68d426
Signed-off-by: Jacek Centkowski <geminica.programs@gmail.com>
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index b130fa5..8f76d90 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -21,6 +21,17 @@
Out of the box, Gerrit includes a plugin that checks the length of the
subject and body lines of commit messages on uploaded commits.
+[[ref-operation-validation]]
+== Ref operation validation
+
+
+Plugins implementing the `RefOperationValidationListener` interface can
+perform additional validation checks against ref creation/deletion operation
+before it is applied to the git repository.
+
+If the ref operation fails the validation, the plugin can throw an exception
+which will cause the operation to fail.
+
[[pre-merge-validation]]
== Pre-merge validation
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index d477e08..6d2db2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -91,6 +91,8 @@
import com.google.gerrit.server.git.validators.MergeValidationListener;
import com.google.gerrit.server.git.validators.MergeValidators;
import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
+import com.google.gerrit.server.git.validators.RefOperationValidationListener;
+import com.google.gerrit.server.git.validators.RefOperationValidators;
import com.google.gerrit.server.git.validators.UploadValidationListener;
import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.group.GroupModule;
@@ -272,6 +274,7 @@
.to(ProjectConfigEntry.UpdateChecker.class);
DynamicSet.setOf(binder(), ChangeListener.class);
DynamicSet.setOf(binder(), CommitValidationListener.class);
+ DynamicSet.setOf(binder(), RefOperationValidationListener.class);
DynamicSet.setOf(binder(), MergeValidationListener.class);
DynamicSet.setOf(binder(), ProjectCreationValidationListener.class);
DynamicSet.setOf(binder(), GroupCreationValidationListener.class);
@@ -294,6 +297,7 @@
bind(AnonymousUser.class);
factory(CommitValidators.Factory.class);
+ factory(RefOperationValidators.Factory.class);
factory(MergeValidators.Factory.class);
factory(ProjectConfigValidator.Factory.class);
factory(NotesBranchUtil.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefOperationReceivedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefOperationReceivedEvent.java
new file mode 100644
index 0000000..d26632b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefOperationReceivedEvent.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2014 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.transport.ReceiveCommand;
+
+public class RefOperationReceivedEvent extends ChangeEvent {
+ public final String type = "ref-received";
+ public ReceiveCommand command;
+ public Project project;
+ public IdentifiedUser user;
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java
new file mode 100644
index 0000000..5864833
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2014 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.server.validators.ValidationException;
+
+public class RefOperationValidationException extends ValidationException {
+ private static final long serialVersionUID = 1L;
+ private final Iterable<ValidationMessage> messages;
+
+ public RefOperationValidationException(String reason,
+ Iterable<ValidationMessage> messages) {
+ super(reason);
+ this.messages = messages;
+ }
+
+ public Iterable<ValidationMessage> getMessages() {
+ return messages;
+ }
+
+ @Override
+ public String getMessage() {
+ StringBuilder msg = new StringBuilder(super.getMessage());
+ for (ValidationMessage error : messages) {
+ msg.append("\n").append(error.getMessage());
+ }
+ return msg.toString();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationListener.java
new file mode 100644
index 0000000..c33ecfc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationListener.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2014 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.RefOperationReceivedEvent;
+import com.google.gerrit.server.validators.ValidationException;
+
+import java.util.List;
+
+/**
+ * Listener to provide validation on operation that is going to be performed on
+ * given ref
+ */
+@ExtensionPoint
+public interface RefOperationValidationListener {
+ /**
+ * Validate a ref operation before it is performed.
+ *
+ * @param refEvent ref operation specification
+ * @return empty list or informational messages on success
+ * @throws ValidationException if the ref operation fails to validate
+ */
+ List<ValidationMessage> onRefOperation(RefOperationReceivedEvent refEvent)
+ throws ValidationException;
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
new file mode 100644
index 0000000..f265d3f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2014 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.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.events.RefOperationReceivedEvent;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class RefOperationValidators {
+ private static final GetErrorMessages GET_ERRORS = new GetErrorMessages();
+ private static final Logger LOG = LoggerFactory
+ .getLogger(RefOperationValidators.class);
+
+ public interface Factory {
+ RefOperationValidators create(Project project, IdentifiedUser user, ReceiveCommand cmd);
+ }
+
+ public static ReceiveCommand getCommand(RefUpdate update, ReceiveCommand.Type type) {
+ return new ReceiveCommand(update.getOldObjectId(), update.getNewObjectId(),
+ update.getName(), type);
+ }
+
+ private final RefOperationReceivedEvent event;
+ private final DynamicSet<RefOperationValidationListener> refOperationValidationListeners;
+
+ @Inject
+ RefOperationValidators(
+ DynamicSet<RefOperationValidationListener> refOperationValidationListeners,
+ @Assisted Project project, @Assisted IdentifiedUser user,
+ @Assisted ReceiveCommand cmd) {
+ this.refOperationValidationListeners = refOperationValidationListeners;
+ event = new RefOperationReceivedEvent();
+ event.command = cmd;
+ event.project = project;
+ event.user = user;
+ }
+
+ public List<ValidationMessage> validateForRefOperation()
+ throws RefOperationValidationException {
+
+ List<ValidationMessage> messages = Lists.newArrayList();
+ boolean withException = false;
+ try {
+ for (RefOperationValidationListener listener : refOperationValidationListeners) {
+ messages.addAll(listener.onRefOperation(event));
+ }
+ } catch (ValidationException e) {
+ messages.add(new ValidationMessage(e.getMessage(), true));
+ withException = true;
+ }
+
+ if (withException) {
+ throwException(messages, event);
+ }
+
+ return messages;
+ }
+
+ private void throwException(Iterable<ValidationMessage> messages,
+ RefOperationReceivedEvent event) throws RefOperationValidationException {
+ Iterable<ValidationMessage> errors = Iterables.filter(messages, GET_ERRORS);
+ String header = String.format(
+ "Ref \"%s\" %S in project %s validation failed", event.command.getRefName(),
+ event.command.getType(), event.project.getName());
+ LOG.error(header);
+ throw new RefOperationValidationException(header, errors);
+ }
+
+ private static class GetErrorMessages implements Predicate<ValidationMessage> {
+ @Override
+ public boolean apply(ValidationMessage input) {
+ return input.isError();
+ }
+ }
+}
\ No newline at end of file