| // 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.findowners; |
| |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.entities.Change.Status; |
| import com.google.gerrit.entities.PatchSetApproval; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.server.account.AccountCache; |
| import com.google.gerrit.server.account.Emails; |
| import com.google.gerrit.server.config.PluginConfigFactory; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.patch.PatchListCache; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.rules.PrologEnvironment; |
| import com.google.gerrit.server.rules.StoredValues; |
| import com.googlecode.prolog_cafe.lang.Prolog; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| |
| /** Check if a change needs owner approval. */ |
| public class Checker { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| // Accept both "Exempt-" and "Exempted-". |
| private static final String EXEMPT_MESSAGE1 = "Exempt-From-Owner-Approval:"; |
| private static final String EXEMPT_MESSAGE2 = "Exempted-From-Owner-Approval:"; |
| |
| private final AccountCache accountCache; |
| private final GitRepositoryManager repoManager; |
| private final Emails emails; |
| private final Config config; |
| private final ProjectState projectState; // could be null when used by FindOwnersIT |
| private final ChangeData changeData; |
| private int minVoteLevel; |
| |
| Checker( |
| AccountCache accountCache, |
| PatchListCache patchListCache, |
| GitRepositoryManager repoManager, |
| Emails emails, |
| PluginConfigFactory configFactory, |
| ProjectState projectState, |
| ChangeData changeData, |
| int v) { |
| this.accountCache = accountCache; |
| this.repoManager = repoManager; |
| this.emails = emails; |
| this.projectState = projectState; |
| this.changeData = changeData; |
| this.config = new Config(configFactory, null, accountCache, patchListCache, emails); |
| minVoteLevel = v; |
| } |
| |
| /** Returns a map from reviewer email to vote value. */ |
| Map<String, Integer> getVotes(ChangeData changeData) { |
| Map<String, Integer> map = new HashMap<>(); |
| for (PatchSetApproval p : changeData.currentApprovals()) { |
| // Only collect non-zero Code-Review votes. |
| if (p.value() != 0 && p.label().equals("Code-Review")) { |
| // Reviewers may have no preferred email, skip them if the preferred email is not set. |
| Optional<String> preferredEmail = |
| accountCache.get(p.accountId()).map(a -> a.account().preferredEmail()); |
| if (preferredEmail.isPresent()) { |
| map.put(preferredEmail.get(), Integer.valueOf(p.value())); |
| } |
| } |
| } |
| // Give CL author a default minVoteLevel vote. |
| // The preferred email of the author may not be set. Pushing changes only requires an email in |
| // the external IDs, but the preferred email may still be null (also emails may have been |
| // deleted after creating the change). Skip the author if it doesn't have a preferred email. |
| Optional<String> author = |
| accountCache.get(changeData.change().getOwner()).map(a -> a.account().preferredEmail()); |
| if (author.isPresent() && (!map.containsKey(author.get()) || map.get(author.get()) == 0)) { |
| map.put(author.get(), minVoteLevel); |
| } |
| return map; |
| } |
| |
| /** Returns true if some owner in owners is "*" or in votes */ |
| boolean findOwnersInVotes(Set<String> owners, Map<String, Integer> votes) { |
| boolean foundVeto = false; |
| boolean foundApproval = false; |
| boolean foundNull = false; |
| for (String owner : owners) { |
| if (owner == null) { |
| foundNull = true; // Something is wrong in OwnersDb! |
| } else if (votes.containsKey(owner)) { |
| int v = votes.get(owner); |
| foundApproval |= (v >= minVoteLevel); |
| foundVeto |= (v < 0); // an owner's -1 vote is a veto |
| } else if (owner.equals("*")) { |
| foundApproval = true; // no specific owner |
| } |
| } |
| if (foundNull) { |
| logger.atSevere().log("Unexpected null owner email for %s", Config.getChangeId(changeData)); |
| } |
| return foundApproval && !foundVeto; |
| } |
| |
| /** Returns 1 if owner approval is found, -1 if missing, 0 if unneeded. */ |
| int findApproval(OwnersDb db) { |
| Map<String, Set<String>> file2Owners = db.findOwners(changeData.currentFilePaths()); |
| if (file2Owners.isEmpty()) { // do not need owner approval |
| return 0; |
| } |
| Map<String, Integer> votes = getVotes(changeData); |
| for (Set<String> owners : file2Owners.values()) { |
| if (!findOwnersInVotes(owners, votes)) { |
| return -1; |
| } |
| } |
| return 1; |
| } |
| |
| /** Returns 1 if owner approval is found, -1 if missing, 0 if unneeded. */ |
| public static int findApproval(Prolog engine, int minVoteLevel) { |
| ChangeData changeData = null; |
| try { |
| changeData = StoredValues.CHANGE_DATA.get(engine); |
| PrologEnvironment env = (PrologEnvironment) engine.control; |
| Checker checker = |
| new Checker( |
| StoredValues.ACCOUNT_CACHE.get(engine), |
| env.getArgs().getPatchListCache(), |
| StoredValues.REPO_MANAGER.get(engine), |
| StoredValues.EMAILS.get(engine), |
| StoredValues.PLUGIN_CONFIG_FACTORY.get(engine), |
| StoredValues.PROJECT_STATE.get(engine), |
| changeData, |
| minVoteLevel); |
| return checker.findApproval(); |
| } catch (StorageException e) { |
| logger.atSevere().withCause(e).log("Exception for %s ", Config.getChangeId(changeData)); |
| return 0; // owner approval may or may not be required. |
| } |
| } |
| |
| /** Returns 1 if owner approval is found, -1 if missing, 0 if unneeded. */ |
| int findApproval() { |
| if (isExemptFromOwnerApproval(changeData)) { |
| return 0; |
| } |
| // One update to a Gerrit change can call submit_rule or submit_filter |
| // many times. So this function should use cached values. |
| OwnersDb db = |
| Cache.getInstance(config, repoManager) |
| .get( |
| true, |
| null, /* allow submit checker to read all OWNERS files */ |
| projectState, |
| accountCache, |
| emails, |
| repoManager, |
| changeData); |
| if (db.getNumOwners() <= 0) { |
| return 0; |
| } |
| if (minVoteLevel <= 0) { |
| minVoteLevel = config.getMinOwnerVoteLevel(projectState, changeData); |
| } |
| logger.atFiner().log("findApproval db key = %s", db.key); |
| return findApproval(db); |
| } |
| |
| /** Returns true if exempt from owner approval. */ |
| static boolean isExemptFromOwnerApproval(ChangeData changeData) { |
| try { |
| String message = changeData.commitMessage(); |
| if (message.contains(EXEMPT_MESSAGE1) || message.contains(EXEMPT_MESSAGE2)) { |
| return true; |
| } |
| } catch (StorageException e) { |
| logger.atSevere().withCause(e).log( |
| "Cannot get commit message for %s", Config.getChangeId(changeData)); |
| return true; // exempt from owner approval due to lack of data |
| } |
| // Abandoned and merged changes do not need approval again. |
| Status status = changeData.change().getStatus(); |
| return (status == Status.ABANDONED || status == Status.MERGED); |
| } |
| } |