LCOV - code coverage report
Current view: top level - server/restapi/change - PostReviewCopyApprovalsOp.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 70 72 97.2 %
Date: 2022-11-19 15:00:39 Functions: 14 14 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2022 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.restapi.change;
      16             : 
      17             : import static com.google.common.collect.ImmutableList.toImmutableList;
      18             : 
      19             : import com.google.auto.factory.AutoFactory;
      20             : import com.google.auto.factory.Provided;
      21             : import com.google.common.collect.ImmutableList;
      22             : import com.google.common.collect.ImmutableTable;
      23             : import com.google.common.collect.Table.Cell;
      24             : import com.google.gerrit.entities.Account;
      25             : import com.google.gerrit.entities.LabelId;
      26             : import com.google.gerrit.entities.PatchSet;
      27             : import com.google.gerrit.entities.PatchSetApproval;
      28             : import com.google.gerrit.server.PatchSetUtil;
      29             : import com.google.gerrit.server.approval.ApprovalCopier;
      30             : import com.google.gerrit.server.update.BatchUpdateOp;
      31             : import com.google.gerrit.server.update.ChangeContext;
      32             : import java.io.IOException;
      33             : import java.util.Optional;
      34             : 
      35             : /**
      36             :  * Batch update operation that copy approvals that have been newly applied on outdated patch sets to
      37             :  * the follow-up patch sets if they are copyable and no non-copied approvals prevent the copying.
      38             :  *
      39             :  * <p>Must be invoked after the batch update operation which applied new approvals on outdated patch
      40             :  * sets (e.g. after {@link PostReviewOp}.
      41             :  */
      42             : @AutoFactory
      43             : public class PostReviewCopyApprovalsOp implements BatchUpdateOp {
      44             :   private final ApprovalCopier approvalCopier;
      45             :   private final PatchSetUtil patchSetUtil;
      46             :   private final PatchSet.Id patchSetId;
      47             : 
      48             :   private ChangeContext ctx;
      49             :   private ImmutableList<PatchSet.Id> followUpPatchSets;
      50             : 
      51             :   PostReviewCopyApprovalsOp(
      52             :       @Provided ApprovalCopier approvalCopier,
      53             :       @Provided PatchSetUtil patchSetUtil,
      54          65 :       PatchSet.Id patchSetId) {
      55          65 :     this.approvalCopier = approvalCopier;
      56          65 :     this.patchSetUtil = patchSetUtil;
      57          65 :     this.patchSetId = patchSetId;
      58          65 :   }
      59             : 
      60             :   @Override
      61             :   public boolean updateChange(ChangeContext ctx) throws IOException {
      62          65 :     if (ctx.getNotes().getCurrentPatchSet().id().equals(patchSetId)) {
      63             :       // the updated patch set is the current patch, there a no follow-up patch set to which new
      64             :       // approvals could be copied
      65          65 :       return false;
      66             :     }
      67             : 
      68           7 :     init(ctx);
      69             : 
      70           7 :     boolean dirty = false;
      71           7 :     ImmutableTable<String, Account.Id, Optional<PatchSetApproval>> newApprovals =
      72           7 :         ctx.getUpdate(patchSetId).getApprovals();
      73           7 :     for (Cell<String, Account.Id, Optional<PatchSetApproval>> cell : newApprovals.cellSet()) {
      74           2 :       String label = cell.getRowKey();
      75           2 :       Account.Id approverId = cell.getColumnKey();
      76           2 :       PatchSetApproval.Key psaKey =
      77           2 :           PatchSetApproval.key(patchSetId, approverId, LabelId.create(label));
      78             : 
      79           2 :       if (isRemoval(cell)) {
      80           1 :         if (removeCopies(psaKey)) {
      81           0 :           dirty = true;
      82             :         }
      83             :         continue;
      84             :       }
      85             : 
      86           2 :       PatchSet patchSet = patchSetUtil.get(ctx.getNotes(), patchSetId);
      87           2 :       PatchSetApproval psaOrig = cell.getValue().get();
      88             : 
      89             :       // Target patch sets to which the approval is copyable.
      90           2 :       ImmutableList<PatchSet.Id> targetPatchSets =
      91           2 :           approvalCopier.forApproval(
      92           2 :               ctx.getNotes(),
      93             :               patchSet,
      94           2 :               psaKey.accountId(),
      95           2 :               psaKey.labelId().get(),
      96           2 :               psaOrig.value());
      97             : 
      98             :       // Iterate over all follow-up patch sets, in patch set order.
      99           2 :       for (PatchSet.Id followUpPatchSetId : followUpPatchSets) {
     100           2 :         if (hasOverrideOf(followUpPatchSetId, psaKey)) {
     101             :           // a non-copied approval exists that overrides any copied approval
     102             :           // -> do not copy the approval to this patch set nor to any follow-up patch sets
     103           2 :           break;
     104             :         }
     105             : 
     106           1 :         if (targetPatchSets.contains(followUpPatchSetId)) {
     107             :           // The approval is copyable to the new patch set.
     108             : 
     109           1 :           if (hasCopyOfWithValue(followUpPatchSetId, psaKey, psaOrig.value())) {
     110             :             // a copy approval with the exact value already exists
     111           0 :             continue;
     112             :           }
     113             : 
     114             :           // add/update the copied approval on the target patch set
     115           1 :           PatchSetApproval copiedPatchSetApproval = psaOrig.copyWithPatchSet(followUpPatchSetId);
     116           1 :           ctx.getUpdate(followUpPatchSetId).putCopiedApproval(copiedPatchSetApproval);
     117           1 :           dirty = true;
     118           1 :         } else {
     119             :           // The approval is not copyable to the new patch set.
     120             : 
     121           1 :           if (hasCopyOf(followUpPatchSetId, psaKey)) {
     122             :             // a copy approval exists and should be removed
     123           1 :             removeCopy(followUpPatchSetId, psaKey);
     124           1 :             dirty = true;
     125             :           }
     126             :         }
     127           1 :       }
     128           2 :     }
     129             : 
     130           7 :     return dirty;
     131             :   }
     132             : 
     133             :   private void init(ChangeContext ctx) {
     134           7 :     this.ctx = ctx;
     135             : 
     136             :     // compute follow-up patch sets (sorted by patch set ID)
     137           7 :     this.followUpPatchSets =
     138           7 :         ctx.getNotes().getPatchSets().keySet().stream()
     139           7 :             .filter(psId -> psId.get() > patchSetId.get())
     140           7 :             .collect(toImmutableList());
     141           7 :   }
     142             : 
     143             :   /**
     144             :    * Whether the given cell entry from the approval table represents the removal of an approval.
     145             :    *
     146             :    * @param cell cell entry from the approval table
     147             :    * @return {@code true} if the approval is not set or the approval has {@code 0} as the value,
     148             :    *     otherwise {@code false}
     149             :    */
     150             :   private boolean isRemoval(Cell<String, Account.Id, Optional<PatchSetApproval>> cell) {
     151           2 :     return cell.getValue().isEmpty() || cell.getValue().get().value() == 0;
     152             :   }
     153             : 
     154             :   /**
     155             :    * Removes copies of the given approval from all follow-up patch sets.
     156             :    *
     157             :    * @param psaKey the key of the patch set approval for which copies should be removed from all
     158             :    *     follow-up patch sets
     159             :    * @return whether any copy approval has been removed
     160             :    */
     161             :   private boolean removeCopies(PatchSetApproval.Key psaKey) {
     162           1 :     boolean dirty = false;
     163           1 :     for (PatchSet.Id followUpPatchSet : followUpPatchSets) {
     164           1 :       if (hasCopyOf(followUpPatchSet, psaKey)) {
     165           1 :         removeCopy(followUpPatchSet, psaKey);
     166             :       } else {
     167             :         // Do not remove copy from this follow-up patch sets and also not from any further follow-up
     168             :         // patch sets (if the further follow-up patch sets have copies they are copies of a
     169             :         // non-copied approval on this follow-up patch set and hence those should not be removed).
     170             :         break;
     171             :       }
     172           1 :     }
     173           1 :     return dirty;
     174             :   }
     175             : 
     176             :   /**
     177             :    * Removes the copy approval with the given key from the given patch set.
     178             :    *
     179             :    * @param patchSet patch set from which the copy approval with the given key should be removed
     180             :    * @param psaKey the key of the patch set approval for which copies should be removed from the
     181             :    *     given patch set
     182             :    */
     183             :   private void removeCopy(PatchSet.Id patchSet, PatchSetApproval.Key psaKey) {
     184           1 :     ctx.getUpdate(patchSet)
     185           1 :         .removeCopiedApprovalFor(
     186           1 :             ctx.getIdentifiedUser().getRealUser().isIdentifiedUser()
     187           1 :                 ? ctx.getIdentifiedUser().getRealUser().getAccountId()
     188           1 :                 : null,
     189           1 :             psaKey.accountId(),
     190           1 :             psaKey.labelId().get());
     191           1 :   }
     192             : 
     193             :   /**
     194             :    * Whether the given patch set has a copy approval with the given key.
     195             :    *
     196             :    * @param patchSetId the ID of the patch for which it should be checked whether it has a copy
     197             :    *     approval with the given key
     198             :    * @param psaKey the key of the patch set approval
     199             :    */
     200             :   private boolean hasCopyOf(PatchSet.Id patchSetId, PatchSetApproval.Key psaKey) {
     201           1 :     return ctx.getNotes().getApprovals().onlyCopied().get(patchSetId).stream()
     202           1 :         .anyMatch(psa -> areAccountAndLabelTheSame(psa.key(), psaKey));
     203             :   }
     204             : 
     205             :   /**
     206             :    * Whether the given patch set has a copy approval with the given key and value.
     207             :    *
     208             :    * @param patchSetId the ID of the patch for which it should be checked whether it has a copy
     209             :    *     approval with the given key and value
     210             :    * @param psaKey the key of the patch set approval
     211             :    */
     212             :   private boolean hasCopyOfWithValue(
     213             :       PatchSet.Id patchSetId, PatchSetApproval.Key psaKey, short value) {
     214           1 :     return ctx.getNotes().getApprovals().onlyCopied().get(patchSetId).stream()
     215           1 :         .anyMatch(psa -> areAccountAndLabelTheSame(psa.key(), psaKey) && psa.value() == value);
     216             :   }
     217             : 
     218             :   /**
     219             :    * Whether the given patch set has a normal approval with the given key that overrides copy
     220             :    * approvals with that key.
     221             :    *
     222             :    * @param patchSetId the ID of the patch for which it should be checked whether it has a normal
     223             :    *     approval with the given key that overrides copy approvals with that key
     224             :    * @param psaKey the key of the patch set approval
     225             :    */
     226             :   private boolean hasOverrideOf(PatchSet.Id patchSetId, PatchSetApproval.Key psaKey) {
     227           2 :     return ctx.getNotes().getApprovals().onlyNonCopied().get(patchSetId).stream()
     228           2 :         .anyMatch(psa -> areAccountAndLabelTheSame(psa.key(), psaKey));
     229             :   }
     230             : 
     231             :   private boolean areAccountAndLabelTheSame(
     232             :       PatchSetApproval.Key psaKey1, PatchSetApproval.Key psaKey2) {
     233           2 :     return psaKey1.accountId().equals(psaKey2.accountId())
     234           2 :         && psaKey1.labelId().equals(psaKey2.labelId());
     235             :   }
     236             : }

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