All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.approval.ApprovalsUtil Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// Copyright (C) 2009 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.approval;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static com.google.gerrit.server.project.ProjectCache.illegalState;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.PatchSetInfo;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.util.LabelVote;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.revwalk.RevWalk;

/**
 * Utility functions to manipulate patchset approvals.
 *
 * 

Approvals are overloaded, they represent both approvals and reviewers which should be CCed on * a change. To ensure that reviewers are not lost there must always be an approval on each patchset * for each reviewer, even if the reviewer hasn't actually given a score to the change. To mark the * "no score" case, a dummy approval, which may live in any of the available categories, with a * score of 0 is used. */ @Singleton public class ApprovalsUtil { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); public static PatchSetApproval.Builder newApproval( PatchSet.Id psId, CurrentUser user, LabelId labelId, int value, Instant when) { PatchSetApproval.Builder b = PatchSetApproval.builder() .key(PatchSetApproval.key(psId, user.getAccountId(), labelId)) .value(value) .granted(when); user.updateRealAccountId(b::realAccountId); return b; } private static Iterable filterApprovals( Iterable psas, Account.Id accountId) { return Iterables.filter(psas, a -> Objects.equals(a.accountId(), accountId)); } private final ApprovalCopier approvalInference; private final PermissionBackend permissionBackend; private final ProjectCache projectCache; private final LabelNormalizer labelNormalizer; @VisibleForTesting @Inject public ApprovalsUtil( ApprovalCopier approvalInference, PermissionBackend permissionBackend, ProjectCache projectCache, LabelNormalizer labelNormalizer) { this.approvalInference = approvalInference; this.permissionBackend = permissionBackend; this.projectCache = projectCache; this.labelNormalizer = labelNormalizer; } /** * Get all reviewers for a change. * * @param notes change notes. * @return reviewers for the change. */ public ReviewerSet getReviewers(ChangeNotes notes) { return notes.load().getReviewers(); } /** * Get updates to reviewer set. * * @param notes change notes. * @return reviewer updates for the change. */ public List getReviewerUpdates(ChangeNotes notes) { return notes.load().getReviewerUpdates(); } public List addReviewers( ChangeUpdate update, LabelTypes labelTypes, Change change, PatchSet ps, PatchSetInfo info, Iterable wantReviewers, Collection existingReviewers) { return addReviewers( update, labelTypes, change, ps.id(), info.getAuthor().getAccount(), info.getCommitter().getAccount(), wantReviewers, existingReviewers); } public List addReviewers( ChangeNotes notes, ChangeUpdate update, LabelTypes labelTypes, Change change, Iterable wantReviewers) { PatchSet.Id psId = change.currentPatchSetId(); Collection existingReviewers; existingReviewers = notes.load().getReviewers().byState(REVIEWER); // Existing reviewers should include pending additions in the REVIEWER // state, taken from ChangeUpdate. existingReviewers = Lists.newArrayList(existingReviewers); for (Map.Entry entry : update.getReviewers().entrySet()) { if (entry.getValue() == REVIEWER) { existingReviewers.add(entry.getKey()); } } return addReviewers( update, labelTypes, change, psId, null, null, wantReviewers, existingReviewers); } private List addReviewers( ChangeUpdate update, LabelTypes labelTypes, Change change, PatchSet.Id psId, Account.Id authorId, Account.Id committerId, Iterable wantReviewers, Collection existingReviewers) { List allTypes = labelTypes.getLabelTypes(); if (allTypes.isEmpty()) { return ImmutableList.of(); } Set need = Sets.newLinkedHashSet(wantReviewers); if (authorId != null && canSee(update.getNotes(), authorId)) { need.add(authorId); } if (committerId != null && canSee(update.getNotes(), committerId)) { need.add(committerId); } need.remove(change.getOwner()); need.removeAll(existingReviewers); if (need.isEmpty()) { return ImmutableList.of(); } List cells = Lists.newArrayListWithCapacity(need.size()); LabelId labelId = Iterables.getLast(allTypes).getLabelId(); for (Account.Id account : need) { cells.add( PatchSetApproval.builder() .key(PatchSetApproval.key(psId, account, labelId)) .value(0) .granted(update.getWhen()) .build()); update.putReviewer(account, REVIEWER); } return Collections.unmodifiableList(cells); } private boolean canSee(ChangeNotes notes, Account.Id accountId) { try { if (!projectCache .get(notes.getProjectName()) .orElseThrow(illegalState(notes.getProjectName())) .statePermitsRead()) { return false; } return permissionBackend.absentUser(accountId).change(notes).test(ChangePermission.READ); } catch (PermissionBackendException e) { logger.atWarning().withCause(e).log( "Failed to check if account %d can see change %d", accountId.get(), notes.getChangeId().get()); return false; } } /** * Adds accounts to a change as reviewers in the CC state. * * @param notes change notes. * @param update change update. * @param wantCCs accounts to CC. * @param keepExistingReviewers whether provided accounts that are already reviewer should be kept * as reviewer or be downgraded to CC * @return whether a change was made. */ public Collection addCcs( ChangeNotes notes, ChangeUpdate update, Collection wantCCs, boolean keepExistingReviewers) { return addCcs(update, wantCCs, notes.load().getReviewers(), keepExistingReviewers); } private Collection addCcs( ChangeUpdate update, Collection wantCCs, ReviewerSet existingReviewers, boolean keepExistingReviewers) { Set need = new LinkedHashSet<>(wantCCs); need.removeAll(existingReviewers.byState(CC)); if (keepExistingReviewers) { need.removeAll(existingReviewers.byState(REVIEWER)); } need.removeAll(update.getReviewers().keySet()); for (Account.Id account : need) { update.putReviewer(account, CC); } return need; } /** * Adds approvals to ChangeUpdate for a new patch set, and writes to NoteDb. * * @param update change update. * @param labelTypes label types for the containing project. * @param ps patch set being approved. * @param user user adding approvals. * @param approvals approvals to add. */ public Iterable addApprovalsForNewPatchSet( ChangeUpdate update, LabelTypes labelTypes, PatchSet ps, CurrentUser user, Map approvals) throws RestApiException, PermissionBackendException { Account.Id accountId = user.getAccountId(); checkArgument( accountId.equals(ps.uploader()), "expected user %s to match patch set uploader %s", accountId, ps.uploader()); if (approvals.isEmpty()) { return ImmutableList.of(); } checkApprovals(approvals, permissionBackend.user(user).change(update.getNotes())); List cells = new ArrayList<>(approvals.size()); Instant ts = update.getWhen(); for (Map.Entry vote : approvals.entrySet()) { Optional lt = labelTypes.byLabel(vote.getKey()); if (!lt.isPresent()) { throw new BadRequestException( String.format("label \"%s\" is not a configured label", vote.getKey())); } cells.add(newApproval(ps.id(), user, lt.get().getLabelId(), vote.getValue(), ts).build()); } for (PatchSetApproval psa : cells) { update.putApproval(psa.label(), psa.value()); } return cells; } public static void checkLabel(LabelTypes labelTypes, String name, Short value) throws BadRequestException { Optional label = labelTypes.byLabel(name); if (!label.isPresent()) { throw new BadRequestException(String.format("label \"%s\" is not a configured label", name)); } if (label.get().getValue(value) == null) { throw new BadRequestException( String.format("label \"%s\": %d is not a valid value", name, value)); } } private static void checkApprovals( Map approvals, PermissionBackend.ForChange forChange) throws AuthException, PermissionBackendException { for (Map.Entry vote : approvals.entrySet()) { String name = vote.getKey(); Short value = vote.getValue(); if (!forChange.test(new LabelPermission.WithValue(name, value))) { throw new AuthException( String.format("applying label \"%s\": %d is restricted", name, value)); } } } public ListMultimap byChangeExcludingCopiedApprovals( ChangeNotes notes) { return notes.load().getApprovals(); } public ListMultimap byChangeWithCopied(ChangeNotes notes) { return notes.load().getApprovalsWithCopied(); } /** * This method should only be used when we want to dynamically compute the approvals. Generally, * the copied approvals are available in {@link ChangeNotes}. However, if the patch-set is just * being created, we need to dynamically compute the approvals so that we can persist them in * storage. The {@link RevWalk} and {@link Config} objects that are being used to create the new * patch-set are required for this method. Here we also add those votes to the provided {@link * ChangeUpdate} object. */ public void persistCopiedApprovals( ChangeNotes notes, PatchSet patchSet, RevWalk revWalk, Config repoConfig, ChangeUpdate changeUpdate) { approvalInference .forPatchSet(notes, patchSet, revWalk, repoConfig) .forEach(a -> changeUpdate.putCopiedApproval(a)); } /** * Gets {@link PatchSetApproval}s for a specified patch-set. The result includes copied votes but * does not include deleted labels. * * @param notes changenotes of the change. * @param psId patch-set id for the change and patch-set we want to get approvals. * @return all approvals for the specified patch-set, including copied votes, not including * deleted labels. */ public Iterable byPatchSet(ChangeNotes notes, PatchSet.Id psId) { List approvalsNotNormalized = notes.load().getApprovalsWithCopied().get(psId); return labelNormalizer.normalize(notes, approvalsNotNormalized).getNormalized(); } public Iterable byPatchSetUser( ChangeNotes notes, PatchSet.Id psId, Account.Id accountId) { return filterApprovals(byPatchSet(notes, psId), accountId); } public PatchSetApproval getSubmitter(ChangeNotes notes, PatchSet.Id c) { if (c == null) { return null; } try { // Submit approval is never copied. return getSubmitter(c, byChangeExcludingCopiedApprovals(notes).get(c)); } catch (StorageException e) { return null; } } public static PatchSetApproval getSubmitter(PatchSet.Id c, Iterable approvals) { if (c == null) { return null; } PatchSetApproval submitter = null; for (PatchSetApproval a : approvals) { if (a.patchSetId().equals(c) && a.value() > 0 && a.isLegacySubmit()) { if (submitter == null || a.granted().compareTo(submitter.granted()) > 0) { submitter = a; } } } return submitter; } public static String renderMessageWithApprovals( int patchSetId, Map n, Map c) { StringBuilder msgs = new StringBuilder("Uploaded patch set " + patchSetId); if (!n.isEmpty()) { boolean first = true; for (Map.Entry e : n.entrySet()) { if (c.containsKey(e.getKey()) && c.get(e.getKey()).value() == e.getValue()) { continue; } if (first) { msgs.append(":"); first = false; } msgs.append(" ").append(LabelVote.create(e.getKey(), e.getValue()).format()); } } return msgs.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy