Merge "New extension to validate branch updates by submit strategies."
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index cccfe5c..0d0717e 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -45,6 +45,18 @@
If the commit fails the validation, the plugin can throw an exception
which will cause the merge to fail.
+[[on-submit-validation]]
+== On submit validation
+
+
+Plugins implementing the `OnSubmitValidationListener` interface can
+perform additional validation checks against ref operations resuling
+from execution of submit operation before they are applied to any git
+repositories (there could be more than one in case of topic submits).
+
+Plugin can throw an exception which will cause submit operation to be
+aborted.
+
[[pre-upload-validation]]
== Pre-upload validation
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 45da3d8..2a39ffd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -29,6 +29,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -45,6 +46,8 @@
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -56,6 +59,7 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.RevisionResource;
@@ -63,8 +67,10 @@
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.validators.ValidationException;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.TestTimeUtil;
import com.google.gwtorm.server.OrmException;
@@ -86,9 +92,13 @@
import org.junit.Test;
import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
@NoHttpd
@@ -110,6 +120,10 @@
@Inject
private BatchUpdate.Factory updateFactory;
+ @Inject
+ private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
+ private RegistrationHandle onSubmitValidatorHandle;
+
private String systemTimeZone;
@Before
@@ -129,6 +143,13 @@
db.close();
}
+ @After
+ public void removeOnSubmitValidator(){
+ if (onSubmitValidatorHandle != null){
+ onSubmitValidatorHandle.remove();
+ }
+ }
+
protected abstract SubmitType getSubmitType();
@Test
@@ -647,6 +668,94 @@
assertThat(getRemoteHead()).isEqualTo(headAfterSubmit);
}
+ @Test
+ public void submitWithValidation() throws Exception {
+ AtomicBoolean called = new AtomicBoolean(false);
+ this.addOnSubmitValidationListener(new OnSubmitValidationListener() {
+ @Override
+ public void preBranchUpdate(Arguments args) throws ValidationException {
+ called.set(true);
+ HashSet<String> refs = Sets.newHashSet(args.getCommands().keySet());
+ assertThat(refs).contains("refs/heads/master");
+ refs.remove("refs/heads/master");
+ if (!refs.isEmpty()){
+ // Some submit strategies need to insert new patchset.
+ assertThat(refs).hasSize(1);
+ assertThat(refs.iterator().next()).startsWith(RefNames.REFS_CHANGES);
+ }
+ }
+ });
+
+ PushOneCommit.Result change = createChange();
+ approve(change.getChangeId());
+ submit(change.getChangeId());
+ assertThat(called.get()).isTrue();
+ }
+
+ @Test
+ public void submitWithValidationMultiRepo() throws Exception {
+ assume().that(isSubmitWholeTopicEnabled()).isTrue();
+ String topic = "test-topic";
+
+ // Create test projects
+ TestRepository<?> repoA =
+ createProjectWithPush("project-a", null, getSubmitType());
+ TestRepository<?> repoB =
+ createProjectWithPush("project-b", null, getSubmitType());
+
+ // Create changes on project-a
+ PushOneCommit.Result change1 =
+ createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
+ PushOneCommit.Result change2 =
+ createChange(repoA, "master", "Change 2", "b.txt", "content", topic);
+
+ // Create changes on project-b
+ PushOneCommit.Result change3 =
+ createChange(repoB, "master", "Change 3", "a.txt", "content", topic);
+ PushOneCommit.Result change4 =
+ createChange(repoB, "master", "Change 4", "b.txt", "content", topic);
+
+ List<PushOneCommit.Result> changes =
+ Lists.newArrayList(change1, change2, change3, change4);
+ for (PushOneCommit.Result change : changes) {
+ approve(change.getChangeId());
+ }
+
+ // Construct validator which will throw on a second call.
+ // Since there are 2 repos, first submit attempt will fail, the second will
+ // succeed.
+ List<String> projectsCalled = new ArrayList<>(4);
+ this.addOnSubmitValidationListener(new OnSubmitValidationListener() {
+ @Override
+ public void preBranchUpdate(Arguments args) throws ValidationException {
+ assertThat(args.getCommands().keySet()).contains("refs/heads/master");
+ try (RevWalk rw = args.newRevWalk()) {
+ rw.parseBody(rw.parseCommit(
+ args.getCommands().get("refs/heads/master").getNewId()));
+ } catch (IOException e) {
+ assertThat(e).isNull();
+ }
+ projectsCalled.add(args.getProject().get());
+ if (projectsCalled.size() == 2) {
+ throw new ValidationException("time to fail");
+ }
+ }
+ });
+ submitWithConflict(change4.getChangeId(), "time to fail");
+ assertThat(projectsCalled).containsExactly(name("project-a"),
+ name("project-b"));
+ for (PushOneCommit.Result change : changes) {
+ change.assertChange(Change.Status.NEW, name(topic), admin);
+ }
+
+ submit(change4.getChangeId());
+ assertThat(projectsCalled).containsExactly(name("project-a"),
+ name("project-b"), name("project-a"), name("project-b"));
+ for (PushOneCommit.Result change : changes) {
+ change.assertChange(Change.Status.MERGED, name(topic), admin);
+ }
+ }
+
private void setChangeStatusToNew(PushOneCommit.Result... changes)
throws Exception {
for (PushOneCommit.Result change : changes) {
@@ -881,6 +990,11 @@
return getRemoteLog(project, "master");
}
+ protected void addOnSubmitValidationListener(OnSubmitValidationListener listener){
+ assertThat(onSubmitValidatorHandle).isNull();
+ onSubmitValidatorHandle = onSubmitValidationListeners.add(listener);
+ }
+
private String getLatestDiff(Repository repo) throws Exception {
ObjectId oldTreeId = repo.resolve("HEAD~1^{tree}");
ObjectId newTreeId = repo.resolve("HEAD^{tree}");
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 df96a54..6012142 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
@@ -128,6 +128,8 @@
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.OnSubmitValidationListener;
+import com.google.gerrit.server.git.validators.OnSubmitValidators;
import com.google.gerrit.server.git.validators.UploadValidationListener;
import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.group.GroupInfoCache;
@@ -350,6 +352,7 @@
DynamicSet.setOf(binder(), CommitValidationListener.class);
DynamicSet.setOf(binder(), ChangeMessageModifier.class);
DynamicSet.setOf(binder(), RefOperationValidationListener.class);
+ DynamicSet.setOf(binder(), OnSubmitValidationListener.class);
DynamicSet.setOf(binder(), MergeValidationListener.class);
DynamicSet.setOf(binder(), ProjectCreationValidationListener.class);
DynamicSet.setOf(binder(), GroupCreationValidationListener.class);
@@ -391,6 +394,7 @@
factory(AbandonOp.Factory.class);
factory(RefOperationValidators.Factory.class);
+ factory(OnSubmitValidators.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/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index 3c09cb0..7419dab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -51,6 +51,7 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.validators.OnSubmitValidators;
import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -532,6 +533,7 @@
private BatchRefUpdate batchRefUpdate;
private boolean closeRepo;
private Order order;
+ private OnSubmitValidators onSubmitValidators;
private boolean updateChangesInParallel;
private RequestId requestId;
@@ -607,6 +609,15 @@
}
/**
+ * Add a validation step for intended ref operations, which will be performed
+ * at the end of {@link RepoOnlyOp#updateRepo(RepoContext)} step.
+ */
+ BatchUpdate setOnSubmitValidators(OnSubmitValidators onSubmitValidators) {
+ this.onSubmitValidators = onSubmitValidators;
+ return this;
+ }
+
+ /**
* Execute {@link Op#updateChange(ChangeContext)} in parallel for each change.
*/
public BatchUpdate updateChangesInParallel() {
@@ -692,6 +703,18 @@
op.updateRepo(ctx);
}
+ if (onSubmitValidators != null && commands != null
+ && !commands.isEmpty()) {
+ // Validation of refs has to take place here and not at the beginning
+ // executeRefUpdates. Otherwise failing validation in a second
+ // BatchUpdate object will happen *after* first object's
+ // executeRefUpdates has finished, hence after first repo's refs have
+ // been updated, which is too late.
+ onSubmitValidators.validate(project,
+ new ReadOnlyRepository(getRepository()),
+ ctx.getInserter().newReader(), commands.getCommands());
+ }
+
if (inserter != null) {
logDebug("Flushing inserter");
inserter.flush();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index 5196ebe..1244ad3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -22,6 +22,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.validators.OnSubmitValidators;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
@@ -107,7 +108,8 @@
if (update == null) {
update = batchUpdateFactory.create(db, getProjectName(), caller, ts)
.setRepository(repo, rw, ins)
- .setRequestId(submissionId);
+ .setRequestId(submissionId)
+ .setOnSubmitValidators(onSubmitValidatorsFactory.create());
}
return update;
}
@@ -157,6 +159,7 @@
private final Map<Project.NameKey, OpenRepo> openRepos;
private final BatchUpdate.Factory batchUpdateFactory;
+ private final OnSubmitValidators.Factory onSubmitValidatorsFactory;
private final GitRepositoryManager repoManager;
private final ProjectCache projectCache;
@@ -169,10 +172,12 @@
MergeOpRepoManager(
GitRepositoryManager repoManager,
ProjectCache projectCache,
- BatchUpdate.Factory batchUpdateFactory) {
+ BatchUpdate.Factory batchUpdateFactory,
+ OnSubmitValidators.Factory onSubmitValidatorsFactory) {
this.repoManager = repoManager;
this.projectCache = projectCache;
this.batchUpdateFactory = batchUpdateFactory;
+ this.onSubmitValidatorsFactory = onSubmitValidatorsFactory;
openRepos = new HashMap<>();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index e0e2ae7..692bc98 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -46,6 +46,7 @@
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.SubmoduleOp;
import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.git.validators.OnSubmitValidators;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache;
@@ -118,6 +119,7 @@
final ProjectCache projectCache;
final PersonIdent serverIdent;
final RebaseChangeOp.Factory rebaseFactory;
+ final OnSubmitValidators.Factory onSubmitValidatorsFactory;
final TagCache tagCache;
final Branch.NameKey destBranch;
@@ -158,6 +160,7 @@
@GerritPersonIdent PersonIdent serverIdent,
ProjectCache projectCache,
RebaseChangeOp.Factory rebaseFactory,
+ OnSubmitValidators.Factory onSubmitValidatorsFactory,
TagCache tagCache,
@Assisted Branch.NameKey destBranch,
@Assisted CommitStatus commits,
@@ -212,6 +215,7 @@
"project not found: %s", destBranch.getParentKey());
this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
this.mergeUtil = mergeUtilFactory.create(project);
+ this.onSubmitValidatorsFactory = onSubmitValidatorsFactory;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
new file mode 100644
index 0000000..f2f397c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2017 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.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.validators.ValidationException;
+
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import java.util.Map;
+
+/**
+ * Listener to validate ref updates performed during submit operation.
+ *
+ * As submit strategies may generate new commits (e.g. Cherry Pick), this
+ * listener allows validation of resulting new commit before destination branch
+ * is updated and new patchset ref is created.
+ *
+ * If you only care about validating the change being submitted and not the
+ * resulting new commit, consider using {@link MergeValidationListener} instead.
+ */
+@ExtensionPoint
+public interface OnSubmitValidationListener {
+ public class Arguments {
+ private Project.NameKey project;
+ private Repository repository;
+ private ObjectReader objectReader;
+ private Map<String, ReceiveCommand> commands;
+
+ public Arguments(NameKey project, Repository repository,
+ ObjectReader objectReader, Map<String, ReceiveCommand> commands) {
+ this.project = project;
+ this.repository = repository;
+ this.objectReader = objectReader;
+ this.commands = commands;
+ }
+
+ public Project.NameKey getProject() {
+ return project;
+ }
+
+ /**
+ * @return a read only repository
+ */
+ public Repository getRepository() {
+ return repository;
+ }
+
+ public RevWalk newRevWalk() {
+ return new RevWalk(objectReader);
+ }
+
+ /**
+ * @return a map from ref to op on it covering all ref ops to be performed
+ * on this repository as part of ongoing submit operation.
+ */
+ public Map<String, ReceiveCommand> getCommands(){
+ return commands;
+ }
+ }
+
+ /**
+ * Called right before branch is updated with new commit or commits as a
+ * result of submit.
+ *
+ * If ValidationException is thrown, submitting is aborted.
+ */
+ void preBranchUpdate(Arguments args) throws ValidationException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java
new file mode 100644
index 0000000..568c597
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2017 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.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.IntegrationException;
+import com.google.gerrit.server.git.validators.OnSubmitValidationListener.Arguments;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import java.util.Map;
+
+public class OnSubmitValidators {
+ public interface Factory {
+ OnSubmitValidators create();
+ }
+
+ private final DynamicSet<OnSubmitValidationListener> listeners;
+
+ @Inject
+ OnSubmitValidators(DynamicSet<OnSubmitValidationListener> listeners) {
+ this.listeners = listeners;
+ }
+
+ public void validate(Project.NameKey project, Repository repo,
+ ObjectReader objectReader, Map<String, ReceiveCommand> commands)
+ throws IntegrationException {
+ try {
+ for (OnSubmitValidationListener listener : this.listeners) {
+ listener.preBranchUpdate(
+ new Arguments(project, repo, objectReader, commands));
+ }
+ } catch (ValidationException e) {
+ throw new IntegrationException(e.getMessage());
+ }
+ }
+}