Merge "BuckUtils: Support missing buck.properties"
diff --git a/Documentation/cmd-set-reviewers.txt b/Documentation/cmd-set-reviewers.txt
index 79f7651..d5b4908 100644
--- a/Documentation/cmd-set-reviewers.txt
+++ b/Documentation/cmd-set-reviewers.txt
@@ -10,17 +10,16 @@
   [--add <REVIEWER> ... | -a <REVIEWER> ...]
   [--remove <REVIEWER> ... | -r <REVIEWER> ...]
   [--]
-  {COMMIT | CHANGE-ID}...
+  {CHANGE-ID}...
 --
 
 == DESCRIPTION
 Adds or removes reviewers to the specified change, sending email
 notifications when changes are made.
 
-Changes should be specified as complete or abbreviated Change-Ids
-such as 'Iac6b2ac2'.  They may also be specified by numeric change
-identifiers, such as '8242' or by complete or abbreviated commit
-SHA-1s.
+Changes can be specified in the
+link:rest-api-changes.html#change-id[same format] supported by the REST
+API.
 
 == OPTIONS
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index c9bea0a..8c488e4 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1404,7 +1404,8 @@
 httpd and sshd threads as some request processing code paths may
 need multiple connections.
 +
-Default is 8.
+Default is <<sshd.threads, sshd.threads>>
+ + <<httpd.maxThreads, httpd.maxThreads>> + 2.
 +
 This setting only applies if
 <<database.connectionPool,database.connectionPool>> is true.
@@ -1422,7 +1423,7 @@
 Maximum number of connections to keep idle in the pool.  If there
 are more idle connections, connections will be closed instead of
 being returned back to the pool.
-Default is 4.
+Default is min(<<database.poolLimit, database.poolLimit>>, 16).
 +
 This setting only applies if
 <<database.connectionPool,database.connectionPool>> is true.
@@ -3142,6 +3143,8 @@
 +
 A size of 0 bytes disables rules, same as rules.enable = false.
 +
+Common unit suffixes of 'k', 'm', or 'g' are supported.
++
 Default is 128 KiB.
 
 [[rules.maxPrologDatabaseSize]]rules.maxPrologDatabaseSize::
@@ -3429,7 +3432,7 @@
 If additional requests are received while all threads are busy they
 are queued and serviced in a first-come-first-served order.
 +
-By default, 1.5x the number of CPUs available to the JVM.
+By default, 2x the number of CPUs available to the JVM.
 
 [[sshd.batchThreads]]sshd.batchThreads::
 +
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 5c3b629..c05c8f0 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -131,7 +131,6 @@
   private static final String APPROVAL_FIELD = ChangeField.APPROVAL.getName();
   private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
   private static final String DELETED_FIELD = ChangeField.DELETED.getName();
-  private static final String ID_FIELD = ChangeField.LEGACY_ID2.getName();
   private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName();
   private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName();
   private static final String REVIEWEDBY_FIELD =
@@ -139,10 +138,6 @@
   private static final String UPDATED_SORT_FIELD =
       sortFieldName(ChangeField.UPDATED);
 
-  private static final ImmutableSet<String> FIELDS = ImmutableSet.of(
-      ADDED_FIELD, APPROVAL_FIELD, CHANGE_FIELD, DELETED_FIELD, ID_FIELD,
-      MERGEABLE_FIELD, PATCH_SET_FIELD, REVIEWEDBY_FIELD);
-
   private static final Map<String, String> CUSTOM_CHAR_MAPPING = ImmutableMap.of(
       "_", " ", ".", " ");
 
@@ -438,10 +433,12 @@
 
         List<ChangeData> result =
             Lists.newArrayListWithCapacity(docs.scoreDocs.length);
+        Set<String> fields = fields(opts);
+        String idFieldName = idFieldName();
         for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
           ScoreDoc sd = docs.scoreDocs[i];
-          Document doc = searchers[sd.shardIndex].doc(sd.doc, FIELDS);
-          result.add(toChangeData(doc));
+          Document doc = searchers[sd.shardIndex].doc(sd.doc, fields);
+          result.add(toChangeData(doc, fields, idFieldName));
         }
 
         final List<ChangeData> r = Collections.unmodifiableList(result);
@@ -477,19 +474,62 @@
     }
   }
 
