LCOV - code coverage report
Current view: top level - server/approval - ApprovalCopier.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 119 128 93.0 %
Date: 2022-11-19 15:00:39 Functions: 12 12 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2014 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.approval;
      16             : 
      17             : import static com.google.common.collect.ImmutableList.toImmutableList;
      18             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      19             : 
      20             : import com.google.auto.value.AutoValue;
      21             : import com.google.common.annotations.VisibleForTesting;
      22             : import com.google.common.collect.HashBasedTable;
      23             : import com.google.common.collect.ImmutableList;
      24             : import com.google.common.collect.ImmutableSet;
      25             : import com.google.common.collect.Table;
      26             : import com.google.common.flogger.FluentLogger;
      27             : import com.google.gerrit.entities.Account;
      28             : import com.google.gerrit.entities.LabelType;
      29             : import com.google.gerrit.entities.LabelTypes;
      30             : import com.google.gerrit.entities.PatchSet;
      31             : import com.google.gerrit.entities.PatchSetApproval;
      32             : import com.google.gerrit.entities.Project;
      33             : import com.google.gerrit.exceptions.StorageException;
      34             : import com.google.gerrit.extensions.client.ChangeKind;
      35             : import com.google.gerrit.index.query.QueryParseException;
      36             : import com.google.gerrit.server.PatchSetUtil;
      37             : import com.google.gerrit.server.change.ChangeKindCache;
      38             : import com.google.gerrit.server.change.LabelNormalizer;
      39             : import com.google.gerrit.server.git.GitRepositoryManager;
      40             : import com.google.gerrit.server.logging.Metadata;
      41             : import com.google.gerrit.server.logging.TraceContext;
      42             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
      43             : import com.google.gerrit.server.notedb.ChangeNotes;
      44             : import com.google.gerrit.server.project.ProjectCache;
      45             : import com.google.gerrit.server.project.ProjectState;
      46             : import com.google.gerrit.server.query.approval.ApprovalContext;
      47             : import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
      48             : import com.google.gerrit.server.util.ManualRequestContext;
      49             : import com.google.gerrit.server.util.OneOffRequestContext;
      50             : import com.google.inject.Inject;
      51             : import com.google.inject.Singleton;
      52             : import java.io.IOException;
      53             : import java.util.Map;
      54             : import java.util.Optional;
      55             : import org.eclipse.jgit.lib.Config;
      56             : import org.eclipse.jgit.lib.Repository;
      57             : import org.eclipse.jgit.revwalk.RevWalk;
      58             : 
      59             : /**
      60             :  * Computes copied approvals for a given patch set.
      61             :  *
      62             :  * <p>Approvals are copied if:
      63             :  *
      64             :  * <ul>
      65             :  *   <li>the approval on the previous patch set matches the copy condition of its label
      66             :  *   <li>the approval is not overridden by a current approval on the patch set
      67             :  * </ul>
      68             :  *
      69             :  * <p>Callers should store the copied approvals in NoteDb when a new patch set is created.
      70             :  */
      71             : @Singleton
      72             : @VisibleForTesting
      73             : public class ApprovalCopier {
      74         146 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      75             : 
      76             :   @AutoValue
      77          51 :   public abstract static class Result {
      78             :     /**
      79             :      * Approvals that have been copied from the previous patch set.
      80             :      *
      81             :      * <p>An approval is copied if:
      82             :      *
      83             :      * <ul>
      84             :      *   <li>the approval on the previous patch set matches the copy condition of its label
      85             :      *   <li>the approval is not overridden by a current approval on the patch set
      86             :      * </ul>
      87             :      */
      88             :     public abstract ImmutableSet<PatchSetApproval> copiedApprovals();
      89             : 
      90             :     /**
      91             :      * Approvals on the previous patch set that have not been copied to the patch set.
      92             :      *
      93             :      * <p>These approvals didn't match the copy condition of their labels and hence haven't been
      94             :      * copied.
      95             :      *
      96             :      * <p>Only returns non-copied approvals of the previous patch set. Approvals from earlier patch
      97             :      * sets that were outdated before are not included.
      98             :      */
      99             :     public abstract ImmutableSet<PatchSetApproval> outdatedApprovals();
     100             : 
     101             :     static Result empty() {
     102           1 :       return create(
     103           1 :           /* copiedApprovals= */ ImmutableSet.of(), /* outdatedApprovals= */ ImmutableSet.of());
     104             :     }
     105             : 
     106             :     @VisibleForTesting
     107             :     public static Result create(
     108             :         ImmutableSet<PatchSetApproval> copiedApprovals,
     109             :         ImmutableSet<PatchSetApproval> outdatedApprovals) {
     110          51 :       return new AutoValue_ApprovalCopier_Result(copiedApprovals, outdatedApprovals);
     111             :     }
     112             :   }
     113             : 
     114             :   private final GitRepositoryManager repoManager;
     115             :   private final ProjectCache projectCache;
     116             :   private final ChangeKindCache changeKindCache;
     117             :   private final PatchSetUtil psUtil;
     118             :   private final LabelNormalizer labelNormalizer;
     119             :   private final ApprovalQueryBuilder approvalQueryBuilder;
     120             :   private final OneOffRequestContext requestContext;
     121             : 
     122             :   @Inject
     123             :   ApprovalCopier(
     124             :       GitRepositoryManager repoManager,
     125             :       ProjectCache projectCache,
     126             :       ChangeKindCache changeKindCache,
     127             :       PatchSetUtil psUtil,
     128             :       LabelNormalizer labelNormalizer,
     129             :       ApprovalQueryBuilder approvalQueryBuilder,
     130         146 :       OneOffRequestContext requestContext) {
     131         146 :     this.repoManager = repoManager;
     132         146 :     this.projectCache = projectCache;
     133         146 :     this.changeKindCache = changeKindCache;
     134         146 :     this.psUtil = psUtil;
     135         146 :     this.labelNormalizer = labelNormalizer;
     136         146 :     this.approvalQueryBuilder = approvalQueryBuilder;
     137         146 :     this.requestContext = requestContext;
     138         146 :   }
     139             : 
     140             :   /**
     141             :    * Returns all copied approvals that apply to the given patch set.
     142             :    *
     143             :    * <p>Approvals are copied if:
     144             :    *
     145             :    * <ul>
     146             :    *   <li>the approval on the previous patch set matches the copy condition of its label
     147             :    *   <li>the approval is not overridden by a current approval on the patch set
     148             :    * </ul>
     149             :    */
     150             :   @VisibleForTesting
     151             :   public Result forPatchSet(ChangeNotes notes, PatchSet ps, RevWalk rw, Config repoConfig) {
     152             :     ProjectState project;
     153          51 :     try (TraceTimer traceTimer =
     154          51 :         TraceContext.newTimer(
     155             :             "Computing labels for patch set",
     156          51 :             Metadata.builder()
     157          51 :                 .changeId(notes.load().getChangeId().get())
     158          51 :                 .patchSetId(ps.id().get())
     159          51 :                 .build())) {
     160          51 :       project =
     161             :           projectCache
     162          51 :               .get(notes.getProjectName())
     163          51 :               .orElseThrow(illegalState(notes.getProjectName()));
     164          51 :       return computeForPatchSet(project.getLabelTypes(), notes, ps, rw, repoConfig);
     165             :     }
     166             :   }
     167             : 
     168             :   /**
     169             :    * Returns all follow-up patch sets of the given patch set to which the given approval is
     170             :    * copyable.
     171             :    *
     172             :    * <p>An approval is considered as copyable to a follow-up patch set if it matches the copy rules
     173             :    * of the label and it is copyable to all intermediate follow-up patch sets as well.
     174             :    *
     175             :    * <p>The returned follow-up patch sets are returned in the order of their patch set IDs.
     176             :    *
     177             :    * <p>Note: This method only checks the copy rules to detect if the approval is copyable. There
     178             :    * are other factors, not checked here, that can prevent the copying of the approval to the
     179             :    * returned follow-up patch sets (e.g. if they already have a matching non-copy approval that
     180             :    * prevents the copying).
     181             :    *
     182             :    * @param changeNotes the change notes
     183             :    * @param sourcePatchSet the patch set on which the approval was applied
     184             :    * @param approverId the account ID of the user that applied the approval
     185             :    * @param label the label of the approval that was applied
     186             :    * @param approvalValue the value of the approval that was applied
     187             :    * @return the follow-up patch sets to which the approval is copyable, ordered by patch set ID
     188             :    */
     189             :   public ImmutableList<PatchSet.Id> forApproval(
     190             :       ChangeNotes changeNotes,
     191             :       PatchSet sourcePatchSet,
     192             :       Account.Id approverId,
     193             :       String label,
     194             :       short approvalValue)
     195             :       throws IOException {
     196           2 :     ImmutableList.Builder<PatchSet.Id> targetPatchSetsBuilder = ImmutableList.builder();
     197             : 
     198           2 :     Optional<LabelType> labelType =
     199             :         projectCache
     200           2 :             .get(changeNotes.getProjectName())
     201           2 :             .orElseThrow(illegalState(changeNotes.getProjectName()))
     202           2 :             .getLabelTypes()
     203           2 :             .byLabel(label);
     204           2 :     if (!labelType.isPresent()) {
     205             :       // no label type exists for this label, hence this approval cannot be copied
     206           0 :       return ImmutableList.of();
     207             :     }
     208             : 
     209           2 :     try (Repository repo = repoManager.openRepository(changeNotes.getProjectName());
     210           2 :         RevWalk revWalk = new RevWalk(repo)) {
     211           2 :       ImmutableList<PatchSet.Id> followUpPatchSets =
     212           2 :           changeNotes.getPatchSets().keySet().stream()
     213           2 :               .filter(psId -> psId.get() > sourcePatchSet.id().get())
     214           2 :               .collect(toImmutableList());
     215           2 :       PatchSet priorPatchSet = sourcePatchSet;
     216             : 
     217             :       // Iterate over the follow-up patch sets in order to copy the approval from their prior patch
     218             :       // set if possible (copy from PS N-1 to PS N).
     219           2 :       for (PatchSet.Id followUpPatchSetId : followUpPatchSets) {
     220           2 :         PatchSet followUpPatchSet = psUtil.get(changeNotes, followUpPatchSetId);
     221           2 :         ChangeKind changeKind =
     222           2 :             changeKindCache.getChangeKind(
     223           2 :                 changeNotes.getProjectName(),
     224             :                 revWalk,
     225           2 :                 repo.getConfig(),
     226           2 :                 priorPatchSet.commitId(),
     227           2 :                 followUpPatchSet.commitId());
     228           2 :         boolean isMerge = isMerge(changeNotes.getProjectName(), revWalk, followUpPatchSet);
     229             : 
     230           2 :         if (canCopy(
     231             :             changeNotes,
     232           2 :             priorPatchSet.id(),
     233             :             followUpPatchSet,
     234             :             approverId,
     235           2 :             labelType.get(),
     236             :             approvalValue,
     237             :             changeKind,
     238             :             isMerge,
     239             :             revWalk,
     240           2 :             repo.getConfig())) {
     241           1 :           targetPatchSetsBuilder.add(followUpPatchSetId);
     242             :         } else {
     243             :           // The approval is not copyable to this follow-up patch set.
     244             :           // This means it's also not copyable to any further follow-up patch set and we should stop
     245             :           // the loop here.
     246             :           break;
     247             :         }
     248           1 :         priorPatchSet = followUpPatchSet;
     249           1 :       }
     250             :     }
     251           2 :     return targetPatchSetsBuilder.build();
     252             :   }
     253             : 
     254             :   private boolean canCopy(
     255             :       ChangeNotes changeNotes,
     256             :       PatchSet.Id sourcePatchSetId,
     257             :       PatchSet targetPatchSet,
     258             :       Account.Id approverId,
     259             :       LabelType labelType,
     260             :       short approvalValue,
     261             :       ChangeKind changeKind,
     262             :       boolean isMerge,
     263             :       RevWalk revWalk,
     264             :       Config repoConfig) {
     265          15 :     if (!labelType.getCopyCondition().isPresent()) {
     266           4 :       return false;
     267             :     }
     268          15 :     ApprovalContext ctx =
     269          15 :         ApprovalContext.create(
     270             :             changeNotes,
     271             :             sourcePatchSetId,
     272             :             approverId,
     273             :             labelType,
     274             :             approvalValue,
     275             :             targetPatchSet,
     276             :             changeKind,
     277             :             isMerge,
     278             :             revWalk,
     279             :             repoConfig);
     280             :     try {
     281             :       // Use a request context to run checks as an internal user with expanded visibility. This is
     282             :       // so that the output of the copy condition does not depend on who is running the current
     283             :       // request (e.g. a group used in this query might not be visible to the person sending this
     284             :       // request).
     285          15 :       try (ManualRequestContext ignored = requestContext.open()) {
     286          15 :         return approvalQueryBuilder
     287          15 :             .parse(labelType.getCopyCondition().get())
     288          15 :             .asMatchable()
     289          15 :             .match(ctx);
     290             :       }
     291           0 :     } catch (QueryParseException e) {
     292           0 :       logger.atWarning().withCause(e).log(
     293             :           "Unable to copy label because config is invalid. This should have been caught before.");
     294           0 :       return false;
     295             :     }
     296             :   }
     297             : 
     298             :   private Result computeForPatchSet(
     299             :       LabelTypes labelTypes,
     300             :       ChangeNotes notes,
     301             :       PatchSet targetPatchSet,
     302             :       RevWalk rw,
     303             :       Config repoConfig) {
     304          51 :     Project.NameKey projectName = notes.getProjectName();
     305          51 :     PatchSet.Id targetPsId = targetPatchSet.id();
     306             : 
     307             :     // Bail out immediately if this is the first patch set. Return only approvals granted on the
     308             :     // given patch set.
     309          51 :     if (targetPsId.get() == 1) {
     310           1 :       return Result.empty();
     311             :     }
     312          51 :     Map.Entry<PatchSet.Id, PatchSet> priorPatchSet =
     313          51 :         notes.load().getPatchSets().lowerEntry(targetPsId);
     314          51 :     if (priorPatchSet == null) {
     315           0 :       return Result.empty();
     316             :     }
     317             : 
     318          51 :     Table<String, Account.Id, PatchSetApproval> currentApprovalsByUser = HashBasedTable.create();
     319          51 :     ImmutableList<PatchSetApproval> nonCopiedApprovalsForGivenPatchSet =
     320          51 :         notes.load().getApprovals().onlyNonCopied().get(targetPatchSet.id());
     321          51 :     nonCopiedApprovalsForGivenPatchSet.forEach(
     322           1 :         psa -> currentApprovalsByUser.put(psa.label(), psa.accountId(), psa));
     323             : 
     324          51 :     Table<String, Account.Id, PatchSetApproval> copiedApprovalsByUser = HashBasedTable.create();
     325          51 :     ImmutableSet.Builder<PatchSetApproval> outdatedApprovalsBuilder = ImmutableSet.builder();
     326             : 
     327          51 :     ImmutableList<PatchSetApproval> priorApprovals =
     328          51 :         notes.load().getApprovals().all().get(priorPatchSet.getKey());
     329             : 
     330             :     // Add labels from the previous patch set to the result in case the label isn't already there
     331             :     // and settings as well as change kind allow copying.
     332          51 :     ChangeKind changeKind =
     333          51 :         changeKindCache.getChangeKind(
     334             :             projectName,
     335             :             rw,
     336             :             repoConfig,
     337          51 :             priorPatchSet.getValue().commitId(),
     338          51 :             targetPatchSet.commitId());
     339          51 :     boolean isMerge = isMerge(projectName, rw, targetPatchSet);
     340          51 :     logger.atFine().log(
     341             :         "change kind for patch set %d of change %d against prior patch set %s is %s",
     342          51 :         targetPatchSet.id().get(),
     343          51 :         targetPatchSet.id().changeId().get(),
     344          51 :         priorPatchSet.getValue().id().changeId(),
     345             :         changeKind);
     346             : 
     347          51 :     for (PatchSetApproval priorPsa : priorApprovals) {
     348          15 :       if (priorPsa.value() == 0) {
     349             :         // approvals with a zero vote record the deletion of a vote,
     350             :         // they should neither be copied nor be reported as outdated, hence just skip them
     351           2 :         continue;
     352             :       }
     353             : 
     354          15 :       Optional<LabelType> labelType = labelTypes.byLabel(priorPsa.labelId());
     355          15 :       if (!labelType.isPresent()) {
     356           2 :         logger.atFine().log(
     357             :             "approval %d on label %s of patch set %d of change %d cannot be copied"
     358             :                 + " to patch set %d because the label no longer exists on project %s",
     359           2 :             priorPsa.value(),
     360           2 :             priorPsa.label(),
     361           2 :             priorPsa.key().patchSetId().get(),
     362           2 :             priorPsa.key().patchSetId().changeId().get(),
     363           2 :             targetPsId.get(),
     364             :             projectName);
     365           2 :         outdatedApprovalsBuilder.add(priorPsa);
     366           2 :         continue;
     367             :       }
     368          14 :       if (canCopy(
     369             :           notes,
     370          14 :           priorPsa.patchSetId(),
     371             :           targetPatchSet,
     372          14 :           priorPsa.accountId(),
     373          14 :           labelType.get(),
     374          14 :           priorPsa.value(),
     375             :           changeKind,
     376             :           isMerge,
     377             :           rw,
     378             :           repoConfig)) {
     379          12 :         if (!currentApprovalsByUser.contains(priorPsa.label(), priorPsa.accountId())) {
     380          12 :           copiedApprovalsByUser.put(
     381          12 :               priorPsa.label(),
     382          12 :               priorPsa.accountId(),
     383          12 :               priorPsa.copyWithPatchSet(targetPatchSet.id()));
     384             :         }
     385             :       } else {
     386          10 :         outdatedApprovalsBuilder.add(priorPsa);
     387          10 :         continue;
     388             :       }
     389          12 :     }
     390             : 
     391          51 :     ImmutableSet<PatchSetApproval> copiedApprovals =
     392          51 :         labelNormalizer.normalize(notes, copiedApprovalsByUser.values()).getNormalized();
     393          51 :     return Result.create(copiedApprovals, outdatedApprovalsBuilder.build());
     394             :   }
     395             : 
     396             :   private boolean isMerge(Project.NameKey project, RevWalk rw, PatchSet patchSet) {
     397             :     try {
     398          51 :       return rw.parseCommit(patchSet.commitId()).getParentCount() > 1;
     399           0 :     } catch (IOException e) {
     400           0 :       throw new StorageException(
     401           0 :           String.format(
     402             :               "failed to check if patch set %d of change %s in project %s is a merge commit",
     403           0 :               patchSet.id().get(), patchSet.id().changeId(), project),
     404             :           e);
     405             :     }
     406             :   }
     407             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750