Migrate project watches to git (part 1)

In git the project watch configuration of a user is stored in a
'watch.config' file in the refs/users/<sharded-id> user branch in the
All-Users repository.

The 'watch.config' file is a git config file that has one 'project'
section for all project watches of a project. This means if we ever
want to watch other entities we can add further sections in this file
(e.g. 'group' sections if we want to support watching of group
modifications).

The project name is used as subsection name and the filters with the
notify types that decide for which events email notifications should
be sent are represented as 'notify' values in the subsection. A
'notify' value is formatted as
"<filter> [<comma-separated-list-of-notify-types>]":

  [project "foo"]
    notify = * [ALL_COMMENTS]
    notify = branch:master [ALL_COMMENTS, NEW_PATCHSETS]
    notify = branch:master owner:self [SUBMITTED_CHANGES]

For a change event a notification will be send if any 'notify' value
of the corresponding project has both, a filter that matches the
change and a notify type that matches the event.

If two notify values in the same subsection have the same filter they
are merged on the next save, taking the union of the notify types.

For watch configurations that notify on no event the list of notify
types is empty:

  [project "foo"]
    notify = branch:master []

Unknown notify types are ignored and removed on save.

To support a live migration on a multi-master Gerrit installation, the
upgrade is done in 2 steps:

- part 1 (this change):
  * always write to both backends (database and git)
  * a configuration option (user.readProjectWatchesFromGit) decides if
    the project watches are read from database or git (default:
    database)
  * upgraded instances write to both backends, old instances only
    read/write from/to the database
  * after upgrading all instances (all still read from the database)
    run a batch to copy all project watches from the database to git
  * update all instances to read from git
  * make sure all instances use the new schema version of the account
    index
- part 2 (next change):
  * bump the database schema version
  * migrate the project watches from database to git (for single
    instance Gerrit servers)
  * delete the database table
  * delete the user.readProjectWatchesFromGit config option

This change requires that the account cache is manually evicted.

Change-Id: I4f8d4e8a762aeb4b46e27bb3bf9e58f91611145d
Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
Edwin Kempin
2016-04-26 13:25:07 +02:00
parent 75eb96921f
commit d73386ecbf
17 changed files with 832 additions and 96 deletions

View File

@@ -118,6 +118,22 @@ public class WatchedProjectsIT extends AbstractDaemonTest {
gApi.accounts().self().setWatchedProjects(projectsToWatch);
}
@Test
public void setAndGetEmptyWatch() throws Exception {
String projectName = createProject(NEW_PROJECT_NAME).get();
List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
ProjectWatchInfo pwi = new ProjectWatchInfo();
pwi.project = projectName;
projectsToWatch.add(pwi);
gApi.accounts().self().setWatchedProjects(projectsToWatch);
List<ProjectWatchInfo> persistedWatchedProjects =
gApi.accounts().self().getWatchedProjects();
assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
}
@Test
public void watchNonExistingProject() throws Exception {
String projectName = NEW_PROJECT_NAME + "3";

View File

@@ -47,4 +47,30 @@ public class ProjectWatchInfo {
.hash(project, filter, notifyNewChanges, notifyNewPatchSets,
notifyAllComments, notifySubmittedChanges, notifyAbandonedChanges);
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append(project);
if (filter != null) {
b.append("%filter=")
.append(filter);
}
b.append("(notifyAbandonedChanges=")
.append(toBoolean(notifyAbandonedChanges))
.append(", notifyAllComments=")
.append(toBoolean(notifyAllComments))
.append(", notifyNewChanges=")
.append(toBoolean(notifyNewChanges))
.append(", notifyNewPatchSets=")
.append(toBoolean(notifyNewPatchSets))
.append(", notifySubmittedChanges=")
.append(toBoolean(notifySubmittedChanges))
.append(")");
return b.toString();
}
private boolean toBoolean(Boolean b) {
return b == null ? false : b;
}
}

View File

@@ -22,8 +22,14 @@ import com.google.gwtorm.client.StringKey;
public final class AccountProjectWatch {
public enum NotifyType {
NEW_CHANGES, NEW_PATCHSETS, ALL_COMMENTS, SUBMITTED_CHANGES,
ABANDONED_CHANGES, ALL
// sort by name, except 'ALL' which should stay last
ABANDONED_CHANGES,
ALL_COMMENTS,
NEW_CHANGES,
NEW_PATCHSETS,
SUBMITTED_CHANGES,
ALL
}
public static final String FILTER_ALL = "*";

View File

@@ -24,9 +24,11 @@ import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup;
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.server.ReviewDb;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gerrit.server.index.account.AccountIndexer;
import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -40,13 +42,16 @@ import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
@@ -135,9 +140,9 @@ public class AccountCacheImpl implements AccountCache {
Account account = new Account(accountId, TimeUtil.nowTs());
account.setActive(false);
Collection<AccountExternalId> ids = Collections.emptySet();
Collection<AccountProjectWatch> projectWatches = Collections.emptySet();
Set<AccountGroup.UUID> anon = ImmutableSet.of();
return new AccountState(account, anon, ids, projectWatches);
return new AccountState(account, anon, ids,
new HashMap<ProjectWatchKey, Set<NotifyType>>());
}
static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
@@ -145,17 +150,24 @@ public class AccountCacheImpl implements AccountCache {
private final GroupCache groupCache;
private final GeneralPreferencesLoader loader;
private final LoadingCache<String, Optional<Account.Id>> byName;
private final boolean readFromGit;
private final Provider<WatchConfig.Accessor> watchConfig;
@Inject
ByIdLoader(SchemaFactory<ReviewDb> sf,
GroupCache groupCache,
GeneralPreferencesLoader loader,
@Named(BYUSER_NAME) LoadingCache<String,
Optional<Account.Id>> byUsername) {
Optional<Account.Id>> byUsername,
@GerritServerConfig Config cfg,
Provider<WatchConfig.Accessor> watchConfig) {
this.schema = sf;
this.groupCache = groupCache;
this.loader = loader;
this.byName = byUsername;
this.readFromGit =
cfg.getBoolean("user", null, "readProjectWatchesFromGit", true);
this.watchConfig = watchConfig;
}
@Override
@@ -171,7 +183,7 @@ public class AccountCacheImpl implements AccountCache {
}
private AccountState load(final ReviewDb db, final Account.Id who)
throws OrmException {
throws OrmException, IOException, ConfigInvalidException {
Account account = db.accounts().get(who);
if (account == null) {
// Account no longer exists? They are anonymous.
@@ -200,9 +212,10 @@ public class AccountCacheImpl implements AccountCache {
account.setGeneralPreferences(GeneralPreferencesInfo.defaults());
}
Collection<AccountProjectWatch> projectWatches =
Collections.unmodifiableCollection(
db.accountProjectWatches().byAccount(who).toList());
Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
readFromGit
? watchConfig.get().getProjectWatches(who)
: GetWatchedProjects.readProjectWatchesFromDb(db, who);
return new AccountState(account, internalGroups, externalIds,
projectWatches);

View File

@@ -24,12 +24,14 @@ import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.CurrentUser.PropertyKey;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class AccountState {
@@ -44,13 +46,13 @@ public class AccountState {
private final Account account;
private final Set<AccountGroup.UUID> internalGroups;
private final Collection<AccountExternalId> externalIds;
private final Collection<AccountProjectWatch> projectWatches;
private final Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
public AccountState(Account account,
Set<AccountGroup.UUID> actualGroups,
Collection<AccountExternalId> externalIds,
Collection<AccountProjectWatch> projectWatches) {
Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
this.account = account;
this.internalGroups = actualGroups;
this.externalIds = externalIds;
@@ -90,7 +92,7 @@ public class AccountState {
}
/** The project watches of the account. */
public Collection<AccountProjectWatch> getProjectWatches() {
public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
return projectWatches;
}

View File

@@ -14,6 +14,8 @@
package com.google.gerrit.server.account;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Response;
@@ -24,12 +26,15 @@ import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
@@ -38,39 +43,52 @@ import java.util.List;
@Singleton
public class DeleteWatchedProjects
implements RestModifyView<AccountResource, List<ProjectWatchInfo>> {
private final Provider<ReviewDb> dbProvider;
private final Provider<IdentifiedUser> self;
private final AccountCache accountCache;
private final WatchConfig.Accessor watchConfig;
@Inject
DeleteWatchedProjects(Provider<ReviewDb> dbProvider,
Provider<IdentifiedUser> self,
AccountCache accountCache) {
AccountCache accountCache,
WatchConfig.Accessor watchConfig) {
this.dbProvider = dbProvider;
this.self = self;
this.accountCache = accountCache;
this.watchConfig = watchConfig;
}
@Override
public Response<?> apply(AccountResource rsrc, List<ProjectWatchInfo> input)
throws AuthException, UnprocessableEntityException, OrmException,
IOException {
IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser()
&& !self.get().getCapabilities().canAdministrateServer()) {
throw new AuthException("It is not allowed to edit project watches "
+ "of other users");
}
if (input == null) {
return Response.none();
}
Account.Id accountId = rsrc.getUser().getAccountId();
deleteFromDb(accountId, input);
deleteFromGit(accountId, input);
accountCache.evict(accountId);
return Response.none();
}
private void deleteFromDb(Account.Id accountId, List<ProjectWatchInfo> input)
throws OrmException, IOException {
ResultSet<AccountProjectWatch> watchedProjects =
dbProvider.get().accountProjectWatches().byAccount(accountId);
HashMap<AccountProjectWatch.Key, AccountProjectWatch>
watchedProjectsMap = new HashMap<>();
HashMap<AccountProjectWatch.Key, AccountProjectWatch> watchedProjectsMap =
new HashMap<>();
for (AccountProjectWatch watchedProject : watchedProjects) {
watchedProjectsMap.put(watchedProject.getKey(), watchedProject);
}
if (input != null) {
List<AccountProjectWatch> watchesToDelete = new LinkedList<>();
for (ProjectWatchInfo projectInfo : input) {
AccountProjectWatch.Key key = new AccountProjectWatch.Key(accountId,
@@ -84,6 +102,16 @@ public class DeleteWatchedProjects
accountCache.evict(accountId);
}
}
return Response.none();
private void deleteFromGit(Account.Id accountId, List<ProjectWatchInfo> input)
throws IOException, ConfigInvalidException {
watchConfig.deleteProjectWatches(accountId, Lists.transform(input,
new Function<ProjectWatchInfo, ProjectWatchKey>() {
@Override
public ProjectWatchKey apply(ProjectWatchInfo info) {
return ProjectWatchKey.create(new Project.NameKey(info.project),
info.filter);
}
}));
}
}

View File

@@ -14,67 +14,120 @@
package com.google.gerrit.server.account;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Singleton
public class GetWatchedProjects implements RestReadView<AccountResource> {
private final Provider<ReviewDb> dbProvider;
private final Provider<IdentifiedUser> self;
private final boolean readFromGit;
private final WatchConfig.Accessor watchConfig;
@Inject
public GetWatchedProjects(Provider<ReviewDb> dbProvider,
Provider<IdentifiedUser> self) {
Provider<IdentifiedUser> self,
@GerritServerConfig Config cfg,
WatchConfig.Accessor watchConfig) {
this.dbProvider = dbProvider;
this.self = self;
this.readFromGit =
cfg.getBoolean("user", null, "readProjectWatchesFromGit", true);
this.watchConfig = watchConfig;
}
@Override
public List<ProjectWatchInfo> apply(AccountResource rsrc)
throws OrmException, AuthException {
throws OrmException, AuthException, IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser()
&& !self.get().getCapabilities().canAdministrateServer()) {
throw new AuthException("It is not allowed to list project watches "
+ "of other users");
}
Account.Id accountId = rsrc.getUser().getAccountId();
Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
readFromGit
? watchConfig.getProjectWatches(accountId)
: readProjectWatchesFromDb(dbProvider.get(), accountId);
List<ProjectWatchInfo> projectWatchInfos = new LinkedList<>();
Iterable<AccountProjectWatch> projectWatches =
dbProvider.get().accountProjectWatches()
.byAccount(rsrc.getUser().getAccountId());
for (AccountProjectWatch a : projectWatches) {
for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : projectWatches
.entrySet()) {
ProjectWatchInfo pwi = new ProjectWatchInfo();
pwi.filter = a.getFilter();
pwi.project = a.getProjectNameKey().get();
pwi.filter = e.getKey().filter();
pwi.project = e.getKey().project().get();
pwi.notifyAbandonedChanges =
toBoolean(
a.isNotify(AccountProjectWatch.NotifyType.ABANDONED_CHANGES));
toBoolean(e.getValue().contains(NotifyType.ABANDONED_CHANGES));
pwi.notifyNewChanges =
toBoolean(a.isNotify(AccountProjectWatch.NotifyType.NEW_CHANGES));
toBoolean(e.getValue().contains(NotifyType.NEW_CHANGES));
pwi.notifyNewPatchSets =
toBoolean(a.isNotify(AccountProjectWatch.NotifyType.NEW_PATCHSETS));
toBoolean(e.getValue().contains(NotifyType.NEW_PATCHSETS));
pwi.notifySubmittedChanges =
toBoolean(
a.isNotify(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES));
toBoolean(e.getValue().contains(NotifyType.SUBMITTED_CHANGES));
pwi.notifyAllComments =
toBoolean(a.isNotify(AccountProjectWatch.NotifyType.ALL_COMMENTS));
toBoolean(e.getValue().contains(NotifyType.ALL_COMMENTS));
projectWatchInfos.add(pwi);
}
Collections.sort(projectWatchInfos, new Comparator<ProjectWatchInfo>() {
@Override
public int compare(ProjectWatchInfo pwi1, ProjectWatchInfo pwi2) {
return ComparisonChain.start()
.compare(pwi1.project, pwi2.project)
.compare(Strings.nullToEmpty(pwi1.filter),
Strings.nullToEmpty(pwi2.filter))
.result();
}
});
return projectWatchInfos;
}
private static Boolean toBoolean(boolean value) {
return value ? true : null;
}
public static Map<ProjectWatchKey, Set<NotifyType>> readProjectWatchesFromDb(
ReviewDb db, Account.Id who) throws OrmException {
Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
new HashMap<>();
for (AccountProjectWatch apw : db.accountProjectWatches().byAccount(who)) {
ProjectWatchKey key =
ProjectWatchKey.create(apw.getProjectNameKey(), apw.getFilter());
Set<NotifyType> notifyValues = EnumSet.noneOf(NotifyType.class);
for (NotifyType notifyType : NotifyType.values()) {
if (apw.isNotify(notifyType)) {
notifyValues.add(notifyType);
}
}
projectWatches.put(key, notifyValues);
}
return projectWatches;
}
}

View File

@@ -22,19 +22,26 @@ import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.project.ProjectsCollection;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import java.io.IOException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Singleton
@@ -45,39 +52,41 @@ public class PostWatchedProjects
private final GetWatchedProjects getWatchedProjects;
private final ProjectsCollection projectsCollection;
private final AccountCache accountCache;
private final WatchConfig.Accessor watchConfig;
@Inject
public PostWatchedProjects(Provider<ReviewDb> dbProvider,
Provider<IdentifiedUser> self,
GetWatchedProjects getWatchedProjects,
ProjectsCollection projectsCollection,
AccountCache accountCache) {
AccountCache accountCache,
WatchConfig.Accessor watchConfig) {
this.dbProvider = dbProvider;
this.self = self;
this.getWatchedProjects = getWatchedProjects;
this.projectsCollection = projectsCollection;
this.accountCache = accountCache;
this.watchConfig = watchConfig;
}
@Override
public List<ProjectWatchInfo> apply(AccountResource rsrc,
List<ProjectWatchInfo> input)
throws OrmException, RestApiException, IOException {
List<ProjectWatchInfo> input) throws OrmException, RestApiException,
IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser()
&& !self.get().getCapabilities().canAdministrateServer()) {
throw new AuthException("not allowed to edit project watches");
}
Account.Id accountId = rsrc.getUser().getAccountId();
List<AccountProjectWatch> accountProjectWatchList =
getAccountProjectWatchList(input, accountId);
dbProvider.get().accountProjectWatches().upsert(accountProjectWatchList);
updateInDb(accountId, input);
updateInGit(accountId, input);
accountCache.evict(accountId);
return getWatchedProjects.apply(rsrc);
}
private List<AccountProjectWatch> getAccountProjectWatchList(
List<ProjectWatchInfo> input, Account.Id accountId)
throws UnprocessableEntityException, BadRequestException, IOException {
private void updateInDb(Account.Id accountId, List<ProjectWatchInfo> input)
throws BadRequestException, UnprocessableEntityException, IOException,
OrmException {
Set<AccountProjectWatch.Key> keys = new HashSet<>();
List<AccountProjectWatch> watchedProjects = new LinkedList<>();
for (ProjectWatchInfo a : input) {
@@ -87,15 +96,11 @@ public class PostWatchedProjects
Project.NameKey projectKey =
projectsCollection.parse(a.project).getNameKey();
AccountProjectWatch.Key key =
new AccountProjectWatch.Key(accountId, projectKey, a.filter);
if (!keys.add(key)) {
throw new BadRequestException(
"duplicate entry for project " + key.getProjectName().get()
+ (!AccountProjectWatch.FILTER_ALL.equals(key.getFilter().get())
? " and filter " + key.getFilter().get()
: ""));
throw new BadRequestException("duplicate entry for project "
+ format(key.getProjectName().get(), key.getFilter().get()));
}
AccountProjectWatch apw = new AccountProjectWatch(key);
apw.setNotify(AccountProjectWatch.NotifyType.ABANDONED_CHANGES,
@@ -110,10 +115,61 @@ public class PostWatchedProjects
toBoolean(a.notifySubmittedChanges));
watchedProjects.add(apw);
}
return watchedProjects;
dbProvider.get().accountProjectWatches().upsert(watchedProjects);
}
private void updateInGit(Account.Id accountId, List<ProjectWatchInfo> input)
throws BadRequestException, UnprocessableEntityException, IOException,
ConfigInvalidException {
watchConfig.upsertProjectWatches(accountId, asMap(input));
}
private Map<ProjectWatchKey, Set<NotifyType>> asMap(
List<ProjectWatchInfo> input) throws BadRequestException,
UnprocessableEntityException, IOException {
Map<ProjectWatchKey, Set<NotifyType>> m = new HashMap<>();
for (ProjectWatchInfo info : input) {
if (info.project == null) {
throw new BadRequestException("project name must be specified");
}
ProjectWatchKey key = ProjectWatchKey.create(
projectsCollection.parse(info.project).getNameKey(), info.filter);
if (m.containsKey(key)) {
throw new BadRequestException(
"duplicate entry for project " + format(info.project, info.filter));
}
Set<NotifyType> notifyValues = EnumSet.noneOf(NotifyType.class);
if (toBoolean(info.notifyAbandonedChanges)) {
notifyValues.add(NotifyType.ABANDONED_CHANGES);
}
if (toBoolean(info.notifyAllComments)) {
notifyValues.add(NotifyType.ALL_COMMENTS);
}
if (toBoolean(info.notifyNewChanges)) {
notifyValues.add(NotifyType.NEW_CHANGES);
}
if (toBoolean(info.notifyNewPatchSets)) {
notifyValues.add(NotifyType.NEW_PATCHSETS);
}
if (toBoolean(info.notifySubmittedChanges)) {
notifyValues.add(NotifyType.SUBMITTED_CHANGES);
}
m.put(key, notifyValues);
}
return m;
}
private boolean toBoolean(Boolean b) {
return b == null ? false : b;
}
private static String format(String project, String filter) {
return project
+ (filter != null && !AccountProjectWatch.FILTER_ALL.equals(filter)
? " and filter " + filter
: "");
}
}

View File

@@ -14,7 +14,6 @@
package com.google.gerrit.server.account;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Function;
@@ -322,6 +321,6 @@ public class VersionedAuthorizedKeys extends VersionedMetaData
}
private void checkLoaded() {
checkNotNull(keys, "SSH keys not loaded yet");
checkState(keys != null, "SSH keys not loaded yet");
}
}

View File

@@ -0,0 +1,340 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.account;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Enums;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.VersionedMetaData;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* watch.config file in the user branch in the All-Users repository that
* contains the watch configuration of the user.
* <p>
* The 'watch.config' file is a git config file that has one 'project' section
* for all project watches of a project.
* <p>
* The project name is used as subsection name and the filters with the notify
* types that decide for which events email notifications should be sent are
* represented as 'notify' values in the subsection. A 'notify' value is
* formatted as '<filter> [<comma-separated-list-of-notify-types>]':
*
* <pre>
* [project "foo"]
* notify = * [ALL_COMMENTS]
* notify = branch:master [ALL_COMMENTS, NEW_PATCHSETS]
* notify = branch:master owner:self [SUBMITTED_CHANGES]
* </pre>
* <p>
* If two notify values in the same subsection have the same filter they are
* merged on the next save, taking the union of the notify types.
* <p>
* For watch configurations that notify on no event the list of notify types is
* empty:
*
* <pre>
* [project "foo"]
* notify = branch:master []
* </pre>
* <p>
* Unknown notify types are ignored and removed on save.
*/
public class WatchConfig extends VersionedMetaData implements AutoCloseable {
@Singleton
public static class Accessor {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
private final IdentifiedUser.GenericFactory userFactory;
@Inject
Accessor(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
Provider<MetaDataUpdate.User> metaDataUpdateFactory,
IdentifiedUser.GenericFactory userFactory) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.userFactory = userFactory;
}
public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches(
Account.Id accountId) throws IOException, ConfigInvalidException {
try (Repository git = repoManager.openRepository(allUsersName);
WatchConfig watchConfig = new WatchConfig(accountId)) {
watchConfig.load(git);
return watchConfig.getProjectWatches();
}
}
public void upsertProjectWatches(Account.Id accountId,
Map<ProjectWatchKey, Set<NotifyType>> newProjectWatches)
throws IOException, ConfigInvalidException {
try (WatchConfig watchConfig = open(accountId)) {
Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
watchConfig.getProjectWatches();
projectWatches.putAll(newProjectWatches);
commit(watchConfig);
}
}
public void deleteProjectWatches(Account.Id accountId,
Collection<ProjectWatchKey> projectWatchKeys)
throws IOException, ConfigInvalidException {
try (WatchConfig watchConfig = open(accountId)) {
Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
watchConfig.getProjectWatches();
boolean commit = false;
for (ProjectWatchKey key : projectWatchKeys) {
if (projectWatches.remove(key) != null) {
commit = true;
}
}
if (commit) {
commit(watchConfig);
}
}
}
private WatchConfig open(Account.Id accountId)
throws IOException, ConfigInvalidException {
Repository git = repoManager.openRepository(allUsersName);
WatchConfig watchConfig = new WatchConfig(accountId);
watchConfig.load(git);
return watchConfig;
}
private void commit(WatchConfig watchConfig)
throws IOException {
try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName,
userFactory.create(watchConfig.accountId))) {
watchConfig.commit(md);
}
}
}
@AutoValue
public abstract static class ProjectWatchKey {
public static ProjectWatchKey create(Project.NameKey project,
@Nullable String filter) {
return new AutoValue_WatchConfig_ProjectWatchKey(project,
Strings.emptyToNull(filter));
}
public abstract Project.NameKey project();
public abstract @Nullable String filter();
}
private static final String WATCH_CONFIG = "watch.config";
private static final String PROJECT = "project";
private static final String KEY_NOTIFY = "notify";
private final Account.Id accountId;
private final String ref;
private Repository git;
private Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
public WatchConfig(Account.Id accountId) {
this.accountId = accountId;
this.ref = RefNames.refsUsers(accountId);
}
@Override
protected String getRefName() {
return ref;
}
@Override
public void load(Repository git) throws IOException, ConfigInvalidException {
checkState(this.git == null);
this.git = git;
super.load(git);
}
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
Config cfg = readConfig(WATCH_CONFIG);
projectWatches = parse(accountId, cfg);
}
@VisibleForTesting
public static Map<ProjectWatchKey, Set<NotifyType>> parse(
Account.Id accountId, Config cfg) throws ConfigInvalidException {
Map<ProjectWatchKey, Set<NotifyType>> projectWatches = new HashMap<>();
for (String projectName : cfg.getSubsections(PROJECT)) {
String[] notifyValues =
cfg.getStringList(PROJECT, projectName, KEY_NOTIFY);
for (String nv : notifyValues) {
if (Strings.isNullOrEmpty(nv)) {
continue;
}
NotifyValue notifyValue = NotifyValue.parse(accountId, projectName, nv);
ProjectWatchKey key = ProjectWatchKey
.create(new Project.NameKey(projectName), notifyValue.filter());
if (!projectWatches.containsKey(key)) {
projectWatches.put(key, EnumSet.noneOf(NotifyType.class));
}
projectWatches.get(key).addAll(notifyValue.notifyTypes());
}
}
return projectWatches;
}
Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
checkLoaded();
return projectWatches;
}
@Override
protected boolean onSave(CommitBuilder commit)
throws IOException, ConfigInvalidException {
checkLoaded();
if (Strings.isNullOrEmpty(commit.getMessage())) {
commit.setMessage("Updated watch configuration\n");
}
Config cfg = readConfig(WATCH_CONFIG);
for (String projectName : cfg.getSubsections(PROJECT)) {
cfg.unset(PROJECT, projectName, KEY_NOTIFY);
}
Multimap<String, String> notifyValuesByProject = ArrayListMultimap.create();
for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : projectWatches
.entrySet()) {
NotifyValue notifyValue =
NotifyValue.create(e.getKey().filter(), e.getValue());
notifyValuesByProject.put(e.getKey().project().get(),
notifyValue.toString());
}
for (Map.Entry<String, Collection<String>> e : notifyValuesByProject.asMap()
.entrySet()) {
cfg.setStringList(PROJECT, e.getKey(), KEY_NOTIFY,
new ArrayList<>(e.getValue()));
}
saveConfig(WATCH_CONFIG, cfg);
return true;
}
@Override
public void close() {
if (git != null) {
git.close();
}
}
private void checkLoaded() {
checkState(projectWatches != null, "project watches not loaded yet");
}
@AutoValue
abstract static class NotifyValue {
public static NotifyValue parse(Account.Id accountId, String project,
String notifyValue) throws ConfigInvalidException {
notifyValue = notifyValue.trim();
int i = notifyValue.lastIndexOf('[');
if (i < 0 || notifyValue.charAt(notifyValue.length() - 1) != ']') {
throw new ConfigInvalidException(String.format(
"Invalid project watch of account %d for project %s: %s",
accountId.get(), project, notifyValue));
}
String filter = notifyValue.substring(0, i).trim();
if (filter.isEmpty() || AccountProjectWatch.FILTER_ALL.equals(filter)) {
filter = null;
}
Set<NotifyType> notifyTypes = EnumSet.noneOf(NotifyType.class);
if (i + 1 < notifyValue.length() - 2) {
for (String nt : Splitter.on(',').trimResults().splitToList(
notifyValue.substring(i + 1, notifyValue.length() - 1))) {
Optional<NotifyType> notifyType =
Enums.getIfPresent(NotifyType.class, nt);
if (!notifyType.isPresent()) {
throw new ConfigInvalidException(String.format(
"Invalid notify type %s in project watch "
+ "of account %d for project %s: %s",
nt, accountId.get(), project, notifyValue));
}
notifyTypes.add(notifyType.get());
}
}
return create(filter, notifyTypes);
}
public static NotifyValue create(@Nullable String filter,
Set<NotifyType> notifyTypes) {
return new AutoValue_WatchConfig_NotifyValue(Strings.emptyToNull(filter),
Sets.immutableEnumSet(notifyTypes));
}
public abstract @Nullable String filter();
public abstract ImmutableSet<NotifyType> notifyTypes();
@Override
public String toString() {
List<NotifyType> notifyTypes = new ArrayList<>(notifyTypes());
StringBuilder notifyValue = new StringBuilder();
notifyValue.append(firstNonNull(filter(), AccountProjectWatch.FILTER_ALL))
.append(" [");
Joiner.on(", ").appendTo(notifyValue, notifyTypes);
notifyValue.append("]");
return notifyValue.toString();
}
}
}