-  private ChangeData toChangeData(Document doc) {
+  @SuppressWarnings("deprecation")
+  private Set<String> fields(QueryOptions opts) {
+    if (schemaHasRequestedField(ChangeField.LEGACY_ID2, opts.fields())
+        || schemaHasRequestedField(ChangeField.CHANGE, opts.fields())
+        || schemaHasRequestedField(ChangeField.LEGACY_ID, opts.fields())) {
+      return opts.fields();
+    }
+    // Request the numeric ID field even if the caller did not request it,
+    // otherwise we can't actually construct a ChangeData.
+    return Sets.union(opts.fields(), ImmutableSet.of(idFieldName()));
+  }
+
+  private boolean schemaHasRequestedField(FieldDef<ChangeData, ?> field,
+      Set<String> requested) {
+    return schema.hasField(field) && requested.contains(field.getName());
+  }
+
+  @SuppressWarnings("deprecation")
+  private String idFieldName() {
+    return schema.getField(ChangeField.LEGACY_ID2, ChangeField.LEGACY_ID).get()
+        .getName();
+  }
+
+  private ChangeData toChangeData(Document doc, Set<String> fields,
+      String idFieldName) {
+    ChangeData cd;
+    // Either change or the ID field was guaranteed to be included in the call
+    // to fields() above.
     BytesRef cb = doc.getBinaryValue(CHANGE_FIELD);
-    if (cb == null) {
-      int id = doc.getField(ID_FIELD).numericValue().intValue();
-      return changeDataFactory.create(db.get(), new Change.Id(id));
+    if (cb != null) {
+      cd = changeDataFactory.create(db.get(),
+          ChangeProtoField.CODEC.decode(cb.bytes, cb.offset, cb.length));
+    } else {
+      int id = doc.getField(idFieldName).numericValue().intValue();
+      cd = changeDataFactory.create(db.get(), new Change.Id(id));
     }
 
-    // Change proto.
-    Change change = ChangeProtoField.CODEC.decode(
-        cb.bytes, cb.offset, cb.length);
-    ChangeData cd = changeDataFactory.create(db.get(), change);
+    if (fields.contains(PATCH_SET_FIELD)) {
+      decodePatchSets(doc, cd);
+    }
+    if (fields.contains(APPROVAL_FIELD)) {
+      decodeApprovals(doc, cd);
+    }
+    if (fields.contains(ADDED_FIELD) && fields.contains(DELETED_FIELD)) {
+      decodeChangedLines(doc, cd);
+    }
+    if (fields.contains(MERGEABLE_FIELD)) {
+      decodeMergeable(doc, cd);
+    }
+    if (fields.contains(REVIEWEDBY_FIELD)) {
+      decodeReviewedBy(doc, cd);
+    }
+    return cd;
+  }
 
-    // Patch sets.
+  private void decodePatchSets(Document doc, ChangeData cd) {
     List<PatchSet> patchSets =
         decodeProtos(doc, PATCH_SET_FIELD, PatchSetProtoField.CODEC);
     if (!patchSets.isEmpty()) {
@@ -497,12 +537,14 @@
       // this cannot be valid since a change needs at least one patch set.
       cd.setPatchSets(patchSets);
     }
+  }
 
-    // Approvals.
+  private void decodeApprovals(Document doc, ChangeData cd) {
     cd.setCurrentApprovals(
         decodeProtos(doc, APPROVAL_FIELD, PatchSetApprovalProtoField.CODEC));
+  }
 
-    // Changed lines.
+  private void decodeChangedLines(Document doc, ChangeData cd) {
     IndexableField added = doc.getField(ADDED_FIELD);
     IndexableField deleted = doc.getField(DELETED_FIELD);
     if (added != null && deleted != null) {
@@ -510,16 +552,18 @@
           added.numericValue().intValue(),
           deleted.numericValue().intValue());
     }
+  }
 
-    // Mergeable.
+  private void decodeMergeable(Document doc, ChangeData cd) {
     String mergeable = doc.get(MERGEABLE_FIELD);
     if ("1".equals(mergeable)) {
       cd.setMergeable(true);
     } else if ("0".equals(mergeable)) {
       cd.setMergeable(false);
     }
+  }
 
-    // Reviewed-by.
+  private void decodeReviewedBy(Document doc, ChangeData cd) {
     IndexableField[] reviewedBy = doc.getFields(REVIEWEDBY_FIELD);
     if (reviewedBy.length > 0) {
       Set<Account.Id> accounts =
@@ -533,8 +577,6 @@
       }
       cd.setReviewedBy(accounts);
     }
-
-    return cd;
   }
 
   private static <T> List<T> decodeProtos(Document doc, String fieldName,
diff --git a/gerrit-openid/BUCK b/gerrit-openid/BUCK
index 78abce8..0a6363b 100644
--- a/gerrit-openid/BUCK
+++ b/gerrit-openid/BUCK
@@ -3,6 +3,9 @@
   srcs = glob(['src/main/java/**/*.java']),
   resources = glob(['src/main/resources/**/*']),
   deps = [
+    '//lib/openid:consumer',
+  ],
+  provided_deps = [
     '//gerrit-common:annotations',
     '//gerrit-common:server',
     '//gerrit-extension-api:api',
@@ -12,13 +15,12 @@
     '//gerrit-server:server',
     '//lib:guava',
     '//lib:gwtorm',
+    '//lib:servlet-api-3_1',
     '//lib/commons:codec',
     '//lib/guice:guice',
     '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
     '//lib/log:api',
-    '//lib/openid:consumer',
   ],
-  provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
 )
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 0684650..adfea594 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
@@ -127,11 +128,14 @@
   private boolean reverseProxy;
 
   @Inject
