| // 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.googlesource.gerrit.plugins.automerger; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Joiner; |
| import com.google.gerrit.common.data.ParameterizedString; |
| import com.google.gerrit.extensions.api.GerritApi; |
| import com.google.gerrit.extensions.client.ListChangesOption; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.common.CommitInfo; |
| import com.google.gerrit.extensions.common.RevisionInfo; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.reviewdb.client.Branch.NameKey; |
| import com.google.gerrit.reviewdb.client.PatchSet.Id; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.git.CodeReviewCommit; |
| import com.google.gerrit.server.git.validators.MergeValidationException; |
| import com.google.gerrit.server.git.validators.MergeValidationListener; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.inject.Inject; |
| import java.io.IOException; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.Repository; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * MergeValidator will validate that all downstream changes are uploaded for review before |
| * submission. |
| */ |
| public class MergeValidator implements MergeValidationListener { |
| private static final Logger log = LoggerFactory.getLogger(MergeValidator.class); |
| |
| protected GerritApi gApi; |
| protected ConfigLoader config; |
| |
| @Inject |
| public MergeValidator(GerritApi gApi, ConfigLoader config) { |
| this.gApi = gApi; |
| this.config = config; |
| } |
| |
| @Override |
| public void onPreMerge( |
| Repository repo, |
| CodeReviewCommit commit, |
| ProjectState destProject, |
| NameKey destBranch, |
| Id patchSetId, |
| IdentifiedUser caller) |
| throws MergeValidationException { |
| int changeId = commit.change().getChangeId(); |
| try { |
| ChangeInfo upstreamChange = |
| gApi.changes().id(changeId).get(EnumSet.of(ListChangesOption.CURRENT_REVISION)); |
| Set<String> missingDownstreams = getMissingDownstreamMerges(upstreamChange); |
| if (!missingDownstreams.isEmpty()) { |
| throw new MergeValidationException(getMissingDownstreamsMessage(missingDownstreams)); |
| } |
| } catch (RestApiException |
| | IOException |
| | ConfigInvalidException |
| | InvalidQueryParameterException e) { |
| log.error("Automerger plugin failed onPreMerge for {}", changeId, e); |
| e.printStackTrace(); |
| throw new MergeValidationException("Error when validating merge for: " + changeId); |
| } |
| } |
| |
| private String getMissingDownstreamsMessage(Set<String> missingDownstreams) |
| throws ConfigInvalidException { |
| String missingDownstreamsMessage = config.getMissingDownstreamsMessage(); |
| ParameterizedString pattern = new ParameterizedString(missingDownstreamsMessage); |
| return pattern.replace(getSubstitutionMap(missingDownstreams)); |
| } |
| |
| private Map<String, String> getSubstitutionMap(Set<String> missingDownstreams) { |
| Map<String, String> substitutionMap = new HashMap<>(); |
| substitutionMap.put("missingDownstreams", Joiner.on(", ").join(missingDownstreams)); |
| return substitutionMap; |
| } |
| |
| @VisibleForTesting |
| protected Set<String> getMissingDownstreamMerges(ChangeInfo upstreamChange) |
| throws RestApiException, IOException, ConfigInvalidException, InvalidQueryParameterException { |
| Set<String> missingDownstreamBranches = new HashSet<>(); |
| |
| Set<String> downstreamBranches = |
| config.getDownstreamBranches(upstreamChange.branch, upstreamChange.project); |
| for (String downstreamBranch : downstreamBranches) { |
| boolean dsExists = false; |
| QueryBuilder queryBuilder = new QueryBuilder(); |
| if (upstreamChange.topic == null || upstreamChange.topic.equals("")) { |
| // If topic is null or empty, we immediately know that downstream is missing. |
| missingDownstreamBranches.add(downstreamBranch); |
| continue; |
| } |
| queryBuilder.addParameter("topic", upstreamChange.topic); |
| queryBuilder.addParameter("branch", downstreamBranch); |
| queryBuilder.addParameter("status", "open"); |
| List<ChangeInfo> changes = |
| gApi.changes() |
| .query(queryBuilder.get()) |
| .withOptions(ListChangesOption.ALL_REVISIONS, ListChangesOption.CURRENT_COMMIT) |
| .get(); |
| for (ChangeInfo change : changes) { |
| RevisionInfo revision = change.revisions.get(change.currentRevision); |
| List<CommitInfo> parents = revision.commit.parents; |
| if (parents.size() > 1) { |
| String secondParent = parents.get(1).commit; |
| if (secondParent.equals(upstreamChange.currentRevision)) { |
| dsExists = true; |
| break; |
| } |
| } |
| } |
| if (!dsExists) { |
| missingDownstreamBranches.add(downstreamBranch); |
| } |
| } |
| return missingDownstreamBranches; |
| } |
| } |