Merge "Add rest API endpoints for batch update submit requirements."
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 8d30728..b9fa35a 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3917,6 +3917,103 @@
HTTP/1.1 204 No Content
----
+[[batch-update-submit-requirements]]
+=== Batch Update Submit Requirements
+--
+'POST /projects/link:#project-name[\{project-name\}]/submit_requirements/'
+--
+
+Creates/updates/deletes multiple submit requirements definitions in this project at once.
+
+The calling user must have write access to the `refs/meta/config` branch of the
+project.
+
+The updates must be specified in the request body as
+link:#batch-submit-requirement-input[BatchSubmitRequirementInput] entity.
+
+The updates are processed in the following order:
+
+1. submit requirements deletions
+2. submit requirements creations
+3. submit requirements updates
+
+.Request
+----
+ POST /projects/My-Project/submit_requirements/ HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "commit_message": "Update Submit Requirements",
+ "delete": [
+ "Old-Review",
+ "Unused-Review"
+ ]
+ }
+----
+
+If the submit requirements updates were done successfully the response is "`200 OK`".
+
+.Response
+----
+ HTTP/1.1 200 OK
+----
+
+[[create-submit-requirements-change]]
+=== Create Submit Requirements Change for review.
+--
+'POST /projects/link:#project-name[\{project-name\}]/submit_requirements:review'
+--
+
+Creates/updates/deletes multiple submit requirements definitions in this project at once.
+
+This takes the same input as link:#batch-update-submit-requirements[Batch Update Submit Requirements],
+but creates a pending change for review. Like
+link:#create-change[Create Change], it returns a link:#change-info[ChangeInfo]
+entity describing the resulting change.
+
+.Request
+----
+ POST /projects/testproj/submit_requirements:review HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "commit_message": "Update Submit Requirements",
+ "delete": [
+ "Old-Review",
+ "Unused-Review"
+ ]
+ }
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "id": "testproj~refs%2Fmeta%2Fconfig~Ieaf185bf90a1fc3b58461e399385e158a20b31a2",
+ "project": "testproj",
+ "branch": "refs/meta/config",
+ "hashtags": [],
+ "change_id": "Ieaf185bf90a1fc3b58461e399385e158a20b31a2",
+ "subject": "Review access change",
+ "status": "NEW",
+ "created": "2017-09-07 14:31:11.852000000",
+ "updated": "2017-09-07 14:31:11.852000000",
+ "submit_type": "CHERRY_PICK",
+ "mergeable": true,
+ "insertions": 2,
+ "deletions": 0,
+ "unresolved_comment_count": 0,
+ "has_review_started": true,
+ "_number": 7,
+ "owner": {
+ "_account_id": 1000000
+ }
+ }
+----
+
[[ids]]
== IDs
@@ -4661,6 +4758,27 @@
entities that describe the updates that should be done for the labels.
|=============================
+[[batch-submit-requirement-input]]
+=== BatchSubmitRequirementInput
+The `BatchSubmitRequirementInput` entity contains information for batch updating submit requirements
+definitions in a project.
+
+[options="header",cols="1,^2,4"]
+|=============================
+|Field Name ||Description
+|`commit_message`|optional|
+Message that should be used to commit the submit requirements updates in the
+`project.config` file to the `refs/meta/config` branch.
+|`delete` |optional|
+List of submit requirements that should be deleted.
+|`create` |optional|
+List of link:#submit-requirement-input[SubmitRequirementInput] entities that
+describe submit requirements that should be created.
+|`update` |optional|
+Map of label names to link:#submit-requirement-input[SubmitRequirementInput]
+entities that describe the updates that should be done for the submit requirements.
+|=============================
+
[[project-access-input]]
=== ProjectAccessInput
The `ProjectAccessInput` describes changes that should be applied to a project
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index cd805d0..7dc070c 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -75,6 +75,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -1587,6 +1588,18 @@
ObjectId.fromString(get(changeId, ListChangesOption.CURRENT_REVISION).currentRevision));
}
+ /** Creates a submit requirement with all required field. */
+ protected void configSubmitRequirement(Project.NameKey project, String name) throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName(name)
+ .setAllowOverrideInChildProjects(true)
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("-label:Code-Review=MIN"))
+ .build());
+ }
+
protected void configSubmitRequirement(
Project.NameKey project, SubmitRequirement submitRequirement) throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index fa78fdb..58fd93a 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -20,6 +20,7 @@
import com.google.gerrit.extensions.api.config.AccessCheckInfo;
import com.google.gerrit.extensions.api.config.AccessCheckInput;
import com.google.gerrit.extensions.common.BatchLabelInput;
+import com.google.gerrit.extensions.common.BatchSubmitRequirementInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.LabelDefinitionInfo;
import com.google.gerrit.extensions.common.ListTagSortOption;
@@ -292,6 +293,21 @@
ChangeInfo labelsReview(BatchLabelInput input) throws RestApiException;
/**
+ * Adds, updates and deletes submit requirements definitions in a batch.
+ *
+ * @param input input that describes additions, updates and deletions of submit requirements
+ */
+ void submitRequirements(BatchSubmitRequirementInput input) throws RestApiException;
+
+ /**
+ * Creates a change with required submit requirements updates.
+ *
+ * <p>See {@link #submitRequirements(BatchSubmitRequirementInput)} for details
+ */
+ @CanIgnoreReturnValue
+ ChangeInfo submitRequirementsReview(BatchSubmitRequirementInput input) throws RestApiException;
+
+ /**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
*/
@@ -506,5 +522,16 @@
public ChangeInfo labelsReview(BatchLabelInput input) throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public void submitRequirements(BatchSubmitRequirementInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public ChangeInfo submitRequirementsReview(BatchSubmitRequirementInput input)
+ throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/extensions/common/BatchSubmitRequirementInput.java b/java/com/google/gerrit/extensions/common/BatchSubmitRequirementInput.java
new file mode 100644
index 0000000..8a5f30d
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/BatchSubmitRequirementInput.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2024 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.extensions.common;
+
+/**
+ * Input for the REST API that describes additions, updates and deletions of submit requirements.
+ */
+public class BatchSubmitRequirementInput extends AbstractBatchInput<SubmitRequirementInput> {}
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index c817b93..66914b7 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -48,6 +48,7 @@
import com.google.gerrit.extensions.api.projects.TagApi;
import com.google.gerrit.extensions.api.projects.TagInfo;
import com.google.gerrit.extensions.common.BatchLabelInput;
+import com.google.gerrit.extensions.common.BatchSubmitRequirementInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.common.LabelDefinitionInfo;
@@ -87,6 +88,8 @@
import com.google.gerrit.server.restapi.project.ListTags;
import com.google.gerrit.server.restapi.project.PostLabels;
import com.google.gerrit.server.restapi.project.PostLabelsReview;
+import com.google.gerrit.server.restapi.project.PostSubmitRequirements;
+import com.google.gerrit.server.restapi.project.PostSubmitRequirementsReview;
import com.google.gerrit.server.restapi.project.ProjectsCollection;
import com.google.gerrit.server.restapi.project.PutConfig;
import com.google.gerrit.server.restapi.project.PutConfigReview;
@@ -151,6 +154,9 @@
private final Provider<ListSubmitRequirements> listSubmitRequirements;
private final PostLabels postLabels;
private final PostLabelsReview postLabelsReview;
+
+ private final PostSubmitRequirements postSubmitRequirements;
+ private final PostSubmitRequirementsReview postSubmitRequirementsReview;
private final LabelApiImpl.Factory labelApi;
private final SubmitRequirementApiImpl.Factory submitRequirementApi;
@@ -196,6 +202,8 @@
PostLabelsReview postLabelsReview,
LabelApiImpl.Factory labelApi,
SubmitRequirementApiImpl.Factory submitRequirementApi,
+ PostSubmitRequirements postSubmitRequirements,
+ PostSubmitRequirementsReview postSubmitRequirementsReview,
@Assisted ProjectResource project) {
this(
permissionBackend,
@@ -239,6 +247,8 @@
postLabelsReview,
labelApi,
submitRequirementApi,
+ postSubmitRequirements,
+ postSubmitRequirementsReview,
null);
}
@@ -284,6 +294,8 @@
PostLabelsReview postLabelsReview,
LabelApiImpl.Factory labelApi,
SubmitRequirementApiImpl.Factory submitRequirementApi,
+ PostSubmitRequirements postSubmitRequirements,
+ PostSubmitRequirementsReview postSubmitRequirementsReview,
@Assisted String name) {
this(
permissionBackend,
@@ -327,6 +339,8 @@
postLabelsReview,
labelApi,
submitRequirementApi,
+ postSubmitRequirements,
+ postSubmitRequirementsReview,
name);
}
@@ -372,6 +386,8 @@
PostLabelsReview postLabelsReview,
LabelApiImpl.Factory labelApi,
SubmitRequirementApiImpl.Factory submitRequirementApi,
+ PostSubmitRequirements postSubmitRequirements,
+ PostSubmitRequirementsReview postSubmitRequirementsReview,
String name) {
this.permissionBackend = permissionBackend;
this.createProject = createProject;
@@ -415,6 +431,8 @@
this.postLabelsReview = postLabelsReview;
this.labelApi = labelApi;
this.submitRequirementApi = submitRequirementApi;
+ this.postSubmitRequirements = postSubmitRequirements;
+ this.postSubmitRequirementsReview = postSubmitRequirementsReview;
}
@Override
@@ -848,4 +866,24 @@
throw asRestApiException("Cannot create change for labels update", e);
}
}
+
+ @Override
+ public void submitRequirements(BatchSubmitRequirementInput input) throws RestApiException {
+ try {
+ @SuppressWarnings("unused")
+ var unused = postSubmitRequirements.apply(checkExists(), input);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot update submit requirements", e);
+ }
+ }
+
+ @Override
+ public ChangeInfo submitRequirementsReview(BatchSubmitRequirementInput input)
+ throws RestApiException {
+ try {
+ return postSubmitRequirementsReview.apply(checkExists(), input).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot create change for submit requirements update", e);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java
new file mode 100644
index 0000000..3a9fb92
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2024 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.restapi.project;
+
+import com.google.gerrit.extensions.common.BatchSubmitRequirementInput;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.SubmitRequirementResource;
+import com.google.inject.Provider;
+import jakarta.inject.Singleton;
+import javax.inject.Inject;
+
+@Singleton
+public class PostSubmitRequirements
+ extends AbstractPostCollection<
+ IdString, SubmitRequirementResource, SubmitRequirementInput, BatchSubmitRequirementInput> {
+ CreateSubmitRequirement createSubmitRequirement;
+ DeleteSubmitRequirement deleteSubmitRequirement;
+ UpdateSubmitRequirement updateSubmitRequirement;
+
+ @Inject
+ public PostSubmitRequirements(
+ RepoMetaDataUpdater updater,
+ Provider<CurrentUser> user,
+ CreateSubmitRequirement createSubmitRequirement,
+ DeleteSubmitRequirement deleteSubmitRequirement,
+ UpdateSubmitRequirement updateSubmitRequirement) {
+ super(updater, user);
+ this.createSubmitRequirement = createSubmitRequirement;
+ this.deleteSubmitRequirement = deleteSubmitRequirement;
+ this.updateSubmitRequirement = updateSubmitRequirement;
+ }
+
+ @Override
+ public String defaultCommitMessage() {
+ return "Update Submit Requirements";
+ }
+
+ @Override
+ protected boolean updateItem(ProjectConfig config, String name, SubmitRequirementInput input)
+ throws BadRequestException, UnprocessableEntityException {
+ // The name and input.name can be different - the item should be renamed.
+ if (config.getSubmitRequirementSections().remove(name) == null) {
+ throw new UnprocessableEntityException(
+ String.format("Submit requirement %s not found", name));
+ }
+ var unused = updateSubmitRequirement.updateSubmitRequirement(config, input.name, input);
+ return true;
+ }
+
+ @Override
+ protected void createItem(ProjectConfig config, SubmitRequirementInput input)
+ throws BadRequestException, ResourceConflictException {
+ var unused = createSubmitRequirement.createSubmitRequirement(config, input.name, input);
+ }
+
+ @Override
+ protected void deleteItem(ProjectConfig config, String name) throws UnprocessableEntityException {
+ if (!deleteSubmitRequirement.deleteSubmitRequirement(config, name)) {
+ throw new UnprocessableEntityException(
+ String.format("Submit requirement %s not found", name));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java
new file mode 100644
index 0000000..82761e7
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2024 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.restapi.project;
+
+import com.google.gerrit.extensions.common.BatchSubmitRequirementInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigChangeCreator;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.inject.Inject;
+import java.io.IOException;
+import javax.inject.Singleton;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class PostSubmitRequirementsReview
+ implements RestModifyView<ProjectResource, BatchSubmitRequirementInput> {
+
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
+ private final PostSubmitRequirements postSubmitRequirements;
+
+ @Inject
+ PostSubmitRequirementsReview(
+ RepoMetaDataUpdater repoMetaDataUpdater, PostSubmitRequirements postSubmitRequirements) {
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
+ this.postSubmitRequirements = postSubmitRequirements;
+ }
+
+ @Override
+ public Response<ChangeInfo> apply(ProjectResource rsrc, BatchSubmitRequirementInput input)
+ throws PermissionBackendException, IOException, ConfigInvalidException, UpdateException,
+ RestApiException {
+ try (ConfigChangeCreator creator =
+ repoMetaDataUpdater.configChangeCreator(
+ rsrc.getNameKey(), input.commitMessage, "Review submit requirements change")) {
+ ProjectConfig config = creator.getConfig();
+ var unused = postSubmitRequirements.updateProjectConfig(config, input);
+ // If config isn't updated, the createChange throws BadRequestException. We don't need
+ // to explicitly check the updateProjectConfig result here.
+ return creator.createChange();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
index 07e1e72..5c8bf3d 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
@@ -117,6 +117,8 @@
put(SUBMIT_REQUIREMENT_KIND).to(UpdateSubmitRequirement.class);
get(SUBMIT_REQUIREMENT_KIND).to(GetSubmitRequirement.class);
delete(SUBMIT_REQUIREMENT_KIND).to(DeleteSubmitRequirement.class);
+ postOnCollection(SUBMIT_REQUIREMENT_KIND).to(PostSubmitRequirements.class);
+ post(PROJECT_KIND, "submit_requirements:review").to(PostSubmitRequirementsReview.class);
child(PROJECT_KIND, "tags").to(TagsCollection.class);
create(TAG_KIND).to(CreateTag.class);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsReviewIT.java b/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsReviewIT.java
new file mode 100644
index 0000000..f9759ac
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsReviewIT.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2024 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.acceptance.api.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.common.BatchSubmitRequirementInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.inject.Inject;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class SubmitRequirementsReviewIT extends AbstractDaemonTest {
+ @Inject private ProjectOperations projectOperations;
+
+ @Test
+ public void createSubmitRequirementsChangeWithDefaultMessage() throws Exception {
+ Project.NameKey testProject = projectOperations.newProject().create();
+ SubmitRequirementInput fooSR = new SubmitRequirementInput();
+ fooSR.name = "Foo";
+ fooSR.description = "SR description";
+ fooSR.applicabilityExpression = "topic:foo";
+ fooSR.submittabilityExpression = "label:code-review=+2";
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.create = ImmutableList.of(fooSR);
+
+ ChangeInfo changeInfo = gApi.projects().name(testProject.get()).submitRequirementsReview(input);
+
+ assertThat(changeInfo.subject).isEqualTo("Review submit requirements change");
+ Config config = new Config();
+ config.fromText(
+ gApi.changes()
+ .id(changeInfo.changeId)
+ .revision(1)
+ .file("project.config")
+ .content()
+ .asString());
+ assertThat(config.getString("submit-requirement", "Foo", "description"))
+ .isEqualTo("SR description");
+ assertThat(config.getString("submit-requirement", "Foo", "applicableIf"))
+ .isEqualTo("topic:foo");
+ assertThat(config.getString("submit-requirement", "Foo", "submittableIf"))
+ .isEqualTo("label:code-review=+2");
+ }
+
+ @Test
+ public void createSubmitRequirementsChangeWithCustomMessage() throws Exception {
+ Project.NameKey testProject = projectOperations.newProject().create();
+ SubmitRequirementInput fooSR = new SubmitRequirementInput();
+ fooSR.name = "Foo";
+ fooSR.description = "SR description";
+ fooSR.applicabilityExpression = "topic:foo";
+ fooSR.submittabilityExpression = "label:code-review=+2";
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.create = ImmutableList.of(fooSR);
+ String customMessage = "test custom message";
+ input.commitMessage = customMessage;
+
+ ChangeInfo changeInfo = gApi.projects().name(testProject.get()).submitRequirementsReview(input);
+ assertThat(changeInfo.subject).isEqualTo(customMessage);
+
+ Config config = new Config();
+ config.fromText(
+ gApi.changes()
+ .id(changeInfo.changeId)
+ .revision(1)
+ .file("project.config")
+ .content()
+ .asString());
+ assertThat(config.getString("submit-requirement", "Foo", "description"))
+ .isEqualTo("SR description");
+ assertThat(config.getString("submit-requirement", "Foo", "applicableIf"))
+ .isEqualTo("topic:foo");
+ assertThat(config.getString("submit-requirement", "Foo", "submittableIf"))
+ .isEqualTo("label:code-review=+2");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index 0cda3ba..18c435f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -90,6 +90,8 @@
RestCall.get("/projects/%s/statistics.git"),
RestCall.get("/projects/%s/submit_requirements"),
RestCall.put("/projects/%s/submit_requirements/new-sr"),
+ RestCall.post("/projects/%s/submit_requirements/"),
+ RestCall.post("/projects/%s/submit_requirements:review"),
RestCall.get("/projects/%s/tags"),
RestCall.put("/projects/%s/tags/new-tag"),
RestCall.post("/projects/%s/tags:delete"));
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PostSubmitRequirementsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/PostSubmitRequirementsIT.java
new file mode 100644
index 0000000..adc0a8e
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/PostSubmitRequirementsIT.java
@@ -0,0 +1,386 @@
+// Copyright (C) 2024 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.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.common.BatchSubmitRequirementInput;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.restapi.project.PostLabels;
+import com.google.inject.Inject;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+/** Tests for the {@link PostLabels} REST endpoint. */
+public class PostSubmitRequirementsIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ @Test
+ public void anonymous() throws Exception {
+ requestScopeOperations.setApiUserAnonymous();
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () ->
+ gApi.projects()
+ .name(allProjects.get())
+ .submitRequirements(new BatchSubmitRequirementInput()));
+ assertThat(thrown).hasMessageThat().contains("Authentication required");
+ }
+
+ @Test
+ public void notAllowed() throws Exception {
+ projectOperations
+ .project(allProjects)
+ .forUpdate()
+ .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .update();
+
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () ->
+ gApi.projects()
+ .name(allProjects.get())
+ .submitRequirements(new BatchSubmitRequirementInput()));
+ assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted");
+ }
+
+ @Test
+ public void deleteNonExistingSR() throws Exception {
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.delete = ImmutableList.of("Foo");
+
+ UnprocessableEntityException thrown =
+ assertThrows(
+ UnprocessableEntityException.class,
+ () -> gApi.projects().name(allProjects.get()).submitRequirements(input));
+ assertThat(thrown).hasMessageThat().contains("Submit requirement Foo not found");
+ }
+
+ @Test
+ public void deleteSR() throws Exception {
+ configSubmitRequirement(project, "Foo");
+ configSubmitRequirement(project, "Bar");
+ assertThat(gApi.projects().name(project.get()).submitRequirements().get()).isNotEmpty();
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.delete = ImmutableList.of("Foo", "Bar");
+ gApi.projects().name(project.get()).submitRequirements(input);
+ assertThat(gApi.projects().name(project.get()).submitRequirements().get()).isEmpty();
+ }
+
+ @Test
+ public void deleteSR_namesAreTrimmed() throws Exception {
+ configSubmitRequirement(project, "Foo");
+ configSubmitRequirement(project, "Bar");
+ assertThat(gApi.projects().name(project.get()).submitRequirements().get()).isNotEmpty();
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.delete = ImmutableList.of(" Foo ", " Bar ");
+ gApi.projects().name(project.get()).submitRequirements(input);
+ assertThat(gApi.projects().name(project.get()).submitRequirements().get()).isEmpty();
+ }
+
+ @Test
+ public void cannotDeleteTheSameSRTwice() throws Exception {
+ configSubmitRequirement(allProjects, "Foo");
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.delete = ImmutableList.of("Foo", "Foo");
+
+ UnprocessableEntityException thrown =
+ assertThrows(
+ UnprocessableEntityException.class,
+ () -> gApi.projects().name(allProjects.get()).submitRequirements(input));
+ assertThat(thrown).hasMessageThat().contains("Submit requirement Foo not found");
+ }
+
+ @Test
+ public void cannotCreateSRWithNameThatIsAlreadyInUse() throws Exception {
+ configSubmitRequirement(allProjects, "Foo");
+ SubmitRequirementInput srInput = new SubmitRequirementInput();
+ srInput.name = "Foo";
+ srInput.allowOverrideInChildProjects = false;
+ srInput.submittabilityExpression = "label:code-review=+2";
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.create = ImmutableList.of(srInput);
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.projects().name(allProjects.get()).submitRequirements(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("submit requirement \"Foo\" conflicts with existing submit requirement \"Foo\"");
+ }
+
+ @Test
+ public void cannotCreateTwoSRWithTheSameName() throws Exception {
+ SubmitRequirementInput srInput = new SubmitRequirementInput();
+ srInput.name = "Foo";
+ srInput.allowOverrideInChildProjects = false;
+ srInput.submittabilityExpression = "label:code-review=+2";
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.create = ImmutableList.of(srInput, srInput);
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.projects().name(project.get()).submitRequirements(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("submit requirement \"Foo\" conflicts with existing submit requirement \"Foo\"");
+ }
+
+ @Test
+ public void cannotCreateTwoSrWithConflictingNames() throws Exception {
+ SubmitRequirementInput sr1Input = new SubmitRequirementInput();
+ sr1Input.name = "Foo";
+ sr1Input.allowOverrideInChildProjects = false;
+ sr1Input.submittabilityExpression = "label:code-review=+2";
+
+ SubmitRequirementInput sr2Input = new SubmitRequirementInput();
+ sr2Input.name = "foo";
+ sr2Input.allowOverrideInChildProjects = false;
+ sr2Input.submittabilityExpression = "label:code-review=+2";
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.create = ImmutableList.of(sr1Input, sr2Input);
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.projects().name(project.get()).submitRequirements(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("submit requirement \"foo\" conflicts with existing submit requirement \"Foo\"");
+ }
+
+ @Test
+ public void createSubmitRequirements() throws Exception {
+ SubmitRequirementInput fooInput = new SubmitRequirementInput();
+ fooInput.name = "Foo";
+ fooInput.allowOverrideInChildProjects = false;
+ fooInput.submittabilityExpression = "label:code-review=+2";
+
+ SubmitRequirementInput barInput = new SubmitRequirementInput();
+ barInput.name = "Bar";
+ barInput.allowOverrideInChildProjects = false;
+ barInput.submittabilityExpression = "label:code-review=+1";
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.create = ImmutableList.of(fooInput, barInput);
+
+ gApi.projects().name(allProjects.get()).submitRequirements(input);
+ assertThat(gApi.projects().name(allProjects.get()).submitRequirement("Foo").get()).isNotNull();
+ assertThat(gApi.projects().name(allProjects.get()).submitRequirement("Bar").get()).isNotNull();
+ }
+
+ @Test
+ public void cannotCreateSRWithIncorrectName() throws Exception {
+ SubmitRequirementInput fooInput = new SubmitRequirementInput();
+ fooInput.name = "Foo ";
+ fooInput.allowOverrideInChildProjects = false;
+ fooInput.submittabilityExpression = "label:code-review=+2";
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.create = ImmutableList.of(fooInput, fooInput);
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).submitRequirements(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Name can only consist of alphanumeric characters");
+ }
+
+ @Test
+ public void cannotCreateSRWithoutName() throws Exception {
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.create = ImmutableList.of(new SubmitRequirementInput());
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).submitRequirements(input));
+ assertThat(thrown).hasMessageThat().contains("Empty submit requirement name");
+ }
+
+ @Test
+ public void updateNonExistingSR() throws Exception {
+ SubmitRequirementInput fooInput = new SubmitRequirementInput();
+ fooInput.name = "Foo2";
+ fooInput.allowOverrideInChildProjects = false;
+ fooInput.submittabilityExpression = "label:code-review=+2";
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.update = ImmutableMap.of("Foo", fooInput);
+
+ UnprocessableEntityException thrown =
+ assertThrows(
+ UnprocessableEntityException.class,
+ () -> gApi.projects().name(allProjects.get()).submitRequirements(input));
+ assertThat(thrown).hasMessageThat().contains("Submit requirement Foo not found");
+ }
+
+ @Test
+ public void updateSR() throws Exception {
+ configSubmitRequirement(project, "Foo");
+ configSubmitRequirement(project, "Bar");
+
+ SubmitRequirementInput fooUpdate = new SubmitRequirementInput();
+ fooUpdate.name = "Foo";
+ fooUpdate.description = "new description";
+ fooUpdate.submittabilityExpression = "-has:submodule-update";
+
+ SubmitRequirementInput barUpdate = new SubmitRequirementInput();
+ barUpdate.name = "Baz";
+ barUpdate.submittabilityExpression = "label:code-review=+1";
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.update = ImmutableMap.of("Foo", fooUpdate, "Bar", barUpdate);
+
+ gApi.projects().name(project.get()).submitRequirements(input);
+
+ assertThat(gApi.projects().name(project.get()).submitRequirement("Foo").get().description)
+ .isEqualTo(fooUpdate.description);
+ assertThat(
+ gApi.projects()
+ .name(project.get())
+ .submitRequirement("Foo")
+ .get()
+ .submittabilityExpression)
+ .isEqualTo(fooUpdate.submittabilityExpression);
+ assertThat(gApi.projects().name(project.get()).submitRequirement("Baz").get()).isNotNull();
+ assertThat(
+ gApi.projects()
+ .name(project.get())
+ .submitRequirement("Baz")
+ .get()
+ .submittabilityExpression)
+ .isEqualTo(barUpdate.submittabilityExpression);
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(project.get()).submitRequirement("Bar").get());
+ }
+
+ @Test
+ public void deleteAndRecreateSR() throws Exception {
+ configSubmitRequirement(project, "Foo");
+
+ SubmitRequirementInput fooUpdate = new SubmitRequirementInput();
+ fooUpdate.name = "Foo";
+ fooUpdate.description = "new description";
+ fooUpdate.submittabilityExpression = "-has:submodule-update";
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.delete = ImmutableList.of("Foo");
+ input.create = ImmutableList.of(fooUpdate);
+
+ gApi.projects().name(project.get()).submitRequirements(input);
+
+ SubmitRequirementInfo fooSR =
+ gApi.projects().name(project.get()).submitRequirement("Foo").get();
+ assertThat(fooSR.description).isEqualTo(fooUpdate.description);
+ assertThat(fooSR.submittabilityExpression).isEqualTo(fooUpdate.submittabilityExpression);
+ }
+
+ @Test
+ public void cannotDeleteAndUpdateSR() throws Exception {
+ configSubmitRequirement(project, "Foo");
+
+ SubmitRequirementInput fooUpdate = new SubmitRequirementInput();
+ fooUpdate.name = "Foo";
+ fooUpdate.description = "new description";
+ fooUpdate.submittabilityExpression = "-has:submodule-update";
+
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.delete = ImmutableList.of("Foo");
+ input.update = ImmutableMap.of("Foo", fooUpdate);
+
+ UnprocessableEntityException thrown =
+ assertThrows(
+ UnprocessableEntityException.class,
+ () -> gApi.projects().name(project.get()).submitRequirements(input));
+ assertThat(thrown).hasMessageThat().contains("Submit requirement Foo not found");
+ }
+
+ @Test
+ public void noOpUpdate() throws Exception {
+ RevCommit refsMetaConfigHead =
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG);
+
+ gApi.projects().name(allProjects.get()).submitRequirements(new BatchSubmitRequirementInput());
+
+ assertThat(projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG))
+ .isEqualTo(refsMetaConfigHead);
+ }
+
+ @Test
+ public void defaultCommitMessage() throws Exception {
+ configSubmitRequirement(allProjects, "Foo");
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.delete = ImmutableList.of("Foo");
+ gApi.projects().name(allProjects.get()).submitRequirements(input);
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Update Submit Requirements");
+ }
+
+ @Test
+ public void withCommitMessage() throws Exception {
+ configSubmitRequirement(allProjects, "Foo");
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.commitMessage = "Batch Update SubmitRequirements";
+ input.delete = ImmutableList.of("Foo");
+ gApi.projects().name(allProjects.get()).submitRequirements(input);
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo(input.commitMessage);
+ }
+
+ @Test
+ public void commitMessageIsTrimmed() throws Exception {
+ configSubmitRequirement(allProjects, "Foo");
+ BatchSubmitRequirementInput input = new BatchSubmitRequirementInput();
+ input.commitMessage = "Batch Update SubmitRequirements ";
+ input.delete = ImmutableList.of("Foo");
+ gApi.projects().name(allProjects.get()).submitRequirements(input);
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Batch Update SubmitRequirements");
+ }
+}