Line data Source code
1 : // Copyright (C) 2021 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.restapi.change; 16 : 17 : import com.google.common.collect.Iterables; 18 : import com.google.gerrit.entities.RefNames; 19 : import com.google.gerrit.entities.SubmitRequirement; 20 : import com.google.gerrit.entities.SubmitRequirementExpression; 21 : import com.google.gerrit.entities.SubmitRequirementResult; 22 : import com.google.gerrit.extensions.common.SubmitRequirementInput; 23 : import com.google.gerrit.extensions.common.SubmitRequirementResultInfo; 24 : import com.google.gerrit.extensions.restapi.BadRequestException; 25 : import com.google.gerrit.extensions.restapi.IdString; 26 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 27 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 28 : import com.google.gerrit.extensions.restapi.Response; 29 : import com.google.gerrit.extensions.restapi.RestApiException; 30 : import com.google.gerrit.extensions.restapi.RestModifyView; 31 : import com.google.gerrit.extensions.restapi.TopLevelResource; 32 : import com.google.gerrit.server.change.ChangeResource; 33 : import com.google.gerrit.server.change.SubmitRequirementsJson; 34 : import com.google.gerrit.server.git.GitRepositoryManager; 35 : import com.google.gerrit.server.notedb.ChangeNotes; 36 : import com.google.gerrit.server.permissions.PermissionBackendException; 37 : import com.google.gerrit.server.project.ProjectConfig; 38 : import com.google.gerrit.server.project.SubmitRequirementsEvaluator; 39 : import com.google.gerrit.server.query.change.ChangeData; 40 : import com.google.inject.Inject; 41 : import java.io.IOException; 42 : import java.util.List; 43 : import java.util.Map.Entry; 44 : import java.util.Optional; 45 : import java.util.stream.Collectors; 46 : import org.eclipse.jgit.errors.ConfigInvalidException; 47 : import org.eclipse.jgit.lib.ObjectId; 48 : import org.eclipse.jgit.lib.Repository; 49 : import org.kohsuke.args4j.Option; 50 : 51 : /** 52 : * A rest view to evaluate (test) a {@link com.google.gerrit.entities.SubmitRequirement} on a given 53 : * change. The submit requirement can be supplied in one of two ways: 54 : * 55 : * <p>1) Using the {@link SubmitRequirementInput}. 56 : * 57 : * <p>2) From a change to the {@link RefNames#REFS_CONFIG} branch and the name of the 58 : * submit-requirement. 59 : */ 60 : public class CheckSubmitRequirement 61 : implements RestModifyView<ChangeResource, SubmitRequirementInput> { 62 : private final SubmitRequirementsEvaluator evaluator; 63 : 64 : @Option(name = "--sr-name") 65 : private String srName; 66 : 67 : @Option(name = "--refs-config-change-id") 68 : private String refsConfigChangeId; 69 : 70 : private final GitRepositoryManager repoManager; 71 : private final ProjectConfig.Factory projectConfigFactory; 72 : private final ChangeData.Factory changeDataFactory; 73 : private final ChangesCollection changesCollection; 74 : private final ChangeNotes.Factory changeNotesFactory; 75 : 76 : public void setSrName(String srName) { 77 1 : this.srName = srName; 78 1 : } 79 : 80 : public void setRefsConfigChangeId(String refsConfigChangeId) { 81 1 : this.refsConfigChangeId = refsConfigChangeId; 82 1 : } 83 : 84 : @Inject 85 : public CheckSubmitRequirement( 86 : SubmitRequirementsEvaluator evaluator, 87 : GitRepositoryManager repoManager, 88 : ProjectConfig.Factory projectConfigFactory, 89 : ChangeData.Factory changeDataFactory, 90 : ChangeNotes.Factory changeNotesFactory, 91 57 : ChangesCollection changesCollection) { 92 57 : this.evaluator = evaluator; 93 57 : this.repoManager = repoManager; 94 57 : this.projectConfigFactory = projectConfigFactory; 95 57 : this.changeDataFactory = changeDataFactory; 96 57 : this.changeNotesFactory = changeNotesFactory; 97 57 : this.changesCollection = changesCollection; 98 57 : } 99 : 100 : @Override 101 : public Response<SubmitRequirementResultInfo> apply( 102 : ChangeResource resource, SubmitRequirementInput input) 103 : throws IOException, PermissionBackendException, RestApiException { 104 1 : if ((srName == null || refsConfigChangeId == null) 105 : && !(srName == null && refsConfigChangeId == null)) { 106 1 : throw new BadRequestException( 107 : "Both 'sr-name' and 'refs-config-change-id' parameters must be set"); 108 : } 109 : SubmitRequirement requirement = 110 1 : srName != null && refsConfigChangeId != null 111 1 : ? createSubmitRequirementFromRequestParams() 112 1 : : createSubmitRequirement(input); 113 1 : SubmitRequirementResult res = 114 1 : evaluator.evaluateRequirement(requirement, resource.getChangeData()); 115 1 : return Response.ok(SubmitRequirementsJson.toInfo(requirement, res)); 116 : } 117 : 118 : private SubmitRequirement createSubmitRequirement(SubmitRequirementInput input) 119 : throws BadRequestException { 120 1 : validateSubmitRequirementInput(input); 121 1 : return SubmitRequirement.builder() 122 1 : .setName(input.name) 123 1 : .setDescription(Optional.ofNullable(input.description)) 124 1 : .setApplicabilityExpression(SubmitRequirementExpression.of(input.applicabilityExpression)) 125 1 : .setSubmittabilityExpression( 126 1 : SubmitRequirementExpression.create(input.submittabilityExpression)) 127 1 : .setOverrideExpression(SubmitRequirementExpression.of(input.overrideExpression)) 128 1 : .setAllowOverrideInChildProjects( 129 1 : input.allowOverrideInChildProjects == null ? true : input.allowOverrideInChildProjects) 130 1 : .build(); 131 : } 132 : 133 : /** 134 : * Loads the submit-requirement identified by the name {@link #srName} from the latest patch-set 135 : * of the change with ID {@link #refsConfigChangeId}. 136 : * 137 : * @return a {@link SubmitRequirement} entity. 138 : * @throws BadRequestException If {@link #refsConfigChangeId} is a non-existent change or not in 139 : * the {@link RefNames#REFS_CONFIG} branch, if the submit-requirement with name {@link 140 : * #srName} does not exist or if the server failed to load the project due to other 141 : * exceptions. 142 : */ 143 : private SubmitRequirement createSubmitRequirementFromRequestParams() 144 : throws IOException, PermissionBackendException, RestApiException { 145 : ChangeResource refsConfigChange; 146 : try { 147 1 : refsConfigChange = 148 1 : changesCollection.parse( 149 1 : TopLevelResource.INSTANCE, IdString.fromDecoded(refsConfigChangeId)); 150 1 : } catch (ResourceNotFoundException e) { 151 1 : throw new BadRequestException( 152 1 : String.format("Change '%s' does not exist", refsConfigChangeId), e); 153 1 : } 154 1 : ChangeNotes notes = changeNotesFactory.createCheckedUsingIndexLookup(refsConfigChange.getId()); 155 1 : ChangeData changeData = changeDataFactory.create(notes); 156 1 : try (Repository git = repoManager.openRepository(changeData.project())) { 157 1 : if (!changeData.change().getDest().branch().equals(RefNames.REFS_CONFIG)) { 158 1 : throw new BadRequestException( 159 1 : String.format("Change '%s' is not in refs/meta/config branch.", refsConfigChangeId)); 160 : } 161 1 : ObjectId revisionId = changeData.currentPatchSet().commitId(); 162 1 : ProjectConfig cfg = projectConfigFactory.create(changeData.project()); 163 : try { 164 1 : cfg.load(git, revisionId); 165 0 : } catch (ConfigInvalidException e) { 166 0 : throw new ResourceConflictException( 167 0 : String.format( 168 : "Failed to load project config for change '%s' from revision '%s'", 169 : refsConfigChangeId, revisionId), 170 : e); 171 1 : } 172 1 : List<Entry<String, SubmitRequirement>> submitRequirements = 173 1 : cfg.getSubmitRequirementSections().entrySet().stream() 174 1 : .filter(entry -> entry.getKey().equals(srName)) 175 1 : .collect(Collectors.toList()); 176 1 : if (submitRequirements.isEmpty()) { 177 1 : throw new BadRequestException( 178 1 : String.format("No submit requirement matching name '%s'", srName)); 179 : } 180 1 : return Iterables.getOnlyElement(submitRequirements).getValue(); 181 : } 182 : } 183 : 184 : private void validateSubmitRequirementInput(SubmitRequirementInput input) 185 : throws BadRequestException { 186 1 : if (input.name == null) { 187 0 : throw new BadRequestException("Field 'name' is missing from input."); 188 : } 189 1 : if (input.submittabilityExpression == null) { 190 0 : throw new BadRequestException("Field 'submittability_expression' is missing from input."); 191 : } 192 1 : } 193 : }