View File

@@ -221,7 +221,7 @@ public class AccountApiImpl implements AccountApi {
public List<ProjectWatchInfo> getWatchedProjects() throws RestApiException {
try {
return getWatchedProjects.apply(account);
} catch (OrmException e) {
} catch (OrmException | IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot get watched projects", e);
}
}
@@ -231,7 +231,7 @@ public class AccountApiImpl implements AccountApi {
List<ProjectWatchInfo> in) throws RestApiException {
try {
return postWatchedProjects.apply(account, in);
} catch (OrmException | IOException e) {
} catch (OrmException | IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot update watched projects", e);
}
}
@@ -241,7 +241,7 @@ public class AccountApiImpl implements AccountApi {
throws RestApiException {
try {
deleteWatchedProjects.apply(account, in);
} catch (OrmException | IOException e) {
} catch (OrmException | IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot delete watched projects", e);
}
}

View File

@@ -20,8 +20,8 @@ import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.SchemaUtil;
@@ -152,11 +152,11 @@ public class AccountField {
"watchedproject", FieldType.EXACT, false) {
@Override
public Iterable<String> get(AccountState input, FillArgs args) {
return FluentIterable.from(input.getProjectWatches())
.transform(new Function<AccountProjectWatch, String>() {
return FluentIterable.from(input.getProjectWatches().keySet())
.transform(new Function<ProjectWatchKey, String>() {
@Override
public String apply(AccountProjectWatch in) {
return in.getProjectNameKey().get();
public String apply(ProjectWatchKey in) {
return in.project().get();
}
}).toSet();
}

View File

@@ -28,6 +28,7 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.git.NotifyConfig;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.Predicate;
@@ -43,6 +44,7 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ProjectWatch {
@@ -94,19 +96,23 @@ public class ProjectWatch {
for (AccountState a : args.accountQueryProvider.get()
.byWatchedProject(project)) {
for (AccountProjectWatch w : a.getProjectWatches()) {
if (add(matching, w, type)) {
Account.Id accountId = a.getAccount().getId();
for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e :
a.getProjectWatches().entrySet()) {
if (add(matching, accountId, e.getKey(), e.getValue(), type)) {
// We only want to prevent matching All-Projects if this filter hits
projectWatchers.add(w.getAccountId());
projectWatchers.add(accountId);
}
}
}
for (AccountState a : args.accountQueryProvider.get()
.byWatchedProject(args.allProjectsName)) {
for (AccountProjectWatch w : a.getProjectWatches()) {
if (!projectWatchers.contains(w.getAccountId())) {
add(matching, w, type);
for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e :
a.getProjectWatches().entrySet()) {
Account.Id accountId = a.getAccount().getId();
if (!projectWatchers.contains(accountId)) {
add(matching, accountId, e.getKey(), e.getValue(), type);
}
}
}
@@ -210,6 +216,26 @@ public class ProjectWatch {
}
}
private boolean add(Watchers matching, Account.Id accountId,
ProjectWatchKey key, Set<NotifyType> watchedTypes, NotifyType type)
throws OrmException {
IdentifiedUser user = args.identifiedUserFactory.create(accountId);
try {
if (filterMatch(user, key.filter())) {
// If we are set to notify on this type, add the user.
// Otherwise, still return true to stop notifications for this user.
if (watchedTypes.contains(type)) {
matching.bcc.accounts.add(accountId);
}
return true;
}
} catch (QueryParseException e) {
// Ignore broken filter expressions.
}
return false;
}
private boolean add(Watchers matching, AccountProjectWatch w, NotifyType type)
throws OrmException {
IdentifiedUser user = args.identifiedUserFactory.create(w.getAccountId());

View File

@@ -15,8 +15,8 @@
package com.google.gerrit.server.query.change;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.query.AndPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryBuilder;
@@ -48,11 +48,11 @@ class IsWatchedByPredicate extends AndPredicate<ChangeData> {
boolean checkIsVisible) throws QueryParseException {
List<Predicate<ChangeData>> r = new ArrayList<>();
ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
for (AccountProjectWatch w : getWatches(args)) {
for (ProjectWatchKey w : getWatches(args)) {
Predicate<ChangeData> f = null;
if (w.getFilter() != null) {
if (w.filter() != null) {
try {
f = builder.parse(w.getFilter());
f = builder.parse(w.filter());
if (QueryBuilder.find(f, IsWatchedByPredicate.class) != null) {
// If the query is going to infinite loop, assume it
// will never match and return null. Yes this test
@@ -66,10 +66,10 @@ class IsWatchedByPredicate extends AndPredicate<ChangeData> {
}
Predicate<ChangeData> p;
if (w.getProjectNameKey().equals(args.allProjectsName)) {
if (w.project().equals(args.allProjectsName)) {
p = null;
} else {
p = builder.project(w.getProjectNameKey().get());
p = builder.project(w.project().get());
}
if (p != null && f != null) {
@@ -91,14 +91,14 @@ class IsWatchedByPredicate extends AndPredicate<ChangeData> {
}
}
private static Collection<AccountProjectWatch> getWatches(
private static Collection<ProjectWatchKey> getWatches(
ChangeQueryBuilder.Arguments args) throws QueryParseException {
CurrentUser user = args.getUser();
if (user.isIdentifiedUser()) {
return args.accountCache.get(args.getUser().getAccountId())
.getProjectWatches();
.getProjectWatches().keySet();
}
return Collections.<AccountProjectWatch> emptySet();
return Collections.<ProjectWatchKey> emptySet();
}
private static List<Predicate<ChangeData>> none() {

View File

@@ -0,0 +1,166 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.account;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.account.WatchConfig.NotifyValue;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class WatchConfigTest {
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void parseWatchConfig() throws Exception {
Config cfg = new Config();
cfg.fromText("[project \"myProject\"]\n"
+ " notify = * [ALL_COMMENTS, NEW_PATCHSETS]\n"
+ " notify = branch:master [NEW_CHANGES]\n"
+ " notify = branch:master [NEW_PATCHSETS]\n"
+ " notify = branch:foo []\n"
+ "[project \"otherProject\"]\n"
+ " notify = [NEW_PATCHSETS]\n"
+ " notify = * [NEW_PATCHSETS, ALL_COMMENTS]\n");
Map<ProjectWatchKey, Set<NotifyType>> projectWatches =
WatchConfig.parse(new Account.Id(1000000), cfg);
Project.NameKey myProject = new Project.NameKey("myProject");
Project.NameKey otherProject = new Project.NameKey("otherProject");
Map<ProjectWatchKey, Set<NotifyType>> expectedProjectWatches =
new HashMap<>();
expectedProjectWatches.put(ProjectWatchKey.create(myProject, null),
EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
expectedProjectWatches.put(
ProjectWatchKey.create(myProject, "branch:master"),
EnumSet.of(NotifyType.NEW_CHANGES, NotifyType.NEW_PATCHSETS));
expectedProjectWatches.put(ProjectWatchKey.create(myProject, "branch:foo"),
EnumSet.noneOf(NotifyType.class));
expectedProjectWatches.put(ProjectWatchKey.create(otherProject, null),
EnumSet.of(NotifyType.NEW_PATCHSETS));
expectedProjectWatches.put(ProjectWatchKey.create(otherProject, null),
EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
assertThat(projectWatches).containsExactlyEntriesIn(expectedProjectWatches);
}
@Test
public void parseInvalidWatchConfig() throws Exception {
Config cfg = new Config();
cfg.fromText("[project \"myProject\"]\n"
+ " notify = * [ALL_COMMENTS, NEW_PATCHSETS]\n"
+ " notify = branch:master [INVALID, NEW_CHANGES]\n"
+ "[project \"otherProject\"]\n"
+ " notify = [NEW_PATCHSETS]\n");
exception.expect(ConfigInvalidException.class);
exception.expectMessage(
"Invalid notify type INVALID in project watch of account 1000000"
+ " for project myProject: branch:master [INVALID, NEW_CHANGES]");
WatchConfig.parse(new Account.Id(1000000), cfg);
}
@Test
public void parseNotifyValue() throws Exception {
assertParseNotifyValue("* []", null, EnumSet.noneOf(NotifyType.class));
assertParseNotifyValue("* [ALL_COMMENTS]", null,
EnumSet.of(NotifyType.ALL_COMMENTS));
assertParseNotifyValue("[]", null, EnumSet.noneOf(NotifyType.class));
assertParseNotifyValue("[ALL_COMMENTS, NEW_PATCHSETS]", null,
EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
assertParseNotifyValue("branch:master []", "branch:master",
EnumSet.noneOf(NotifyType.class));
assertParseNotifyValue("branch:master || branch:stable []",
"branch:master || branch:stable", EnumSet.noneOf(NotifyType.class));
assertParseNotifyValue("branch:master [ALL_COMMENTS]", "branch:master",
EnumSet.of(NotifyType.ALL_COMMENTS));
assertParseNotifyValue("branch:master [ALL_COMMENTS, NEW_PATCHSETS]",
"branch:master",
EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
assertParseNotifyValue("* [ALL]", null, EnumSet.of(NotifyType.ALL));
}
@Test
public void parseInvalidNotifyValue() {
assertParseNotifyValueFails("* [] illegal-characters-at-the-end");
assertParseNotifyValueFails("* [INVALID]");
assertParseNotifyValueFails("* [ALL_COMMENTS, UNKNOWN]");
assertParseNotifyValueFails("* [ALL_COMMENTS NEW_CHANGES]");
assertParseNotifyValueFails("* [ALL_COMMENTS, NEW_CHANGES");
assertParseNotifyValueFails("* ALL_COMMENTS, NEW_CHANGES]");
}
@Test
public void toNotifyValue() throws Exception {
assertToNotifyValue(null, EnumSet.noneOf(NotifyType.class), "* []");
assertToNotifyValue("*", EnumSet.noneOf(NotifyType.class), "* []");
assertToNotifyValue(null, EnumSet.of(NotifyType.ALL_COMMENTS),
"* [ALL_COMMENTS]");
assertToNotifyValue("branch:master", EnumSet.noneOf(NotifyType.class),
"branch:master []");
assertToNotifyValue("branch:master",
EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS),
"branch:master [ALL_COMMENTS, NEW_PATCHSETS]");
assertToNotifyValue("branch:master",
EnumSet.of(NotifyType.ABANDONED_CHANGES, NotifyType.ALL_COMMENTS,
NotifyType.NEW_CHANGES, NotifyType.NEW_PATCHSETS,
NotifyType.SUBMITTED_CHANGES),
"branch:master [ABANDONED_CHANGES, ALL_COMMENTS, NEW_CHANGES,"
+ " NEW_PATCHSETS, SUBMITTED_CHANGES]");
assertToNotifyValue("*", EnumSet.of(NotifyType.ALL), "* [ALL]");
}
private static void assertParseNotifyValue(String notifyValue,
String expectedFilter, Set<NotifyType> expectedNotifyTypes)
throws ConfigInvalidException {
NotifyValue nv = parseNotifyValue(notifyValue);
assertThat(nv.filter()).isEqualTo(expectedFilter);
assertThat(nv.notifyTypes()).containsExactlyElementsIn(expectedNotifyTypes);
}
private static void assertToNotifyValue(String filter,
Set<NotifyType> notifyTypes, String expectedNotifyValue) {
NotifyValue nv = NotifyValue.create(filter, notifyTypes);
assertThat(nv.toString()).isEqualTo(expectedNotifyValue);
}
private static void assertParseNotifyValueFails(String notifyValue) {
try {
parseNotifyValue(notifyValue);
fail("expected ConfigInvalidException for notifyValue: " + notifyValue);
} catch (ConfigInvalidException e) {
// Expected.
}
}
private static NotifyValue parseNotifyValue(String notifyValue)
throws ConfigInvalidException {
return NotifyValue.parse(new Account.Id(1000000), "project", notifyValue);
}
}

View File

@@ -25,9 +25,10 @@ import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
@@ -35,6 +36,8 @@ import org.junit.Before;
import org.junit.Test;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;
public class FromAddressGeneratorProviderTest {
private Config config;
@@ -300,6 +303,6 @@ public class FromAddressGeneratorProviderTest {
account.setPreferredEmail(email);
return new AccountState(account, Collections.<AccountGroup.UUID> emptySet(),
Collections.<AccountExternalId> emptySet(),
Collections.<AccountProjectWatch> emptySet());
new HashMap<ProjectWatchKey, Set<NotifyType>>());
}
}

View File

@@ -19,12 +19,14 @@ import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/** Fake implementation of {@link AccountCache} for testing. */
public class FakeAccountCache implements AccountCache {
@@ -76,6 +78,6 @@ public class FakeAccountCache implements AccountCache {
private static AccountState newState(Account account) {
return new AccountState(account, ImmutableSet.<AccountGroup.UUID> of(),
ImmutableSet.<AccountExternalId> of(),
ImmutableSet.<AccountProjectWatch> of());
new HashMap<ProjectWatchKey, Set<NotifyType>>());
}
}