Add an endpoint to test submit requirements on a change
We add the 'POST /changes/{change-id}/check.submit_requirement' endpoint
to evaluate and return the result of a submit requirement on a change.
This endpoint is useful to test submit requirements before adding them
to the project config. The endpoint accepts a SubmitRequirementInput as
input.
We use the POST http method since we need to send a
SubmitRequirementInput in the request body, however this endpoint does
not modify the change resource.
Change-Id: I73d72ae74a14bd56ea91da5b6c17549416ee1c15
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 4d7df11..be24bc7 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2818,6 +2818,53 @@
}
----
+[[check-submit-requirement]]
+=== Check Submit Requirement
+--
+'POST /changes/link:#change-id[\{change-id\}]/check.submit_requirement'
+--
+
+Tests a submit requirement and returns the result as a
+link:#submit-requirement-result-info[SubmitRequirementResultInfo]. The request
+body must contain a link:#submit-requirement-input[SubmitRequirementInput].
+
+Note that this endpoint does not modify the change resource.
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check.submit_requirement HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "name": "Code-Review",
+ "submittability_expression": "label:Code-Review=+2"
+ }
+----
+
+As response a link:#submit-requirement-result-info[SubmitRequirementResultInfo]
+entity is returned that describes the submit requirement result.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "name": "Code-Review",
+ "status": "SATISFIED",
+ "submittability_expression_result": {
+ "expression": "label:Code-Review=+2",
+ "fulfilled": true,
+ "passingAtoms": [
+ "label:Code-Review=+2"
+ ]
+ },
+ "is_legacy": false
+ }
+----
+
[[edit-endpoints]]
== Change Edit Endpoints
@@ -8232,6 +8279,32 @@
contains the list of predicates that are not fulfilled for the change.
|===========================
+[[submit-requirement-input]]
+=== SubmitRequirementInput
+The `SubmitRequirementInput` entity describes a submit requirement.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`name`||
+The submit requirement name.
+|`description`|optional|
+Description of the submit requirement.
+|`applicability_expression`|optional|
+Query expression that can be evaluated on any change. If evaluated to true on a
+change, the submit requirement is then applicable for this change.
+If not specified, the submit requirement is applicable for all changes.
+|`submittability_expression`||
+Query expression that can be evaluated on any change. If evaluated to true on a
+change, the submit requirement is fulfilled and not blocking change submission.
+|`override_expression`|optional|
+Query expression that can be evaluated on any change. If evaluated to true on a
+change, the submit requirement is overridden and not blocking change submission.
+|`allow_override_in_child_projects`|optional|
+Whether this submit requirement can be overridden in child projects. Default is
+`true`.
+|===========================
+
[[submit-requirement-result-info]]
=== SubmitRequirementResultInfo
The `SubmitRequirementResultInfo` describes the result of evaluating a
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 690ba4e..2224649 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -29,6 +29,8 @@
import com.google.gerrit.extensions.common.PureRevertInfo;
import com.google.gerrit.extensions.common.RevertSubmissionInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -425,6 +427,10 @@
ChangeInfo check(FixInput fix) throws RestApiException;
+ /** Returns the result of evaluating the {@link SubmitRequirementInput} input on the change. */
+ SubmitRequirementResultInfo checkSubmitRequirement(SubmitRequirementInput input)
+ throws RestApiException;
+
void index() throws RestApiException;
/** Check if this change is a pure revert of the change stored in revertOf. */
@@ -761,6 +767,12 @@
}
@Override
+ public SubmitRequirementResultInfo checkSubmitRequirement(SubmitRequirementInput input)
+ throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public void index() throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/common/SubmitRequirementInput.java b/java/com/google/gerrit/extensions/common/SubmitRequirementInput.java
new file mode 100644
index 0000000..96045d4
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/SubmitRequirementInput.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2021 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;
+
+/** API Input describing a submit requirement entity. */
+public class SubmitRequirementInput {
+ /** Submit requirement name. */
+ public String name;
+
+ /** Submit requirement description. */
+ public String description;
+
+ /**
+ * Query expression that can be evaluated on any change. If evaluated to true on a change, the
+ * submit requirement is then applicable on this change.
+ */
+ public String applicabilityExpression;
+
+ /**
+ * Query expression that can be evaluated on any change. If evaluated to true on a change, the
+ * submit requirement is fulfilled and not blocking change submission.
+ */
+ public String submittabilityExpression;
+
+ /**
+ * Query expression that can be evaluated on any change. If evaluated to true on a change, the
+ * submit requirement is overridden and not blocking change submission.
+ */
+ public String overrideExpression;
+
+ /** Whether this submit requirement can be overridden in child projects. */
+ public Boolean allowOverrideInChildProjects;
+}
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index cbaf49e..a49061d 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -56,6 +56,8 @@
import com.google.gerrit.extensions.common.PureRevertInfo;
import com.google.gerrit.extensions.common.RevertSubmissionInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -74,6 +76,7 @@
import com.google.gerrit.server.restapi.change.ChangeIncludedIn;
import com.google.gerrit.server.restapi.change.ChangeMessages;
import com.google.gerrit.server.restapi.change.Check;
+import com.google.gerrit.server.restapi.change.CheckSubmitRequirement;
import com.google.gerrit.server.restapi.change.CreateMergePatchSet;
import com.google.gerrit.server.restapi.change.DeleteAssignee;
import com.google.gerrit.server.restapi.change.DeleteChange;
@@ -164,6 +167,7 @@
private final Provider<ListChangeDrafts> listDraftsProvider;
private final ChangeEditApiImpl.Factory changeEditApi;
private final Check check;
+ private final CheckSubmitRequirement checkSubmitRequirement;
private final Index index;
private final Move move;
private final PostPrivate postPrivate;
@@ -218,6 +222,7 @@
Provider<ListChangeDrafts> listDraftsProvider,
ChangeEditApiImpl.Factory changeEditApi,
Check check,
+ CheckSubmitRequirement checkSubmitRequirement,
Index index,
Move move,
PostPrivate postPrivate,
@@ -270,6 +275,7 @@
this.listDraftsProvider = listDraftsProvider;
this.changeEditApi = changeEditApi;
this.check = check;
+ this.checkSubmitRequirement = checkSubmitRequirement;
this.index = index;
this.move = move;
this.postPrivate = postPrivate;
@@ -708,6 +714,16 @@
}
@Override
+ public SubmitRequirementResultInfo checkSubmitRequirement(SubmitRequirementInput input)
+ throws RestApiException {
+ try {
+ return checkSubmitRequirement.apply(change, input).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot check submit requirement", e);
+ }
+ }
+
+ @Override
public void index() throws RestApiException {
try {
index.apply(change, new Input());
diff --git a/java/com/google/gerrit/server/restapi/change/CheckSubmitRequirement.java b/java/com/google/gerrit/server/restapi/change/CheckSubmitRequirement.java
new file mode 100644
index 0000000..15728ce
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/CheckSubmitRequirement.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2021 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.change;
+
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
+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.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.SubmitRequirementsJson;
+import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
+import com.google.inject.Inject;
+import java.util.Optional;
+
+/**
+ * A rest view to evaluate (test) a {@link com.google.gerrit.entities.SubmitRequirement} on a given
+ * change.
+ *
+ * <p>TODO(ghareeb): Can this class be made singleton?
+ */
+public class CheckSubmitRequirement
+ implements RestModifyView<ChangeResource, SubmitRequirementInput> {
+ private final SubmitRequirementsEvaluator evaluator;
+
+ @Inject
+ public CheckSubmitRequirement(SubmitRequirementsEvaluator evaluator) {
+ this.evaluator = evaluator;
+ }
+
+ @Override
+ public Response<SubmitRequirementResultInfo> apply(
+ ChangeResource resource, SubmitRequirementInput input)
+ throws AuthException, BadRequestException, ResourceConflictException, Exception {
+ SubmitRequirement requirement = createSubmitRequirement(input);
+ SubmitRequirementResult res =
+ evaluator.evaluateRequirement(requirement, resource.getChangeData());
+ return Response.ok(SubmitRequirementsJson.toInfo(requirement, res));
+ }
+
+ private SubmitRequirement createSubmitRequirement(SubmitRequirementInput input)
+ throws BadRequestException {
+ validateSubmitRequirementInput(input);
+ return SubmitRequirement.builder()
+ .setName(input.name)
+ .setDescription(Optional.ofNullable(input.description))
+ .setApplicabilityExpression(SubmitRequirementExpression.of(input.applicabilityExpression))
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create(input.submittabilityExpression))
+ .setOverrideExpression(SubmitRequirementExpression.of(input.overrideExpression))
+ .setAllowOverrideInChildProjects(
+ input.allowOverrideInChildProjects == null ? true : input.allowOverrideInChildProjects)
+ .build();
+ }
+
+ private void validateSubmitRequirementInput(SubmitRequirementInput input)
+ throws BadRequestException {
+ if (input.name == null) {
+ throw new BadRequestException("Field 'name' is missing from input.");
+ }
+ if (input.submittabilityExpression == null) {
+ throw new BadRequestException("Field 'submittability_expression' is missing from input.");
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index 000a17e..4d955fb 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -123,6 +123,7 @@
post(CHANGE_KIND, "wip").to(SetWorkInProgress.class);
post(CHANGE_KIND, "ready").to(SetReadyForReview.class);
put(CHANGE_KIND, "message").to(PutMessage.class);
+ post(CHANGE_KIND, "check.submit_requirement").to(CheckSubmitRequirement.class);
get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
child(CHANGE_KIND, "reviewers").to(Reviewers.class);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 52202d7..e657c89 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -153,6 +153,7 @@
import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.SubmitRecordInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status;
import com.google.gerrit.extensions.common.TrackingIdInfo;
@@ -4074,6 +4075,90 @@
}
@Test
+ public void checkSubmitRequirement_satisfied() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ SubmitRequirementInput in =
+ createSubmitRequirementInput(
+ "Code-Review", /* submittabilityExpression= */ "label:Code-Review=+2");
+
+ SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in);
+ assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED);
+
+ voteLabel(changeId, "Code-Review", 2);
+ result = gApi.changes().id(changeId).checkSubmitRequirement(in);
+ assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED);
+ }
+
+ @Test
+ public void checkSubmitRequirement_notApplicable() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ SubmitRequirementInput in =
+ createSubmitRequirementInput(
+ "Code-Review",
+ /* applicableIf= */ "branch:non-existent",
+ /* submittableIf= */ "label:Code-Review=+2",
+ /* overrideIf= */ null);
+
+ SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in);
+ assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.NOT_APPLICABLE);
+
+ voteLabel(changeId, "Code-Review", 2);
+ result = gApi.changes().id(changeId).checkSubmitRequirement(in);
+ assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.NOT_APPLICABLE);
+ }
+
+ @Test
+ public void checkSubmitRequirement_overridden() throws Exception {
+ configLabel("Override-Label", LabelFunction.NO_OP); // label function has no effect
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel("Override-Label")
+ .ref("refs/heads/master")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ SubmitRequirementInput in =
+ createSubmitRequirementInput(
+ "Code-Review",
+ /* applicableIf= */ null,
+ /* submittableIf= */ "label:Code-Review=+2",
+ /* overrideIf= */ "label:Override-Label=+1");
+
+ SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in);
+ assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED);
+
+ voteLabel(changeId, "Code-Review", 2);
+ result = gApi.changes().id(changeId).checkSubmitRequirement(in);
+ assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED);
+
+ voteLabel(changeId, "Override-Label", 1);
+ result = gApi.changes().id(changeId).checkSubmitRequirement(in);
+ assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.OVERRIDDEN);
+ }
+
+ @Test
+ public void checkSubmitRequirement_error() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ SubmitRequirementInput in =
+ createSubmitRequirementInput("Code-Review", /* submittabilityExpression= */ "!!!");
+
+ SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in);
+ assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.ERROR);
+ }
+
+ @Test
public void submitRequirement_withLabelEqualsMax() throws Exception {
configSubmitRequirement(
project,
@@ -5393,4 +5478,22 @@
return Optional.of(record);
}
}
+
+ private static SubmitRequirementInput createSubmitRequirementInput(
+ String name, String submittabilityExpression) {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = name;
+ input.submittabilityExpression = submittabilityExpression;
+ return input;
+ }
+
+ private static SubmitRequirementInput createSubmitRequirementInput(
+ String name, String applicableIf, String submittableIf, String overrideIf) {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = name;
+ input.applicabilityExpression = applicableIf;
+ input.submittabilityExpression = submittableIf;
+ input.overrideExpression = overrideIf;
+ return input;
+ }
}