Pass notification settings through BatchUpdate's Context

Previously, any notification settings that a BatchUpdateOp wanted to
respect in its postUpdate method needed to be resolved from the
corresponding REST API input and plumbed through the Op constructor so
they could be looked up as needed. Explicit can be good, but this verged
into the territory of _too_ explicit.

Instead, treat the notification settings as a property of the
BatchUpdate. This is somewhat magical, but in this case the magic is
justified. Conceptually, associating notification settings with the
BatchUpdate makes sense: there is a single set of notification settings
that is parsed at the top level from the input, and those settings
should apply to *all* emails sent as a result of that BatchUpdate,
regardless of the particulars of where they're sent from. Putting them
in the Context allows us to treat them as a per-BatchUpdate singleton.

We retain the ability to override the NotifyHandling enum on a
per-*change* basis, rather than a per-*op* basis, to account for the
reduced notifications from WIP changes.

Without this change, there's always the possibility of having multiple
Ops in the same BatchUpdate that send different emails with different
notification settings. For example, we had special code in PostReview
to ignore the notification settings embedded in the constituent
AddReviewerInputs of a ReviewInput. After this change, we still ignore
those settings (as the REST API docs specify), but we don't have to do
anything special to do so: we just set the NotifyResolver.Result on the
BatchUpdate to the one from the ReviewInput, completely ignoring the
AddReviewerInputs.

This change also has the effect of removing any logic around
notification settings from ReviewerAdder; all the notification logic is
in the caller. We still retain a separate mechanism for suppressing all
emails from ReviewerAdder, so that PostReview can send only a single
email. However, this is stored as a simple boolean, rather than passing
around a whole NotifyResolver.Result. It's still not completely ideal,
but considering everything else ReviewerAdder has to deal with, it's a
small win. Plus, simplifying ReviewerAdder will pave the way for
rewriting it in the near future.

One downside here is that any check to set NotifyHandling based on
properties of the change (e.g. the WIP bit) is racy, since the change
was read outside of the BatchUpdate. The risk and consequences of such a
race are low enough that the benefits described above still outweigh it.
(And it's not like it's the only such race, even though we do try to
keep them to a minimum.)

This change should have minimal behavior changes. One exception is that
moving around calls to NotifyResolver#resolve from the body of a try
block to the top level of a REST API handler might result in a 400/422
for invalid input in notify_details rather than logging and silently not
sending an email. Some endpoints already had this behavior, so making
the failure explicit and consistent is considered a feature.

Change-Id: If6f8a44f382f57b9cb10490f74c2b144e904ece8
This commit is contained in:
Dave Borowitz
2019-01-30 14:12:34 -08:00
parent 8e24b76649
commit 0312eb2315
39 changed files with 249 additions and 216 deletions

View File