-  JettyServer(@GerritServerConfig final Config cfg, final SitePaths site,
-      final JettyEnv env, final HttpLogFactory httpLogFactory) {
+  JettyServer(@GerritServerConfig Config cfg,
+      ThreadSettingsConfig threadSettingsConfig,
+      SitePaths site,
+      JettyEnv env,
+      HttpLogFactory httpLogFactory) {
     this.site = site;
 
-    httpd = new Server(threadPool(cfg));
+    httpd = new Server(threadPool(cfg, threadSettingsConfig));
     httpd.setConnectors(listen(httpd, cfg));
 
     Handler app = makeContext(env, cfg);
@@ -315,8 +319,8 @@
     return site.resolve(path);
   }
 
-  private ThreadPool threadPool(Config cfg) {
-    int maxThreads = cfg.getInt("httpd", null, "maxthreads", 25);
+  private ThreadPool threadPool(Config cfg, ThreadSettingsConfig threadSettingsConfig) {
+    int maxThreads = threadSettingsConfig.getHttpdMaxThreads();
     int minThreads = cfg.getInt("httpd", null, "minthreads", 5);
     int maxQueued = cfg.getInt("httpd", null, "maxqueued", 200);
     int idleTimeout = (int)MILLISECONDS.convert(60, SECONDS);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
index 7f6313d..6443e21 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.inject.Inject;
@@ -39,9 +40,10 @@
   SiteLibraryBasedDataSourceProvider(SitePaths site,
       @GerritServerConfig Config cfg,
       MetricMaker metrics,
+      ThreadSettingsConfig tsc,
       DataSourceProvider.Context ctx,
       DataSourceType dst) {
-    super(cfg, metrics, ctx, dst);
+    super(cfg, metrics, tsc, ctx, dst);
     libdir = site.lib_dir;
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
index 44361aa..8db3125 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.pgm.util;
 
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.inject.Injector;
@@ -34,14 +35,15 @@
     return limitThreads(
         dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class)),
         dbInjector.getInstance(DataSourceType.class),
+        dbInjector.getInstance(ThreadSettingsConfig.class),
         threads);
   }
 
-  private static int limitThreads(Config cfg, DataSourceType dst, int threads) {
+  private static int limitThreads(Config cfg, DataSourceType dst,
+      ThreadSettingsConfig threadSettingsConfig, int threads) {
     boolean usePool = cfg.getBoolean("database", "connectionpool",
         dst.usePool());
-    int poolLimit = cfg.getInt("database", "poollimit",
-        DataSourceProvider.DEFAULT_POOL_LIMIT);
+    int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
     if (usePool && threads > poolLimit) {
       log.warn("Limiting program to " + poolLimit
           + " threads due to database.poolLimit");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 6c80d26..0cbe540 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -14,15 +14,13 @@
 
 package com.google.gerrit.server;
 
-import static com.google.gerrit.server.query.change.ChangeData.asChanges;
-
 import com.google.common.base.Function;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -43,6 +41,7 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gwtorm.server.OrmConcurrencyException;
@@ -73,6 +72,7 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -180,6 +180,7 @@
   private final Provider<IdentifiedUser> user;
   private final Provider<ReviewDb> db;
   private final Provider<InternalChangeQuery> queryProvider;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final RevertedSender.Factory revertedSenderFactory;
   private final ChangeInserter.Factory changeInserterFactory;
   private final GitRepositoryManager gitManager;
@@ -193,6 +194,7 @@
   ChangeUtil(Provider<IdentifiedUser> user,
       Provider<ReviewDb> db,
       Provider<InternalChangeQuery> queryProvider,
+      ChangeControl.GenericFactory changeControlFactory,
       RevertedSender.Factory revertedSenderFactory,
       ChangeInserter.Factory changeInserterFactory,
       GitRepositoryManager gitManager,
@@ -204,6 +206,7 @@
     this.user = user;
     this.db = db;
     this.queryProvider = queryProvider;
+    this.changeControlFactory = changeControlFactory;
     this.revertedSenderFactory = revertedSenderFactory;
     this.changeInserterFactory = changeInserterFactory;
     this.gitManager = gitManager;
@@ -425,34 +428,51 @@
    *
    * @param id change identifier, either a numeric ID, a Change-Id, or
    *     project~branch~id triplet.
-   * @return all matching changes, even if they are not visible to the current
-   *     user.
+   * @param user user to wrap in controls.
+   * @return possibly-empty list of controls for all matching changes,
+   *     corresponding to the given user; may or may not be visible.
+   * @throws OrmException if an error occurred querying the database.
    */
-  public List<Change> findChanges(String id)
-      throws OrmException, ResourceNotFoundException {
+  public List<ChangeControl> findChanges(String id, CurrentUser user)
+      throws OrmException {
     // Try legacy id
     if (id.matches("^[1-9][0-9]*$")) {
-      Change c = db.get().changes().get(Change.Id.parse(id));
-      if (c != null) {
-        return ImmutableList.of(c);
+      try {
+        return ImmutableList.of(
+            changeControlFactory.controlFor(Change.Id.parse(id), user));
+      } catch (NoSuchChangeException e) {
+        return Collections.emptyList();
       }
-      return Collections.emptyList();
     }
 
+    // Use the index to search for changes, but don't return any stored fields,
+    // to force rereading in case the index is stale.
+    InternalChangeQuery query = queryProvider.get()
+        .setRequestedFields(ImmutableSet.<String> of());
+
     // Try isolated changeId
     if (!id.contains("~")) {
-      return asChanges(queryProvider.get().byKeyPrefix(id));
+      return asChangeControls(query.byKeyPrefix(id));
     }
 
     // Try change triplet
     Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id);
     if (triplet.isPresent()) {
-      return asChanges(queryProvider.get().byBranchKey(
+      return asChangeControls(query.byBranchKey(
           triplet.get().branch(),
           triplet.get().id()));
     }
 
-    throw new ResourceNotFoundException(id);
+    return Collections.emptyList();
+  }
+
+  private List<ChangeControl> asChangeControls(List<ChangeData> cds)
+      throws OrmException {
+    List<ChangeControl> ctls = new ArrayList<>(cds.size());
+    for (ChangeData cd : cds) {
+      ctls.add(cd.changeControl(user.get()));
+    }
+    return ctls;
   }
 
   private static void deleteOnlyDraftPatchSetPreserveRef(ReviewDb db,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
index a3c0d37..76ab25a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
@@ -72,7 +72,7 @@
       user.asyncStarredChanges();
 
       ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
-      if (user.getStarredChanges().contains(change.getChange().getId())) {
+      if (user.getStarredChanges().contains(change.getId())) {
         return new AccountResource.StarredChange(user, change);
       }
       throw new ResourceNotFoundException(id);
@@ -141,7 +141,7 @@
         dbProvider.get().starredChanges().insert(Collections.singleton(
             new StarredChange(new StarredChange.Key(
                 rsrc.getUser().getAccountId(),
-                change.getChange().getId()))));
+                change.getId()))));
       } catch (OrmDuplicateKeyException e) {
         return Response.none();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index b55642c..b7617a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -132,7 +132,7 @@
 
   @Override
   public String id() {
-    return Integer.toString(change.getChange().getId().get());
+    return Integer.toString(change.getId().get());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index cb3729b..2d06e93 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -177,7 +177,7 @@
       if (edit.isPresent()) {
         throw new ResourceConflictException(String.format(
             "edit already exists for the change %s",
-            resource.getChange().getChangeId()));
+            resource.getId()));
       }
       edit = createEdit();
       if (!Strings.isNullOrEmpty(path)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index 03d189f..934a408 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -49,6 +49,10 @@
     return control;
   }
 
+  public Change.Id getId() {
+    return getControl().getId();
+  }
+
   public Change getChange() {
     return getControl().getChange();
   }
@@ -90,7 +94,7 @@
   public String getETag() {
     CurrentUser user = control.getUser();
     Hasher h = Hashing.md5().newHasher()
-        .putBoolean(user.getStarredChanges().contains(getChange().getId()));
+        .putBoolean(user.getStarredChanges().contains(getId()));
     prepareETag(h, user);
     return h.hash().toString();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index 1648a5d..c671534 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -24,11 +24,11 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -42,8 +42,8 @@
 public class ChangesCollection implements
     RestCollection<TopLevelResource, ChangeResource>,
     AcceptsPost<TopLevelResource> {
+  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> user;
-  private final ChangeControl.GenericFactory changeControlFactory;
   private final Provider<QueryChanges> queryFactory;
   private final DynamicMap<RestView<ChangeResource>> views;
   private final ChangeUtil changeUtil;
@@ -52,15 +52,15 @@
 
   @Inject
   ChangesCollection(
+      Provider<ReviewDb> db,
       Provider<CurrentUser> user,
-      ChangeControl.GenericFactory changeControlFactory,
       Provider<QueryChanges> queryFactory,
       DynamicMap<RestView<ChangeResource>> views,
       ChangeUtil changeUtil,
       CreateChange createChange,
       ChangeIndexer changeIndexer) {
+    this.db = db;
     this.user = user;
-    this.changeControlFactory = changeControlFactory;
     this.queryFactory = queryFactory;
     this.views = views;
     this.changeUtil = changeUtil;
@@ -81,8 +81,8 @@
   @Override
   public ChangeResource parse(TopLevelResource root, IdString id)
       throws ResourceNotFoundException, OrmException {
-    List<Change> changes = changeUtil.findChanges(id.encoded());
-    if (changes.isEmpty()) {
+    List<ChangeControl> ctls = changeUtil.findChanges(id.encoded(), user.get());
+    if (ctls.isEmpty()) {
       Integer changeId = Ints.tryParse(id.get());
       if (changeId != null) {
         try {
@@ -92,17 +92,15 @@
         }
       }
     }
-    if (changes.size() != 1) {
+    if (ctls.size() != 1) {
       throw new ResourceNotFoundException(id);
     }
 
-    ChangeControl control;
-    try {
-      control = changeControlFactory.validateFor(changes.get(0), user.get());
-    } catch (NoSuchChangeException e) {
+    ChangeControl ctl = ctls.get(0);
+    if (!ctl.isVisible(db.get())) {
       throw new ResourceNotFoundException(id);
     }
-    return new ChangeResource(control);
+    return new ChangeResource(ctl);
   }
 
   public ChangeResource parse(Change.Id id)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index 3768738..576ae76 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectsCollection;
@@ -154,19 +155,19 @@
       ObjectId parentCommit;
       List<String> groups;
       if (input.baseChange != null) {
-        List<Change> changes = changeUtil.findChanges(input.baseChange);
-        if (changes.size() != 1) {
+        List<ChangeControl> ctls = changeUtil.findChanges(
+            input.baseChange, rsrc.getControl().getUser());
+        if (ctls.size() != 1) {
           throw new InvalidChangeOperationException(
               "Base change not found: " + input.baseChange);
         }
-        Change change = Iterables.getOnlyElement(changes);
-        if (!rsrc.getControl().controlFor(change).isVisible(db.get())) {
+        ChangeControl ctl = Iterables.getOnlyElement(ctls);
+        if (!ctl.isVisible(db.get())) {
           throw new InvalidChangeOperationException(
               "Base change not found: " + input.baseChange);
         }
-        PatchSet ps = db.get().patchSets().get(
-            new PatchSet.Id(change.getId(),
-            change.currentPatchSetId().get()));
+        PatchSet ps =
+            db.get().patchSets().get(ctl.getChange().currentPatchSetId());
         parentCommit = ObjectId.fromString(ps.getRevision().get());
         groups = ps.getGroups();
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
index b276aae..2a99202 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
@@ -85,8 +85,7 @@
   public UiAction.Description getDescription(ChangeResource rsrc) {
     try {
       return new UiAction.Description()
-        .setTitle(String.format("Delete draft change %d",
-            rsrc.getChange().getChangeId()))
+        .setTitle("Delete draft change " + rsrc.getId())
         .setVisible(allowDrafts
             && rsrc.getChange().getStatus() == Status.DRAFT
             && rsrc.getControl().canDeleteDraft(dbProvider.get()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index 6c37252..3a70e09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -75,7 +75,7 @@
       throws AuthException, ResourceNotFoundException, OrmException,
       IOException {
     ChangeControl control = rsrc.getControl();
-    Change.Id changeId = rsrc.getChange().getId();
+    Change.Id changeId = rsrc.getId();
     ReviewDb db = dbProvider.get();
     ChangeUpdate update = updateFactory.create(rsrc.getControl());
 
@@ -103,13 +103,13 @@
       if (del.isEmpty()) {
         throw new ResourceNotFoundException();
       }
-      ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getChange().getId(), db);
+      ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getId(), db);
       db.patchSetApprovals().delete(del);
       update.removeReviewer(rsrc.getUser().getAccountId());
 
       if (msg.length() > 0) {
         ChangeMessage changeMessage =
-            new ChangeMessage(new ChangeMessage.Key(rsrc.getChange().getId(),
+            new ChangeMessage(new ChangeMessage.Key(rsrc.getId(),
                 ChangeUtil.messageUUID(db)),
                 control.getUser().getAccountId(),
                 TimeUtil.nowTs(), rsrc.getChange().currentPatchSetId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 9a658a9..9a72b07 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -237,8 +237,7 @@
 
     if (message != null) {
       changeMessage = new ChangeMessage(
-          new ChangeMessage.Key(
-              ctl.getChange().getId(), ChangeUtil.messageUUID(db)),
+          new ChangeMessage.Key(ctl.getId(), ChangeUtil.messageUUID(db)),
           ctx.getUser().getAccountId(), ctx.getWhen(), patchSet.getId());
       changeMessage.setMessage(message);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
index a3fc2e1..6d7720d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -52,7 +52,7 @@
           req.getChange().getProject(), req.getControl().getUser(),
           TimeUtil.nowTs())) {
       SetHashtagsOp op = hashtagsFactory.create(input);
-      bu.addOp(req.getChange().getId(), op);
+      bu.addOp(req.getId(), op);
       bu.execute();
       return Response.<ImmutableSortedSet<String>> ok(op.getUpdatedHashtags());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index 3ab84ab..1766ed2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -230,9 +230,9 @@
     ReviewDb db = dbProvider.get();
     ChangeUpdate update = updateFactory.create(rsrc.getControl());
     List<PatchSetApproval> added;
-    db.changes().beginTransaction(rsrc.getChange().getId());
+    db.changes().beginTransaction(rsrc.getId());
     try {
-      ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getChange().getId(), db);
+      ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getId(), db);
       added = approvalsUtil.addReviewers(db, rsrc.getNotes(), update,
           rsrc.getControl().getLabelTypes(), rsrc.getChange(),
           reviewers.keySet());
@@ -243,7 +243,7 @@
 
     update.commit();
     CheckedFuture<?, IOException> indexFuture =
-        indexer.indexAsync(rsrc.getChange().getId());
+        indexer.indexAsync(rsrc.getId());
     result.reviewers = Lists.newArrayListWithCapacity(added.size());
     for (PatchSetApproval psa : added) {
       // New reviewers have value 0, don't bother normalizing.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index ae12497..df629bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -77,7 +77,7 @@
     Op op = new Op(ctl, input != null ? input : new Input());
     try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
         req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
-      u.addOp(req.getChange().getId(), op);
+      u.addOp(req.getId(), op);
       u.execute();
     }
     return Strings.isNullOrEmpty(op.newTopicName)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index ce2e81d..421fced 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -87,7 +87,7 @@
     Op op = new Op(input);
     try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
         req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
-      u.addOp(req.getChange().getId(), op).execute();
+      u.addOp(req.getId(), op).execute();
     }
     return json.create(ChangeJson.NO_OPTIONS).format(op.change);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
index bb5775b..734ebb6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -129,7 +129,7 @@
       // Chance of collision rises; look at all patch sets on the change.
       List<RevisionResource> out = Lists.newArrayList();
       for (PatchSet ps : dbProvider.get().patchSets()
-          .byChange(change.getChange().getId())) {
+          .byChange(change.getId())) {
         if (ps.getRevision() != null && ps.getRevision().get().startsWith(id)) {
           out.add(new RevisionResource(change, ps));
         }
@@ -141,7 +141,7 @@
   private List<RevisionResource> byLegacyPatchSetId(ChangeResource change,
       String id) throws OrmException {
     PatchSet ps = dbProvider.get().patchSets().get(new PatchSet.Id(
-        change.getChange().getId(),
+        change.getId(),
         Integer.parseInt(id)));
     if (ps != null) {
       return Collections.singletonList(new RevisionResource(change, ps));
@@ -161,8 +161,7 @@
       throws AuthException, IOException {
     Optional<ChangeEdit> edit = editUtil.byChange(change.getChange());
     if (edit.isPresent()) {
-      PatchSet ps = new PatchSet(new PatchSet.Id(
-          change.getChange().getId(), 0));
+      PatchSet ps = new PatchSet(new PatchSet.Id(change.getId(), 0));
       ps.setRevision(edit.get().getRevision());
       if (revid == null || edit.get().getRevision().equals(revid)) {
         return Collections.singletonList(
@@ -174,7 +173,7 @@
 
   private static List<RevisionResource> toResources(final ChangeResource change,
       Iterable<PatchSet> patchSets) {
-    final Change.Id changeId = change.getChange().getId();
+    final Change.Id changeId = change.getId();
     return FluentIterable.from(patchSets)
         .filter(new Predicate<PatchSet>() {
           @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ThreadSettingsConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ThreadSettingsConfig.java
new file mode 100644
index 0000000..c62583e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ThreadSettingsConfig.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 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.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class ThreadSettingsConfig {
+  private final int sshdThreads;
+  private final int httpdMaxThreads;
+  private final int sshdBatchThreads;
+  private final int databasePoolLimit;
+
+  @Inject
+  ThreadSettingsConfig(@GerritServerConfig Config cfg) {
+    int cores = Runtime.getRuntime().availableProcessors();
+    sshdThreads = cfg.getInt("sshd", "threads", 2 * cores);
+    httpdMaxThreads = cfg.getInt("httpd", "maxThreads", 25);
+    int defaultDatabasePoolLimit = sshdThreads + httpdMaxThreads + 2;
+    databasePoolLimit =
+        cfg.getInt("database", "poolLimit", defaultDatabasePoolLimit);
+    sshdBatchThreads = cores == 1 ? 1 : 2;
+  }
+
+  public int getDatabasePoolLimit() {
+    return databasePoolLimit;
+  }
+
+  public int getHttpdMaxThreads() {
+    return httpdMaxThreads;
+  }
+
+  public int getSshdThreads() {
+    return sshdThreads;
+  }
+
+  public int getSshdBatchTreads() {
+    return sshdBatchThreads;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
index 0dc7aa9..097ab6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
@@ -116,7 +116,7 @@
     LabelTypes labelTypes = ctl.getLabelTypes();
     for (PatchSetApproval psa : approvals) {
       Change.Id changeId = psa.getKey().getParentKey().getParentKey();
-      checkArgument(changeId.equals(ctl.getChange().getId()),
+      checkArgument(changeId.equals(ctl.getId()),
           "Approval %s does not match change %s",
           psa.getKey(), ctl.getChange().getKey());
       if (psa.isSubmit()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
index 683f8cf..28fa9f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
@@ -50,7 +50,7 @@
     int backendLimit = opts.config().maxLimit();
     int limit = Ints.saturatedCast((long) opts.limit() + opts.start());
     limit = Math.min(limit, backendLimit);
-    return QueryOptions.create(opts.config(), 0, limit);
+    return QueryOptions.create(opts.config(), 0, limit, opts.fields());
   }
 
   private final ChangeIndex index;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
index df70292..a605461 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
@@ -61,6 +61,8 @@
   }
 
   private final ImmutableMap<String, FieldDef<T, ?>> fields;
+  private final ImmutableMap<String, FieldDef<T, ?>> storedFields;
+
   private int version;
 
   protected Schema(Iterable<FieldDef<T, ?>> fields) {
@@ -71,10 +73,15 @@
   public Schema(int version, Iterable<FieldDef<T, ?>> fields) {
     this.version = version;
     ImmutableMap.Builder<String, FieldDef<T, ?>> b = ImmutableMap.builder();
+    ImmutableMap.Builder<String, FieldDef<T, ?>> sb = ImmutableMap.builder();
     for (FieldDef<T, ?> f : fields) {
       b.put(f.getName(), f);
+      if (f.isStored()) {
+        sb.put(f.getName(), f);
+      }
     }
     this.fields = b.build();
+    this.storedFields = sb.build();
   }
 
   public final int getVersion() {
@@ -95,6 +102,14 @@
   }
 
   /**
+   * @return all fields in this schema where {@link FieldDef#isStored()} is
+   *     true.
+   */
+  public final ImmutableMap<String, FieldDef<T, ?>> getStoredFields() {
+    return storedFields;
+  }
+
+  /**
    * Look up fields in this schema.
    *
    * @param first the preferred field to look up.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index fb41027..22c8da4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -86,8 +86,7 @@
   }
 
   public void setPatchSetId(PatchSet.Id psId) {
-    checkArgument(psId == null
-        || psId.getParentKey().equals(getChange().getId()));
+    checkArgument(psId == null || psId.getParentKey().equals(ctl.getId()));
     this.psId = psId;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index bd8f797..acd2852 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -98,7 +98,7 @@
     IdentifiedUser user = ctl.getUser().asIdentifiedUser();
     this.accountId = user.getAccountId();
     this.changeNotes = getChangeNotes().load();
-    this.draftNotes = draftNotesFactory.create(ctl.getChange().getId(),
+    this.draftNotes = draftNotesFactory.create(ctl.getId(),
         user.getAccountId());
 
     this.upsertComments = Lists.newArrayList();
@@ -273,7 +273,7 @@
 
   @Override
   protected String getRefName() {
-    return RefNames.refsDraftComments(accountId, getChange().getId());
+    return RefNames.refsDraftComments(accountId, ctl.getId());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 1d8e507..08632dd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -387,7 +387,7 @@
 
   @Override
   protected String getRefName() {
-    return ChangeNoteUtil.changeRefName(getChange().getId());
+    return ChangeNoteUtil.changeRefName(ctl.getId());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 8a50114..606ca78d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -90,7 +90,7 @@
         throws NoSuchChangeException, OrmException {
       ChangeControl c = controlFor(change, user);
       if (!c.isVisible(db.get())) {
-        throw new NoSuchChangeException(c.getChange().getId());
+        throw new NoSuchChangeException(c.getId());
       }
       return c;
     }
@@ -153,6 +153,10 @@
     return getProjectControl().getProject();
   }
 
+  public Change.Id getId() {
+    return notes.getChangeId();
+  }
+
   public Change getChange() {
     return notes.getChange();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index c06029b..a3fb523 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -416,7 +416,7 @@
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     this.mergeabilityCache = mergeabilityCache;
-    legacyId = c.getChange().getId();
+    legacyId = c.getId();
     change = c.getChange();
     changeControl = c;
     notes = c.getNotes();
@@ -544,6 +544,23 @@
     return changeControl;
   }
 
+  public ChangeControl changeControl(CurrentUser user) throws OrmException {
+    if (changeControl != null) {
+      throw new IllegalStateException(
+          "user already specified: " + changeControl.getUser());
+    }
+    try {
+      if (change != null) {
+        changeControl = changeControlFactory.controlFor(change, user);
+      } else {
+        changeControl = changeControlFactory.controlFor(legacyId, user);
+      }
+    } catch (NoSuchChangeException e) {
+      throw new OrmException(e);
+    }
+    return changeControl;
+  }
+
   void cacheVisibleTo(ChangeControl ctl) {
     visibleTo = ctl.getUser();
     changeControl = ctl;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index a2ccc3a..2dde972 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -106,6 +106,11 @@
     return this;
   }
 
+  public InternalChangeQuery setRequestedFields(Set<String> fields) {
+    qp.setRequestedFields(fields);
+    return this;
+  }
+
   public List<ChangeData> byKey(Change.Key key) throws OrmException {
     return byKeyPrefix(key.get());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java
index 1964fa5..a70f892 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java
@@ -17,29 +17,36 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.server.index.IndexConfig;
 
+import java.util.Set;
+
 @AutoValue
 public abstract class QueryOptions {
-  public static QueryOptions create(IndexConfig config, int start, int limit) {
+  public static QueryOptions create(IndexConfig config, int start, int limit,
+      Set<String> fields) {
     checkArgument(start >= 0, "start must be nonnegative: %s", start);
     checkArgument(limit > 0, "limit must be positive: %s", limit);
-    return new AutoValue_QueryOptions(config, start, limit);
+    return new AutoValue_QueryOptions(config, start, limit,
+        ImmutableSet.copyOf(fields));
   }
 
   public static QueryOptions oneResult() {
-    return create(IndexConfig.createDefault(), 0, 1);
+    return create(IndexConfig.createDefault(), 0, 1,
+        ImmutableSet.<String> of());
   }
 
   public abstract IndexConfig config();
   public abstract int start();
   public abstract int limit();
+  public abstract ImmutableSet<String> fields();
 
   public QueryOptions withLimit(int newLimit) {
-    return create(config(), start(), newLimit);
+    return create(config(), start(), newLimit, fields());
   }
 
   public QueryOptions withStart(int newStart) {
-    return create(config(), newStart, limit());
+    return create(config(), newStart, limit(), fields());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index c3c70b3..1a6ae02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.metrics.Description;
@@ -25,6 +26,8 @@
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.IndexRewriter;
@@ -39,11 +42,13 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 public class QueryProcessor {
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> userProvider;
   private final ChangeControl.GenericFactory changeControlFactory;
+  private final IndexCollection indexes;
   private final IndexRewriter rewriter;
   private final IndexConfig indexConfig;
   private final Metrics metrics;
@@ -51,17 +56,20 @@
   private int limitFromCaller;
   private int start;
   private boolean enforceVisibility = true;
+  private Set<String> requestedFields;
 
   @Inject
   QueryProcessor(Provider<ReviewDb> db,
       Provider<CurrentUser> userProvider,
       ChangeControl.GenericFactory changeControlFactory,
+      IndexCollection indexes,
       IndexRewriter rewriter,
       IndexConfig indexConfig,
       Metrics metrics) {
     this.db = db;
     this.userProvider = userProvider;
     this.changeControlFactory = changeControlFactory;
+    this.indexes = indexes;
     this.rewriter = rewriter;
     this.indexConfig = indexConfig;
     this.metrics = metrics;
@@ -82,6 +90,11 @@
     return this;
   }
 
+  public QueryProcessor setRequestedFields(Set<String> fields) {
+    requestedFields = fields;
+    return this;
+  }
+
   /**
    * Query for changes that match a structured query.
    *
@@ -150,7 +163,8 @@
             "Cannot go beyond page " + indexConfig.maxPages() + "of results");
       }
 
-      QueryOptions opts = QueryOptions.create(indexConfig, start, limit + 1);
+      QueryOptions opts = QueryOptions.create(
+          indexConfig, start, limit + 1, getRequestedFields());
       Predicate<ChangeData> s = rewriter.rewrite(q, opts);
       if (!(s instanceof ChangeDataSource)) {
         q = Predicate.and(open(), q);
@@ -184,6 +198,16 @@
     return out;
   }
 
+  private Set<String> getRequestedFields() {
+    if (requestedFields != null) {
+      return requestedFields;
+    }
+    ChangeIndex index = indexes.getSearchIndex();
+    return index != null
+        ? index.getSchema().getStoredFields().keySet()
+        : ImmutableSet.<String> of();
+  }
+
   boolean isDisabled() {
     return getPermittedLimit() <= 0;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index e0482f9..0c3bf67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.config.ConfigSection;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gwtorm.jdbc.SimpleDataSource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -47,21 +48,22 @@
 @Singleton
 public class DataSourceProvider implements Provider<DataSource>,
     LifecycleListener {
-  public static final int DEFAULT_POOL_LIMIT = 8;
-
   private final Config cfg;
   private final MetricMaker metrics;
   private final Context ctx;
   private final DataSourceType dst;
+  private final ThreadSettingsConfig threadSettingsConfig;
   private DataSource ds;
 
   @Inject
   protected DataSourceProvider(@GerritServerConfig Config cfg,
       MetricMaker metrics,
+      ThreadSettingsConfig threadSettingsConfig,
       Context ctx,
       DataSourceType dst) {
     this.cfg = cfg;
     this.metrics = metrics;
+    this.threadSettingsConfig = threadSettingsConfig;
     this.ctx = ctx;
     this.dst = dst;
   }
@@ -127,9 +129,11 @@
       if (password != null && !password.isEmpty()) {
         ds.setPassword(password);
       }
-      ds.setMaxActive(cfg.getInt("database", "poollimit", DEFAULT_POOL_LIMIT));
+      int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
+      ds.setMaxActive(poolLimit);
       ds.setMinIdle(cfg.getInt("database", "poolminidle", 4));
-      ds.setMaxIdle(cfg.getInt("database", "poolmaxidle", 4));
+      ds.setMaxIdle(
+          cfg.getInt("database", "poolmaxidle", Math.min(poolLimit, 16)));
       ds.setMaxWait(ConfigUtil.getTimeUnit(cfg, "database", null,
           "poolmaxwait", MILLISECONDS.convert(30, SECONDS), MILLISECONDS));
       ds.setInitialSize(ds.getMinIdle());
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index 6988459..5370156 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -63,6 +63,10 @@
 		AWK=/usr/xpg4/bin/awk
 	fi
 
+	# Get core.commentChar from git config or use default symbol
+	commentChar=`git config --get core.commentChar`
+	commentChar=${commentChar:-#}
+
 	# How this works:
 	# - parse the commit message as (textLine+ blankLine*)*
 	# - assume textLine+ to be a footer until proven otherwise
@@ -81,8 +85,8 @@
 		blankLines = 0
 	}
 
-	# Skip lines starting with "#" without any spaces before it.
-	/^#/ { next }
+	# Skip lines starting with commentChar without any spaces before it.
+	/^'"$commentChar"'/ { next }
 
 	# Skip the line starting with the diff command and everything after it,
 	# up to the end of the file, assuming it is only patch data.
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java
index 7b1185e..f44d172 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java
@@ -25,6 +25,7 @@
 import static com.google.gerrit.server.query.Predicate.or;
 import static org.junit.Assert.assertEquals;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.Predicate;
@@ -285,7 +286,8 @@
   }
 
   private static QueryOptions options(int start, int limit) {
-    return QueryOptions.create(CONFIG, start, limit);
+    return QueryOptions.create(CONFIG, start, limit,
+        ImmutableSet.<String> of());
   }
 
   private Set<Change.Status> status(String query) throws QueryParseException {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index dd2667f..49e1796 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -56,6 +56,7 @@
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.ChangeControl;
@@ -1263,6 +1264,30 @@
     cd.messages();
   }
 
+  @Test
+  public void prepopulateOnlyRequestedFields() throws Exception {
+    assume().that(notesMigration.enabled()).isFalse();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change = insert(newChange(repo, null, null, null, null));
+
+    db = new DisabledReviewDb();
+    requestContext.setContext(newRequestContext(userId));
+    // Use QueryProcessor directly instead of API so we get ChangeDatas back.
+    List<ChangeData> cds = queryProcessor
+        .setRequestedFields(ImmutableSet.of(
+            ChangeField.PATCH_SET.getName(),
+            ChangeField.CHANGE.getName()))
+        .queryChanges(queryBuilder.parse(change.getId().toString()))
+        .changes();
+    assertThat(cds).hasSize(1);
+
+    ChangeData cd = cds.get(0);
+    cd.change();
+    cd.patchSets();
+
+    exception.expect(DisabledReviewDb.Disabled.class);
+    cd.currentApprovals();
+  }
 
   protected ChangeInserter newChange(
       TestRepository<Repo> repo,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
index 7e7899b..53a9805 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
@@ -78,6 +78,13 @@
     // Ignore.
   }
 
+  @Override
+  @Ignore
+  @Test
+  public void prepopulateOnlyRequestedFields() throws Exception {
+    // Ignore.
+  }
+
   @Test
   public void isReviewed() throws Exception {
     clockStepMs = MILLISECONDS.convert(2, MINUTES);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
index aa7b38b..f35dbea 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
@@ -108,6 +108,7 @@
     ChangeNotes notes = new ChangeNotes(repoManager, migration, allUsers, c)
         .load();
     expect(ctl.getNotes()).andStubReturn(notes);
+    expect(ctl.getId()).andStubReturn(c.getId());
     EasyMock.replay(ctl);
     return ctl;
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
index f78aba4..88e1142 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
@@ -31,11 +32,13 @@
   private final WorkQueue.Executor batchExecutor;
 
   @Inject
-  public CommandExecutorQueueProvider(@GerritServerConfig final Config config,
-      final WorkQueue queues) {
-    final int cores = Runtime.getRuntime().availableProcessors();
-    poolSize = config.getInt("sshd", "threads", 3 * cores / 2);
-    batchThreads = config.getInt("sshd", "batchThreads", cores == 1 ? 1 : 2);
+  public CommandExecutorQueueProvider(
+      @GerritServerConfig Config config,
+      ThreadSettingsConfig threadsSettingsConfig,
+      WorkQueue queues) {
+    poolSize = threadsSettingsConfig.getSshdThreads();
+    batchThreads = config.getInt("sshd", "batchThreads",
+        threadsSettingsConfig.getSshdBatchTreads());
     if (batchThreads > poolSize) {
       poolSize += batchThreads;
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index e9043f7..db32b3f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -18,9 +18,8 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
@@ -28,14 +27,10 @@
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -47,7 +42,9 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 @CommandMetaData(name = "set-reviewers", description = "Add or remove reviewers on a change")
@@ -69,7 +66,7 @@
   @Argument(index = 0, required = true, multiValued = true, metaVar = "COMMIT", usage = "changes to modify")
   void addChange(String token) {
     try {
-      changes.addAll(parseChangeId(token));
+      addChangeImpl(token);
     } catch (UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
     } catch (OrmException e) {
@@ -81,9 +78,6 @@
   private ReviewDb db;
 
   @Inject
-  private Provider<InternalChangeQuery> queryProvider;
-
-  @Inject
   private ReviewerResource.Factory reviewerFactory;
 
   @Inject
@@ -96,24 +90,25 @@
   private Provider<CurrentUser> userProvider;
 
   @Inject
-  private ChangeControl.GenericFactory changeControlFactory;
-
-  @Inject
   private ChangesCollection changesCollection;
 
+  @Inject
+  private ChangeUtil changeUtil;
+
   private Set<Account.Id> toRemove = new HashSet<>();
-  private Set<Change.Id> changes = new HashSet<>();
+
+  private Map<Change.Id, ChangeResource> changes = new LinkedHashMap<>();
 
   @Override
   protected void run() throws UnloggedFailure {
     boolean ok = true;
-    for (Change.Id changeId : changes) {
+    for (ChangeResource rsrc : changes.values()) {
       try {
-        ok &= modifyOne(changeId);
+        ok &= modifyOne(rsrc);
       } catch (Exception err) {
         ok = false;
-        log.error("Error updating reviewers on change " + changeId, err);
-        writeError("fatal", "internal error while updating " + changeId);
+        log.error("Error updating reviewers on change " + rsrc.getId(), err);
+        writeError("fatal", "internal error while updating " + rsrc.getId());
       }
     }
 
@@ -122,8 +117,7 @@
     }
   }
 
-  private boolean modifyOne(Change.Id changeId) throws Exception {
-    ChangeResource changeRsrc = changesCollection.parse(changeId);
+  private boolean modifyOne(ChangeResource changeRsrc) throws Exception {
     boolean ok = true;
 
     // Remove reviewers
@@ -168,92 +162,28 @@
     return ok;
   }
 
-  private Set<Change.Id> parseChangeId(String idstr)
-      throws UnloggedFailure, OrmException {
-    Set<Change.Id> matched = new HashSet<>(4);
-    boolean isCommit = idstr.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$");
-
-    // By newer style changeKey?
-    //
-    boolean changeKeyParses = idstr.matches("^I[0-9a-f]*$");
-    if (changeKeyParses) {
-      for (ChangeData cd : queryProvider.get().byKeyPrefix(idstr)) {
-        matchChange(matched, cd.change());
+  private void addChangeImpl(String id) throws UnloggedFailure, OrmException {
+    List<ChangeControl> matched =
+        changeUtil.findChanges(id, userProvider.get());
+    List<ChangeControl> toAdd = new ArrayList<>(changes.size());
+    for (ChangeControl ctl : matched) {
+      Change c = ctl.getChange();
+      if (!changes.containsKey(c.getId()) && inProject(c)
+          && ctl.isVisible(db)) {
+        toAdd.add(ctl);
       }
     }
-
-    // By commit?
-    //
-    if (isCommit) {
-      RevId id = new RevId(idstr);
-      ResultSet<PatchSet> patches;
-      if (id.isComplete()) {
-        patches = db.patchSets().byRevision(id);
-      } else {
-        patches = db.patchSets().byRevisionRange(id, id.max());
-      }
-
-      for (PatchSet ps : patches) {
-        matchChange(matched, ps.getId().getParentKey());
-      }
-    }
-
-    // By older style changeId?
-    //
-    boolean changeIdParses = false;
-    if (idstr.matches("^[1-9][0-9]*$")) {
-      Change.Id id;
-      try {
-        id = Change.Id.parse(idstr);
-        changeIdParses = true;
-      } catch (IllegalArgumentException e) {
-        id = null;
-        changeIdParses = false;
-      }
-
-      if (changeIdParses) {
-        matchChange(matched, id);
-      }
-    }
-
-    if (!changeKeyParses && !isCommit && !changeIdParses) {
-      throw error("\"" + idstr + "\" is not a valid change");
-    }
-
-    switch (matched.size()) {
+    switch (toAdd.size()) {
       case 0:
-        throw error("\"" + idstr + "\" no such change");
+        throw error("\"" + id + "\" no such change");
 
       case 1:
-        return matched;
+        ChangeControl ctl = toAdd.get(0);
+        changes.put(ctl.getId(), changesCollection.parse(ctl));
+        break;
 
       default:
-        throw error("\"" + idstr + "\" matches multiple changes");
-    }
-  }
-
-  private void matchChange(Set<Change.Id> matched, Change.Id changeId) {
-    if (changeId != null && !matched.contains(changeId)) {
-      try {
-        matchChange(matched, db.changes().get(changeId));
-      } catch (OrmException e) {
-        log.warn("Error reading change " + changeId, e);
-      }
-    }
-  }
-
-  private void matchChange(Set<Change.Id> matched, Change change) {
-    try {
-      if (change != null
-          && inProject(change)
-          && changeControlFactory.controlFor(change,
-                userProvider.get()).isVisible(db)) {
-        matched.add(change.getId());
-      }
-    } catch (NoSuchChangeException e) {
-      // Ignore this change.
-    } catch (OrmException e) {
-      log.warn("Error reading change " + change.getId(), e);
+        throw error("\"" + id + "\" matches multiple changes");
     }
   }