Add notify section in project.config

The notify section allows project owners to include emails to users
directly from project.config. This removes the need to create fake
user accounts to always BCC a group mailing list. For example:

  [notify "dev-group"]
    email = dev-team <dev-team@example.com>
    filter = branch:master visibleto:dev

Internal groups may also be used as a mailing list, automatically
BCCing any user that is a member of the group if the group can see
the change:

  [access "refs/heads/*"]
    read = group Developers
  [notify "reviewers"]
    email = group Developers

Change-Id: I02bd05b562e420a4742ff27ffacad8f20648e4f0
This commit is contained in:
Shawn O. Pearce
2012-04-26 16:24:12 -07:00
parent 68e49477c4
commit 57bec12129
17 changed files with 597 additions and 98 deletions

View File

@@ -14,18 +14,25 @@
package com.google.gerrit.server.mail;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupInclude;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
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.client.Project;
import com.google.gerrit.reviewdb.client.StarredChange;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.NotifyConfig;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListEntry;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
@@ -34,17 +41,17 @@ import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.SingleGroupUser;
import com.google.gwtorm.server.OrmException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
@@ -305,53 +312,148 @@ public abstract class ChangeEmail extends OutgoingEmail {
// Just don't BCC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
log.warn("Cannot BCC users that starred updated change", err);
}
}
/** BCC any user who has set "notify all comments" on this project. */
protected void bccWatchesNotifyAllComments() {
/** BCC users and groups that want notification of events. */
protected void bccWatches(NotifyType type) {
try {
// BCC anyone else who has interest in this project's changes
//
for (final AccountProjectWatch w : getWatches()) {
if (w.isNotify(NotifyType.ALL_COMMENTS)) {
add(RecipientType.BCC, w.getAccountId());
}
Watchers matching = getWatches(type);
for (Account.Id user : matching.accounts) {
add(RecipientType.BCC, user);
}
for (Address addr : matching.emails) {
add(RecipientType.BCC, addr);
}
} catch (OrmException err) {
// Just don't CC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
log.warn("Cannot BCC watchers for " + type, err);
}
}
/** Returns all watches that are relevant */
protected final List<AccountProjectWatch> getWatches() throws OrmException {
protected final Watchers getWatches(NotifyType type) throws OrmException {
Watchers matching = new Watchers();
if (changeData == null) {
return Collections.emptyList();
return matching;
}
List<AccountProjectWatch> matching = new ArrayList<AccountProjectWatch>();
Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
for (AccountProjectWatch w : args.db.get().accountProjectWatches()
.byProject(change.getProject())) {
projectWatchers.add(w.getAccountId());
add(matching, w);
}
for (AccountProjectWatch w : args.db.get().accountProjectWatches()
.byProject(args.allProjectsName)) {
if (!projectWatchers.contains(w.getAccountId())) {
if (w.isNotify(type)) {
add(matching, w);
}
}
return Collections.unmodifiableList(matching);
for (AccountProjectWatch w : args.db.get().accountProjectWatches()
.byProject(args.allProjectsName)) {
if (!projectWatchers.contains(w.getAccountId()) && w.isNotify(type)) {
add(matching, w);
}
}
ProjectState state = projectState;
while (state != null) {
for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
if (nc.isNotify(type)) {
try {
add(matching, nc, state.getProject().getNameKey());
} catch (QueryParseException e) {
log.warn(String.format(
"Project %s has invalid notify %s filter \"%s\": %s",
state.getProject().getName(), nc.getName(),
nc.getFilter(), e.getMessage()));
}
}
}
state = state.getParentState();
}
return matching;
}
protected static class Watchers {
protected final Set<Account.Id> accounts = Sets.newHashSet();
protected final Set<Address> emails = Sets.newHashSet();
}
@SuppressWarnings("unchecked")
private void add(List<AccountProjectWatch> matching, AccountProjectWatch w)
private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
throws OrmException, QueryParseException {
for (GroupReference ref : nc.getGroups()) {
AccountGroup group = args.groupCache.get(ref.getUUID());
if (group == null) {
log.warn(String.format(
"Project %s has invalid group %s in notify section %s",
project.get(), ref.getName(), nc.getName()));
continue;
}
if (group.getType() != AccountGroup.Type.INTERNAL) {
log.warn(String.format(
"Project %s cannot use group %s of type %s in notify section %s",
project.get(), ref.getName(), group.getType(), nc.getName()));
continue;
}
ChangeQueryBuilder qb = args.queryBuilder.create(new SingleGroupUser(
args.capabilityControlFactory,
ref.getUUID()));
qb.setAllowFile(true);
Predicate<ChangeData> p = qb.is_visible();
if (nc.getFilter() != null) {
p = Predicate.and(qb.parse(nc.getFilter()), p);
p = args.queryRewriter.get().rewrite(p);
}
if (p.match(changeData)) {
recursivelyAddAllAccounts(matching, group);
}
}
if (!nc.getAddresses().isEmpty()) {
if (nc.getFilter() != null) {
ChangeQueryBuilder qb = args.queryBuilder.create(args.anonymousUser);
qb.setAllowFile(true);
Predicate<ChangeData> p = qb.parse(nc.getFilter());
p = args.queryRewriter.get().rewrite(p);
if (p.match(changeData)) {
matching.emails.addAll(nc.getAddresses());
}
} else {
matching.emails.addAll(nc.getAddresses());
}
}
}
private void recursivelyAddAllAccounts(Watchers matching, AccountGroup group)
throws OrmException {
Set<AccountGroup.Id> seen = Sets.newHashSet();
Queue<AccountGroup.Id> scan = Lists.newLinkedList();
scan.add(group.getId());
seen.add(group.getId());
while (!scan.isEmpty()) {
AccountGroup.Id next = scan.remove();
for (AccountGroupMember m : args.db.get().accountGroupMembers()
.byGroup(next)) {
matching.accounts.add(m.getAccountId());
}
for (AccountGroupInclude m : args.db.get().accountGroupIncludes()
.byGroup(next)) {
if (seen.add(m.getIncludeId())) {
scan.add(m.getIncludeId());
}
}
}
}
@SuppressWarnings("unchecked")
private void add(Watchers matching, AccountProjectWatch w)
throws OrmException {
IdentifiedUser user =
args.identifiedUserFactory.create(args.db, w.getAccountId());
@@ -363,13 +465,13 @@ public abstract class ChangeEmail extends OutgoingEmail {
p = Predicate.and(qb.parse(w.getFilter()), p);
p = args.queryRewriter.get().rewrite(p);
if (p.match(changeData)) {
matching.add(w);
matching.accounts.add(w.getAccountId());
}
} catch (QueryParseException e) {
// Ignore broken filter expressions.
}
} else if (p.match(changeData)) {
matching.add(w);
matching.accounts.add(w.getAccountId());
}
}