diff options
Diffstat (limited to 'java/com/google/gerrit/server/change/AddReviewersOp.java')
-rw-r--r-- | java/com/google/gerrit/server/change/AddReviewersOp.java | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java new file mode 100644 index 0000000000..6c0aa942db --- /dev/null +++ b/java/com/google/gerrit/server/change/AddReviewersOp.java @@ -0,0 +1,278 @@ +// Copyright (C) 2017 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.change; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.gerrit.extensions.client.ReviewerState.CC; +import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Streams; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.extensions.client.ReviewerState; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.mail.Address; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.PatchSetApproval; +import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.extensions.events.ReviewerAdded; +import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.notedb.ReviewerStateInternal; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.Context; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public class AddReviewersOp implements BatchUpdateOp { + public interface Factory { + + /** + * Create a new op. + * + * <p>Users may be added by account or by email addresses, as determined by {@code accountIds} + * and {@code addresses}. The reviewer state for both accounts and email addresses is determined + * by {@code state}. + * + * @param accountIds account IDs to add. + * @param addresses email addresses to add. + * @param state resulting reviewer state. + * @param notify notification handling. + * @param accountsToNotify additional accounts to notify. + * @return batch update operation. + */ + AddReviewersOp create( + Set<Account.Id> accountIds, + Collection<Address> addresses, + ReviewerState state, + @Nullable NotifyHandling notify, + ListMultimap<RecipientType, Account.Id> accountsToNotify); + } + + @AutoValue + public abstract static class Result { + public abstract ImmutableList<PatchSetApproval> addedReviewers(); + + public abstract ImmutableList<Address> addedReviewersByEmail(); + + public abstract ImmutableList<Account.Id> addedCCs(); + + public abstract ImmutableList<Address> addedCCsByEmail(); + + static Builder builder() { + return new AutoValue_AddReviewersOp_Result.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setAddedReviewers(Iterable<PatchSetApproval> addedReviewers); + + abstract Builder setAddedReviewersByEmail(Iterable<Address> addedReviewersByEmail); + + abstract Builder setAddedCCs(Iterable<Account.Id> addedCCs); + + abstract Builder setAddedCCsByEmail(Iterable<Address> addedCCsByEmail); + + abstract Result build(); + } + } + + private final ApprovalsUtil approvalsUtil; + private final PatchSetUtil psUtil; + private final ReviewerAdded reviewerAdded; + private final AccountCache accountCache; + private final ProjectCache projectCache; + private final AddReviewersEmail addReviewersEmail; + private final NotesMigration migration; + private final Set<Account.Id> accountIds; + private final Collection<Address> addresses; + private final ReviewerState state; + private final NotifyHandling notify; + private final ListMultimap<RecipientType, Account.Id> accountsToNotify; + + // Unlike addedCCs, addedReviewers is a PatchSetApproval because the AddReviewerResult returned + // via the REST API is supposed to include vote information. + private List<PatchSetApproval> addedReviewers = ImmutableList.of(); + private Collection<Address> addedReviewersByEmail = ImmutableList.of(); + private Collection<Account.Id> addedCCs = ImmutableList.of(); + private Collection<Address> addedCCsByEmail = ImmutableList.of(); + + private Change change; + private PatchSet patchSet; + private Result opResult; + + @Inject + AddReviewersOp( + ApprovalsUtil approvalsUtil, + PatchSetUtil psUtil, + ReviewerAdded reviewerAdded, + AccountCache accountCache, + ProjectCache projectCache, + AddReviewersEmail addReviewersEmail, + NotesMigration migration, + @Assisted Set<Account.Id> accountIds, + @Assisted Collection<Address> addresses, + @Assisted ReviewerState state, + @Assisted @Nullable NotifyHandling notify, + @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) { + checkArgument(state == REVIEWER || state == CC, "must be %s or %s: %s", REVIEWER, CC, state); + this.approvalsUtil = approvalsUtil; + this.psUtil = psUtil; + this.reviewerAdded = reviewerAdded; + this.accountCache = accountCache; + this.projectCache = projectCache; + this.addReviewersEmail = addReviewersEmail; + this.migration = migration; + + this.accountIds = accountIds; + this.addresses = addresses; + this.state = state; + this.notify = notify; + this.accountsToNotify = accountsToNotify; + } + + void setPatchSet(PatchSet patchSet) { + this.patchSet = requireNonNull(patchSet); + } + + @Override + public boolean updateChange(ChangeContext ctx) + throws RestApiException, OrmException, IOException { + change = ctx.getChange(); + if (!accountIds.isEmpty()) { + if (migration.readChanges() && state == CC) { + addedCCs = + approvalsUtil.addCcs( + ctx.getNotes(), ctx.getUpdate(change.currentPatchSetId()), accountIds); + } else { + addedReviewers = + approvalsUtil.addReviewers( + ctx.getDb(), + ctx.getNotes(), + ctx.getUpdate(change.currentPatchSetId()), + projectCache.checkedGet(change.getProject()).getLabelTypes(change.getDest()), + change, + accountIds); + } + } + + ImmutableList<Address> addressesToAdd = ImmutableList.of(); + ReviewerStateInternal internalState = ReviewerStateInternal.fromReviewerState(state); + if (migration.readChanges()) { + // TODO(dborowitz): This behavior should live in ApprovalsUtil or something, like addCcs does. + ImmutableSet<Address> existing = ctx.getNotes().getReviewersByEmail().byState(internalState); + addressesToAdd = + addresses.stream().filter(a -> !existing.contains(a)).collect(toImmutableList()); + + if (state == CC) { + addedCCsByEmail = addressesToAdd; + } else { + addedReviewersByEmail = addressesToAdd; + } + for (Address a : addressesToAdd) { + ctx.getUpdate(change.currentPatchSetId()).putReviewerByEmail(a, internalState); + } + } + if (addedCCs.isEmpty() && addedReviewers.isEmpty() && addressesToAdd.isEmpty()) { + return false; + } + + checkAdded(); + + if (patchSet == null) { + patchSet = requireNonNull(psUtil.current(ctx.getDb(), ctx.getNotes())); + } + return true; + } + + private void checkAdded() { + // Should only affect either reviewers or CCs, not both. But the logic in updateChange is + // complex, so programmer error is conceivable. + boolean addedAnyReviewers = !addedReviewers.isEmpty() || !addedReviewersByEmail.isEmpty(); + boolean addedAnyCCs = !addedCCs.isEmpty() || !addedCCsByEmail.isEmpty(); + checkState( + !(addedAnyReviewers && addedAnyCCs), + "should not have added both reviewers and CCs:\n" + + "Arguments:\n" + + " accountIds=%s\n" + + " addresses=%s\n" + + "Results:\n" + + " addedReviewers=%s\n" + + " addedReviewersByEmail=%s\n" + + " addedCCs=%s\n" + + " addedCCsByEmail=%s", + accountIds, + addresses, + addedReviewers, + addedReviewersByEmail, + addedCCs, + addedCCsByEmail); + } + + @Override + public void postUpdate(Context ctx) throws Exception { + opResult = + Result.builder() + .setAddedReviewers(addedReviewers) + .setAddedReviewersByEmail(addedReviewersByEmail) + .setAddedCCs(addedCCs) + .setAddedCCsByEmail(addedCCsByEmail) + .build(); + addReviewersEmail.emailReviewers( + ctx.getUser().asIdentifiedUser(), + change, + Lists.transform(addedReviewers, PatchSetApproval::getAccountId), + addedCCs, + addedReviewersByEmail, + addedCCsByEmail, + notify, + accountsToNotify, + !change.isWorkInProgress()); + if (!addedReviewers.isEmpty()) { + List<AccountState> reviewers = + addedReviewers.stream() + .map(r -> accountCache.get(r.getAccountId())) + .flatMap(Streams::stream) + .collect(toList()); + reviewerAdded.fire(change, patchSet, reviewers, ctx.getAccount(), ctx.getWhen()); + } + } + + public Result getResult() { + checkState(opResult != null, "Batch update wasn't executed yet"); + return opResult; + } +} |