@@ -45,7 +45,6 @@ public class AbandonOp implements BatchUpdateOp {
private final ChangeAbandoned changeAbandoned; private final ChangeAbandoned changeAbandoned;
private final String msgTxt; private final String msgTxt;
private final NotifyResolver.Result notify;
private final AccountState accountState; private final AccountState accountState;
private Change change; private Change change;
@@ -54,9 +53,7 @@ public class AbandonOp implements BatchUpdateOp {
public interface Factory { public interface Factory {
AbandonOp create( AbandonOp create(
@Assisted @Nullable AccountState accountState, @Assisted @Nullable AccountState accountState, @Assisted @Nullable String msgTxt);
@Assisted @Nullable String msgTxt,
@Assisted NotifyResolver.Result notify);
} }
@Inject @Inject
@@ -66,8 +63,7 @@ public class AbandonOp implements BatchUpdateOp {
PatchSetUtil psUtil, PatchSetUtil psUtil,
ChangeAbandoned changeAbandoned, ChangeAbandoned changeAbandoned,
@Assisted @Nullable AccountState accountState, @Assisted @Nullable AccountState accountState,
@Assisted @Nullable String msgTxt, @Assisted @Nullable String msgTxt) {
@Assisted NotifyResolver.Result notify) {
this.abandonedSenderFactory = abandonedSenderFactory; this.abandonedSenderFactory = abandonedSenderFactory;
this.cmUtil = cmUtil; this.cmUtil = cmUtil;
this.psUtil = psUtil; this.psUtil = psUtil;
@@ -75,7 +71,6 @@ public class AbandonOp implements BatchUpdateOp {
this.accountState = accountState; this.accountState = accountState;
this.msgTxt = Strings.nullToEmpty(msgTxt); this.msgTxt = Strings.nullToEmpty(msgTxt);
this.notify = notify;
} }
@Nullable @Nullable
@@ -114,6 +109,7 @@ public class AbandonOp implements BatchUpdateOp {
@Override @Override
public void postUpdate(Context ctx) throws OrmException { public void postUpdate(Context ctx) throws OrmException {
NotifyResolver.Result notify = ctx.getNotify(change.getId());
try { try {
ReplyToChangeSender cm = abandonedSenderFactory.create(ctx.getProject(), change.getId()); ReplyToChangeSender cm = abandonedSenderFactory.create(ctx.getProject(), change.getId());
if (accountState != null) { if (accountState != null) {

View File

@@ -65,14 +65,10 @@ public class AddReviewersOp implements BatchUpdateOp {
* @param accountIds account IDs to add. * @param accountIds account IDs to add.
* @param addresses email addresses to add. * @param addresses email addresses to add.
* @param state resulting reviewer state. * @param state resulting reviewer state.
* @param notify notification handling.
* @return batch update operation. * @return batch update operation.
*/ */
AddReviewersOp create( AddReviewersOp create(
Set<Account.Id> accountIds, Set<Account.Id> accountIds, Collection<Address> addresses, ReviewerState state);
Collection<Address> addresses,
ReviewerState state,
NotifyResolver.Result notify);
} }
@AutoValue @AutoValue
@@ -112,7 +108,6 @@ public class AddReviewersOp implements BatchUpdateOp {
private final Set<Account.Id> accountIds; private final Set<Account.Id> accountIds;
private final Collection<Address> addresses; private final Collection<Address> addresses;
private final ReviewerState state; private final ReviewerState state;
private final NotifyResolver.Result notify;
// Unlike addedCCs, addedReviewers is a PatchSetApproval because the AddReviewerResult returned // Unlike addedCCs, addedReviewers is a PatchSetApproval because the AddReviewerResult returned
// via the REST API is supposed to include vote information. // via the REST API is supposed to include vote information.
@@ -121,6 +116,7 @@ public class AddReviewersOp implements BatchUpdateOp {
private Collection<Account.Id> addedCCs = ImmutableList.of(); private Collection<Account.Id> addedCCs = ImmutableList.of();
private Collection<Address> addedCCsByEmail = ImmutableList.of(); private Collection<Address> addedCCsByEmail = ImmutableList.of();
private boolean sendEmail = true;
private Change change; private Change change;
private PatchSet patchSet; private PatchSet patchSet;
private Result opResult; private Result opResult;
@@ -135,8 +131,7 @@ public class AddReviewersOp implements BatchUpdateOp {
AddReviewersEmail addReviewersEmail, AddReviewersEmail addReviewersEmail,
@Assisted Set<Account.Id> accountIds, @Assisted Set<Account.Id> accountIds,
@Assisted Collection<Address> addresses, @Assisted Collection<Address> addresses,
@Assisted ReviewerState state, @Assisted ReviewerState state) {
@Assisted NotifyResolver.Result notify) {
checkArgument(state == REVIEWER || state == CC, "must be %s or %s: %s", REVIEWER, CC, state); checkArgument(state == REVIEWER || state == CC, "must be %s or %s: %s", REVIEWER, CC, state);
this.approvalsUtil = approvalsUtil; this.approvalsUtil = approvalsUtil;
this.psUtil = psUtil; this.psUtil = psUtil;
@@ -148,7 +143,13 @@ public class AddReviewersOp implements BatchUpdateOp {
this.accountIds = accountIds; this.accountIds = accountIds;
this.addresses = addresses; this.addresses = addresses;
this.state = state; this.state = state;
this.notify = notify; }
// TODO(dborowitz): This mutable setter is ugly, but a) it's less ugly than adding boolean args
// all the way through the constructor stack, and b) this class is slated to be completely
// rewritten.
public void suppressEmail() {
this.sendEmail = false;
} }
void setPatchSet(PatchSet patchSet) { void setPatchSet(PatchSet patchSet) {
@@ -237,14 +238,16 @@ public class AddReviewersOp implements BatchUpdateOp {
.setAddedCCs(addedCCs) .setAddedCCs(addedCCs)
.setAddedCCsByEmail(addedCCsByEmail) .setAddedCCsByEmail(addedCCsByEmail)
.build(); .build();
addReviewersEmail.emailReviewers( if (sendEmail) {
ctx.getUser().asIdentifiedUser(), addReviewersEmail.emailReviewers(
change, ctx.getUser().asIdentifiedUser(),
Lists.transform(addedReviewers, PatchSetApproval::getAccountId), change,
addedCCs, Lists.transform(addedReviewers, PatchSetApproval::getAccountId),
addedReviewersByEmail, addedCCs,
addedCCsByEmail, addedReviewersByEmail,
notify); addedCCsByEmail,
ctx.getNotify(change.getId()));
}
if (!addedReviewers.isEmpty()) { if (!addedReviewers.isEmpty()) {
List<AccountState> reviewers = List<AccountState> reviewers =
addedReviewers addedReviewers

View File

@@ -56,6 +56,7 @@ public class BatchAbandon {
} }
AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null; AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
try (BatchUpdate u = updateFactory.create(project, user, TimeUtil.nowTs())) { try (BatchUpdate u = updateFactory.create(project, user, TimeUtil.nowTs())) {
u.setNotify(notify);
for (ChangeData change : changes) { for (ChangeData change : changes) {
if (!project.equals(change.project())) { if (!project.equals(change.project())) {
throw new ResourceConflictException( throw new ResourceConflictException(
@@ -63,7 +64,7 @@ public class BatchAbandon {
"Project name \"%s\" doesn't match \"%s\"", "Project name \"%s\" doesn't match \"%s\"",
change.project().get(), project.get())); change.project().get(), project.get()));
} }
u.addOp(change.getId(), abandonOpFactory.create(accountState, msgTxt, notify)); u.addOp(change.getId(), abandonOpFactory.create(accountState, msgTxt));
} }
u.execute(); u.execute();
} }

View File

@@ -120,7 +120,6 @@ public class ChangeInserter implements InsertChangeOp {
private boolean workInProgress; private boolean workInProgress;
private List<String> groups = Collections.emptyList(); private List<String> groups = Collections.emptyList();
private boolean validate = true; private boolean validate = true;
private NotifyResolver.Result notify = NotifyResolver.Result.all();
private Map<String, Short> approvals; private Map<String, Short> approvals;
private RequestScopePropagator requestScopePropagator; private RequestScopePropagator requestScopePropagator;
private boolean fireRevisionCreated; private boolean fireRevisionCreated;
@@ -251,11 +250,6 @@ public class ChangeInserter implements InsertChangeOp {
return this; return this;
} }
public ChangeInserter setNotify(NotifyResolver.Result notify) {
this.notify = notify;
return this;
}
public ChangeInserter setReviewersAndCcs( public ChangeInserter setReviewersAndCcs(
Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) { Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) {
return setReviewersAndCcsAsStrings( return setReviewersAndCcsAsStrings(
@@ -447,6 +441,7 @@ public class ChangeInserter implements InsertChangeOp {
@Override @Override
public void postUpdate(Context ctx) throws Exception { public void postUpdate(Context ctx) throws Exception {
reviewerAdditions.postUpdate(ctx); reviewerAdditions.postUpdate(ctx);
NotifyResolver.Result notify = ctx.getNotify(change.getId());
if (sendMail && notify.shouldNotify()) { if (sendMail && notify.shouldNotify()) {
Runnable sender = Runnable sender =
new Runnable() { new Runnable() {

View File

@@ -531,12 +531,12 @@ public class ConsistencyChecker {
} }
} }
bu.setNotify(NotifyResolver.Result.none());
bu.addOp( bu.addOp(
notes.getChangeId(), notes.getChangeId(),
inserter inserter
.setValidate(false) .setValidate(false)
.setFireRevisionCreated(false) .setFireRevisionCreated(false)
.setNotify(NotifyResolver.Result.none())
.setAllowClosed(true) .setAllowClosed(true)
.setMessage("Patch set for merged commit inserted by consistency checker")); .setMessage("Patch set for merged commit inserted by consistency checker"));
bu.addOp(notes.getChangeId(), new FixMergedOp(notFound)); bu.addOp(notes.getChangeId(), new FixMergedOp(notFound));

View File

@@ -63,6 +63,10 @@ public class NotifyResolver {
// TODO(dborowitz): Should be ImmutableSetMultimap. // TODO(dborowitz): Should be ImmutableSetMultimap.
public abstract ImmutableListMultimap<RecipientType, Account.Id> accounts(); public abstract ImmutableListMultimap<RecipientType, Account.Id> accounts();
public Result withHandling(NotifyHandling notifyHandling) {
return create(notifyHandling, accounts());
}
public boolean shouldNotify() { public boolean shouldNotify() {
return !accounts().isEmpty() || handling().compareTo(NotifyHandling.NONE) > 0; return !accounts().isEmpty() || handling().compareTo(NotifyHandling.NONE) > 0;
} }

View File

@@ -91,7 +91,6 @@ public class PatchSetInserter implements BatchUpdateOp {
private boolean checkAddPatchSetPermission = true; private boolean checkAddPatchSetPermission = true;
private List<String> groups = Collections.emptyList(); private List<String> groups = Collections.emptyList();
private boolean fireRevisionCreated = true; private boolean fireRevisionCreated = true;
private NotifyResolver.Result notify = NotifyResolver.Result.all();
private boolean allowClosed; private boolean allowClosed;
// Fields set during some phase of BatchUpdate.Op. // Fields set during some phase of BatchUpdate.Op.
@@ -165,11 +164,6 @@ public class PatchSetInserter implements BatchUpdateOp {
return this; return this;
} }
public PatchSetInserter setNotify(NotifyResolver.Result notify) {
this.notify = requireNonNull(notify);
return this;
}
public PatchSetInserter setAllowClosed(boolean allowClosed) { public PatchSetInserter setAllowClosed(boolean allowClosed) {
this.allowClosed = allowClosed; this.allowClosed = allowClosed;
return this; return this;
@@ -218,7 +212,7 @@ public class PatchSetInserter implements BatchUpdateOp {
psUtil.insert( psUtil.insert(
ctx.getRevWalk(), ctx.getUpdate(psId), psId, commitId, newGroups, null, description); ctx.getRevWalk(), ctx.getUpdate(psId), psId, commitId, newGroups, null, description);
if (notify.handling() != NotifyHandling.NONE) { if (ctx.getNotify(change.getId()).handling() != NotifyHandling.NONE) {
oldReviewers = approvalsUtil.getReviewers(ctx.getNotes()); oldReviewers = approvalsUtil.getReviewers(ctx.getNotes());
} }
@@ -247,6 +241,7 @@ public class PatchSetInserter implements BatchUpdateOp {
@Override @Override
public void postUpdate(Context ctx) throws OrmException { public void postUpdate(Context ctx) throws OrmException {
NotifyResolver.Result notify = ctx.getNotify(change.getId());
if (notify.shouldNotify()) { if (notify.shouldNotify()) {
try { try {
ReplacePatchSetSender cm = replacePatchSetFactory.create(ctx.getProject(), change.getId()); ReplacePatchSetSender cm = replacePatchSetFactory.create(ctx.getProject(), change.getId());

View File

@@ -182,7 +182,6 @@ public class RebaseChangeOp implements BatchUpdateOp {
patchSetInserterFactory patchSetInserterFactory
.create(notes, rebasedPatchSetId, rebasedCommit) .create(notes, rebasedPatchSetId, rebasedCommit)
.setDescription("Rebase") .setDescription("Rebase")
.setNotify(NotifyResolver.Result.none())
.setFireRevisionCreated(fireRevisionCreated) .setFireRevisionCreated(fireRevisionCreated)
.setCheckAddPatchSetPermission(checkAddPatchSetPermission) .setCheckAddPatchSetPermission(checkAddPatchSetPermission)
.setValidate(validate); .setValidate(validate);

View File

@@ -38,7 +38,6 @@ import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.mail.Address; import com.google.gerrit.mail.Address;
@@ -148,7 +147,6 @@ public class ReviewerAdder {
private final AccountLoader.Factory accountLoaderFactory; private final AccountLoader.Factory accountLoaderFactory;
private final Config cfg; private final Config cfg;
private final ReviewerJson json; private final ReviewerJson json;
private final NotifyResolver notifyResolver;
private final ProjectCache projectCache; private final ProjectCache projectCache;
private final Provider<AnonymousUser> anonymousProvider; private final Provider<AnonymousUser> anonymousProvider;
private final AddReviewersOp.Factory addReviewersOpFactory; private final AddReviewersOp.Factory addReviewersOpFactory;
@@ -163,7 +161,6 @@ public class ReviewerAdder {
AccountLoader.Factory accountLoaderFactory, AccountLoader.Factory accountLoaderFactory,
@GerritServerConfig Config cfg, @GerritServerConfig Config cfg,
ReviewerJson json, ReviewerJson json,
NotifyResolver notifyResolver,
ProjectCache projectCache, ProjectCache projectCache,
Provider<AnonymousUser> anonymousProvider, Provider<AnonymousUser> anonymousProvider,
AddReviewersOp.Factory addReviewersOpFactory, AddReviewersOp.Factory addReviewersOpFactory,
@@ -175,7 +172,6 @@ public class ReviewerAdder {
this.accountLoaderFactory = accountLoaderFactory; this.accountLoaderFactory = accountLoaderFactory;
this.cfg = cfg; this.cfg = cfg;
this.json = json; this.json = json;
this.notifyResolver = notifyResolver;
this.projectCache = projectCache; this.projectCache = projectCache;
this.anonymousProvider = anonymousProvider; this.anonymousProvider = anonymousProvider;
this.addReviewersOpFactory = addReviewersOpFactory; this.addReviewersOpFactory = addReviewersOpFactory;
@@ -201,23 +197,17 @@ public class ReviewerAdder {
ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup) ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
throws OrmException, IOException, PermissionBackendException, ConfigInvalidException { throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
requireNonNull(input.reviewer); requireNonNull(input.reviewer);
NotifyResolver.Result notify;
try {
notify = resolveNotify(notes, input);
} catch (BadRequestException e) {
return fail(input, FailureType.OTHER, e.getMessage());
}
boolean confirmed = input.confirmed(); boolean confirmed = input.confirmed();
boolean allowByEmail = boolean allowByEmail =
projectCache projectCache
.checkedGet(notes.getProjectName()) .checkedGet(notes.getProjectName())
.is(BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL); .is(BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL);
ReviewerAddition byAccountId = addByAccountId(input, notes, user, notify); ReviewerAddition byAccountId = addByAccountId(input, notes, user);
ReviewerAddition wholeGroup = null; ReviewerAddition wholeGroup = null;
if (!byAccountId.exactMatchFound) { if (!byAccountId.exactMatchFound) {
wholeGroup = addWholeGroup(input, notes, user, notify, confirmed, allowGroup, allowByEmail); wholeGroup = addWholeGroup(input, notes, user, confirmed, allowGroup, allowByEmail);
if (wholeGroup != null && wholeGroup.exactMatchFound) { if (wholeGroup != null && wholeGroup.exactMatchFound) {
return wholeGroup; return wholeGroup;
} }
@@ -239,17 +229,7 @@ public class ReviewerAdder {
return wholeGroup; return wholeGroup;
} }
return addByEmail(input, notes, user, notify); return addByEmail(input, notes, user);
}
private NotifyResolver.Result resolveNotify(ChangeNotes notes, AddReviewerInput input)
throws BadRequestException, OrmException, ConfigInvalidException, IOException {
NotifyHandling notifyHandling = input.notify;
if (notifyHandling == null) {
notifyHandling =
notes.getChange().isWorkInProgress() ? NotifyHandling.NONE : NotifyHandling.ALL;
}
return notifyResolver.resolve(notifyHandling, input.notifyDetails);
} }
public ReviewerAddition ccCurrentUser(CurrentUser user, RevisionResource revision) { public ReviewerAddition ccCurrentUser(CurrentUser user, RevisionResource revision) {
@@ -259,13 +239,12 @@ public class ReviewerAdder {
revision.getUser(), revision.getUser(),
ImmutableSet.of(user.getAccountId()), ImmutableSet.of(user.getAccountId()),
null, null,
NotifyResolver.Result.none(),
true); true);
} }
@Nullable @Nullable
private ReviewerAddition addByAccountId( private ReviewerAddition addByAccountId(
AddReviewerInput input, ChangeNotes notes, CurrentUser user, NotifyResolver.Result notify) AddReviewerInput input, ChangeNotes notes, CurrentUser user)
throws OrmException, PermissionBackendException, IOException, ConfigInvalidException { throws OrmException, PermissionBackendException, IOException, ConfigInvalidException {
IdentifiedUser reviewerUser; IdentifiedUser reviewerUser;
boolean exactMatchFound = false; boolean exactMatchFound = false;
@@ -283,13 +262,7 @@ public class ReviewerAdder {
if (isValidReviewer(notes.getChange().getDest(), reviewerUser.getAccount())) { if (isValidReviewer(notes.getChange().getDest(), reviewerUser.getAccount())) {
return new ReviewerAddition( return new ReviewerAddition(
input, input, notes, user, ImmutableSet.of(reviewerUser.getAccountId()), null, exactMatchFound);
notes,
user,
ImmutableSet.of(reviewerUser.getAccountId()),
null,
notify,
exactMatchFound);
} }
return fail( return fail(
input, input,
@@ -302,7 +275,6 @@ public class ReviewerAdder {
AddReviewerInput input, AddReviewerInput input,
ChangeNotes notes, ChangeNotes notes,
CurrentUser user, CurrentUser user,
NotifyResolver.Result notify,
boolean confirmed, boolean confirmed,
boolean allowGroup, boolean allowGroup,
boolean allowByEmail) boolean allowByEmail)
@@ -374,12 +346,11 @@ public class ReviewerAdder {
} }
} }
return new ReviewerAddition(input, notes, user, reviewers, null, notify, true); return new ReviewerAddition(input, notes, user, reviewers, null, true);
} }
@Nullable @Nullable
private ReviewerAddition addByEmail( private ReviewerAddition addByEmail(AddReviewerInput input, ChangeNotes notes, CurrentUser user)
AddReviewerInput input, ChangeNotes notes, CurrentUser user, NotifyResolver.Result notify)
throws PermissionBackendException { throws PermissionBackendException {
try { try {
permissionBackend.user(anonymousProvider.get()).change(notes).check(ChangePermission.READ); permissionBackend.user(anonymousProvider.get()).change(notes).check(ChangePermission.READ);
@@ -397,7 +368,7 @@ public class ReviewerAdder {
FailureType.NOT_FOUND, FailureType.NOT_FOUND,
MessageFormat.format(ChangeMessages.get().reviewerInvalid, input.reviewer)); MessageFormat.format(ChangeMessages.get().reviewerInvalid, input.reviewer));
} }
return new ReviewerAddition(input, notes, user, null, ImmutableList.of(adr), notify, true); return new ReviewerAddition(input, notes, user, null, ImmutableList.of(adr), true);
} }
private boolean isValidReviewer(Branch.NameKey branch, Account member) private boolean isValidReviewer(Branch.NameKey branch, Account member)
@@ -452,7 +423,6 @@ public class ReviewerAdder {
CurrentUser caller, CurrentUser caller,
@Nullable Iterable<Account.Id> reviewers, @Nullable Iterable<Account.Id> reviewers,
@Nullable Iterable<Address> reviewersByEmail, @Nullable Iterable<Address> reviewersByEmail,
NotifyResolver.Result notify,
boolean exactMatchFound) { boolean exactMatchFound) {
checkArgument( checkArgument(
reviewers != null || reviewersByEmail != null, reviewers != null || reviewersByEmail != null,
@@ -467,7 +437,7 @@ public class ReviewerAdder {
this.reviewersByEmail = this.reviewersByEmail =
reviewersByEmail == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewersByEmail); reviewersByEmail == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewersByEmail);
this.caller = caller.asIdentifiedUser(); this.caller = caller.asIdentifiedUser();
op = addReviewersOpFactory.create(this.reviewers, this.reviewersByEmail, state(), notify); op = addReviewersOpFactory.create(this.reviewers, this.reviewersByEmail, state());
this.exactMatchFound = exactMatchFound; this.exactMatchFound = exactMatchFound;
} }
@@ -557,7 +527,12 @@ public class ReviewerAdder {
.collect(toImmutableList()); .collect(toImmutableList());
List<ReviewerAddition> additions = new ArrayList<>(); List<ReviewerAddition> additions = new ArrayList<>();
for (AddReviewerInput input : sorted) { for (AddReviewerInput input : sorted) {
additions.add(prepare(notes, user, input, allowGroup)); ReviewerAddition addition = prepare(notes, user, input, allowGroup);
if (addition.op != null) {
// Assume any callers preparing a list of batch insertions are handling their own email.
addition.op.suppressEmail();
}
additions.add(addition);
} }
return new ReviewerAdditionList(additions); return new ReviewerAdditionList(additions);
} }

View File

@@ -14,7 +14,6 @@
package com.google.gerrit.server.change; package com.google.gerrit.server.change;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
@@ -91,9 +90,9 @@ public class WorkInProgressOp implements BatchUpdateOp {
private final PatchSetUtil psUtil; private final PatchSetUtil psUtil;
private final boolean workInProgress; private final boolean workInProgress;
private final Input in; private final Input in;
private final NotifyResolver.Result notify;
private final WorkInProgressStateChanged stateChanged; private final WorkInProgressStateChanged stateChanged;
private boolean sendEmail = true;
private Change change; private Change change;
private ChangeNotes notes; private ChangeNotes notes;
private PatchSet ps; private PatchSet ps;
@@ -113,10 +112,10 @@ public class WorkInProgressOp implements BatchUpdateOp {
this.stateChanged = stateChanged; this.stateChanged = stateChanged;
this.workInProgress = workInProgress; this.workInProgress = workInProgress;
this.in = in; this.in = in;
notify = }
NotifyResolver.Result.create(
MoreObjects.firstNonNull( public void suppressEmail() {
in.notify, workInProgress ? NotifyHandling.NONE : NotifyHandling.ALL)); this.sendEmail = false;
} }
@Override @Override
@@ -160,7 +159,10 @@ public class WorkInProgressOp implements BatchUpdateOp {
@Override @Override
public void postUpdate(Context ctx) { public void postUpdate(Context ctx) {
stateChanged.fire(change, ps, ctx.getAccount(), ctx.getWhen()); stateChanged.fire(change, ps, ctx.getAccount(), ctx.getWhen());
if (workInProgress || notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) < 0) { NotifyResolver.Result notify = ctx.getNotify(change.getId());
if (workInProgress
|| notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) < 0
|| !sendEmail) {
return; return;
} }
email email

View File

@@ -169,8 +169,7 @@ public class ChangeEditUtil {
RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet); RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet);
PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId()); PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
PatchSetInserter inserter = PatchSetInserter inserter = patchSetInserterFactory.create(notes, psId, squashed);
patchSetInserterFactory.create(notes, psId, squashed).setNotify(notify);
StringBuilder message = StringBuilder message =
new StringBuilder("Patch Set ").append(inserter.getPatchSetId().get()).append(": "); new StringBuilder("Patch Set ").append(inserter.getPatchSetId().get()).append(": ");
@@ -191,6 +190,7 @@ public class ChangeEditUtil {
try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.nowTs())) { try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.nowTs())) {
bu.setRepository(repo, rw, oi); bu.setRepository(repo, rw, oi);
bu.setNotify(notify);
bu.addOp(change.getId(), inserter.setMessage(message.toString())); bu.addOp(change.getId(), inserter.setMessage(message.toString()));
bu.addOp( bu.addOp(
change.getId(), change.getId(),

View File

@@ -682,7 +682,7 @@ class ReceiveCommits {
// Update superproject gitlinks if required. // Update superproject gitlinks if required.
if (!branches.isEmpty()) { if (!branches.isEmpty()) {
try (MergeOpRepoManager orm = ormProvider.get()) { try (MergeOpRepoManager orm = ormProvider.get()) {
orm.setContext(TimeUtil.nowTs(), user); orm.setContext(TimeUtil.nowTs(), user, NotifyResolver.Result.none());
SubmoduleOp op = subOpFactory.create(branches, orm); SubmoduleOp op = subOpFactory.create(branches, orm);
op.updateSuperProjects(); op.updateSuperProjects();
} catch (SubmoduleException e) { } catch (SubmoduleException e) {
@@ -787,9 +787,15 @@ class ReceiveCommits {
RevWalk rw = new RevWalk(reader)) { RevWalk rw = new RevWalk(reader)) {
bu.setRepository(repo, rw, ins); bu.setRepository(repo, rw, ins);
bu.setRefLogMessage("push"); bu.setRefLogMessage("push");
if (magicBranch != null) {
bu.setNotify(magicBranch.getNotifyForNewChange());
}
logger.atFine().log("Adding %d replace requests", newChanges.size()); logger.atFine().log("Adding %d replace requests", newChanges.size());
for (ReplaceRequest replace : replaceByChange.values()) { for (ReplaceRequest replace : replaceByChange.values()) {
if (magicBranch != null) {
bu.setNotifyHandling(replace.ontoChange, magicBranch.getNotifyHandling(replace.notes));
}
replace.addOps(bu, replaceProgress); replace.addOps(bu, replaceProgress);
} }
@@ -1578,31 +1584,25 @@ class ReceiveCommits {
} }
NotifyResolver.Result getNotifyForNewChange() { NotifyResolver.Result getNotifyForNewChange() {
return getNotifyImpl(null);
}
NotifyResolver.Result getNotify(ChangeNotes notes) {
return getNotifyImpl(requireNonNull(notes));
}
private NotifyResolver.Result getNotifyImpl(@Nullable ChangeNotes notes) {
NotifyHandling notifyHandling = this.notifyHandling;
if (notifyHandling == null) {
if (workInProgress || (notes != null && !ready && notes.getChange().isWorkInProgress())) {
notifyHandling = NotifyHandling.OWNER;
} else {
notifyHandling = NotifyHandling.ALL;
}
}
return NotifyResolver.Result.create( return NotifyResolver.Result.create(
notifyHandling, firstNonNull(notifyHandling, workInProgress ? NotifyHandling.OWNER : NotifyHandling.ALL),
ImmutableListMultimap.<RecipientType, Account.Id>builder() ImmutableListMultimap.<RecipientType, Account.Id>builder()
.putAll(RecipientType.TO, notifyTo) .putAll(RecipientType.TO, notifyTo)
.putAll(RecipientType.CC, notifyCc) .putAll(RecipientType.CC, notifyCc)
.putAll(RecipientType.BCC, notifyBcc) .putAll(RecipientType.BCC, notifyBcc)
.build()); .build());
} }
NotifyHandling getNotifyHandling(ChangeNotes notes) {
requireNonNull(notes);
if (notifyHandling != null) {
return notifyHandling;
}
if (workInProgress || (!ready && notes.getChange().isWorkInProgress())) {
return NotifyHandling.OWNER;
}
return NotifyHandling.ALL;
}
} }
/** /**
@@ -2440,13 +2440,13 @@ class ReceiveCommits {
msg.append("\n").append(magicBranch.message); msg.append("\n").append(magicBranch.message);
} }
bu.setNotify(magicBranch.getNotifyForNewChange());
bu.insertChange( bu.insertChange(
ins.setReviewersAndCcsAsStrings( ins.setReviewersAndCcsAsStrings(
magicBranch.getCombinedReviewers(fromFooters), magicBranch.getCombinedReviewers(fromFooters),
magicBranch.getCombinedCcs(fromFooters)) magicBranch.getCombinedCcs(fromFooters))
.setApprovals(approvals) .setApprovals(approvals)
.setMessage(msg.toString()) .setMessage(msg.toString())
.setNotify(magicBranch.getNotifyForNewChange())
.setRequestScopePropagator(requestScopePropagator) .setRequestScopePropagator(requestScopePropagator)
.setSendMail(true) .setSendMail(true)
.setPatchSetDescription(magicBranch.message)); .setPatchSetDescription(magicBranch.message));

View File

@@ -512,8 +512,7 @@ public class ReplaceOp implements BatchUpdateOp {
} }
} }
NotifyResolver.Result notify = NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
magicBranch != null ? magicBranch.getNotify(notes) : NotifyResolver.Result.all();
if (shouldPublishComments()) { if (shouldPublishComments()) {
emailCommentsFactory emailCommentsFactory
.create( .create(
@@ -554,9 +553,7 @@ public class ReplaceOp implements BatchUpdateOp {
cm.setFrom(ctx.getAccount().getAccount().getId()); cm.setFrom(ctx.getAccount().getAccount().getId());
cm.setPatchSet(newPatchSet, info); cm.setPatchSet(newPatchSet, info);
cm.setChangeMessage(msg.getMessage(), ctx.getWhen()); cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
if (magicBranch != null) { cm.setNotify(ctx.getNotify(notes.getChangeId()));
cm.setNotify(magicBranch.getNotify(notes));
}
cm.addReviewers( cm.addReviewers(
Streams.concat( Streams.concat(
oldRecipients.getReviewers().stream(), oldRecipients.getReviewers().stream(),

View File

@@ -45,7 +45,6 @@ import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.Emails; import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.change.EmailReviewComments; import com.google.gerrit.server.change.EmailReviewComments;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.config.UrlFormatter; import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.extensions.events.CommentAdded; import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.mail.MailFilter; import com.google.gerrit.server.mail.MailFilter;
@@ -314,7 +313,7 @@ public class MailProcessor {
// Send email notifications // Send email notifications
outgoingMailFactory outgoingMailFactory
.create( .create(
NotifyResolver.Result.all(), ctx.getNotify(notes.getChangeId()),
notes, notes,
patchSet, patchSet,
ctx.getUser().asIdentifiedUser(), ctx.getUser().asIdentifiedUser(),

View File

@@ -120,8 +120,9 @@ public class Abandon extends RetryingRestModifyView<ChangeResource, AbandonInput
NotifyResolver.Result notify) NotifyResolver.Result notify)
throws RestApiException, UpdateException { throws RestApiException, UpdateException {
AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null; AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
AbandonOp op = abandonOpFactory.create(accountState, msgTxt, notify); AbandonOp op = abandonOpFactory.create(accountState, msgTxt);
try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.nowTs())) { try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.nowTs())) {
u.setNotify(notify);
u.addOp(notes.getChangeId(), op).execute(); u.addOp(notes.getChangeId(), op).execute();
} }
return op.getChange(); return op.getChange();

View File

@@ -249,11 +249,12 @@ public class CherryPickChange {
} }
try (BatchUpdate bu = batchUpdateFactory.create(project, identifiedUser, now)) { try (BatchUpdate bu = batchUpdateFactory.create(project, identifiedUser, now)) {
bu.setRepository(git, revWalk, oi); bu.setRepository(git, revWalk, oi);
bu.setNotify(resolveNotify(input));
Change.Id changeId; Change.Id changeId;
if (destChanges.size() == 1) { if (destChanges.size() == 1) {
// The change key exists on the destination branch. The cherry pick // The change key exists on the destination branch. The cherry pick
// will be added as a new patch set. // will be added as a new patch set.
changeId = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit, input); changeId = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit);
} else { } else {
// Change key not found on destination branch. We can create a new // Change key not found on destination branch. We can create a new
// change. // change.
@@ -318,19 +319,12 @@ public class CherryPickChange {
} }
private Change.Id insertPatchSet( private Change.Id insertPatchSet(
BatchUpdate bu, BatchUpdate bu, Repository git, ChangeNotes destNotes, CodeReviewCommit cherryPickCommit)
Repository git, throws IOException {
ChangeNotes destNotes,
CodeReviewCommit cherryPickCommit,
CherryPickInput input)
throws IOException, OrmException, BadRequestException, ConfigInvalidException {
Change destChange = destNotes.getChange(); Change destChange = destNotes.getChange();
PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId()); PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit); PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit);
NotifyResolver.Result notify = resolveNotify(input); inserter.setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".");
inserter
.setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".")
.setNotify(notify);
bu.addOp(destChange.getId(), inserter); bu.addOp(destChange.getId(), inserter);
return destChange.getId(); return destChange.getId();
} }
@@ -343,19 +337,17 @@ public class CherryPickChange {
@Nullable Change sourceChange, @Nullable Change sourceChange,
ObjectId sourceCommit, ObjectId sourceCommit,
CherryPickInput input) CherryPickInput input)
throws OrmException, IOException, BadRequestException, ConfigInvalidException { throws OrmException, IOException {
Change.Id changeId = new Change.Id(seq.nextChangeId()); Change.Id changeId = new Change.Id(seq.nextChangeId());
ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName); ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
Branch.NameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest(); Branch.NameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
NotifyResolver.Result notify = resolveNotify(input);
ins.setMessage( ins.setMessage(
messageForDestinationChange( messageForDestinationChange(
ins.getPatchSetId(), sourceBranch, sourceCommit, cherryPickCommit)) ins.getPatchSetId(), sourceBranch, sourceCommit, cherryPickCommit))
.setTopic(topic) .setTopic(topic)
.setWorkInProgress( .setWorkInProgress(
(sourceChange != null && sourceChange.isWorkInProgress()) (sourceChange != null && sourceChange.isWorkInProgress())
|| !cherryPickCommit.getFilesWithGitConflicts().isEmpty()) || !cherryPickCommit.getFilesWithGitConflicts().isEmpty());
.setNotify(notify);
if (input.keepReviewers && sourceChange != null) { if (input.keepReviewers && sourceChange != null) {
ReviewerSet reviewerSet = ReviewerSet reviewerSet =
approvalsUtil.getReviewers(changeNotesFactory.createChecked(sourceChange)); approvalsUtil.getReviewers(changeNotesFactory.createChecked(sourceChange));

View File

@@ -305,12 +305,11 @@ public class CreateChange
ins.setPrivate(input.isPrivate); ins.setPrivate(input.isPrivate);
ins.setWorkInProgress(input.workInProgress); ins.setWorkInProgress(input.workInProgress);
ins.setGroups(groups); ins.setGroups(groups);
NotifyResolver.Result notify =
notifyResolver.resolve(
firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
ins.setNotify(notify);
try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) { try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) {
bu.setRepository(git, rw, oi); bu.setRepository(git, rw, oi);
bu.setNotify(
notifyResolver.resolve(
firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
bu.insertChange(ins); bu.insertChange(ins);
bu.execute(); bu.execute();
} }

View File

@@ -183,9 +183,9 @@ public class CreateMergePatchSet
patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit); patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit);
try (BatchUpdate bu = updateFactory.create(project, me, now)) { try (BatchUpdate bu = updateFactory.create(project, me, now)) {
bu.setRepository(git, rw, oi); bu.setRepository(git, rw, oi);
bu.setNotify(NotifyResolver.Result.none());
psInserter psInserter
.setMessage("Uploaded patch set " + nextPsId.get() + ".") .setMessage("Uploaded patch set " + nextPsId.get() + ".")
.setNotify(NotifyResolver.Result.none())
.setCheckAddPatchSetPermission(false); .setCheckAddPatchSetPermission(false);
if (groups != null) { if (groups != null) {
psInserter.setGroups(groups); psInserter.setGroups(groups);

View File

@@ -15,8 +15,11 @@
package com.google.gerrit.server.restapi.change; package com.google.gerrit.server.restapi.change;
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerResource; import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.BatchUpdateOp;
@@ -57,6 +60,7 @@ public class DeleteReviewer
rsrc.getChangeResource().getProject(), rsrc.getChangeResource().getProject(),
rsrc.getChangeResource().getUser(), rsrc.getChangeResource().getUser(),
TimeUtil.nowTs())) { TimeUtil.nowTs())) {
bu.setNotify(getNotify(rsrc.getChange(), input));
BatchUpdateOp op; BatchUpdateOp op;
if (rsrc.isByEmail()) { if (rsrc.isByEmail()) {
op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail(), input); op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail(), input);
@@ -68,4 +72,12 @@ public class DeleteReviewer
} }
return Response.none(); return Response.none();
} }
private static NotifyResolver.Result getNotify(Change change, DeleteReviewerInput input) {
NotifyHandling notifyHandling = input.notify;
if (notifyHandling == null) {
notifyHandling = change.isWorkInProgress() ? NotifyHandling.NONE : NotifyHandling.ALL;
}
return NotifyResolver.Result.create(notifyHandling);
}
} }

View File

@@ -14,11 +14,8 @@
package com.google.gerrit.server.restapi.change; package com.google.gerrit.server.restapi.change;
import static com.google.common.base.MoreObjects.firstNonNull;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.mail.Address; import com.google.gerrit.mail.Address;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -42,7 +39,6 @@ public class DeleteReviewerByEmailOp implements BatchUpdateOp {
} }
private final DeleteReviewerSender.Factory deleteReviewerSenderFactory; private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
private final NotifyResolver notifyResolver;
private final Address reviewer; private final Address reviewer;
private final DeleteReviewerInput input; private final DeleteReviewerInput input;
@@ -52,11 +48,9 @@ public class DeleteReviewerByEmailOp implements BatchUpdateOp {
@Inject @Inject
DeleteReviewerByEmailOp( DeleteReviewerByEmailOp(
DeleteReviewerSender.Factory deleteReviewerSenderFactory, DeleteReviewerSender.Factory deleteReviewerSenderFactory,
NotifyResolver notifyResolver,
@Assisted Address reviewer, @Assisted Address reviewer,
@Assisted DeleteReviewerInput input) { @Assisted DeleteReviewerInput input) {
this.deleteReviewerSenderFactory = deleteReviewerSenderFactory; this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
this.notifyResolver = notifyResolver;
this.reviewer = reviewer; this.reviewer = reviewer;
this.input = input; this.input = input;
} }
@@ -81,17 +75,8 @@ public class DeleteReviewerByEmailOp implements BatchUpdateOp {
@Override @Override
public void postUpdate(Context ctx) { public void postUpdate(Context ctx) {
if (input.notify == null) {
if (change.isWorkInProgress()) {
input.notify = NotifyHandling.NONE;
} else {
input.notify = NotifyHandling.ALL;
}
}
try { try {
NotifyResolver.Result notify = NotifyResolver.Result notify = ctx.getNotify(change.getId());
notifyResolver.resolve(
firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
if (!notify.shouldNotify()) { if (!notify.shouldNotify()) {
return; return;
} }

View File

@@ -69,7 +69,6 @@ public class DeleteReviewerOp implements BatchUpdateOp {
private final ReviewerDeleted reviewerDeleted; private final ReviewerDeleted reviewerDeleted;
private final Provider<IdentifiedUser> user; private final Provider<IdentifiedUser> user;
private final DeleteReviewerSender.Factory deleteReviewerSenderFactory; private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
private final NotifyResolver notifyResolver;
private final RemoveReviewerControl removeReviewerControl; private final RemoveReviewerControl removeReviewerControl;
private final ProjectCache projectCache; private final ProjectCache projectCache;
@@ -91,7 +90,6 @@ public class DeleteReviewerOp implements BatchUpdateOp {
ReviewerDeleted reviewerDeleted, ReviewerDeleted reviewerDeleted,
Provider<IdentifiedUser> user, Provider<IdentifiedUser> user,
DeleteReviewerSender.Factory deleteReviewerSenderFactory, DeleteReviewerSender.Factory deleteReviewerSenderFactory,
NotifyResolver notifyResolver,
RemoveReviewerControl removeReviewerControl, RemoveReviewerControl removeReviewerControl,
ProjectCache projectCache, ProjectCache projectCache,
@Assisted AccountState reviewerAccount, @Assisted AccountState reviewerAccount,
@@ -103,7 +101,6 @@ public class DeleteReviewerOp implements BatchUpdateOp {
this.reviewerDeleted = reviewerDeleted; this.reviewerDeleted = reviewerDeleted;
this.user = user; this.user = user;
this.deleteReviewerSenderFactory = deleteReviewerSenderFactory; this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
this.notifyResolver = notifyResolver;
this.removeReviewerControl = removeReviewerControl; this.removeReviewerControl = removeReviewerControl;
this.projectCache = projectCache; this.projectCache = projectCache;
this.reviewer = reviewerAccount; this.reviewer = reviewerAccount;
@@ -170,15 +167,16 @@ public class DeleteReviewerOp implements BatchUpdateOp {
@Override @Override
public void postUpdate(Context ctx) { public void postUpdate(Context ctx) {
if (input.notify == null) { NotifyResolver.Result notify = ctx.getNotify(currChange.getId());
if (currChange.isWorkInProgress()) { if (input.notify == null
input.notify = oldApprovals.isEmpty() ? NotifyHandling.NONE : NotifyHandling.OWNER; && currChange.isWorkInProgress()
} else { && !oldApprovals.isEmpty()
input.notify = NotifyHandling.ALL; && notify.handling().compareTo(NotifyHandling.OWNER) < 0) {
} // Override NotifyHandling from the context to notify owner if votes were removed on a WIP
// change.
notify = notify.withHandling(NotifyHandling.OWNER);
} }
try { try {
NotifyResolver.Result notify = notifyResolver.resolve(input.notify, input.notifyDetails);
if (notify.shouldNotify()) { if (notify.shouldNotify()) {
emailReviewers(ctx.getProject(), currChange, changeMessage, notify); emailReviewers(ctx.getProject(), currChange, changeMessage, notify);
} }
@@ -193,7 +191,7 @@ public class DeleteReviewerOp implements BatchUpdateOp {
changeMessage.getMessage(), changeMessage.getMessage(),
newApprovals, newApprovals,
oldApprovals, oldApprovals,
input.notify, notify.handling(),
ctx.getWhen()); ctx.getWhen());
} }

View File

@@ -62,6 +62,7 @@ import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton @Singleton
public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Response<?>> { public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Response<?>> {
@@ -104,7 +105,7 @@ public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteI
@Override @Override
protected Response<?> applyImpl( protected Response<?> applyImpl(
BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input) BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input)
throws RestApiException, UpdateException, IOException { throws RestApiException, UpdateException, IOException, OrmException, ConfigInvalidException {
if (input == null) { if (input == null) {
input = new DeleteVoteInput(); input = new DeleteVoteInput();
} }
@@ -124,6 +125,9 @@ public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteI
try (BatchUpdate bu = try (BatchUpdate bu =
updateFactory.create( updateFactory.create(
change.getProject(), r.getChangeResource().getUser(), TimeUtil.nowTs())) { change.getProject(), r.getChangeResource().getUser(), TimeUtil.nowTs())) {
bu.setNotify(
notifyResolver.resolve(
firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
bu.addOp( bu.addOp(
change.getId(), change.getId(),
new Op( new Op(
@@ -219,9 +223,7 @@ public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteI
IdentifiedUser user = ctx.getIdentifiedUser(); IdentifiedUser user = ctx.getIdentifiedUser();
try { try {
NotifyResolver.Result notify = NotifyResolver.Result notify = ctx.getNotify(change.getId());
notifyResolver.resolve(
firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
if (notify.shouldNotify()) { if (notify.shouldNotify()) {
ReplyToChangeSender cm = deleteVoteSenderFactory.create(ctx.getProject(), change.getId()); ReplyToChangeSender cm = deleteVoteSenderFactory.create(ctx.getProject(), change.getId());
cm.setFrom(user.getAccountId()); cm.setFrom(user.getAccountId());

View File

@@ -255,8 +255,6 @@ public class PostReview
input.notify = defaultNotify(revision.getChange(), input); input.notify = defaultNotify(revision.getChange(), input);
} }
NotifyResolver.Result notify = notifyResolver.resolve(input.notify, input.notifyDetails);
Map<String, AddReviewerResult> reviewerJsonResults = null; Map<String, AddReviewerResult> reviewerJsonResults = null;
List<ReviewerAddition> reviewerResults = Lists.newArrayList(); List<ReviewerAddition> reviewerResults = Lists.newArrayList();
boolean hasError = false; boolean hasError = false;
@@ -264,12 +262,6 @@ public class PostReview
if (input.reviewers != null) { if (input.reviewers != null) {
reviewerJsonResults = Maps.newHashMap(); reviewerJsonResults = Maps.newHashMap();
for (AddReviewerInput reviewerInput : input.reviewers) { for (AddReviewerInput reviewerInput : input.reviewers) {
// Prevent individual AddReviewersOps from sending one email each. Instead, we call
// batchEmailReviewers at the very end to send out a single email.
// TODO(dborowitz): I think this still sends out separate emails if any of input.reviewers
// specifies explicit accountsToNotify. Unclear whether that's a good thing.
reviewerInput.notify = NotifyHandling.NONE;
ReviewerAddition result = ReviewerAddition result =
reviewerAdder.prepare(revision.getNotes(), revision.getUser(), reviewerInput, true); reviewerAdder.prepare(revision.getNotes(), revision.getUser(), reviewerInput, true);
reviewerJsonResults.put(reviewerInput.reviewer, result.result); reviewerJsonResults.put(reviewerInput.reviewer, result.result);
@@ -312,6 +304,7 @@ public class PostReview
// updated set of reviewers. Also keep track of whether the user added // updated set of reviewers. Also keep track of whether the user added
// themselves as a reviewer or to the CC list. // themselves as a reviewer or to the CC list.
for (ReviewerAddition reviewerResult : reviewerResults) { for (ReviewerAddition reviewerResult : reviewerResults) {
reviewerResult.op.suppressEmail(); // Send a single batch email below.
bu.addOp(revision.getChange().getId(), reviewerResult.op); bu.addOp(revision.getChange().getId(), reviewerResult.op);
if (!ccOrReviewer && reviewerResult.result.reviewers != null) { if (!ccOrReviewer && reviewerResult.result.reviewers != null) {
for (ReviewerInfo reviewerInfo : reviewerResult.result.reviewers) { for (ReviewerInfo reviewerInfo : reviewerResult.result.reviewers) {
@@ -336,6 +329,7 @@ public class PostReview
// isn't being explicitly added, and isn't voting on any label. // isn't being explicitly added, and isn't voting on any label.
// Automatically CC them on this change so they receive replies. // Automatically CC them on this change so they receive replies.
ReviewerAddition selfAddition = reviewerAdder.ccCurrentUser(revision.getUser(), revision); ReviewerAddition selfAddition = reviewerAdder.ccCurrentUser(revision.getUser(), revision);
selfAddition.op.suppressEmail();
bu.addOp(revision.getChange().getId(), selfAddition.op); bu.addOp(revision.getChange().getId(), selfAddition.op);
} }
@@ -353,19 +347,21 @@ public class PostReview
output.ready = true; output.ready = true;
} }
// Suppress notifications in WorkInProgressOp, we'll take care of WorkInProgressOp wipOp =
// them in this endpoint. workInProgressOpFactory.create(input.workInProgress, new WorkInProgressOp.Input());
WorkInProgressOp.Input wipIn = new WorkInProgressOp.Input(); wipOp.suppressEmail();
wipIn.notify = NotifyHandling.NONE; bu.addOp(revision.getChange().getId(), wipOp);
bu.addOp(
revision.getChange().getId(),
workInProgressOpFactory.create(input.workInProgress, wipIn));
} }
// Add the review op. // Add the review op.
bu.addOp( bu.addOp(
revision.getChange().getId(), revision.getChange().getId(),
new Op(projectState, revision.getPatchSet().getId(), input, notify)); new Op(projectState, revision.getPatchSet().getId(), input));
// Notify based on ReviewInput, ignoring the notify settings from any AddReviewerInputs.
NotifyResolver.Result notify =
notifyResolver.resolve(getNotifyHandling(input, output, revision), input.notifyDetails);
bu.setNotify(notify);
bu.execute(); bu.execute();
@@ -382,6 +378,17 @@ public class PostReview
return Response.ok(output); return Response.ok(output);
} }
private NotifyHandling getNotifyHandling(
ReviewInput input, ReviewResult output, RevisionResource revision) {
if (input.notify != null) {
return input.notify;
}
if ((output.ready != null && output.ready) || !revision.getChange().isWorkInProgress()) {
return NotifyHandling.ALL;
}
return NotifyHandling.NONE;
}
private NotifyHandling defaultNotify(Change c, ReviewInput in) { private NotifyHandling defaultNotify(Change c, ReviewInput in) {
boolean workInProgress = c.isWorkInProgress(); boolean workInProgress = c.isWorkInProgress();
if (in.workInProgress) { if (in.workInProgress) {
@@ -828,7 +835,6 @@ public class PostReview
private final ProjectState projectState; private final ProjectState projectState;
private final PatchSet.Id psId; private final PatchSet.Id psId;
private final ReviewInput in; private final ReviewInput in;
private final NotifyResolver.Result notify;
private IdentifiedUser user; private IdentifiedUser user;
private ChangeNotes notes; private ChangeNotes notes;
@@ -839,12 +845,10 @@ public class PostReview
private Map<String, Short> approvals = new HashMap<>(); private Map<String, Short> approvals = new HashMap<>();
private Map<String, Short> oldApprovals = new HashMap<>(); private Map<String, Short> oldApprovals = new HashMap<>();
private Op( private Op(ProjectState projectState, PatchSet.Id psId, ReviewInput in) {
ProjectState projectState, PatchSet.Id psId, ReviewInput in, NotifyResolver.Result notify) {
this.projectState = projectState; this.projectState = projectState;
this.psId = psId; this.psId = psId;
this.in = in; this.in = in;
this.notify = requireNonNull(notify);
} }
@Override @Override
@@ -867,6 +871,7 @@ public class PostReview
if (message == null) { if (message == null) {
return; return;
} }
NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
if (notify.shouldNotify()) { if (notify.shouldNotify()) {
email email
.create(notify, notes, ps, user, message, comments, in.message, labelDelta) .create(notify, notes, ps, user, message, comments, in.message, labelDelta)

View File

@@ -16,10 +16,12 @@ package com.google.gerrit.server.restapi.change;
import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult; import com.google.gerrit.extensions.api.changes.AddReviewerResult;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerAdder; import com.google.gerrit.server.change.ReviewerAdder;
import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition; import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
import com.google.gerrit.server.change.ReviewerResource; import com.google.gerrit.server.change.ReviewerResource;
@@ -42,13 +44,18 @@ public class PostReviewers
ChangeResource, ReviewerResource, AddReviewerInput, AddReviewerResult> { ChangeResource, ReviewerResource, AddReviewerInput, AddReviewerResult> {
private final ChangeData.Factory changeDataFactory; private final ChangeData.Factory changeDataFactory;
private final NotifyResolver notifyResolver;
private final ReviewerAdder reviewerAdder; private final ReviewerAdder reviewerAdder;
@Inject @Inject
PostReviewers( PostReviewers(
ChangeData.Factory changeDataFactory, RetryHelper retryHelper, ReviewerAdder reviewerAdder) { ChangeData.Factory changeDataFactory,
RetryHelper retryHelper,
NotifyResolver notifyResolver,
ReviewerAdder reviewerAdder) {
super(retryHelper); super(retryHelper);
this.changeDataFactory = changeDataFactory; this.changeDataFactory = changeDataFactory;
this.notifyResolver = notifyResolver;
this.reviewerAdder = reviewerAdder; this.reviewerAdder = reviewerAdder;
} }
@@ -67,6 +74,7 @@ public class PostReviewers
} }
try (BatchUpdate bu = try (BatchUpdate bu =
updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
bu.setNotify(resolveNotify(rsrc, input));
Change.Id id = rsrc.getChange().getId(); Change.Id id = rsrc.getChange().getId();
bu.addOp(id, addition.op); bu.addOp(id, addition.op);
bu.execute(); bu.execute();
@@ -76,4 +84,14 @@ public class PostReviewers
addition.gatherResults(changeDataFactory.create(rsrc.getProject(), rsrc.getId())); addition.gatherResults(changeDataFactory.create(rsrc.getProject(), rsrc.getId()));
return addition.result; return addition.result;
} }
private NotifyResolver.Result resolveNotify(ChangeResource rsrc, AddReviewerInput input)
throws BadRequestException, OrmException, ConfigInvalidException, IOException {
NotifyHandling notifyHandling = input.notify;
if (notifyHandling == null) {
notifyHandling =
rsrc.getChange().isWorkInProgress() ? NotifyHandling.NONE : NotifyHandling.ALL;
}
return notifyResolver.resolve(notifyHandling, input.notifyDetails);
}
} }

View File

@@ -99,6 +99,7 @@ public class PutAssignee extends RetryingRestModifyView<ChangeResource, Assignee
bu.addOp(rsrc.getId(), op); bu.addOp(rsrc.getId(), op);
ReviewerAddition reviewersAddition = addAssigneeAsCC(rsrc, input.assignee); ReviewerAddition reviewersAddition = addAssigneeAsCC(rsrc, input.assignee);
reviewersAddition.op.suppressEmail();
bu.addOp(rsrc.getId(), reviewersAddition.op); bu.addOp(rsrc.getId(), reviewersAddition.op);
bu.execute(); bu.execute();

View File

@@ -140,8 +140,7 @@ public class PutMessage
inserter.setMessage( inserter.setMessage(
String.format("Patch Set %s: Commit message was updated.", psId.getId())); String.format("Patch Set %s: Commit message was updated.", psId.getId()));
inserter.setDescription("Edit commit message"); inserter.setDescription("Edit commit message");
NotifyResolver.Result notify = resolveNotify(input, resource); bu.setNotify(resolveNotify(input, resource));
inserter.setNotify(notify);
bu.addOp(resource.getChange().getId(), inserter); bu.addOp(resource.getChange().getId(), inserter);
bu.execute(); bu.execute();
} }

View File

@@ -33,6 +33,7 @@ import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeJson; import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.RebaseChangeOp; import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.change.RebaseUtil; import com.google.gerrit.server.change.RebaseUtil;
import com.google.gerrit.server.change.RebaseUtil.Base; import com.google.gerrit.server.change.RebaseUtil.Base;
@@ -120,6 +121,8 @@ public class Rebase extends RetryingRestModifyView<RevisionResource, RebaseInput
throw new ResourceConflictException( throw new ResourceConflictException(
"cannot rebase merge commits or commit with no ancestor"); "cannot rebase merge commits or commit with no ancestor");
} }
// TODO(dborowitz): Why no notification? This seems wrong; dig up blame.
bu.setNotify(NotifyResolver.Result.none());
bu.setRepository(repo, rw, oi); bu.setRepository(repo, rw, oi);
bu.addOp( bu.addOp(
change.getId(), change.getId(),

View File

@@ -224,7 +224,6 @@ public class Revert extends RetryingRestModifyView<ChangeResource, RevertInput,
.create(changeId, revertCommit, notes.getChange().getDest().get()) .create(changeId, revertCommit, notes.getChange().getDest().get())
.setTopic(changeToRevert.getTopic()); .setTopic(changeToRevert.getTopic());
ins.setMessage("Uploaded patch set 1."); ins.setMessage("Uploaded patch set 1.");
ins.setNotify(notify);
ReviewerSet reviewerSet = approvalsUtil.getReviewers(notes); ReviewerSet reviewerSet = approvalsUtil.getReviewers(notes);
@@ -239,8 +238,9 @@ public class Revert extends RetryingRestModifyView<ChangeResource, RevertInput,
try (BatchUpdate bu = updateFactory.create(project, user, now)) { try (BatchUpdate bu = updateFactory.create(project, user, now)) {
bu.setRepository(git, revWalk, oi); bu.setRepository(git, revWalk, oi);
bu.setNotify(notify);
bu.insertChange(ins); bu.insertChange(ins);
bu.addOp(changeId, new NotifyOp(changeToRevert, ins, notify)); bu.addOp(changeId, new NotifyOp(changeToRevert, ins));
bu.addOp(changeToRevert.getId(), new PostRevertedMessageOp(computedChangeId)); bu.addOp(changeToRevert.getId(), new PostRevertedMessageOp(computedChangeId));
bu.execute(); bu.execute();
} }
@@ -275,12 +275,10 @@ public class Revert extends RetryingRestModifyView<ChangeResource, RevertInput,
private class NotifyOp implements BatchUpdateOp { private class NotifyOp implements BatchUpdateOp {
private final Change change; private final Change change;
private final ChangeInserter ins; private final ChangeInserter ins;
private final NotifyResolver.Result notify;
NotifyOp(Change change, ChangeInserter ins, NotifyResolver.Result notify) { NotifyOp(Change change, ChangeInserter ins) {
this.change = change; this.change = change;
this.ins = ins; this.ins = ins;
this.notify = notify;
} }
@Override @Override
@@ -289,7 +287,7 @@ public class Revert extends RetryingRestModifyView<ChangeResource, RevertInput,
try { try {
RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId()); RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
cm.setFrom(ctx.getAccountId()); cm.setFrom(ctx.getAccountId());
cm.setNotify(notify); cm.setNotify(ctx.getNotify(change.getId()));
cm.send(); cm.send();
} catch (Exception err) { } catch (Exception err) {
logger.atSevere().withCause(err).log( logger.atSevere().withCause(err).log(

View File

@@ -14,9 +14,11 @@
package com.google.gerrit.server.restapi.change; package com.google.gerrit.server.restapi.change;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.gerrit.extensions.conditions.BooleanCondition.and; import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
import static com.google.gerrit.extensions.conditions.BooleanCondition.or; import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestApiException;
@@ -26,6 +28,7 @@ import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.WorkInProgressOp; import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.change.WorkInProgressOp.Input; import com.google.gerrit.server.change.WorkInProgressOp.Input;
import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.GlobalPermission;
@@ -77,6 +80,7 @@ public class SetReadyForReview extends RetryingRestModifyView<ChangeResource, In
try (BatchUpdate bu = try (BatchUpdate bu =
updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.ALL)));
bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input)); bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input));
bu.execute(); bu.execute();
return Response.ok(""); return Response.ok("");

View File

@@ -14,9 +14,11 @@
package com.google.gerrit.server.restapi.change; package com.google.gerrit.server.restapi.change;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.gerrit.extensions.conditions.BooleanCondition.and; import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
import static com.google.gerrit.extensions.conditions.BooleanCondition.or; import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestApiException;
@@ -26,6 +28,7 @@ import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.WorkInProgressOp; import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.change.WorkInProgressOp.Input; import com.google.gerrit.server.change.WorkInProgressOp.Input;
import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.GlobalPermission;
@@ -77,6 +80,7 @@ public class SetWorkInProgress extends RetryingRestModifyView<ChangeResource, In
try (BatchUpdate bu = try (BatchUpdate bu =
updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.NONE)));
bu.addOp(rsrc.getChange().getId(), opFactory.create(true, input)); bu.addOp(rsrc.getChange().getId(), opFactory.create(true, input));
bu.execute(); bu.execute();
return Response.ok(""); return Response.ok("");

View File

@@ -533,7 +533,7 @@ public class MergeOp implements AutoCloseable {
orm.close(); orm.close();
} }
orm = ormProvider.get(); orm = ormProvider.get();
orm.setContext(ts, caller); orm.setContext(ts, caller, notify);
} }
private ChangeSet reloadChanges(ChangeSet changeSet) { private ChangeSet reloadChanges(ChangeSet changeSet) {
@@ -676,7 +676,6 @@ public class MergeOp implements AutoCloseable {
commitStatus, commitStatus,
submissionId, submissionId,
submitInput, submitInput,
notify,
submoduleOp, submoduleOp,
dryrun); dryrun);
strategies.add(strategy); strategies.add(strategy);

View File

@@ -15,12 +15,14 @@
package com.google.gerrit.server.submit; package com.google.gerrit.server.submit;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.git.CodeReviewCommit; import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.GitRepositoryManager;
@@ -110,6 +112,7 @@ public class MergeOpRepoManager implements AutoCloseable {
batchUpdateFactory batchUpdateFactory
.create(getProjectName(), caller, ts) .create(getProjectName(), caller, ts)
.setRepository(repo, rw, ins) .setRepository(repo, rw, ins)
.setNotify(notify)
.setOnSubmitValidators(onSubmitValidatorsFactory.create()); .setOnSubmitValidators(onSubmitValidatorsFactory.create());
} }
return update; return update;
@@ -158,6 +161,7 @@ public class MergeOpRepoManager implements AutoCloseable {
private Timestamp ts; private Timestamp ts;
private IdentifiedUser caller; private IdentifiedUser caller;
private NotifyResolver.Result notify;
@Inject @Inject
MergeOpRepoManager( MergeOpRepoManager(
@@ -173,9 +177,10 @@ public class MergeOpRepoManager implements AutoCloseable {
openRepos = new HashMap<>(); openRepos = new HashMap<>();
} }
public void setContext(Timestamp ts, IdentifiedUser caller) { public void setContext(Timestamp ts, IdentifiedUser caller, NotifyResolver.Result notify) {
this.ts = ts; this.ts = requireNonNull(ts);
this.caller = caller; this.caller = requireNonNull(caller);
this.notify = requireNonNull(notify);
} }
public OpenRepo getRepo(Project.NameKey project) throws NoSuchProjectException, IOException { public OpenRepo getRepo(Project.NameKey project) throws NoSuchProjectException, IOException {
@@ -200,7 +205,7 @@ public class MergeOpRepoManager implements AutoCloseable {
throws NoSuchProjectException, IOException { throws NoSuchProjectException, IOException {
List<BatchUpdate> updates = new ArrayList<>(projects.size()); List<BatchUpdate> updates = new ArrayList<>(projects.size());
for (Project.NameKey project : projects) { for (Project.NameKey project : projects) {
updates.add(getRepo(project).getUpdate().setRefLogMessage("merged")); updates.add(getRepo(project).getUpdate().setNotify(notify).setRefLogMessage("merged"));
} }
return updates; return updates;
} }

View File

@@ -29,7 +29,6 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.change.LabelNormalizer; import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.RebaseChangeOp; import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.change.TestSubmitInput; import com.google.gerrit.server.change.TestSubmitInput;
import com.google.gerrit.server.extensions.events.ChangeMerged; import com.google.gerrit.server.extensions.events.ChangeMerged;
@@ -92,7 +91,6 @@ public abstract class SubmitStrategy {
Set<CodeReviewCommit> incoming, Set<CodeReviewCommit> incoming,
RequestId submissionId, RequestId submissionId,
SubmitInput submitInput, SubmitInput submitInput,
NotifyResolver.Result notify,
SubmoduleOp submoduleOp, SubmoduleOp submoduleOp,
boolean dryrun); boolean dryrun);
} }
@@ -124,7 +122,6 @@ public abstract class SubmitStrategy {
final RequestId submissionId; final RequestId submissionId;
final SubmitType submitType; final SubmitType submitType;
final SubmitInput submitInput; final SubmitInput submitInput;
final NotifyResolver.Result notify;
final SubmoduleOp submoduleOp; final SubmoduleOp submoduleOp;
final ProjectState project; final ProjectState project;
@@ -163,7 +160,6 @@ public abstract class SubmitStrategy {
@Assisted RequestId submissionId, @Assisted RequestId submissionId,
@Assisted SubmitType submitType, @Assisted SubmitType submitType,
@Assisted SubmitInput submitInput, @Assisted SubmitInput submitInput,
@Assisted NotifyResolver.Result notify,
@Assisted SubmoduleOp submoduleOp, @Assisted SubmoduleOp submoduleOp,
@Assisted boolean dryrun) { @Assisted boolean dryrun) {
this.accountCache = accountCache; this.accountCache = accountCache;
@@ -192,7 +188,6 @@ public abstract class SubmitStrategy {
this.submissionId = submissionId; this.submissionId = submissionId;
this.submitType = submitType; this.submitType = submitType;
this.submitInput = submitInput; this.submitInput = submitInput;
this.notify = notify;
this.submoduleOp = submoduleOp; this.submoduleOp = submoduleOp;
this.dryrun = dryrun; this.dryrun = dryrun;

View File

@@ -19,7 +19,6 @@ import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.git.CodeReviewCommit; import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.MergeTip; import com.google.gerrit.server.git.MergeTip;
@@ -55,7 +54,6 @@ public class SubmitStrategyFactory {
CommitStatus commitStatus, CommitStatus commitStatus,
RequestId submissionId, RequestId submissionId,
SubmitInput submitInput, SubmitInput submitInput,
NotifyResolver.Result notify,
SubmoduleOp submoduleOp, SubmoduleOp submoduleOp,
boolean dryrun) boolean dryrun)
throws IntegrationException { throws IntegrationException {
@@ -72,7 +70,6 @@ public class SubmitStrategyFactory {
incoming, incoming,
submissionId, submissionId,
submitInput, submitInput,
notify,
submoduleOp, submoduleOp,
dryrun); dryrun);
switch (submitType) { switch (submitType) {

View File

@@ -501,7 +501,7 @@ abstract class SubmitStrategyOp implements BatchUpdateOp {
// have failed fast in one of the other steps. // have failed fast in one of the other steps.
try { try {
args.mergedSenderFactory args.mergedSenderFactory
.create(ctx.getProject(), getId(), submitter.getAccountId(), args.notify) .create(ctx.getProject(), getId(), submitter.getAccountId(), ctx.getNotify(getId()))
.sendAsync(); .sendAsync();
} catch (Exception e) { } catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email merged notification for %s", getId()); logger.atSevere().withCause(e).log("Cannot email merged notification for %s", getId());

View File

@@ -30,6 +30,7 @@ import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multiset; import com.google.common.collect.Multiset;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -40,6 +41,7 @@ import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.validators.OnSubmitValidators; import com.google.gerrit.server.git.validators.OnSubmitValidators;
@@ -229,6 +231,12 @@ public class BatchUpdate implements AutoCloseable {
public CurrentUser getUser() { public CurrentUser getUser() {
return user; return user;
} }
@Override
public NotifyResolver.Result getNotify(Change.Id changeId) {
NotifyHandling notifyHandling = perChangeNotifyHandling.get(changeId);
return notifyHandling != null ? notify.withHandling(notifyHandling) : notify;
}
} }
private class RepoContextImpl extends ContextImpl implements RepoContext { private class RepoContextImpl extends ContextImpl implements RepoContext {
@@ -302,12 +310,14 @@ public class BatchUpdate implements AutoCloseable {
MultimapBuilder.linkedHashKeys().arrayListValues().build(); MultimapBuilder.linkedHashKeys().arrayListValues().build();
private final Map<Change.Id, Change> newChanges = new HashMap<>(); private final Map<Change.Id, Change> newChanges = new HashMap<>();
private final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>(); private final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>();
private final Map<Change.Id, NotifyHandling> perChangeNotifyHandling = new HashMap<>();
private RepoView repoView; private RepoView repoView;
private BatchRefUpdate batchRefUpdate; private BatchRefUpdate batchRefUpdate;
private OnSubmitValidators onSubmitValidators; private OnSubmitValidators onSubmitValidators;
private PushCertificate pushCert; private PushCertificate pushCert;
private String refLogMessage; private String refLogMessage;
private NotifyResolver.Result notify = NotifyResolver.Result.all();
@Inject @Inject
BatchUpdate( BatchUpdate(
@@ -364,6 +374,32 @@ public class BatchUpdate implements AutoCloseable {
return this; return this;
} }
/**
* Set the default notification settings for all changes in the batch.
*
* @param notify notification settings.
* @return this.
*/
public BatchUpdate setNotify(NotifyResolver.Result notify) {
this.notify = requireNonNull(notify);
return this;
}
/**
* Override the {@link NotifyHandling} on a per-change basis.
*
* <p>Only the handling enum can be overridden; all changes share the same value for {@link
* NotifyResolver.Result#accounts()}.
*
* @param changeId change ID.
* @param notifyHandling notify handling.
* @return this.
*/
public BatchUpdate setNotifyHandling(Change.Id changeId, NotifyHandling notifyHandling) {
this.perChangeNotifyHandling.put(changeId, requireNonNull(notifyHandling));
return this;
}
/** /**
* Add a validation step for intended ref operations, which will be performed at the end of {@link * Add a validation step for intended ref operations, which will be performed at the end of {@link
* RepoOnlyOp#updateRepo(RepoContext)} step. * RepoOnlyOp#updateRepo(RepoContext)} step.

View File

@@ -17,10 +17,12 @@ package com.google.gerrit.server.update;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.change.NotifyResolver;
import java.io.IOException; import java.io.IOException;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.TimeZone; import java.util.TimeZone;
@@ -85,6 +87,18 @@ public interface Context {
*/ */
CurrentUser getUser(); CurrentUser getUser();
/**
* Get the notification settings configured by the caller.
*
* <p>If there are multiple changes in a batch, they may have different settings. For example, WIP
* changes may have reduced {@code NotifyHandling} levels, and may be in a batch with non-WIP
* changes.
*
* @param changeId change ID
* @return notification settings.
*/
NotifyResolver.Result getNotify(Change.Id changeId);
/** /**
* Get the identified user performing the update. * Get the identified user performing the update.
* *

View File

@@ -749,11 +749,11 @@ public class ConsistencyCheckerIT extends AbstractDaemonTest {
ChangeInserter ins; ChangeInserter ins;
try (BatchUpdate bu = newUpdate(owner.getId())) { try (BatchUpdate bu = newUpdate(owner.getId())) {
RevCommit commit = patchSetCommit(new PatchSet.Id(id, 1)); RevCommit commit = patchSetCommit(new PatchSet.Id(id, 1));
bu.setNotify(NotifyResolver.Result.none());
ins = ins =
changeInserterFactory changeInserterFactory
.create(id, commit, dest) .create(id, commit, dest)
.setValidate(false) .setValidate(false)
.setNotify(NotifyResolver.Result.none())
.setFireRevisionCreated(false) .setFireRevisionCreated(false)
.setSendMail(false); .setSendMail(false);
bu.insertChange(ins).execute(); bu.insertChange(ins).execute();
@@ -773,12 +773,12 @@ public class ConsistencyCheckerIT extends AbstractDaemonTest {
private ChangeNotes incrementPatchSet(ChangeNotes notes, RevCommit commit) throws Exception { private ChangeNotes incrementPatchSet(ChangeNotes notes, RevCommit commit) throws Exception {
PatchSetInserter ins; PatchSetInserter ins;
try (BatchUpdate bu = newUpdate(notes.getChange().getOwner())) { try (BatchUpdate bu = newUpdate(notes.getChange().getOwner())) {
bu.setNotify(NotifyResolver.Result.none());
ins = ins =
patchSetInserterFactory patchSetInserterFactory
.create(notes, nextPatchSetId(notes), commit) .create(notes, nextPatchSetId(notes), commit)
.setValidate(false) .setValidate(false)
.setFireRevisionCreated(false) .setFireRevisionCreated(false);
.setNotify(NotifyResolver.Result.none());
bu.addOp(notes.getChangeId(), ins).execute(); bu.addOp(notes.getChangeId(), ins).execute();
} }
return reload(notes); return reload(notes);

View File

@@ -3176,7 +3176,6 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
PatchSetInserter inserter = PatchSetInserter inserter =
patchSetFactory patchSetFactory
.create(changeNotesFactory.createChecked(c), new PatchSet.Id(c.getId(), n), commit) .create(changeNotesFactory.createChecked(c), new PatchSet.Id(c.getId(), n), commit)
.setNotify(NotifyResolver.Result.none())
.setFireRevisionCreated(false) .setFireRevisionCreated(false)
.setValidate(false); .setValidate(false);
try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.nowTs()); try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.nowTs());
@@ -3184,6 +3183,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
ObjectReader reader = oi.newReader(); ObjectReader reader = oi.newReader();
RevWalk rw = new RevWalk(reader)) { RevWalk rw = new RevWalk(reader)) {
bu.setRepository(repo.getRepository(), rw, oi); bu.setRepository(repo.getRepository(), rw, oi);
bu.setNotify(NotifyResolver.Result.none());
bu.addOp(c.getId(), inserter); bu.addOp(c.getId(), inserter);
bu.execute(); bu.execute();
} }