blob: aeb2c2e0250c5537bbbd4332c3a1796b14d1c70c [file] [log] [blame]
// Copyright (C) 2020 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.restapi.change;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.api.changes.AttentionSetInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.AddToAttentionSetOp;
import com.google.gerrit.server.change.AttentionSetUnchangedOp;
import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
import com.google.gerrit.server.change.ReviewerAdder;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.util.AttentionSetUtil;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
/**
* This class is used by {@link PostReview} to update the attention set when performing a review.
*/
public class PostReviewAttentionSet {
private final PermissionBackend permissionBackend;
private final AddToAttentionSetOp.Factory addToAttentionSetOpFactory;
private final RemoveFromAttentionSetOp.Factory removeFromAttentionSetOpFactory;
private final ApprovalsUtil approvalsUtil;
private final AccountResolver accountResolver;
@Inject
PostReviewAttentionSet(
PermissionBackend permissionBackend,
AddToAttentionSetOp.Factory addToAttentionSetOpFactory,
RemoveFromAttentionSetOp.Factory removeFromAttentionSetOpFactory,
ApprovalsUtil approvalsUtil,
AccountResolver accountResolver) {
this.permissionBackend = permissionBackend;
this.addToAttentionSetOpFactory = addToAttentionSetOpFactory;
this.removeFromAttentionSetOpFactory = removeFromAttentionSetOpFactory;
this.approvalsUtil = approvalsUtil;
this.accountResolver = accountResolver;
}
/**
* Adjusts the attention set by adding and removing users. If the same user should be added and
* removed or added/removed twice, the user will only be added/removed once, based on first
* addition/removal.
*/
public void updateAttentionSet(
BatchUpdate bu,
RevisionResource revision,
ReviewInput input,
List<ReviewerAdder.ReviewerAddition> reviewerResults)
throws BadRequestException, IOException, PermissionBackendException,
UnprocessableEntityException, ConfigInvalidException {
processManualUpdates(bu, revision, input);
if (input.ignoreDefaultAttentionSetRules) {
// If We ignore default attention set rules it means we need to pass this information to
// ChangeUpdate. Also, we should stop all other attention set update that are part of
// this method (that happen in PostReview.
bu.addOp(revision.getChange().getId(), new AttentionSetUnchangedOp());
return;
}
processRules(bu, revision, input, reviewerResults);
}
/**
* Process the default rules of the attention set. All of the default rules except adding/removing
* reviewers and entering/exiting WIP state are done here, and the rest are done in {@link
* ChangeUpdate}
*/
private void processRules(
BatchUpdate bu,
RevisionResource revision,
ReviewInput input,
List<ReviewerAdder.ReviewerAddition> reviewerResults) {
// Replying removes the publishing user from the attention set.
RemoveFromAttentionSetOp removeFromAttentionSetOp =
removeFromAttentionSetOpFactory.create(revision.getAccountId(), "removed on reply");
bu.addOp(revision.getChange().getId(), removeFromAttentionSetOp);
// The rest of the conditions only apply if the change is ready for review
if (!isReadyForReview(revision, input)) {
return;
}
Account.Id uploader = revision.getPatchSet().uploader();
Account.Id owner = revision.getChange().getOwner();
Account.Id currentUser = revision.getAccountId();
if (currentUser.equals(uploader) && !uploader.equals(owner)) {
// When the uploader replies, add the owner to the attention set.
AddToAttentionSetOp addToAttentionSetOp =
addToAttentionSetOpFactory.create(owner, "uploader replied");
bu.addOp(revision.getChange().getId(), addToAttentionSetOp);
}
if (currentUser.equals(uploader) || currentUser.equals(owner)) {
// When the owner or uploader replies, add the reviewers to the attention set.
// Filter by users that are currently reviewers.
Set<Account.Id> finalCCs =
reviewerResults.stream()
.filter(r -> r.result.ccs == null)
.map(r -> r.reviewers)
.flatMap(x -> x.stream())
.collect(toSet());
for (Account.Id reviewer :
approvalsUtil.getReviewers(revision.getChangeResource().getNotes()).byState(REVIEWER)
.stream()
.filter(r -> !finalCCs.contains(r))
.collect(toList())) {
AddToAttentionSetOp addToAttentionSetOp =
addToAttentionSetOpFactory.create(reviewer, "owner or uploader replied");
bu.addOp(revision.getChange().getId(), addToAttentionSetOp);
}
}
if (!currentUser.equals(uploader) && !currentUser.equals(owner)) {
// When neither the uploader nor the owner (reviewer or cc) replies, add the owner and the
// uploader to the attention set.
AddToAttentionSetOp addToAttentionSetOp =
addToAttentionSetOpFactory.create(owner, "reviewer or cc replied");
bu.addOp(revision.getChange().getId(), addToAttentionSetOp);
if (owner.get() != uploader.get()) {
addToAttentionSetOp = addToAttentionSetOpFactory.create(uploader, "reviewer or cc replied");
bu.addOp(revision.getChange().getId(), addToAttentionSetOp);
}
}
}
/** Process the manual updates of the attention set. */
private void processManualUpdates(BatchUpdate bu, RevisionResource revision, ReviewInput input)
throws BadRequestException, IOException, PermissionBackendException,
UnprocessableEntityException, ConfigInvalidException {
Set<Account.Id> accountsChangedInCommit = new HashSet<>();
// If we specify a user to remove, and the user is in the attention set, we remove it.
if (input.removeFromAttentionSet != null) {
for (AttentionSetInput remove : input.removeFromAttentionSet) {
removeFromAttentionSet(bu, revision, remove, accountsChangedInCommit);
}
}
// If we don't specify a user to remove, but we specify addition for that user, the user will be
// added if they are not in the attention set yet.
if (input.addToAttentionSet != null) {
for (AttentionSetInput add : input.addToAttentionSet) {
addToAttentionSet(bu, revision, add, accountsChangedInCommit);
}
}
}
private static boolean isReadyForReview(RevisionResource revision, ReviewInput input) {
return (!revision.getChange().isWorkInProgress() && !input.workInProgress) || input.ready;
}
private void addToAttentionSet(
BatchUpdate bu,
RevisionResource revision,
AttentionSetInput add,
Set<Account.Id> accountsChangedInCommitv)
throws BadRequestException, IOException, PermissionBackendException,
UnprocessableEntityException, ConfigInvalidException {
AttentionSetUtil.validateInput(add);
Account.Id attentionUserId =
getAccountIdAndValidateUser(revision, add.user, accountsChangedInCommitv);
AddToAttentionSetOp addToAttentionSetOp =
addToAttentionSetOpFactory.create(attentionUserId, add.reason);
bu.addOp(revision.getChange().getId(), addToAttentionSetOp);
}
private void removeFromAttentionSet(
BatchUpdate bu,
RevisionResource revision,
AttentionSetInput remove,
Set<Account.Id> accountsChangedInCommit)
throws BadRequestException, IOException, PermissionBackendException,
UnprocessableEntityException, ConfigInvalidException {
AttentionSetUtil.validateInput(remove);
Account.Id attentionUserId =
getAccountIdAndValidateUser(revision, remove.user, accountsChangedInCommit);
RemoveFromAttentionSetOp removeFromAttentionSetOp =
removeFromAttentionSetOpFactory.create(attentionUserId, remove.reason);
bu.addOp(revision.getChange().getId(), removeFromAttentionSetOp);
}
private Account.Id getAccountIdAndValidateUser(
RevisionResource revision, String user, Set<Account.Id> accountsChangedInCommit)
throws ConfigInvalidException, IOException, PermissionBackendException,
UnprocessableEntityException, BadRequestException {
Account.Id attentionUserId = accountResolver.resolve(user).asUnique().account().id();
try {
permissionBackend
.absentUser(attentionUserId)
.change(revision.getNotes())
.check(ChangePermission.READ);
} catch (AuthException e) {
throw new UnprocessableEntityException(
"Can't add to attention set: Read not permitted for " + attentionUserId, e);
}
if (accountsChangedInCommit.contains(attentionUserId)) {
throw new BadRequestException(
String.format(
"%s can not be added/removed twice, and can not be added and "
+ "removed at the same time",
user));
}
accountsChangedInCommit.add(attentionUserId);
return attentionUserId;
}
}