Merge "Merge branch 'stable-2.15'"
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index a4945c7..7a30f0c 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -319,7 +319,8 @@
public final String ccerByEmail = "ccByEmail@example.com";
private final Map<NotifyType, TestAccount> watchers = new HashMap<>();
private final Map<String, TestAccount> accountsByEmail = new HashMap<>();
- boolean supportReviewersByEmail;
+
+ public boolean supportReviewersByEmail;
private String usersCacheKey() {
return description.getClassName();
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index e558d00..2ed0f20 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.change;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.extensions.client.ReviewerState.CC;
@@ -163,6 +164,10 @@
this.accountsToNotify = accountsToNotify;
}
+ void setPatchSet(PatchSet patchSet) {
+ this.patchSet = checkNotNull(patchSet);
+ }
+
@Override
public boolean updateChange(ChangeContext ctx)
throws RestApiException, OrmException, IOException {
@@ -207,7 +212,9 @@
checkAdded();
- patchSet = psUtil.current(ctx.getDb(), ctx.getNotes());
+ if (patchSet == null) {
+ patchSet = checkNotNull(psUtil.current(ctx.getDb(), ctx.getNotes()));
+ }
return true;
}
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 38c97f7..831d388 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -16,32 +16,42 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
+import static com.google.gerrit.server.change.ReviewerAdder.newAddReviewerInputFromCommitIdentity;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
-import static java.util.stream.Collectors.toSet;
import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.change.ReviewerAdder.InternalAddReviewerInput;
+import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
+import com.google.gerrit.server.change.ReviewerAdder.ReviewerAdditionList;
import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.extensions.events.CommentAdded;
@@ -50,11 +60,8 @@
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.mail.send.CreateChangeSender;
-import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
-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.project.ProjectCache;
@@ -71,12 +78,12 @@
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
+import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -101,7 +108,7 @@
private final CommitValidators.Factory commitValidatorsFactory;
private final RevisionCreated revisionCreated;
private final CommentAdded commentAdded;
- private final NotesMigration migration;
+ private final ReviewerAdder reviewerAdder;
private final Change.Id changeId;
private final PatchSet.Id psId;
@@ -119,14 +126,13 @@
private boolean validate = true;
private NotifyHandling notify = NotifyHandling.ALL;
private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
- private Set<Account.Id> reviewers;
- private Set<Account.Id> extraCC;
private Map<String, Short> approvals;
private RequestScopePropagator requestScopePropagator;
private boolean fireRevisionCreated;
private boolean sendMail;
private boolean updateRef;
private Change.Id revertOf;
+ private ImmutableList<InternalAddReviewerInput> reviewerInputs;
// Fields set during the insertion process.
private ReceiveCommand cmd;
@@ -136,6 +142,7 @@
private PatchSet patchSet;
private String pushCert;
private ProjectState projectState;
+ private ReviewerAdditionList reviewerAdditions;
@Inject
ChangeInserter(
@@ -150,7 +157,7 @@
CommitValidators.Factory commitValidatorsFactory,
CommentAdded commentAdded,
RevisionCreated revisionCreated,
- NotesMigration migration,
+ ReviewerAdder reviewerAdder,
@Assisted Change.Id changeId,
@Assisted ObjectId commitId,
@Assisted String refName) {
@@ -165,14 +172,13 @@
this.commitValidatorsFactory = commitValidatorsFactory;
this.revisionCreated = revisionCreated;
this.commentAdded = commentAdded;
- this.migration = migration;
+ this.reviewerAdder = reviewerAdder;
this.changeId = changeId;
this.psId = new PatchSet.Id(changeId, INITIAL_PATCH_SET_ID);
this.commitId = commitId.copy();
this.refName = refName;
- this.reviewers = Collections.emptySet();
- this.extraCC = Collections.emptySet();
+ this.reviewerInputs = ImmutableList.of();
this.approvals = Collections.emptyMap();
this.fireRevisionCreated = true;
this.sendMail = true;
@@ -261,13 +267,22 @@
return this;
}
- public ChangeInserter setReviewers(Set<Account.Id> reviewers) {
- this.reviewers = reviewers;
- return this;
+ public ChangeInserter setReviewersAndCcs(
+ Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) {
+ return setReviewersAndCcsAsStrings(
+ Iterables.transform(reviewers, Account.Id::toString),
+ Iterables.transform(ccs, Account.Id::toString));
}
- public ChangeInserter setExtraCC(Set<Account.Id> extraCC) {
- this.extraCC = extraCC;
+ public ChangeInserter setReviewersAndCcsAsStrings(
+ Iterable<String> reviewers, Iterable<String> ccs) {
+ reviewerInputs =
+ Streams.concat(
+ Streams.stream(reviewers)
+ .distinct()
+ .map(id -> newAddReviewerInput(id, ReviewerState.REVIEWER)),
+ Streams.stream(ccs).distinct().map(id -> newAddReviewerInput(id, ReviewerState.CC)))
+ .collect(toImmutableList());
return this;
}
@@ -371,7 +386,8 @@
@Override
public boolean updateChange(ChangeContext ctx)
- throws RestApiException, OrmException, IOException, PermissionBackendException {
+ throws RestApiException, OrmException, IOException, PermissionBackendException,
+ ConfigInvalidException {
change = ctx.getChange(); // Use defensive copy created by ChangeControl.
ReviewDb db = ctx.getDb();
patchSetInfo =
@@ -415,29 +431,23 @@
*/
update.fixStatus(change.getStatus());
- Set<Account.Id> reviewersToAdd = new HashSet<>(reviewers);
- if (migration.readChanges()) {
- approvalsUtil.addCcs(
- ctx.getNotes(), update, filterOnChangeVisibility(db, ctx.getNotes(), extraCC));
- } else {
- reviewersToAdd.addAll(extraCC);
+ reviewerAdditions =
+ reviewerAdder.prepare(
+ ctx.getDb(), ctx.getNotes(), ctx.getUser(), getReviewerInputs(), true);
+ Optional<ReviewerAddition> reviewerError = reviewerAdditions.getFailures().stream().findFirst();
+ if (reviewerError.isPresent()) {
+ throw new UnprocessableEntityException(reviewerError.get().result.error);
}
+ reviewerAdditions.updateChange(ctx, patchSet);
LabelTypes labelTypes = projectState.getLabelTypes();
- approvalsUtil.addReviewers(
- db,
- update,
- labelTypes,
- change,
- patchSet,
- patchSetInfo,
- filterOnChangeVisibility(db, ctx.getNotes(), reviewersToAdd),
- Collections.<Account.Id>emptySet());
approvalsUtil.addApprovalsForNewPatchSet(
db, update, labelTypes, patchSet, ctx.getUser(), approvals);
+
// Check if approvals are changing in with this update. If so, add current user to reviewers.
// Note that this is done separately as addReviewers is filtering out the change owner as
// reviewer which is needed in several other code paths.
+ // TODO(dborowitz): Still necessary?
if (!approvals.isEmpty()) {
update.putReviewer(ctx.getAccountId(), REVIEWER);
}
@@ -454,37 +464,9 @@
return true;
}
- private Set<Account.Id> filterOnChangeVisibility(
- final ReviewDb db, ChangeNotes notes, Set<Account.Id> accounts) {
- return accounts
- .stream()
- .filter(
- accountId -> {
- if (!projectState.statePermitsRead()) {
- return false;
- }
-
- try {
- permissionBackend
- .absentUser(accountId)
- .change(notes)
- .database(db)
- .check(ChangePermission.READ);
- return true;
- } catch (PermissionBackendException e) {
- logger.atWarning().withCause(e).log(
- "Failed to check if account %d can see change %d",
- accountId.get(), notes.getChangeId().get());
- return false;
- } catch (AuthException e) {
- return false;
- }
- })
- .collect(toSet());
- }
-
@Override
- public void postUpdate(Context ctx) throws OrmException, IOException {
+ public void postUpdate(Context ctx) throws Exception {
+ reviewerAdditions.postUpdate(ctx);
if (sendMail && (notify != NotifyHandling.NONE || !accountsToNotify.isEmpty())) {
Runnable sender =
new Runnable() {
@@ -497,8 +479,17 @@
cm.setPatchSet(patchSet, patchSetInfo);
cm.setNotify(notify);
cm.setAccountsToNotify(accountsToNotify);
- cm.addReviewers(reviewers);
- cm.addExtraCC(extraCC);
+ cm.addReviewers(
+ reviewerAdditions
+ .flattenResults(AddReviewersOp.Result::addedReviewers)
+ .stream()
+ .map(PatchSetApproval::getAccountId)
+ .collect(toImmutableSet()));
+ cm.addReviewersByEmail(
+ reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewersByEmail));
+ cm.addExtraCC(reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs));
+ cm.addExtraCCByEmail(
+ reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCsByEmail));
cm.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log(
@@ -575,4 +566,33 @@
throw new ResourceConflictException(e.getFullMessage());
}
}
+
+ private static InternalAddReviewerInput newAddReviewerInput(
+ String reviewer, ReviewerState state) {
+ // Disable individual emails when adding reviewers, as all reviewers will receive the single
+ // bulk new change email.
+ InternalAddReviewerInput input =
+ ReviewerAdder.newAddReviewerInput(reviewer, state, NotifyHandling.NONE);
+
+ // Ignore failures for reasons like the reviewer being inactive or being unable to see the
+ // change. This is required for the push path, where it automatically sets reviewers from
+ // certain commit footers: putting a nonexistent user in a footer should not cause an error. In
+ // theory we could provide finer control to do this for some reviewers and not others, but it's
+ // not worth complicating the ChangeInserter interface further at this time.
+ input.otherFailureBehavior = ReviewerAdder.FailureBehavior.IGNORE;
+
+ return input;
+ }
+
+ private ImmutableList<InternalAddReviewerInput> getReviewerInputs() {
+ return Streams.concat(
+ reviewerInputs.stream(),
+ Streams.stream(
+ newAddReviewerInputFromCommitIdentity(
+ change, patchSetInfo.getAuthor().getAccount(), NotifyHandling.NONE)),
+ Streams.stream(
+ newAddReviewerInputFromCommitIdentity(
+ change, patchSetInfo.getCommitter().getAccount(), NotifyHandling.NONE)))
+ .collect(toImmutableList());
+ }
}
diff --git a/java/com/google/gerrit/server/change/ReviewerAdder.java b/java/com/google/gerrit/server/change/ReviewerAdder.java
index f2be90d..441a7d4 100644
--- a/java/com/google/gerrit/server/change/ReviewerAdder.java
+++ b/java/com/google/gerrit/server/change/ReviewerAdder.java
@@ -17,14 +17,19 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.extensions.client.ReviewerState.CC;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import static java.util.Comparator.comparing;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Streams;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
@@ -36,12 +41,15 @@
import com.google.gerrit.extensions.common.AccountInfo;
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.UnprocessableEntityException;
import com.google.gerrit.mail.Address;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AnonymousUser;
@@ -63,13 +71,20 @@
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.Context;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
import java.util.Set;
+import java.util.function.Function;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@@ -77,12 +92,62 @@
public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
public static final int DEFAULT_MAX_REVIEWERS = 20;
+ public enum FailureBehavior {
+ FAIL,
+ IGNORE;
+ }
+
+ private enum FailureType {
+ NOT_FOUND,
+ OTHER;
+ }
+
+ // TODO(dborowitz): Subclassing is not the right way to do this. We should instead use an internal
+ // type in the public interfaces of ReviewerAdder, rather than passing around the REST API type
+ // internally.
+ public static class InternalAddReviewerInput extends AddReviewerInput {
+ /**
+ * Behavior when identifying reviewers fails for any reason <em>besides</em> the input not
+ * resolving to an account/group/email.
+ */
+ public FailureBehavior otherFailureBehavior = FailureBehavior.FAIL;
+ }
+
+ public static InternalAddReviewerInput newAddReviewerInput(
+ Account.Id reviewer, ReviewerState state, NotifyHandling notify) {
+ // AccountResolver always resolves by ID if the input string is numeric.
+ return newAddReviewerInput(reviewer.toString(), state, notify);
+ }
+
+ public static InternalAddReviewerInput newAddReviewerInput(
+ String reviewer, ReviewerState state, NotifyHandling notify) {
+ InternalAddReviewerInput in = new InternalAddReviewerInput();
+ in.reviewer = reviewer;
+ in.state = state;
+ in.notify = notify;
+ return in;
+ }
+
+ public static Optional<InternalAddReviewerInput> newAddReviewerInputFromCommitIdentity(
+ Change change, @Nullable Account.Id accountId, NotifyHandling notify) {
+ if (accountId == null || accountId.equals(change.getOwner())) {
+ // If git ident couldn't be resolved to a user, or if it's not forged, do nothing.
+ return Optional.empty();
+ }
+
+ InternalAddReviewerInput in = new InternalAddReviewerInput();
+ in.reviewer = accountId.toString();
+ in.state = REVIEWER;
+ in.notify = notify;
+ in.otherFailureBehavior = FailureBehavior.IGNORE;
+ return Optional.of(in);
+ }
+
private final AccountResolver accountResolver;
private final PermissionBackend permissionBackend;
private final GroupResolver groupResolver;
private final GroupMembers groupMembers;
private final AccountLoader.Factory accountLoaderFactory;
- private final Provider<ReviewDb> dbProvider;
private final Config cfg;
private final ReviewerJson json;
private final NotesMigration migration;
@@ -99,7 +164,6 @@
GroupResolver groupResolver,
GroupMembers groupMembers,
AccountLoader.Factory accountLoaderFactory,
- Provider<ReviewDb> db,
@GerritServerConfig Config cfg,
ReviewerJson json,
NotesMigration migration,
@@ -113,7 +177,6 @@
this.groupResolver = groupResolver;
this.groupMembers = groupMembers;
this.accountLoaderFactory = accountLoaderFactory;
- this.dbProvider = db;
this.cfg = cfg;
this.json = json;
this.migration = migration;
@@ -127,6 +190,7 @@
/**
* Prepare application of a single {@link AddReviewerInput}.
*
+ * @param db database.
* @param notes change notes.
* @param user user performing the reviewer addition.
* @param input input describing user or group to add as a reviewer.
@@ -140,41 +204,29 @@
* @throws ConfigInvalidException
*/
public ReviewerAddition prepare(
- ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
+ ReviewDb db, ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
- Branch.NameKey dest = notes.getChange().getDest();
- String reviewer = checkNotNull(input.reviewer);
- ReviewerState state = input.state();
- NotifyHandling notify = input.notify;
+ checkNotNull(input.reviewer);
ListMultimap<RecipientType, Account.Id> accountsToNotify;
try {
accountsToNotify = notifyUtil.resolveAccounts(input.notifyDetails);
} catch (BadRequestException e) {
- return fail(reviewer, e.getMessage());
+ return fail(input, FailureType.OTHER, e.getMessage());
}
boolean confirmed = input.confirmed();
boolean allowByEmail =
projectCache
- .checkedGet(dest.getParentKey())
+ .checkedGet(notes.getProjectName())
.is(BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL);
ReviewerAddition byAccountId =
- addByAccountId(
- reviewer, dest, user, state, notify, accountsToNotify, allowGroup, allowByEmail);
+ addByAccountId(db, input, notes, user, accountsToNotify, allowGroup, allowByEmail);
ReviewerAddition wholeGroup = null;
if (byAccountId == null || !byAccountId.exactMatchFound) {
wholeGroup =
addWholeGroup(
- reviewer,
- dest,
- user,
- state,
- notify,
- accountsToNotify,
- confirmed,
- allowGroup,
- allowByEmail);
+ db, input, notes, user, accountsToNotify, confirmed, allowGroup, allowByEmail);
if (wholeGroup != null && wholeGroup.exactMatchFound) {
return wholeGroup;
}
@@ -187,28 +239,26 @@
return wholeGroup;
}
- return addByEmail(reviewer, notes, user, state, notify, accountsToNotify);
+ return addByEmail(db, input, notes, user, accountsToNotify);
}
public ReviewerAddition ccCurrentUser(CurrentUser user, RevisionResource revision) {
return new ReviewerAddition(
- user.getUserName().orElse(null),
+ newAddReviewerInput(user.getUserName().orElse(null), CC, NotifyHandling.NONE),
+ revision.getNotes(),
revision.getUser(),
ImmutableSet.of(user.getAccountId()),
null,
- CC,
- NotifyHandling.NONE,
ImmutableListMultimap.of(),
true);
}
@Nullable
private ReviewerAddition addByAccountId(
- String reviewer,
- Branch.NameKey dest,
+ ReviewDb db,
+ AddReviewerInput input,
+ ChangeNotes notes,
CurrentUser user,
- ReviewerState state,
- NotifyHandling notify,
ListMultimap<RecipientType, Account.Id> accountsToNotify,
boolean allowGroup,
boolean allowByEmail)
@@ -216,9 +266,9 @@
IdentifiedUser reviewerUser;
boolean exactMatchFound = false;
try {
- reviewerUser = accountResolver.parse(reviewer);
- if (reviewer.equalsIgnoreCase(reviewerUser.getName())
- || reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
+ reviewerUser = accountResolver.parse(input.reviewer);
+ if (input.reviewer.equalsIgnoreCase(reviewerUser.getName())
+ || input.reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
exactMatchFound = true;
}
} catch (UnprocessableEntityException | AuthException e) {
@@ -226,39 +276,44 @@
if (!allowGroup && !allowByEmail) {
// Only return failure if we aren't going to try other interpretations.
return fail(
- reviewer, MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, reviewer));
+ input,
+ FailureType.NOT_FOUND,
+ MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
}
return null;
}
- if (isValidReviewer(dest, reviewerUser.getAccount())) {
+ if (isValidReviewer(db, notes.getChange().getDest(), reviewerUser.getAccount())) {
return new ReviewerAddition(
- reviewer,
+ input,
+ notes,
user,
ImmutableSet.of(reviewerUser.getAccountId()),
null,
- state,
- notify,
accountsToNotify,
exactMatchFound);
}
if (!reviewerUser.getAccount().isActive()) {
- if (allowByEmail && state == CC) {
+ if (allowByEmail && input.state() == CC) {
return null;
}
- return fail(reviewer, MessageFormat.format(ChangeMessages.get().reviewerInactive, reviewer));
+ return fail(
+ input,
+ FailureType.OTHER,
+ MessageFormat.format(ChangeMessages.get().reviewerInactive, input.reviewer));
}
return fail(
- reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer));
+ input,
+ FailureType.OTHER,
+ MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, input.reviewer));
}
@Nullable
private ReviewerAddition addWholeGroup(
- String reviewer,
- Branch.NameKey dest,
+ ReviewDb db,
+ AddReviewerInput input,
+ ChangeNotes notes,
CurrentUser user,
- ReviewerState state,
- NotifyHandling notify,
ListMultimap<RecipientType, Account.Id> accountsToNotify,
boolean confirmed,
boolean allowGroup,
@@ -270,27 +325,32 @@
GroupDescription.Basic group;
try {
- group = groupResolver.parseInternal(reviewer);
+ // TODO(dborowitz): This currently doesn't work in the push path because InternalGroupBackend
+ // depends on the Provider<CurrentUser> which returns anonymous in that path.
+ group = groupResolver.parseInternal(input.reviewer);
} catch (UnprocessableEntityException e) {
if (!allowByEmail) {
return fail(
- reviewer,
- MessageFormat.format(ChangeMessages.get().reviewerNotFoundUserOrGroup, reviewer));
+ input,
+ FailureType.NOT_FOUND,
+ MessageFormat.format(ChangeMessages.get().reviewerNotFoundUserOrGroup, input.reviewer));
}
return null;
}
if (!isLegalReviewerGroup(group.getGroupUUID())) {
return fail(
- reviewer, MessageFormat.format(ChangeMessages.get().groupIsNotAllowed, group.getName()));
+ input,
+ FailureType.OTHER,
+ MessageFormat.format(ChangeMessages.get().groupIsNotAllowed, group.getName()));
}
Set<Account.Id> reviewers = new HashSet<>();
Set<Account> members;
try {
- members = groupMembers.listAccounts(group.getGroupUUID(), dest.getParentKey());
+ members = groupMembers.listAccounts(group.getGroupUUID(), notes.getProjectName());
} catch (NoSuchProjectException e) {
- return fail(reviewer, e.getMessage());
+ return fail(input, FailureType.OTHER, e.getMessage());
}
// if maxAllowed is set to 0, it is allowed to add any number of
@@ -298,7 +358,8 @@
int maxAllowed = cfg.getInt("addreviewer", "maxAllowed", DEFAULT_MAX_REVIEWERS);
if (maxAllowed > 0 && members.size() > maxAllowed) {
return fail(
- reviewer,
+ input,
+ FailureType.OTHER,
MessageFormat.format(ChangeMessages.get().groupHasTooManyMembers, group.getName()));
}
@@ -307,56 +368,62 @@
cfg.getInt("addreviewer", "maxWithoutConfirmation", DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
if (!confirmed && maxWithoutConfirmation > 0 && members.size() > maxWithoutConfirmation) {
return fail(
- reviewer,
+ input,
+ FailureType.OTHER,
true,
MessageFormat.format(
ChangeMessages.get().groupManyMembersConfirmation, group.getName(), members.size()));
}
for (Account member : members) {
- if (isValidReviewer(dest, member)) {
+ if (isValidReviewer(db, notes.getChange().getDest(), member)) {
reviewers.add(member.getId());
}
}
- return new ReviewerAddition(
- reviewer, user, reviewers, null, state, notify, accountsToNotify, true);
+ return new ReviewerAddition(input, notes, user, reviewers, null, accountsToNotify, true);
}
@Nullable
private ReviewerAddition addByEmail(
- String reviewer,
+ ReviewDb db,
+ AddReviewerInput input,
ChangeNotes notes,
CurrentUser user,
- ReviewerState state,
- NotifyHandling notify,
ListMultimap<RecipientType, Account.Id> accountsToNotify)
throws PermissionBackendException {
try {
permissionBackend
.user(anonymousProvider.get())
- .database(dbProvider)
+ .database(db)
.change(notes)
.check(ChangePermission.READ);
} catch (AuthException e) {
return fail(
- reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer));
+ input,
+ FailureType.OTHER,
+ MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, input.reviewer));
}
if (!migration.readChanges()) {
// addByEmail depends on NoteDb.
return fail(
- reviewer, MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, reviewer));
+ input,
+ FailureType.NOT_FOUND,
+ MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
}
- Address adr = Address.tryParse(reviewer);
+ Address adr = Address.tryParse(input.reviewer);
if (adr == null || !validator.isValid(adr.getEmail())) {
- return fail(reviewer, MessageFormat.format(ChangeMessages.get().reviewerInvalid, reviewer));
+ return fail(
+ input,
+ FailureType.NOT_FOUND,
+ MessageFormat.format(ChangeMessages.get().reviewerInvalid, input.reviewer));
}
return new ReviewerAddition(
- reviewer, user, null, ImmutableList.of(adr), state, notify, accountsToNotify, true);
+ input, notes, user, null, ImmutableList.of(adr), accountsToNotify, true);
}
- private boolean isValidReviewer(Branch.NameKey branch, Account member)
+ private boolean isValidReviewer(ReviewDb db, Branch.NameKey branch, Account member)
throws PermissionBackendException {
if (!member.isActive()) {
return false;
@@ -368,7 +435,7 @@
// see private changes.
permissionBackend
.absentUser(member.getId())
- .database(dbProvider)
+ .database(db)
.ref(branch)
.check(RefPermission.READ);
return true;
@@ -377,12 +444,13 @@
}
}
- private ReviewerAddition fail(String reviewer, String error) {
- return fail(reviewer, false, error);
+ private ReviewerAddition fail(AddReviewerInput input, FailureType failureType, String error) {
+ return fail(input, failureType, false, error);
}
- private ReviewerAddition fail(String reviewer, boolean confirm, String error) {
- ReviewerAddition addition = new ReviewerAddition(reviewer);
+ private ReviewerAddition fail(
+ AddReviewerInput input, FailureType failureType, boolean confirm, String error) {
+ ReviewerAddition addition = new ReviewerAddition(input, failureType);
addition.result.confirm = confirm ? true : null;
addition.result.error = error;
return addition;
@@ -393,45 +461,57 @@
@Nullable public final AddReviewersOp op;
public final ImmutableSet<Account.Id> reviewers;
public final ImmutableSet<Address> reviewersByEmail;
- public final ReviewerState state;
@Nullable final IdentifiedUser caller;
final boolean exactMatchFound;
+ private final AddReviewerInput input;
+ @Nullable private final FailureType failureType;
- private ReviewerAddition(String reviewer) {
- result = new AddReviewerResult(reviewer);
+ private ReviewerAddition(AddReviewerInput input, FailureType failureType) {
+ this.input = input;
+ this.failureType = checkNotNull(failureType);
+ result = new AddReviewerResult(input.reviewer);
op = null;
reviewers = ImmutableSet.of();
reviewersByEmail = ImmutableSet.of();
- state = REVIEWER;
caller = null;
exactMatchFound = false;
}
private ReviewerAddition(
- String reviewer,
+ AddReviewerInput input,
+ ChangeNotes notes,
CurrentUser caller,
@Nullable Iterable<Account.Id> reviewers,
@Nullable Iterable<Address> reviewersByEmail,
- ReviewerState state,
- @Nullable NotifyHandling notify,
ListMultimap<RecipientType, Account.Id> accountsToNotify,
boolean exactMatchFound) {
checkArgument(
reviewers != null || reviewersByEmail != null,
"must have either reviewers or reviewersByEmail");
- result = new AddReviewerResult(reviewer);
- this.reviewers = reviewers == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewers);
+ this.input = input;
+ this.failureType = null;
+ result = new AddReviewerResult(input.reviewer);
+ // Always silently ignore adding the owner as any type of reviewer on their own change. They
+ // may still be implicitly added as a reviewer if they vote, but not via the reviewer API.
+ this.reviewers = omitOwner(notes, reviewers);
this.reviewersByEmail =
reviewersByEmail == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewersByEmail);
- this.state = state;
this.caller = caller.asIdentifiedUser();
op =
addReviewersOpFactory.create(
- this.reviewers, this.reviewersByEmail, state, notify, accountsToNotify);
+ this.reviewers, this.reviewersByEmail, state(), input.notify, accountsToNotify);
this.exactMatchFound = exactMatchFound;
}
+ private ImmutableSet<Account.Id> omitOwner(ChangeNotes notes, Iterable<Account.Id> reviewers) {
+ return reviewers != null
+ ? Streams.stream(reviewers)
+ .filter(id -> !id.equals(notes.getChange().getOwner()))
+ .collect(toImmutableSet())
+ : ImmutableSet.of();
+ }
+
public void gatherResults(ChangeData cd) throws OrmException, PermissionBackendException {
checkState(op != null, "addition did not result in an update op");
checkState(op.getResult() != null, "op did not return a result");
@@ -439,7 +519,7 @@
// Generate result details and fill AccountLoader. This occurs outside
// the Op because the accounts are in a different table.
AddReviewersOp.Result opResult = op.getResult();
- if (migration.readChanges() && state == CC) {
+ if (migration.readChanges() && state() == CC) {
result.ccs = Lists.newArrayListWithCapacity(opResult.addedCCs().size());
for (Account.Id accountId : opResult.addedCCs()) {
result.ccs.add(json.format(new ReviewerInfo(accountId.get()), accountId, cd));
@@ -465,9 +545,123 @@
}
}
}
+
+ public ReviewerState state() {
+ return input.state();
+ }
+
+ public boolean isFailure() {
+ return failureType != null;
+ }
+
+ public boolean isIgnorableFailure() {
+ checkState(failureType != null);
+ FailureBehavior behavior =
+ (input instanceof InternalAddReviewerInput)
+ ? ((InternalAddReviewerInput) input).otherFailureBehavior
+ : FailureBehavior.FAIL;
+ return failureType == FailureType.OTHER && behavior == FailureBehavior.IGNORE;
+ }
}
public static boolean isLegalReviewerGroup(AccountGroup.UUID groupUUID) {
return !SystemGroupBackend.isSystemGroup(groupUUID);
}
+
+ public ReviewerAdditionList prepare(
+ ReviewDb db,
+ ChangeNotes notes,
+ CurrentUser user,
+ Iterable<? extends AddReviewerInput> inputs,
+ boolean allowGroup)
+ throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
+ // Process CC ops before reviewer ops, so a user that appears in both lists ends up as a
+ // reviewer; the last call to ChangeUpdate#putReviewer wins. This can happen if the caller
+ // specifies the same string twice, or less obviously if they specify multiple groups with
+ // overlapping members.
+ // TODO(dborowitz): Consider changing interface to allow excluding reviewers that were
+ // previously processed, to proactively prevent overlap so we don't have to rely on this subtle
+ // behavior.
+ ImmutableList<AddReviewerInput> sorted =
+ Streams.stream(inputs)
+ .sorted(
+ comparing(
+ i -> i.state(), Ordering.explicit(ReviewerState.CC, ReviewerState.REVIEWER)))
+ .collect(toImmutableList());
+ List<ReviewerAddition> additions = new ArrayList<>();
+ for (AddReviewerInput input : sorted) {
+ additions.add(prepare(db, notes, user, input, allowGroup));
+ }
+ return new ReviewerAdditionList(additions);
+ }
+
+ // TODO(dborowitz): This class works, but ultimately feels wrong. It seems like an op but isn't
+ // really an op, it's a collection of ops, and it's only called from the body of other ops. We
+ // could make this class an op, but we would still have AddReviewersOp. Better would probably be
+ // to design a single op that supports combining multiple AddReviewerInputs together. That would
+ // probably also subsume the Addition class itself, which would be a good thing.
+ public static class ReviewerAdditionList {
+ private final ImmutableList<ReviewerAddition> additions;
+
+ private ReviewerAdditionList(List<ReviewerAddition> additions) {
+ this.additions = ImmutableList.copyOf(additions);
+ }
+
+ public ImmutableList<ReviewerAddition> getFailures() {
+ return additions
+ .stream()
+ .filter(a -> a.isFailure() && !a.isIgnorableFailure())
+ .collect(toImmutableList());
+ }
+
+ // We never call updateRepo on the addition ops, which is only ok because it's a no-op.
+
+ public void updateChange(ChangeContext ctx, PatchSet patchSet)
+ throws OrmException, RestApiException, IOException {
+ for (ReviewerAddition addition : additions()) {
+ addition.op.setPatchSet(patchSet);
+ addition.op.updateChange(ctx);
+ }
+ }
+
+ public void postUpdate(Context ctx) throws Exception {
+ for (ReviewerAddition addition : additions()) {
+ if (addition.op != null) {
+ addition.op.postUpdate(ctx);
+ }
+ }
+ }
+
+ public <T> ImmutableSet<T> flattenResults(
+ Function<AddReviewersOp.Result, ? extends Collection<T>> func) {
+ additions()
+ .forEach(
+ a ->
+ checkArgument(
+ a.op != null && a.op.getResult() != null, "missing result on %s", a));
+ return additions()
+ .stream()
+ .map(a -> a.op.getResult())
+ .map(func)
+ .flatMap(Collection::stream)
+ .collect(toImmutableSet());
+ }
+
+ private ImmutableList<ReviewerAddition> additions() {
+ return additions
+ .stream()
+ .filter(
+ a -> {
+ if (a.isFailure()) {
+ if (a.isIgnorableFailure()) {
+ return false;
+ }
+ // Shouldn't happen, caller should have checked that there were no errors.
+ throw new IllegalStateException("error in addition: " + a.result.error);
+ }
+ return true;
+ })
+ .collect(toImmutableList());
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 2e51c17..6e800d0 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -18,6 +18,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
@@ -47,6 +48,7 @@
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
@@ -55,6 +57,7 @@
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Sets;
import com.google.common.collect.SortedSetMultimap;
+import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.Nullable;
@@ -73,6 +76,7 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
import com.google.gerrit.reviewdb.client.Branch;
@@ -841,10 +845,12 @@
} catch (ResourceConflictException e) {
addError(e.getMessage());
reject(magicBranchCmd, "conflict");
- } catch (RestApiException | IOException err) {
- logger.atSevere().withCause(err).log(
- "Can't insert change/patch set for %s", project.getName());
- reject(magicBranchCmd, "internal server error: " + err.getMessage());
+ } catch (BadRequestException | UnprocessableEntityException e) {
+ logger.atFine().withCause(e).log("Rejecting due to client error");
+ reject(magicBranchCmd, e.getMessage());
+ } catch (RestApiException | IOException e) {
+ logger.atSevere().withCause(e).log("Can't insert change/patch set for %s", project.getName());
+ reject(magicBranchCmd, "internal server error: " + e.getMessage());
}
if (magicBranch != null && magicBranch.submit) {
@@ -1327,8 +1333,8 @@
private final boolean defaultPublishComments;
Branch.NameKey dest;
PermissionBackend.ForRef perm;
- Set<Account.Id> reviewer = Sets.newLinkedHashSet();
- Set<Account.Id> cc = Sets.newLinkedHashSet();
+ Set<String> reviewer = Sets.newLinkedHashSet();
+ Set<String> cc = Sets.newLinkedHashSet();
Map<String, Short> labels = new HashMap<>();
String message;
List<RevCommit> baseCommit;
@@ -1418,15 +1424,15 @@
@Option(
name = "--reviewer",
aliases = {"-r"},
- metaVar = "USER",
+ metaVar = "REVIEWER",
usage = "add reviewer to changes")
- void reviewer(Account.Id id) {
- reviewer.add(id);
+ void reviewer(String str) {
+ reviewer.add(str);
}
- @Option(name = "--cc", metaVar = "USER", usage = "add user as CC to changes")
- void cc(Account.Id id) {
- cc.add(id);
+ @Option(name = "--cc", metaVar = "CC", usage = "add CC to changes")
+ void cc(String str) {
+ cc.add(str);
}
@Option(
@@ -1500,8 +1506,38 @@
: false;
}
- MailRecipients getMailRecipients() {
- return new MailRecipients(reviewer, cc);
+ /**
+ * Get reviewer strings from magic branch options, combined with additional recipients computed
+ * from some other place.
+ *
+ * <p>The set of reviewers on a change includes strings passed explicitly via options as well as
+ * account IDs computed from the commit message itself.
+ *
+ * @param additionalRecipients recipients parsed from the commit.
+ * @return set of reviewer strings to pass to {@code ReviewerAdder}.
+ */
+ ImmutableSet<String> getCombinedReviewers(MailRecipients additionalRecipients) {
+ return getCombinedReviewers(reviewer, additionalRecipients.getReviewers());
+ }
+
+ /**
+ * Get CC strings from magic branch options, combined with additional recipients computed from
+ * some other place.
+ *
+ * <p>The set of CCs on a change includes strings passed explicitly via options as well as
+ * account IDs computed from the commit message itself.
+ *
+ * @param additionalRecipients recipients parsed from the commit.
+ * @return set of CC strings to pass to {@code ReviewerAdder}.
+ */
+ ImmutableSet<String> getCombinedCcs(MailRecipients additionalRecipients) {
+ return getCombinedReviewers(cc, additionalRecipients.getCcOnly());
+ }
+
+ private static ImmutableSet<String> getCombinedReviewers(
+ Set<String> strings, Set<Account.Id> ids) {
+ return Streams.concat(strings.stream(), ids.stream().map(Account.Id::toString))
+ .collect(toImmutableSet());
}
ListMultimap<RecipientType, Account.Id> getAccountsToNotify() {
@@ -2392,12 +2428,16 @@
final PatchSet.Id psId = ins.setGroups(groups).getPatchSetId();
Account.Id me = user.getAccountId();
List<FooterLine> footerLines = commit.getFooterLines();
- MailRecipients recipients = new MailRecipients();
checkNotNull(magicBranch);
- recipients.add(magicBranch.getMailRecipients());
+
+ // TODO(dborowitz): Support reviewers by email from footers? Maybe not: kernel developers
+ // with AOSP accounts already complain about these notifications, and that would make it
+ // worse. Might be better to get rid of the feature entirely:
+ // https://groups.google.com/d/topic/repo-discuss/tIFxY7L4DXk/discussion
+ MailRecipients fromFooters = getRecipientsFromFooters(accountResolver, footerLines);
+ fromFooters.remove(me);
+
Map<String, Short> approvals = magicBranch.labels;
- recipients.add(getRecipientsFromFooters(accountResolver, footerLines));
- recipients.remove(me);
StringBuilder msg =
new StringBuilder(
ApprovalsUtil.renderMessageWithApprovals(
@@ -2408,8 +2448,9 @@
}
bu.insertChange(
- ins.setReviewers(recipients.getReviewers())
- .setExtraCC(recipients.getCcOnly())
+ ins.setReviewersAndCcsAsStrings(
+ magicBranch.getCombinedReviewers(fromFooters),
+ magicBranch.getCombinedCcs(fromFooters))
.setApprovals(approvals)
.setMessage(msg.toString())
.setNotify(magicBranch.getNotify())
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 2a46f3b..a580cf6 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -14,7 +14,10 @@
package com.google.gerrit.server.git.receive;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
+import static com.google.gerrit.server.change.ReviewerAdder.newAddReviewerInputFromCommitIdentity;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromReviewers;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
@@ -23,12 +26,16 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -43,8 +50,13 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PublishCommentUtil;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.change.AddReviewersOp;
import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.change.EmailReviewComments;
+import com.google.gerrit.server.change.ReviewerAdder;
+import com.google.gerrit.server.change.ReviewerAdder.InternalAddReviewerInput;
+import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
+import com.google.gerrit.server.change.ReviewerAdder.ReviewerAdditionList;
import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.extensions.events.RevisionCreated;
@@ -75,6 +87,8 @@
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
+import java.util.stream.Stream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -117,6 +131,7 @@
private final PatchSetUtil psUtil;
private final ReplacePatchSetSender.Factory replacePatchSetFactory;
private final ProjectCache projectCache;
+ private final ReviewerAdder reviewerAdder;
private final ProjectState projectState;
private final Branch.NameKey dest;
@@ -131,7 +146,6 @@
private List<String> groups;
private final Map<String, Short> approvals = new HashMap<>();
- private final MailRecipients recipients = new MailRecipients();
private RevCommit commit;
private ReceiveCommand cmd;
private ChangeNotes notes;
@@ -142,6 +156,8 @@
private String rejectMessage;
private MergedByPushOp mergedByPushOp;
private RequestScopePropagator requestScopePropagator;
+ private ReviewerAdditionList reviewerAdditions;
+ private MailRecipients oldRecipients;
@Inject
ReplaceOp(
@@ -161,6 +177,7 @@
ReplacePatchSetSender.Factory replacePatchSetFactory,
ProjectCache projectCache,
@SendEmailExecutor ExecutorService sendEmailExecutor,
+ ReviewerAdder reviewerAdder,
@Assisted ProjectState projectState,
@Assisted Branch.NameKey dest,
@Assisted boolean checkMergedInto,
@@ -188,6 +205,7 @@
this.replacePatchSetFactory = replacePatchSetFactory;
this.projectCache = projectCache;
this.sendEmailExecutor = sendEmailExecutor;
+ this.reviewerAdder = reviewerAdder;
this.projectState = projectState;
this.dest = dest;
@@ -228,7 +246,8 @@
@Override
public boolean updateChange(ChangeContext ctx)
- throws RestApiException, OrmException, IOException, PermissionBackendException {
+ throws RestApiException, OrmException, IOException, PermissionBackendException,
+ ConfigInvalidException {
notes = ctx.getNotes();
Change change = notes.getChange();
if (change == null || change.getStatus().isClosed()) {
@@ -240,13 +259,15 @@
groups = prevPs != null ? prevPs.getGroups() : ImmutableList.<String>of();
}
+ ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getNotes());
+ oldRecipients = getRecipientsFromReviewers(cd.reviewers());
+
ChangeUpdate update = ctx.getUpdate(patchSetId);
update.setSubjectForCommit("Create patch set " + patchSetId.get());
String reviewMessage = null;
String psDescription = null;
if (magicBranch != null) {
- recipients.add(magicBranch.getMailRecipients());
reviewMessage = magicBranch.message;
psDescription = magicBranch.message;
approvals.putAll(magicBranch.labels);
@@ -294,10 +315,7 @@
psDescription);
update.setPsDescription(psDescription);
- recipients.add(getRecipientsFromFooters(accountResolver, commit.getFooterLines()));
- recipients.remove(ctx.getAccountId());
- ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getNotes());
- MailRecipients oldRecipients = getRecipientsFromReviewers(cd.reviewers());
+ MailRecipients fromFooters = getRecipientsFromFooters(accountResolver, commit.getFooterLines());
Iterable<PatchSetApproval> newApprovals =
approvalsUtil.addApprovalsForNewPatchSet(
ctx.getDb(),
@@ -313,15 +331,19 @@
ctx.getRevWalk(),
ctx.getRepoView().getConfig(),
newApprovals);
- approvalsUtil.addReviewers(
- ctx.getDb(),
- update,
- projectState.getLabelTypes(),
- change,
- newPatchSet,
- info,
- recipients.getReviewers(),
- oldRecipients.getAll());
+
+ reviewerAdditions =
+ reviewerAdder.prepare(
+ ctx.getDb(),
+ ctx.getNotes(),
+ ctx.getUser(),
+ getReviewerInputs(magicBranch, fromFooters, ctx.getChange(), info),
+ true);
+ Optional<ReviewerAddition> reviewerError = reviewerAdditions.getFailures().stream().findFirst();
+ if (reviewerError.isPresent()) {
+ throw new UnprocessableEntityException(reviewerError.get().result.error);
+ }
+ reviewerAdditions.updateChange(ctx, newPatchSet);
// Check if approvals are changing in with this update. If so, add current user to reviewers.
// Note that this is done separately as addReviewers is filtering out the change owner as
@@ -330,8 +352,6 @@
update.putReviewer(ctx.getAccountId(), REVIEWER);
}
- recipients.add(oldRecipients);
-
msg = createChangeMessage(ctx, reviewMessage);
cmUtil.addChangeMessage(ctx.getDb(), update, msg);
@@ -344,6 +364,51 @@
return true;
}
+ private static ImmutableList<AddReviewerInput> getReviewerInputs(
+ @Nullable MagicBranchInput magicBranch,
+ MailRecipients fromFooters,
+ Change change,
+ PatchSetInfo psInfo) {
+ // Disable individual emails when adding reviewers, as all reviewers will receive the single
+ // bulk new change email.
+ Stream<AddReviewerInput> inputs =
+ Streams.concat(
+ Streams.stream(
+ newAddReviewerInputFromCommitIdentity(
+ change, psInfo.getAuthor().getAccount(), NotifyHandling.NONE)),
+ Streams.stream(
+ newAddReviewerInputFromCommitIdentity(
+ change, psInfo.getCommitter().getAccount(), NotifyHandling.NONE)));
+ if (magicBranch != null) {
+ inputs =
+ Streams.concat(
+ inputs,
+ magicBranch
+ .getCombinedReviewers(fromFooters)
+ .stream()
+ .map(r -> newAddReviewerInput(r, ReviewerState.REVIEWER)),
+ magicBranch
+ .getCombinedCcs(fromFooters)
+ .stream()
+ .map(r -> newAddReviewerInput(r, ReviewerState.CC)));
+ }
+ return inputs.collect(toImmutableList());
+ }
+
+ private static InternalAddReviewerInput newAddReviewerInput(
+ String reviewer, ReviewerState state) {
+ // Disable individual emails when adding reviewers, as all reviewers will receive the single
+ // bulk new patch set email.
+ InternalAddReviewerInput input =
+ ReviewerAdder.newAddReviewerInput(reviewer, state, NotifyHandling.NONE);
+
+ // Ignore failures for reasons like the reviewer being inactive or being unable to see the
+ // change. See discussion in ChangeInserter.
+ input.otherFailureBehavior = ReviewerAdder.FailureBehavior.IGNORE;
+
+ return input;
+ }
+
private ChangeMessage createChangeMessage(ChangeContext ctx, String reviewMessage)
throws OrmException, IOException {
String approvalMessage =
@@ -455,6 +520,7 @@
@Override
public void postUpdate(Context ctx) throws Exception {
+ reviewerAdditions.postUpdate(ctx);
if (changeKind != ChangeKind.TRIVIAL_REBASE) {
// TODO(dborowitz): Merge email templates so we only have to send one.
Runnable e = new ReplaceEmailTask(ctx);
@@ -513,8 +579,20 @@
cm.setNotify(magicBranch.getNotify(notes));
cm.setAccountsToNotify(magicBranch.getAccountsToNotify());
}
- cm.addReviewers(recipients.getReviewers());
- cm.addExtraCC(recipients.getCcOnly());
+ cm.addReviewers(
+ Streams.concat(
+ oldRecipients.getReviewers().stream(),
+ reviewerAdditions
+ .flattenResults(AddReviewersOp.Result::addedReviewers)
+ .stream()
+ .map(PatchSetApproval::getAccountId))
+ .collect(toImmutableSet()));
+ cm.addExtraCC(
+ Streams.concat(
+ oldRecipients.getCcOnly().stream(),
+ reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs).stream())
+ .collect(toImmutableSet()));
+ // TODO(dborowitz): Support byEmail
cm.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log(
diff --git a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
index 4099e14..bbf8d95 100644
--- a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
+++ b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
@@ -27,7 +27,7 @@
* <p>This class is thread-safe.
*/
public class ResultChangeIds {
- enum Key {
+ public enum Key {
CREATED,
REPLACED,
AUTOCLOSED,
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 65c652d..5061926 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -381,7 +381,7 @@
reviewers.remove(user.get().getAccountId());
Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC));
ccs.remove(user.get().getAccountId());
- ins.setReviewers(reviewers).setExtraCC(ccs);
+ ins.setReviewersAndCcs(reviewers, ccs);
}
bu.insertChange(ins);
return changeId;
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index e47c582..8b5ed68 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -288,7 +288,8 @@
reviewerInput.notify = NotifyHandling.NONE;
ReviewerAddition result =
- reviewerAdder.prepare(revision.getNotes(), revision.getUser(), reviewerInput, true);
+ reviewerAdder.prepare(
+ db.get(), revision.getNotes(), revision.getUser(), reviewerInput, true);
reviewerJsonResults.put(reviewerInput.reviewer, result.result);
if (result.result.error != null) {
hasError = true;
@@ -443,10 +444,10 @@
List<Address> toByEmail = new ArrayList<>();
List<Address> ccByEmail = new ArrayList<>();
for (ReviewerAddition addition : reviewerAdditions) {
- if (addition.state == ReviewerState.REVIEWER) {
+ if (addition.state() == ReviewerState.REVIEWER) {
to.addAll(addition.reviewers);
toByEmail.addAll(addition.reviewersByEmail);
- } else if (addition.state == ReviewerState.CC) {
+ } else if (addition.state() == ReviewerState.CC) {
cc.addAll(addition.reviewers);
ccByEmail.addAll(addition.reviewersByEmail);
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index 9147d44..3e66189 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -68,7 +68,8 @@
throw new BadRequestException("missing reviewer field");
}
- ReviewerAddition addition = reviewerAdder.prepare(rsrc.getNotes(), rsrc.getUser(), input, true);
+ ReviewerAddition addition =
+ reviewerAdder.prepare(dbProvider.get(), rsrc.getNotes(), rsrc.getUser(), input, true);
if (addition.op == null) {
return addition.result;
}
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
index 7ca674f..7878ce5 100644
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -124,7 +124,7 @@
reviewerInput.state = ReviewerState.CC;
reviewerInput.confirmed = true;
reviewerInput.notify = NotifyHandling.NONE;
- return reviewerAdder.prepare(rsrc.getNotes(), rsrc.getUser(), reviewerInput, false);
+ return reviewerAdder.prepare(db.get(), rsrc.getNotes(), rsrc.getUser(), reviewerInput, false);
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index 6383b29..7309fde 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -237,11 +237,9 @@
reviewers.add(changeToRevert.getOwner());
reviewers.addAll(reviewerSet.byState(ReviewerStateInternal.REVIEWER));
reviewers.remove(user.getAccountId());
- ins.setReviewers(reviewers);
-
Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC));
ccs.remove(user.getAccountId());
- ins.setExtraCC(ccs);
+ ins.setReviewersAndCcs(reviewers, ccs);
ins.setRevertOf(changeIdToRevert);
try (BatchUpdate bu = updateFactory.create(db.get(), project, user, now)) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 57f1be1..7063b27 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -1436,8 +1436,9 @@
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
+ assertThat(m.from().getName()).isEqualTo("Administrator (Code Review)");
assertThat(m.rcpt()).containsExactly(user.emailAddress);
- assertThat(m.body()).contains(admin.fullName + " has uploaded this change for review");
+ assertThat(m.body()).contains("I'd like you to do a code review");
assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
assertMailReplyTo(m, admin.email);
}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 505c165..e5906a2 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -764,7 +764,7 @@
input.notify = NotifyHandling.ALL;
sender.clear();
gApi.changes().id(changeId).current().cherryPick(input);
- assertNotifyCc(admin);
+ assertNotifyTo(admin);
// Disable the notification. 'admin' as a reviewer should not be notified any more.
input.destination = "branch-2";
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 488eb9f..78a3d15 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance.git;
+import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
@@ -35,6 +36,7 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.testing.Util.category;
import static com.google.gerrit.server.project.testing.Util.value;
+import static java.util.Comparator.comparing;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
@@ -57,7 +59,9 @@
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -485,7 +489,12 @@
r.assertOkStatus();
assertThat(sender.getMessages()).hasSize(1);
Message m = sender.getMessages().get(0);
- assertThat(m.rcpt()).containsExactly(user.emailAddress);
+ if (notesMigration.readChanges()) {
+ assertThat(m.rcpt()).containsExactly(user.emailAddress);
+ } else {
+ // CCs are considered reviewers in the storage layer and so get notified.
+ assertThat(m.rcpt()).containsExactly(user.emailAddress, user2.emailAddress);
+ }
sender.clear();
r = pushTo(pushSpec + ",notify=" + NotifyHandling.ALL);
@@ -565,7 +574,49 @@
+ nonExistingEmail
+ ",cc="
+ user.email);
- r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
+ r.assertErrorStatus(nonExistingEmail + " does not identify a registered user or group");
+ }
+
+ @Test
+ public void pushForMasterWithCcByEmail() throws Exception {
+ ConfigInput conf = new ConfigInput();
+ conf.enableReviewerByEmail = InheritableBoolean.TRUE;
+ gApi.projects().name(project.get()).config(conf);
+
+ PushOneCommit.Result r =
+ pushTo("refs/for/master%cc=non.existing.1@example.com,cc=non.existing.2@example.com");
+ if (notesMigration.readChanges()) {
+ r.assertOkStatus();
+
+ ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS);
+ ImmutableList<AccountInfo> ccs =
+ firstNonNull(ci.reviewers.get(ReviewerState.CC), ImmutableList.<AccountInfo>of())
+ .stream()
+ .sorted(comparing((AccountInfo a) -> a.email))
+ .collect(toImmutableList());
+ assertThat(ccs).hasSize(2);
+ assertThat(ccs.get(0).email).isEqualTo("non.existing.1@example.com");
+ assertThat(ccs.get(0)._accountId).isNull();
+ assertThat(ccs.get(1).email).isEqualTo("non.existing.2@example.com");
+ assertThat(ccs.get(1)._accountId).isNull();
+ } else {
+ r.assertErrorStatus("non.existing.1@example.com does not identify a registered user");
+ }
+ }
+
+ @Test
+ public void pushForMasterWithCcGroup() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+ String group = name("group");
+ GroupInput gin = new GroupInput();
+ gin.name = group;
+ gin.members = ImmutableList.of(user.username, user2.username);
+ gin.visibleToAll = true; // TODO(dborowitz): Shouldn't be necessary; see ReviewerAdder.
+ gApi.groups().create(gin);
+
+ PushOneCommit.Result r = pushTo("refs/for/master%cc=" + group);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, null, ImmutableList.of(), ImmutableList.of(user, user2));
}
@Test
@@ -605,7 +656,49 @@
+ nonExistingEmail
+ ",r="
+ user.email);
- r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
+ r.assertErrorStatus(nonExistingEmail + " does not identify a registered user or group");
+ }
+
+ @Test
+ public void pushForMasterWithReviewerByEmail() throws Exception {
+ ConfigInput conf = new ConfigInput();
+ conf.enableReviewerByEmail = InheritableBoolean.TRUE;
+ gApi.projects().name(project.get()).config(conf);
+
+ PushOneCommit.Result r =
+ pushTo("refs/for/master%r=non.existing.1@example.com,r=non.existing.2@example.com");
+ if (notesMigration.readChanges()) {
+ r.assertOkStatus();
+
+ ChangeInfo ci = get(r.getChangeId(), DETAILED_LABELS);
+ ImmutableList<AccountInfo> reviewers =
+ firstNonNull(ci.reviewers.get(ReviewerState.REVIEWER), ImmutableList.<AccountInfo>of())
+ .stream()
+ .sorted(comparing((AccountInfo a) -> a.email))
+ .collect(toImmutableList());
+ assertThat(reviewers).hasSize(2);
+ assertThat(reviewers.get(0).email).isEqualTo("non.existing.1@example.com");
+ assertThat(reviewers.get(0)._accountId).isNull();
+ assertThat(reviewers.get(1).email).isEqualTo("non.existing.2@example.com");
+ assertThat(reviewers.get(1)._accountId).isNull();
+ } else {
+ r.assertErrorStatus("non.existing.1@example.com does not identify a registered user");
+ }
+ }
+
+ @Test
+ public void pushForMasterWithReviewerGroup() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+ String group = name("group");
+ GroupInput gin = new GroupInput();
+ gin.name = group;
+ gin.members = ImmutableList.of(user.username, user2.username);
+ gin.visibleToAll = true; // TODO(dborowitz): Shouldn't be necessary; see ReviewerAdder.
+ gApi.groups().create(gin);
+
+ PushOneCommit.Result r = pushTo("refs/for/master%r=" + group);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, null, ImmutableList.of(user, user2), ImmutableList.of());
}
@Test
@@ -2254,6 +2347,7 @@
String name = "reviewers for " + (i + 1);
if (expectedReviewer != null) {
assertThat(cd.reviewers().all()).named(name).containsExactly(expectedReviewer.getId());
+ // Remove reviewer from PS1 so we can test adding this same reviewer on PS2 below.
gApi.changes().id(cd.getId().get()).reviewer(expectedReviewer.getId().toString()).remove();
}
assertThat(byCommit(c).reviewers().all()).named(name).isEmpty();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 2daccc0..209d0a2 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -1088,15 +1088,34 @@
@Test
public void createReviewableChangeWithReviewersAndCcs() throws Exception {
- // TODO(logan): Support reviewers/CCs-by-email via push option.
StagedPreChange spc =
stagePreChange(
"refs/for/master",
users -> ImmutableList.of("r=" + users.reviewer.username, "cc=" + users.ccer.username));
+ FakeEmailSenderSubject subject =
+ assertThat(sender).sent("newchange", spc).to(spc.reviewer, spc.watchingProjectOwner);
+ if (notesMigration.readChanges()) {
+ subject.cc(spc.ccer);
+ } else {
+ // CCs are considered reviewers in the storage layer.
+ subject.to(spc.ccer);
+ }
+ subject.bcc(NEW_CHANGES, NEW_PATCHSETS).noOneElse();
+ }
+
+ @Test
+ public void createReviewableChangeWithReviewersAndCcsByEmailInNoteDb() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ StagedPreChange spc =
+ stagePreChange(
+ "refs/for/master",
+ users -> ImmutableList.of("r=nobody1@example.com,cc=nobody2@example.com"));
+ spc.supportReviewersByEmail = true;
assertThat(sender)
.sent("newchange", spc)
- .to(spc.reviewer, spc.watchingProjectOwner)
- .cc(spc.ccer)
+ .to("nobody1@example.com")
+ .to(spc.watchingProjectOwner)
+ .cc("nobody2@example.com")
.bcc(NEW_CHANGES, NEW_PATCHSETS)
.noOneElse();
}
@@ -1727,6 +1746,7 @@
.sent("newpatchset", sc)
.notTo(sc.owner) // TODO(logan): This shouldn't be sent *from* the owner.
.to(sc.reviewer)
+ .to(other)
.cc(sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
@@ -1742,6 +1762,7 @@
.sent("newpatchset", sc)
.notTo(sc.owner) // TODO(logan): This shouldn't be sent *from* the owner.
.to(sc.reviewer, sc.ccer)
+ .to(other)
.noOneElse();
}
@@ -1755,6 +1776,7 @@
.sent("newpatchset", sc)
.notTo(sc.owner) // TODO(logan): This shouldn't be sent *from* the owner.
.to(sc.reviewer)
+ .to(other)
.cc(sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
@@ -1769,6 +1791,7 @@
assertThat(sender)
.sent("newpatchset", sc)
.to(sc.reviewer, sc.ccer)
+ .to(other)
.notTo(sc.owner) // TODO(logan): This shouldn't be sent *from* the owner.
.noOneElse();
}
diff --git a/lib/jackson/BUILD b/lib/jackson/BUILD
index 5c15193..e09f57e 100644
--- a/lib/jackson/BUILD
+++ b/lib/jackson/BUILD
@@ -5,5 +5,9 @@
java_library(
name = "jackson-core",
data = ["//lib:LICENSE-Apache2.0"],
+ visibility = [
+ "//java/com/google/gerrit/elasticsearch:__pkg__",
+ "//plugins:__pkg__",
+ ],
exports = ["@jackson-core//jar"],
)
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
index 29d554e..f534771 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
@@ -29,8 +29,8 @@
<template>
<style include="shared-styles">
gr-avatar {
- height: 10em;
- width: 10em;
+ height: 120px;
+ width: 120px;
margin-right: .15em;
vertical-align: -.25em;
}
@@ -44,7 +44,7 @@
<span class="title"></span>
<span class="value">
<gr-avatar account="[[_account]]"
- image-size="32"></gr-avatar>
+ image-size="120"></gr-avatar>
</span>
</section>
<section class$="[[_hideAvatarChangeUrl(_avatarChangeUrl)]]">