blob: 03225df9a8825e4ec0a53fe31033684ad67b37bc [file] [log] [blame]
// 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.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.StoredValues;
import com.google.gwtorm.server.OrmException;
import com.googlecode.prolog_cafe.lang.Prolog;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Check if a change needs owner approval. */
public class Checker {
private static final Logger log = LoggerFactory.getLogger(Checker.class);
// 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 Repository repository;
private ChangeData changeData;
private int minVoteLevel;
Checker(Repository repository, ChangeData changeData, int v) {
this.repository = repository;
this.changeData = changeData;
minVoteLevel = v;
}
/** Returns a map from reviewer email to vote value. */
Map<String, Integer> getVotes(AccountCache accountCache, ChangeData changeData)
throws OrmException {
Map<String, Integer> map = new HashMap<>();
for (PatchSetApproval p : changeData.currentApprovals()) {
if (p.getValue() != 0) {
map.put(
accountCache.get(p.getAccountId()).getAccount().getPreferredEmail(),
Integer.valueOf(p.getValue()));
}
}
// Give CL author a default minVoteLevel vote.
String author =
accountCache.get(changeData.change().getOwner()).getAccount().getPreferredEmail();
if (!map.containsKey(author) || map.get(author) == 0) {
map.put(author, 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;
for (String owner : owners) {
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
}
}
return foundApproval && !foundVeto;
}
/** Returns 1 if owner approval is found, -1 if missing, 0 if unneeded. */
int findApproval(AccountCache accountCache, OwnersDb db) throws OrmException, IOException {
Map<String, Set<String>> file2Owners = db.findOwners(changeData.currentFilePaths());
if (file2Owners.size() == 0) { // do not need owner approval
return 0;
}
Map<String, Integer> votes = getVotes(accountCache, 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);
ProjectState projectState = StoredValues.PROJECT_STATE.get(engine);
AccountCache accountCache = StoredValues.ACCOUNT_CACHE.get(engine);
Emails emails = StoredValues.EMAILS.get(engine);
Repository repository = StoredValues.REPOSITORY.get(engine);
return new Checker(repository, changeData, minVoteLevel)
.findApproval(projectState, accountCache, emails);
} catch (OrmException | IOException e) {
log.error("Exception for " + Config.getChangeId(changeData), e);
return 0; // owner approval may or may not be required.
}
}
/** Returns true if exempt from owner approval. */
static boolean isExemptFromOwnerApproval(ChangeData changeData) throws OrmException {
try {
String message = changeData.commitMessage();
if (message.contains(EXEMPT_MESSAGE1) || message.contains(EXEMPT_MESSAGE2)) {
return true;
}
} catch (IOException | OrmException e) {
log.error("Cannot get commit message for " + Config.getChangeId(changeData), e);
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);
}
int findApproval(ProjectState projectState, AccountCache accountCache, Emails emails)
throws OrmException, IOException {
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().get(projectState, accountCache, emails, repository, changeData);
if (db.getNumOwners() <= 0) {
return 0;
}
if (minVoteLevel <= 0) {
minVoteLevel = Config.getMinOwnerVoteLevel(changeData);
}
log.trace("findApproval db key = " + db.key);
return findApproval(accountCache, db);
}
}