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 : }