Line data Source code
1 : // Copyright (C) 2009 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.mail.send; 16 : 17 : import static com.google.common.collect.ImmutableList.toImmutableList; 18 : 19 : import com.google.common.base.Supplier; 20 : import com.google.common.base.Suppliers; 21 : import com.google.common.collect.ImmutableList; 22 : import com.google.common.flogger.FluentLogger; 23 : import com.google.gerrit.common.Nullable; 24 : import com.google.gerrit.entities.Account; 25 : import com.google.gerrit.entities.Change; 26 : import com.google.gerrit.entities.NotifyConfig.NotifyType; 27 : import com.google.gerrit.entities.PatchSetApproval; 28 : import com.google.gerrit.entities.Project; 29 : import com.google.gerrit.entities.SubmitRequirement; 30 : import com.google.gerrit.entities.SubmitRequirementResult; 31 : import com.google.gerrit.exceptions.EmailException; 32 : import com.google.gerrit.extensions.api.changes.NotifyHandling; 33 : import com.google.gerrit.extensions.api.changes.RecipientType; 34 : import com.google.gerrit.extensions.client.ChangeKind; 35 : import com.google.gerrit.server.util.LabelVote; 36 : import com.google.inject.Inject; 37 : import com.google.inject.assistedinject.Assisted; 38 : import java.util.ArrayList; 39 : import java.util.Collection; 40 : import java.util.HashSet; 41 : import java.util.List; 42 : import java.util.Map; 43 : import java.util.Set; 44 : import org.eclipse.jgit.lib.ObjectId; 45 : 46 : /** Send notice of new patch sets for reviewers. */ 47 : public class ReplacePatchSetSender extends ReplyToChangeSender { 48 50 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 49 : 50 : public interface Factory { 51 : ReplacePatchSetSender create( 52 : Project.NameKey project, 53 : Change.Id changeId, 54 : ChangeKind changeKind, 55 : ObjectId preUpdateMetaId, 56 : Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults); 57 : } 58 : 59 50 : private final Set<Account.Id> reviewers = new HashSet<>(); 60 50 : private final Set<Account.Id> extraCC = new HashSet<>(); 61 : private final ChangeKind changeKind; 62 50 : private final Set<PatchSetApproval> outdatedApprovals = new HashSet<>(); 63 : private final Supplier<Map<SubmitRequirement, SubmitRequirementResult>> 64 : preUpdateSubmitRequirementResultsSupplier; 65 : private final Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults; 66 : 67 : @Inject 68 : public ReplacePatchSetSender( 69 : EmailArguments args, 70 : @Assisted Project.NameKey project, 71 : @Assisted Change.Id changeId, 72 : @Assisted ChangeKind changeKind, 73 : @Assisted ObjectId preUpdateMetaId, 74 : @Assisted 75 : Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults) { 76 50 : super(args, "newpatchset", newChangeData(args, project, changeId)); 77 50 : this.changeKind = changeKind; 78 : 79 50 : this.preUpdateSubmitRequirementResultsSupplier = 80 50 : Suppliers.memoize( 81 : () -> 82 : // Triggers an (expensive) evaluation of the submit requirements. This is OK since 83 : // all callers sent this email asynchronously, see EmailNewPatchSet. 84 50 : newChangeData(args, project, changeId, preUpdateMetaId) 85 50 : .submitRequirementsIncludingLegacy()); 86 : 87 50 : this.postUpdateSubmitRequirementResults = postUpdateSubmitRequirementResults; 88 50 : } 89 : 90 : @Override 91 : protected boolean shouldSendMessage() { 92 50 : if (!isChangeNoLongerSubmittable() && changeKind.isTrivialRebase()) { 93 17 : logger.atFine().log( 94 : "skip email because new patch set is a trivial rebase that didn't make the change" 95 : + " non-submittable"); 96 17 : return false; 97 : } 98 : 99 50 : return super.shouldSendMessage(); 100 : } 101 : 102 : public void addReviewers(Collection<Account.Id> cc) { 103 50 : reviewers.addAll(cc); 104 50 : } 105 : 106 : public void addExtraCC(Collection<Account.Id> cc) { 107 50 : extraCC.addAll(cc); 108 50 : } 109 : 110 : public void addOutdatedApproval(@Nullable Collection<PatchSetApproval> outdatedApprovals) { 111 50 : if (outdatedApprovals != null) { 112 50 : this.outdatedApprovals.addAll(outdatedApprovals); 113 : } 114 50 : } 115 : 116 : @Override 117 : protected void init() throws EmailException { 118 50 : super.init(); 119 : 120 50 : if (fromId != null) { 121 : // Don't call yourself a reviewer of your own patch set. 122 : // 123 50 : reviewers.remove(fromId); 124 : } 125 50 : if (args.settings.sendNewPatchsetEmails) { 126 50 : if (notify.handling() == NotifyHandling.ALL 127 6 : || notify.handling() == NotifyHandling.OWNER_REVIEWERS) { 128 50 : reviewers.stream().forEach(r -> add(RecipientType.TO, r)); 129 50 : extraCC.stream().forEach(cc -> add(RecipientType.CC, cc)); 130 : } 131 50 : rcptToAuthors(RecipientType.CC); 132 : } 133 50 : bccStarredBy(); 134 50 : includeWatchers(NotifyType.NEW_PATCHSETS, !change.isWorkInProgress() && !change.isPrivate()); 135 50 : } 136 : 137 : @Override 138 : protected void formatChange() throws EmailException { 139 50 : appendText(textTemplate("ReplacePatchSet")); 140 50 : if (useHtml()) { 141 50 : appendHtml(soyHtmlTemplate("ReplacePatchSetHtml")); 142 : } 143 50 : } 144 : 145 : @Nullable 146 : public ImmutableList<String> getReviewerNames() { 147 50 : List<String> names = new ArrayList<>(); 148 50 : for (Account.Id id : reviewers) { 149 15 : if (id.equals(fromId)) { 150 12 : continue; 151 : } 152 12 : names.add(getNameFor(id)); 153 12 : } 154 50 : if (names.isEmpty()) { 155 49 : return null; 156 : } 157 12 : return names.stream().sorted().collect(toImmutableList()); 158 : } 159 : 160 : private ImmutableList<String> formatOutdatedApprovals() { 161 50 : return outdatedApprovals.stream() 162 50 : .map( 163 : outdatedApproval -> 164 11 : String.format( 165 : "%s by %s", 166 11 : LabelVote.create(outdatedApproval.label(), outdatedApproval.value()).format(), 167 11 : getNameFor(outdatedApproval.accountId()))) 168 50 : .sorted() 169 50 : .collect(toImmutableList()); 170 : } 171 : 172 : @Override 173 : protected void setupSoyContext() { 174 50 : super.setupSoyContext(); 175 50 : soyContextEmailData.put("reviewerNames", getReviewerNames()); 176 50 : soyContextEmailData.put("outdatedApprovals", formatOutdatedApprovals()); 177 : 178 50 : if (isChangeNoLongerSubmittable()) { 179 9 : soyContext.put("unsatisfiedSubmitRequirements", formatUnsatisfiedSubmitRequirements()); 180 9 : soyContext.put( 181 : "oldSubmitRequirements", 182 9 : formatSubmitRequirments(preUpdateSubmitRequirementResultsSupplier.get())); 183 9 : soyContext.put( 184 9 : "newSubmitRequirements", formatSubmitRequirments(postUpdateSubmitRequirementResults)); 185 : } 186 50 : } 187 : 188 : /** 189 : * Checks whether the change is no longer submittable. 190 : * 191 : * @return {@code true} if the change has been submittable before the update and is no longer 192 : * submittable after the update has been applied, otherwise {@code false} 193 : */ 194 : private boolean isChangeNoLongerSubmittable() { 195 50 : boolean isSubmittablePreUpdate = 196 50 : preUpdateSubmitRequirementResultsSupplier.get().values().stream() 197 50 : .allMatch(SubmitRequirementResult::fulfilled); 198 50 : logger.atFine().log( 199 : "the submitability of change %s before the update is %s", 200 50 : change.getId(), isSubmittablePreUpdate); 201 50 : if (!isSubmittablePreUpdate) { 202 49 : return false; 203 : } 204 : 205 13 : boolean isSubmittablePostUpdate = 206 13 : postUpdateSubmitRequirementResults.values().stream() 207 13 : .allMatch(SubmitRequirementResult::fulfilled); 208 13 : logger.atFine().log( 209 : "the submitability of change %s after the update is %s", 210 13 : change.getId(), isSubmittablePostUpdate); 211 13 : return !isSubmittablePostUpdate; 212 : } 213 : 214 : private ImmutableList<String> formatUnsatisfiedSubmitRequirements() { 215 9 : return postUpdateSubmitRequirementResults.entrySet().stream() 216 9 : .filter(e -> SubmitRequirementResult.Status.UNSATISFIED.equals(e.getValue().status())) 217 9 : .map(Map.Entry::getKey) 218 9 : .map(SubmitRequirement::name) 219 9 : .sorted() 220 9 : .collect(toImmutableList()); 221 : } 222 : 223 : private static ImmutableList<String> formatSubmitRequirments( 224 : Map<SubmitRequirement, SubmitRequirementResult> submitRequirementResults) { 225 9 : return submitRequirementResults.entrySet().stream() 226 9 : .map( 227 : e -> { 228 9 : if (e.getValue().errorMessage().isPresent()) { 229 0 : return String.format( 230 : "%s: %s (%s)", 231 0 : e.getKey().name(), 232 0 : e.getValue().status().name(), 233 0 : e.getValue().errorMessage().get()); 234 : } 235 9 : return String.format("%s: %s", e.getKey().name(), e.getValue().status().name()); 236 : }) 237 9 : .sorted() 238 9 : .collect(toImmutableList()); 239 : } 240 : }