Merge "Update padding of file action buttons"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 45bc045..91a837d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1133,6 +1133,18 @@
 +
 Default is true.
 
+[[change.api.allowedIdentifier]]change.api.allowedIdentifier::
++
+Change identifier(s) that are allowed on the API. See
+link:rest-api-changes.html#change-id[Change Id] for more information.
++
+Possible values are `ALL`, `TRIPLET`, `NUMERIC_ID`, `I_HASH`, and
+`COMMIT_HASH` or any combination of those as a string list.
+`PROJECT_NUMERIC_ID` is always allowed and doesn't need to be listed
+explicitly.
++
+Default is `ALL`.
+
 [[change.cacheAutomerge]]change.cacheAutomerge::
 +
 When reviewing diff commits, the left-hand side shows the output of the
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index dbcbe49..a3f1e55 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -161,11 +161,14 @@
 `plugin.hook(endpointName, opt_options)`
 
 See list of supported link:pg-plugin-endpoints.html[endpoints].
+
 Note: TODO
 
 === registerCustomComponent
 `plugin.registerCustomComponent(endpointName, opt_moduleName, opt_options)`
 
+See list of supported link:pg-plugin-endpoints.html[endpoints].
+
 Note: TODO
 
 === registerStyleModule
@@ -239,6 +242,28 @@
 
 Note: TODO
 
+=== screen
+`plugin.screen(screenName, opt_moduleName)`
+
+.Params:
+- `*string* screenName` URL path fragment of the screen, e.g.
+`/x/pluginname/*screenname*`
+- `*string* opt_moduleName` (Optional) Web component to be instantiated for this
+screen.
+
+.Returns:
+- Instance of GrDomHook.
+
+=== screenUrl
+`plugin.url(opt_screenName)`
+
+.Params:
+- `*string* screenName` (optional) URL path fragment of the screen, e.g.
+`/x/pluginname/*screenname*`
+
+.Returns:
+- Absolute URL for the screen, e.g. `http://localhost/base/x/pluginname/screenname`
+
 === theme
 `plugin.theme()`
 
diff --git a/Documentation/pg-plugin-project-api.txt b/Documentation/pg-plugin-project-api.txt
index 897430c..df0a6f4 100644
--- a/Documentation/pg-plugin-project-api.txt
+++ b/Documentation/pg-plugin-project-api.txt
@@ -13,7 +13,7 @@
 - *checkVisibleCallback* function to configure command visibility.
 
 .Returns
-- GrProjectApi for chainging.
+- GrProjectApi for chaining.
 
 `checkVisibleCallback(projectName, projectConfig)`
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 1cfd3e1..1606b8a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5384,11 +5384,12 @@
 
 [[change-id]]
 === \{change-id\}
-Identifier that uniquely identifies one change.
+Identifier that uniquely identifies one change. It contains the URL-encoded
+project name as well as the change number: "'$$<project>~<numericId>$$'"
 
-This can be:
+Depending on the server's configuration, Gerrit can still support the following
+deprecated identifiers. These will be removed in a future release:
 
-* an ID of the change in the format "'$$<project>~<numericId>$$'"
 * an ID of the change in the format "'$$<project>~<branch>~<Change-Id>$$'",
   where for the branch the `refs/heads/` prefix can be omitted
   ("$$myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940$$")
@@ -5396,6 +5397,10 @@
   ("I8473b95934b5732ac55d26311a706c9c2bde9940")
 * a numeric change ID ("4247")
 
+If you need more time to migrate off of old change IDs, please see
+link:config-gerrit.html#change.api.allowedIdentifier[change.api.allowedIdentifier]
+for more information on how to enable the use of deprecated identifiers.
+
 [[comment-id]]
 === \{comment-id\}
 UUID of a published comment.
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
index cb1cc23..8d1c326 100755
--- a/contrib/abandon_stale.py
+++ b/contrib/abandon_stale.py
@@ -186,15 +186,21 @@
         abandon_message += "\n\n" + options.message
     for change in stale_changes:
         number = change["_number"]
-        try:
-            owner = change["owner"]["name"]
-        except:
-            owner = "Unknown"
+        project = ""
+        if len(options.projects) != 1:
+            project = "%s: " % change["project"]
+        owner = ""
+        if options.verbose:
+            try:
+                o = change["owner"]["name"]
+            except KeyError:
+                o = "Unknown"
+            owner = " (%s)" % o
         subject = change["subject"]
         if len(subject) > 70:
             subject = subject[:65] + " [...]"
         change_id = change["id"]
-        logging.info("%s (%s): %s", number, owner, subject)
+        logging.info("%s%s: %s%s", number, owner, project, subject)
         if options.dry_run:
             continue
 
diff --git a/java/com/google/gerrit/extensions/restapi/DeprecatedIdentifierException.java b/java/com/google/gerrit/extensions/restapi/DeprecatedIdentifierException.java
new file mode 100644
index 0000000..aa28cfc
--- /dev/null
+++ b/java/com/google/gerrit/extensions/restapi/DeprecatedIdentifierException.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2017 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.extensions.restapi;
+
+/** Named resource was accessed using a deprecated identifier. */
+public class DeprecatedIdentifierException extends BadRequestException {
+  private static final long serialVersionUID = 1L;
+
+  /** Requested resource using a deprecated identifier. */
+  public DeprecatedIdentifierException(String msg) {
+    super(msg);
+  }
+}
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index 65943fc..d7cbdb8 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -15,6 +15,7 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/git/receive",
         "//java/com/google/gerrit/server/ioutil",
+        "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/http",
         "//java/com/google/gwtexpui/linker:server",
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index 936044d..9624241 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -20,6 +20,7 @@
         "//java/com/google/gerrit/server/api",
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/git/receive",
+        "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/sshd",
         "//lib:guava",
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index b5995a8..5b9cf3b 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -55,7 +55,6 @@
 import com.google.gerrit.server.config.GerritOptions;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerConfigModule;
-import com.google.gerrit.server.config.RestCacheAdminModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.events.EventBroker;
 import com.google.gerrit.server.events.StreamEventsApiListener;
@@ -79,6 +78,7 @@
 import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.project.DefaultPermissionBackendModule;
 import com.google.gerrit.server.project.DefaultProjectNameLockManager;
+import com.google.gerrit.server.restapi.config.RestCacheAdminModule;
 import com.google.gerrit.server.schema.DataSourceModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DataSourceType;
diff --git a/java/com/google/gerrit/httpd/restapi/ConfigRestApiServlet.java b/java/com/google/gerrit/httpd/restapi/ConfigRestApiServlet.java
index 87df4cf..4d56036 100644
--- a/java/com/google/gerrit/httpd/restapi/ConfigRestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/ConfigRestApiServlet.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.restapi;
 
-import com.google.gerrit.server.config.ConfigCollection;
+import com.google.gerrit.server.restapi.config.ConfigCollection;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index 3dce217..a255020 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -38,6 +38,7 @@
         "//java/com/google/gerrit/server/api",
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/git/receive",
+        "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/sshd",
         "//java/com/google/gwtexpui/linker:server",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 16d81b7..4bc06d0 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -64,7 +64,6 @@
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritOptions;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.RestCacheAdminModule;
 import com.google.gerrit.server.events.EventBroker;
 import com.google.gerrit.server.events.StreamEventsApiListener;
 import com.google.gerrit.server.git.GarbageCollectionModule;
@@ -88,6 +87,7 @@
 import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.project.DefaultPermissionBackendModule;
 import com.google.gerrit.server.project.DefaultProjectNameLockManager;
+import com.google.gerrit.server.restapi.config.RestCacheAdminModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.InMemoryAccountPatchReviewStore;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
diff --git a/java/com/google/gerrit/reviewdb/client/Project.java b/java/com/google/gerrit/reviewdb/client/Project.java
index d656b68..c66b646 100644
--- a/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/java/com/google/gerrit/reviewdb/client/Project.java
@@ -200,7 +200,8 @@
    * Returns the name key of the parent project.
    *
    * @param allProjectsName name key of the wild project
-   * @return name key of the parent project, {@code null} if this project is the wild project
+   * @return name key of the parent project, {@code null} if this project is the All-Projects
+   *     project
    */
   public Project.NameKey getParent(Project.NameKey allProjectsName) {
     if (parent != null) {
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 6052a63..fb1fc28 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -100,6 +100,7 @@
         ":server",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/server/git/receive",
+        "//java/com/google/gerrit/server/restapi",
         "//lib:blame-cache",
         "//lib:guava",
         "//lib:soy",
diff --git a/java/com/google/gerrit/server/ChangeFinder.java b/java/com/google/gerrit/server/ChangeFinder.java
index 4b7cbac..cb82778 100644
--- a/java/com/google/gerrit/server/ChangeFinder.java
+++ b/java/com/google/gerrit/server/ChangeFinder.java
@@ -17,8 +17,10 @@
 import com.google.common.base.Throwables;
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.extensions.restapi.DeprecatedIdentifierException;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.metrics.Counter1;
 import com.google.gerrit.metrics.Description;
@@ -30,6 +32,8 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.change.ChangeTriplet;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -46,6 +50,7 @@
 import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
 
 @Singleton
 public class ChangeFinder {
@@ -60,10 +65,11 @@
     };
   }
 
-  private enum ChangeIdType {
+  public enum ChangeIdType {
+    ALL,
     TRIPLET,
     NUMERIC_ID,
-    CHANGE_ID,
+    I_HASH,
     PROJECT_NUMERIC_ID,
     COMMIT_HASH
   }
@@ -74,6 +80,7 @@
   private final Provider<ReviewDb> reviewDb;
   private final ChangeNotes.Factory changeNotesFactory;
   private final Counter1<ChangeIdType> changeIdCounter;
+  private final ImmutableSet<ChangeIdType> allowedIdTypes;
 
   @Inject
   ChangeFinder(
@@ -82,7 +89,8 @@
       Provider<InternalChangeQuery> queryProvider,
       Provider<ReviewDb> reviewDb,
       ChangeNotes.Factory changeNotesFactory,
-      MetricMaker metricMaker) {
+      MetricMaker metricMaker,
+      @GerritServerConfig Config config) {
     this.indexConfig = indexConfig;
     this.changeIdProjectCache = changeIdProjectCache;
     this.queryProvider = queryProvider;
@@ -95,16 +103,41 @@
                 .setRate()
                 .setUnit("requests"),
             Field.ofEnum(ChangeIdType.class, "change_id_type"));
+    List<ChangeIdType> configuredChangeIdTypes =
+        ConfigUtil.getEnumList(config, "change", "api", "allowedIdentifier", ChangeIdType.ALL);
+    // Ensure that PROJECT_NUMERIC_ID can't be removed
+    configuredChangeIdTypes.add(ChangeIdType.PROJECT_NUMERIC_ID);
+    this.allowedIdTypes = ImmutableSet.copyOf(configuredChangeIdTypes);
   }
 
   /**
    * Find changes matching the given identifier.
    *
-   * @param id change identifier, either a numeric ID, a Change-Id, or project~branch~id triplet.
+   * @param id change identifier.
    * @return possibly-empty list of notes for all matching changes; may or may not be visible.
    * @throws OrmException if an error occurred querying the database.
    */
   public List<ChangeNotes> find(String id) throws OrmException {
+    try {
+      return find(id, false);
+    } catch (DeprecatedIdentifierException e) {
+      // This can't happen because we don't enforce deprecation
+      throw new OrmException(e);
+    }
+  }
+
+  /**
+   * Find changes matching the given identifier.
+   *
+   * @param id change identifier.
+   * @param enforceDeprecation boolean to see if we should throw {@link
+   *     DeprecatedIdentifierException} in case the identifier is deprecated
+   * @return possibly-empty list of notes for all matching changes; may or may not be visible.
+   * @throws OrmException if an error occurred querying the database
+   * @throws DeprecatedIdentifierException if the identifier is deprecated.
+   */
+  public List<ChangeNotes> find(String id, boolean enforceDeprecation)
+      throws OrmException, DeprecatedIdentifierException {
     if (id.isEmpty()) {
       return Collections.emptyList();
     }
@@ -115,7 +148,7 @@
       // Try project~numericChangeId
       Integer n = Ints.tryParse(id.substring(z + 1));
       if (n != null) {
-        changeIdCounter.increment(ChangeIdType.PROJECT_NUMERIC_ID);
+        checkIdType(ChangeIdType.PROJECT_NUMERIC_ID, enforceDeprecation, n.toString());
         return fromProjectNumber(id.substring(0, z), n.intValue());
       }
     }
@@ -124,7 +157,7 @@
       // Try numeric changeId
       Integer n = Ints.tryParse(id);
       if (n != null) {
-        changeIdCounter.increment(ChangeIdType.NUMERIC_ID);
+        checkIdType(ChangeIdType.NUMERIC_ID, enforceDeprecation, n.toString());
         return find(new Change.Id(n));
       }
     }
@@ -135,7 +168,7 @@
 
     // Try commit hash
     if (id.matches("^([0-9a-fA-F]{" + RevId.ABBREV_LEN + "," + RevId.LEN + "})$")) {
-      changeIdCounter.increment(ChangeIdType.COMMIT_HASH);
+      checkIdType(ChangeIdType.COMMIT_HASH, enforceDeprecation, id);
       return asChangeNotes(query.byCommit(id));
     }
 
@@ -144,7 +177,7 @@
       Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id, y, z);
       if (triplet.isPresent()) {
         ChangeTriplet t = triplet.get();
-        changeIdCounter.increment(ChangeIdType.TRIPLET);
+        checkIdType(ChangeIdType.TRIPLET, enforceDeprecation, triplet.get().toString());
         return asChangeNotes(query.byBranchKey(t.branch(), t.id()));
       }
     }
@@ -152,7 +185,7 @@
     // Try isolated Ihash... format ("Change-Id: Ihash").
     List<ChangeNotes> notes = asChangeNotes(query.byKeyPrefix(id));
     if (!notes.isEmpty()) {
-      changeIdCounter.increment(ChangeIdType.CHANGE_ID);
+      checkIdType(ChangeIdType.I_HASH, enforceDeprecation, id);
     }
     return notes;
   }
@@ -222,4 +255,18 @@
     }
     return notes;
   }
+
+  private void checkIdType(ChangeIdType type, boolean enforceDeprecation, String val)
+      throws DeprecatedIdentifierException {
+    if (enforceDeprecation
+        && !allowedIdTypes.contains(ChangeIdType.ALL)
+        && !allowedIdTypes.contains(type)) {
+      throw new DeprecatedIdentifierException(
+          String.format(
+              "The provided change identifier %s is deprecated. "
+                  + "Use 'project~changeNumber' instead.",
+              val));
+    }
+    changeIdCounter.increment(type);
+  }
 }
diff --git a/java/com/google/gerrit/server/ReviewersUtil.java b/java/com/google/gerrit/server/ReviewersUtil.java
index c566e59..a8410d8 100644
--- a/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/ReviewersUtil.java
@@ -18,36 +18,41 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.extensions.common.GroupBaseInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.index.query.QueryResult;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
 import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.SuggestReviewers;
+import com.google.gerrit.server.index.account.AccountField;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.account.AccountPredicates;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
-import com.google.gerrit.server.query.account.AccountQueryProcessor;
 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 java.io.IOException;
 import java.util.ArrayList;
@@ -108,30 +113,36 @@
 
   private final AccountLoader accountLoader;
   private final AccountQueryBuilder accountQueryBuilder;
-  private final Provider<AccountQueryProcessor> queryProvider;
   private final GroupBackend groupBackend;
   private final GroupMembers groupMembers;
   private final ReviewerRecommender reviewerRecommender;
   private final Metrics metrics;
+  private final AccountIndexCollection accountIndexes;
+  private final IndexConfig indexConfig;
+  private final AccountControl.Factory accountControlFactory;
 
   @Inject
   ReviewersUtil(
       AccountLoader.Factory accountLoaderFactory,
       AccountQueryBuilder accountQueryBuilder,
-      Provider<AccountQueryProcessor> queryProvider,
       GroupBackend groupBackend,
       GroupMembers groupMembers,
       ReviewerRecommender reviewerRecommender,
-      Metrics metrics) {
+      Metrics metrics,
+      AccountIndexCollection accountIndexes,
+      IndexConfig indexConfig,
+      AccountControl.Factory accountControlFactory) {
     Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS);
     fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
     this.accountLoader = accountLoaderFactory.create(fillOptions);
     this.accountQueryBuilder = accountQueryBuilder;
-    this.queryProvider = queryProvider;
     this.groupBackend = groupBackend;
     this.groupMembers = groupMembers;
     this.reviewerRecommender = reviewerRecommender;
     this.metrics = metrics;
+    this.accountIndexes = accountIndexes;
+    this.indexConfig = indexConfig;
+    this.accountControlFactory = accountControlFactory;
   }
 
   public interface VisibilityControl {
@@ -167,7 +178,9 @@
         if (filteredRecommendations.size() >= limit) {
           break;
         }
-        if (visibilityControl.isVisibleTo(reviewer)) {
+        // Check if change is visible to reviewer and if the current user can see reviewer
+        if (visibilityControl.isVisibleTo(reviewer)
+            && accountControlFactory.get().canSee(reviewer)) {
           filteredRecommendations.add(reviewer);
         }
       }
@@ -191,14 +204,27 @@
   private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers) throws OrmException {
     try (Timer0.Context ctx = metrics.queryAccountsLatency.start()) {
       try {
-        QueryResult<AccountState> result =
-            queryProvider
-                .get()
-                .setUserProvidedLimit(suggestReviewers.getLimit() * CANDIDATE_LIST_MULTIPLIER)
-                .query(
-                    AccountPredicates.andActive(
-                        accountQueryBuilder.defaultQuery(suggestReviewers.getQuery())));
-        return result.entities().stream().map(a -> a.getAccount().getId()).collect(toList());
+        // For performance reasons we don't use AccountQueryProvider as it would always load the
+        // complete account from the cache (or worse, from NoteDb) even though we only need the ID
+        // which we can directly get from the returned results.
+        ResultSet<FieldBundle> result =
+            accountIndexes
+                .getSearchIndex()
+                .getSource(
+                    Predicate.and(
+                        AccountPredicates.isActive(),
+                        accountQueryBuilder.defaultQuery(suggestReviewers.getQuery())),
+                    QueryOptions.create(
+                        indexConfig,
+                        0,
+                        suggestReviewers.getLimit() * CANDIDATE_LIST_MULTIPLIER,
+                        ImmutableSet.of(AccountField.ID.getName())))
+                .readRaw();
+        return result
+            .toList()
+            .stream()
+            .map(f -> new Account.Id(f.getValue(AccountField.ID).intValue()))
+            .collect(toList());
       } catch (QueryParseException e) {
         return ImmutableList.of();
       }
diff --git a/java/com/google/gerrit/server/account/GroupMembers.java b/java/com/google/gerrit/server/account/GroupMembers.java
index 3bb9de2..a5876d8 100644
--- a/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/java/com/google/gerrit/server/account/GroupMembers.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 
 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.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
@@ -52,6 +53,25 @@
     this.projectCache = projectCache;
   }
 
+  /**
+   * Recursively enumerate the members of the given group. Should not be used with the
+   * PROJECT_OWNERS magical group.
+   */
+  public Set<Account> listAccounts(AccountGroup.UUID groupUUID) throws IOException {
+    if (SystemGroupBackend.PROJECT_OWNERS.equals(groupUUID)) {
+      throw new IllegalStateException("listAccounts called with PROJECT_OWNERS argument");
+    }
+    try {
+      return listAccounts(groupUUID, null, new HashSet<AccountGroup.UUID>());
+    } catch (NoSuchProjectException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  /**
+   * Recursively enumerate the members of the given group. The project should be specified so the
+   * PROJECT_OWNERS magical group can be expanded.
+   */
   public Set<Account> listAccounts(AccountGroup.UUID groupUUID, Project.NameKey project)
       throws NoSuchProjectException, IOException {
     return listAccounts(groupUUID, project, new HashSet<AccountGroup.UUID>());
@@ -59,7 +79,7 @@
 
   private Set<Account> listAccounts(
       final AccountGroup.UUID groupUUID,
-      final Project.NameKey project,
+      @Nullable final Project.NameKey project,
       final Set<AccountGroup.UUID> seen)
       throws NoSuchProjectException, IOException {
     if (SystemGroupBackend.PROJECT_OWNERS.equals(groupUUID)) {
@@ -94,7 +114,7 @@
   }
 
   private Set<Account> getGroupMembers(
-      InternalGroup group, Project.NameKey project, Set<AccountGroup.UUID> seen)
+      InternalGroup group, @Nullable Project.NameKey project, Set<AccountGroup.UUID> seen)
       throws NoSuchProjectException, IOException {
     seen.add(group.getGroupUUID());
     GroupControl groupControl = groupControlFactory.controlFor(new InternalGroupDescription(group));
diff --git a/java/com/google/gerrit/server/account/StarredChanges.java b/java/com/google/gerrit/server/account/StarredChanges.java
index 38c95e6..6dfd132 100644
--- a/java/com/google/gerrit/server/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/account/StarredChanges.java
@@ -72,7 +72,7 @@
 
   @Override
   public AccountResource.StarredChange parse(AccountResource parent, IdString id)
-      throws ResourceNotFoundException, OrmException, PermissionBackendException {
+      throws RestApiException, OrmException, PermissionBackendException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     if (starredChangesUtil
@@ -103,7 +103,7 @@
 
   @Override
   public RestModifyView<AccountResource, EmptyInput> create(AccountResource parent, IdString id)
-      throws UnprocessableEntityException {
+      throws RestApiException {
     try {
       return createProvider.get().setChange(changes.parse(TopLevelResource.INSTANCE, id));
     } catch (ResourceNotFoundException e) {
diff --git a/java/com/google/gerrit/server/account/Stars.java b/java/com/google/gerrit/server/account/Stars.java
index 9019ad7..5eb8d7b 100644
--- a/java/com/google/gerrit/server/account/Stars.java
+++ b/java/com/google/gerrit/server/account/Stars.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
@@ -66,7 +66,7 @@
 
   @Override
   public Star parse(AccountResource parent, IdString id)
-      throws ResourceNotFoundException, OrmException, PermissionBackendException {
+      throws RestApiException, OrmException, PermissionBackendException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     Set<String> labels = starredChangesUtil.getLabels(user.getAccountId(), change.getId());
diff --git a/java/com/google/gerrit/server/api/BUILD b/java/com/google/gerrit/server/api/BUILD
index 2741a0a..910ecd3 100644
--- a/java/com/google/gerrit/server/api/BUILD
+++ b/java/com/google/gerrit/server/api/BUILD
@@ -11,6 +11,7 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/restapi",
         "//lib:guava",
         "//lib:gwtorm",
         "//lib:servlet-api-3_1",
diff --git a/java/com/google/gerrit/server/api/config/ServerImpl.java b/java/com/google/gerrit/server/api/config/ServerImpl.java
index 2148d97..ce87d1c 100644
--- a/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -24,13 +24,13 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.config.CheckConsistency;
 import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.config.GetDiffPreferences;
-import com.google.gerrit.server.config.GetPreferences;
-import com.google.gerrit.server.config.GetServerInfo;
-import com.google.gerrit.server.config.SetDiffPreferences;
-import com.google.gerrit.server.config.SetPreferences;
+import com.google.gerrit.server.restapi.config.CheckConsistency;
+import com.google.gerrit.server.restapi.config.GetDiffPreferences;
+import com.google.gerrit.server.restapi.config.GetPreferences;
+import com.google.gerrit.server.restapi.config.GetServerInfo;
+import com.google.gerrit.server.restapi.config.SetDiffPreferences;
+import com.google.gerrit.server.restapi.config.SetPreferences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/change/ChangesCollection.java b/java/com/google/gerrit/server/change/ChangesCollection.java
index a0b6c96..6ce661e 100644
--- a/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -81,8 +81,8 @@
 
   @Override
   public ChangeResource parse(TopLevelResource root, IdString id)
-      throws ResourceNotFoundException, OrmException, PermissionBackendException {
-    List<ChangeNotes> notes = changeFinder.find(id.encoded());
+      throws RestApiException, OrmException, PermissionBackendException {
+    List<ChangeNotes> notes = changeFinder.find(id.encoded(), true);
     if (notes.isEmpty()) {
       throw new ResourceNotFoundException(id);
     } else if (notes.size() != 1) {
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index a149935..e10197f 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -52,6 +52,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.BatchUpdateReviewDb;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.update.RetryHelper;
@@ -667,7 +668,7 @@
     public boolean updateChange(ChangeContext ctx)
         throws OrmException, PatchSetInfoNotAvailableException {
       // Delete dangling key references.
-      ReviewDb db = DeleteChangeOp.unwrap(ctx.getDb());
+      ReviewDb db = BatchUpdateReviewDb.unwrap(ctx.getDb());
       accountPatchReviewStore.get().clearReviewed(psId);
       db.changeMessages().delete(db.changeMessages().byChange(psId.getParentKey()));
       db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId));
diff --git a/java/com/google/gerrit/server/change/DeleteChangeOp.java b/java/com/google/gerrit/server/change/DeleteChangeOp.java
index 8df6e59..130d040 100644
--- a/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -47,16 +46,6 @@
     return cfg.getBoolean("change", "allowDrafts", true);
   }
 
-  static ReviewDb unwrap(ReviewDb db) {
-    // This is special. We want to delete exactly the rows that are present in
-    // the database, even when reading everything else from NoteDb, so we need
-    // to bypass the write-only wrapper.
-    if (db instanceof BatchUpdateReviewDb) {
-      db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
-    }
-    return ReviewDbUtil.unwrapDb(db);
-  }
-
   private final PatchSetUtil psUtil;
   private final StarredChangesUtil starredChangesUtil;
   private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
@@ -123,7 +112,11 @@
   private void deleteChangeElementsFromDb(ChangeContext ctx, Change.Id id) throws OrmException {
     // Only delete from ReviewDb here; deletion from NoteDb is handled in
     // BatchUpdate.
-    ReviewDb db = unwrap(ctx.getDb());
+    //
+    // This is special. We want to delete exactly the rows that are present in
+    // the database, even when reading everything else from NoteDb, so we need
+    // to bypass the write-only wrapper.
+    ReviewDb db = BatchUpdateReviewDb.unwrap(ctx.getDb());
     db.patchComments().delete(db.patchComments().byChange(id));
     db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
     db.patchSets().delete(db.patchSets().byChange(id));
diff --git a/java/com/google/gerrit/server/change/PreviewSubmit.java b/java/com/google/gerrit/server/change/PreviewSubmit.java
index 3c83f81..21266a0 100644
--- a/java/com/google/gerrit/server/change/PreviewSubmit.java
+++ b/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -29,11 +29,12 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.LimitedByteArrayOutputStream.LimitExceededException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeOpRepoManager;
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
+import com.google.gerrit.server.ioutil.LimitedByteArrayOutputStream;
+import com.google.gerrit.server.ioutil.LimitedByteArrayOutputStream.LimitExceededException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.update.UpdateException;
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 224bf29..51a9fea 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -173,6 +173,7 @@
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.query.change.ConflictsCacheImpl;
+import com.google.gerrit.server.restapi.config.ConfigRestModule;
 import com.google.gerrit.server.rules.PrologModule;
 import com.google.gerrit.server.rules.RulesCache;
 import com.google.gerrit.server.ssh.SshAddressesModule;
@@ -305,7 +306,7 @@
     install(new com.google.gerrit.server.access.Module());
     install(new com.google.gerrit.server.account.Module());
     install(new com.google.gerrit.server.change.Module());
-    install(new com.google.gerrit.server.config.Module());
+    install(new ConfigRestModule());
     install(new com.google.gerrit.server.group.Module(groupsMigration));
     install(new com.google.gerrit.server.project.Module());
 
diff --git a/java/com/google/gerrit/server/ioutil/BUILD b/java/com/google/gerrit/server/ioutil/BUILD
index d45cf73..06843c5 100644
--- a/java/com/google/gerrit/server/ioutil/BUILD
+++ b/java/com/google/gerrit/server/ioutil/BUILD
@@ -4,6 +4,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/reviewdb:client",
+        "//lib:guava",
         "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
         "//lib/jgit/org.eclipse.jgit:jgit",
     ],
diff --git a/java/com/google/gerrit/server/change/LimitedByteArrayOutputStream.java b/java/com/google/gerrit/server/ioutil/LimitedByteArrayOutputStream.java
similarity index 85%
rename from java/com/google/gerrit/server/change/LimitedByteArrayOutputStream.java
rename to java/com/google/gerrit/server/ioutil/LimitedByteArrayOutputStream.java
index facc03c..015887b 100644
--- a/java/com/google/gerrit/server/change/LimitedByteArrayOutputStream.java
+++ b/java/com/google/gerrit/server/ioutil/LimitedByteArrayOutputStream.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.change;
+package com.google.gerrit.server.ioutil;
 
 import static com.google.common.base.Preconditions.checkArgument;
 
@@ -20,7 +20,8 @@
 import java.io.IOException;
 import java.io.OutputStream;
 
-class LimitedByteArrayOutputStream extends OutputStream {
+/** A stream that throws an exception if it consumes data beyond a configured byte count. */
+public class LimitedByteArrayOutputStream extends OutputStream {
 
   private final int maxSize;
   private final ByteArrayOutputStream buffer;
@@ -32,7 +33,7 @@
    * @param max the maximum size in bytes which may be stored.
    * @param initial the initial size. It must be smaller than the max size.
    */
-  LimitedByteArrayOutputStream(int max, int initial) {
+  public LimitedByteArrayOutputStream(int max, int initial) {
     checkArgument(initial <= max);
     maxSize = max;
     buffer = new ByteArrayOutputStream(initial);
@@ -61,7 +62,7 @@
     return buffer.toByteArray();
   }
 
-  static class LimitExceededException extends IOException {
+  public static class LimitExceededException extends IOException {
     private static final long serialVersionUID = 1L;
   }
 }
diff --git a/java/com/google/gerrit/server/project/ChildProjects.java b/java/com/google/gerrit/server/project/ChildProjects.java
new file mode 100644
index 0000000..0b174f6
--- /dev/null
+++ b/java/com/google/gerrit/server/project/ChildProjects.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2017 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.project;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Retrieve child projects (ie. projects whose access inherits from a given parent.) */
+@Singleton
+public class ChildProjects {
+  private final ProjectCache projectCache;
+  private final PermissionBackend permissionBackend;
+  private final Provider<CurrentUser> user;
+  private final AllProjectsName allProjects;
+  private final ProjectJson json;
+
+  @Inject
+  ChildProjects(
+      ProjectCache projectCache,
+      PermissionBackend permissionBackend,
+      Provider<CurrentUser> user,
+      AllProjectsName allProjectsName,
+      ProjectJson json) {
+    this.projectCache = projectCache;
+    this.permissionBackend = permissionBackend;
+    this.user = user;
+    this.allProjects = allProjectsName;
+    this.json = json;
+  }
+
+  /** Gets all child projects recursively. */
+  public List<ProjectInfo> list(Project.NameKey parent) throws PermissionBackendException {
+    Map<Project.NameKey, Project> projects = readAllProjects();
+    Multimap<Project.NameKey, Project.NameKey> children = parentToChildren(projects);
+    PermissionBackend.WithUser perm = permissionBackend.user(user);
+
+    List<ProjectInfo> results = new ArrayList<>();
+    depthFirstFormat(results, perm, projects, children, parent);
+    return results;
+  }
+
+  private Map<Project.NameKey, Project> readAllProjects() {
+    Map<Project.NameKey, Project> projects = new HashMap<>();
+    for (Project.NameKey name : projectCache.all()) {
+      ProjectState c = projectCache.get(name);
+      if (c != null) {
+        projects.put(c.getNameKey(), c.getProject());
+      }
+    }
+    return projects;
+  }
+
+  /** Map of parent project to direct child. */
+  private Multimap<Project.NameKey, Project.NameKey> parentToChildren(
+      Map<Project.NameKey, Project> projects) {
+    Multimap<Project.NameKey, Project.NameKey> m = ArrayListMultimap.create();
+    for (Map.Entry<Project.NameKey, Project> e : projects.entrySet()) {
+      if (!allProjects.equals(e.getKey())) {
+        m.put(e.getValue().getParent(allProjects), e.getKey());
+      }
+    }
+    return m;
+  }
+
+  private void depthFirstFormat(
+      List<ProjectInfo> results,
+      PermissionBackend.WithUser perm,
+      Map<Project.NameKey, Project> projects,
+      Multimap<Project.NameKey, Project.NameKey> children,
+      Project.NameKey parent)
+      throws PermissionBackendException {
+    List<Project.NameKey> canSee =
+        perm.filter(ProjectPermission.ACCESS, children.get(parent))
+            .stream()
+            .sorted()
+            .collect(toList());
+    children.removeAll(parent); // removing all entries prevents cycles.
+
+    for (Project.NameKey c : canSee) {
+      results.add(json.format(projects.get(c)));
+      depthFirstFormat(results, perm, projects, children, c);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/project/CommitsCollection.java b/java/com/google/gerrit/server/project/CommitsCollection.java
index a504a1f..481c2c8 100644
--- a/java/com/google/gerrit/server/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/project/CommitsCollection.java
@@ -20,9 +20,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
@@ -31,11 +29,9 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.List;
-import java.util.Map;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -48,19 +44,19 @@
 
   private final DynamicMap<RestView<CommitResource>> views;
   private final GitRepositoryManager repoManager;
-  private final VisibleRefFilter.Factory refFilter;
   private final Provider<InternalChangeQuery> queryProvider;
+  private final Reachable reachable;
 
   @Inject
   public CommitsCollection(
       DynamicMap<RestView<CommitResource>> views,
       GitRepositoryManager repoManager,
-      VisibleRefFilter.Factory refFilter,
-      Provider<InternalChangeQuery> queryProvider) {
+      Provider<InternalChangeQuery> queryProvider,
+      Reachable reachable) {
     this.views = views;
     this.repoManager = repoManager;
-    this.refFilter = refFilter;
     this.queryProvider = queryProvider;
+    this.reachable = reachable;
   }
 
   @Override
@@ -114,21 +110,6 @@
       log.error("Cannot look up change for commit " + commit.name() + " in " + project, e);
     }
 
-    return isReachableFrom(state, repo, commit, repo.getAllRefs());
-  }
-
-  public boolean isReachableFrom(
-      ProjectState state, Repository repo, RevCommit commit, Map<String, Ref> refs) {
-    try (RevWalk rw = new RevWalk(repo)) {
-      refs = refFilter.create(state, repo).filter(refs, true);
-      return IncludedInResolver.includedInAny(repo, rw, commit, refs.values());
-    } catch (IOException e) {
-      log.error(
-          String.format(
-              "Cannot verify permissions to commit object %s in repository %s",
-              commit.name(), state.getNameKey()),
-          e);
-      return false;
-    }
+    return reachable.fromRefs(state, repo, commit, repo.getAllRefs());
   }
 }
diff --git a/java/com/google/gerrit/server/project/ListChildProjects.java b/java/com/google/gerrit/server/project/ListChildProjects.java
index e5fe37d..9de0c87 100644
--- a/java/com/google/gerrit/server/project/ListChildProjects.java
+++ b/java/com/google/gerrit/server/project/ListChildProjects.java
@@ -16,8 +16,6 @@
 
 import static java.util.stream.Collectors.toList;
 
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
@@ -28,7 +26,6 @@
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -44,6 +41,7 @@
   private final Provider<CurrentUser> user;
   private final AllProjectsName allProjects;
   private final ProjectJson json;
+  private final ChildProjects childProjects;
 
   @Inject
   ListChildProjects(
@@ -51,12 +49,14 @@
       PermissionBackend permissionBackend,
       Provider<CurrentUser> user,
       AllProjectsName allProjectsName,
-      ProjectJson json) {
+      ProjectJson json,
+      ChildProjects childProjects) {
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
     this.user = user;
     this.allProjects = allProjectsName;
     this.json = json;
+    this.childProjects = childProjects;
   }
 
   public void setRecursive(boolean recursive) {
@@ -66,8 +66,9 @@
   @Override
   public List<ProjectInfo> apply(ProjectResource rsrc) throws PermissionBackendException {
     if (recursive) {
-      return recursiveChildProjects(rsrc.getNameKey());
+      return childProjects.list(rsrc.getNameKey());
     }
+
     return directChildProjects(rsrc.getNameKey());
   }
 
@@ -88,58 +89,4 @@
         .map((p) -> json.format(children.get(p)))
         .collect(toList());
   }
-
-  private List<ProjectInfo> recursiveChildProjects(Project.NameKey parent)
-      throws PermissionBackendException {
-    Map<Project.NameKey, Project> projects = readAllProjects();
-    Multimap<Project.NameKey, Project.NameKey> children = parentToChildren(projects);
-    PermissionBackend.WithUser perm = permissionBackend.user(user);
-
-    List<ProjectInfo> results = new ArrayList<>();
-    depthFirstFormat(results, perm, projects, children, parent);
-    return results;
-  }
-
-  private Map<Project.NameKey, Project> readAllProjects() {
-    Map<Project.NameKey, Project> projects = new HashMap<>();
-    for (Project.NameKey name : projectCache.all()) {
-      ProjectState c = projectCache.get(name);
-      if (c != null) {
-        projects.put(c.getNameKey(), c.getProject());
-      }
-    }
-    return projects;
-  }
-
-  /** Map of parent project to direct child. */
-  private Multimap<Project.NameKey, Project.NameKey> parentToChildren(
-      Map<Project.NameKey, Project> projects) {
-    Multimap<Project.NameKey, Project.NameKey> m = ArrayListMultimap.create();
-    for (Map.Entry<Project.NameKey, Project> e : projects.entrySet()) {
-      if (!allProjects.equals(e.getKey())) {
-        m.put(e.getValue().getParent(allProjects), e.getKey());
-      }
-    }
-    return m;
-  }
-
-  private void depthFirstFormat(
-      List<ProjectInfo> results,
-      PermissionBackend.WithUser perm,
-      Map<Project.NameKey, Project> projects,
-      Multimap<Project.NameKey, Project.NameKey> children,
-      Project.NameKey parent)
-      throws PermissionBackendException {
-    List<Project.NameKey> canSee =
-        perm.filter(ProjectPermission.ACCESS, children.get(parent))
-            .stream()
-            .sorted()
-            .collect(toList());
-    children.removeAll(parent); // removing all entries prevents cycles.
-
-    for (Project.NameKey c : canSee) {
-      results.add(json.format(projects.get(c)));
-      depthFirstFormat(results, perm, projects, children, c);
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/project/ProjectControl.java b/java/com/google/gerrit/server/project/ProjectControl.java
index 0e62b3fdc..97d0173 100644
--- a/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/java/com/google/gerrit/server/project/ProjectControl.java
@@ -17,8 +17,6 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
@@ -60,18 +58,11 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Access control management for a user accessing a project's data. */
 class ProjectControl {
-  private static final Logger log = LoggerFactory.getLogger(ProjectControl.class);
-
   static class GenericFactory {
     private final ProjectCache projectCache;
 
@@ -112,7 +103,7 @@
   private final PermissionBackend.WithUser perm;
   private final CurrentUser user;
   private final ProjectState state;
-  private final CommitsCollection commits;
+  private final Reachable reachable;
   private final ChangeControl.Factory changeControlFactory;
   private final PermissionCollection.Factory permissionFilter;
 
@@ -125,7 +116,7 @@
       @GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
       @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
       PermissionCollection.Factory permissionFilter,
-      CommitsCollection commits,
+      Reachable reachable,
       ChangeControl.Factory changeControlFactory,
       PermissionBackend permissionBackend,
       @Assisted CurrentUser who,
@@ -134,7 +125,7 @@
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.permissionFilter = permissionFilter;
-    this.commits = commits;
+    this.reachable = reachable;
     this.perm = permissionBackend.user(who);
     user = who;
     state = ps;
@@ -173,6 +164,10 @@
     return ctl;
   }
 
+  boolean isReachableFromHeadsOrTags(Repository repo, RevCommit commit) {
+    return reachable.fromHeadsOrTags(state, repo, commit);
+  }
+
   CurrentUser getUser() {
     return user;
   }
@@ -352,26 +347,6 @@
     }
   }
 
-  boolean isReachableFromHeadsOrTags(Repository repo, RevCommit commit) {
-    try {
-      RefDatabase refdb = repo.getRefDatabase();
-      Collection<Ref> heads = refdb.getRefs(Constants.R_HEADS).values();
-      Collection<Ref> tags = refdb.getRefs(Constants.R_TAGS).values();
-      Map<String, Ref> refs = Maps.newHashMapWithExpectedSize(heads.size() + tags.size());
-      for (Ref r : Iterables.concat(heads, tags)) {
-        refs.put(r.getName(), r);
-      }
-      return commits.isReachableFrom(state, repo, commit, refs);
-    } catch (IOException e) {
-      log.error(
-          String.format(
-              "Cannot verify permissions to commit object %s in repository %s",
-              commit.name(), getProject().getNameKey()),
-          e);
-      return false;
-    }
-  }
-
   boolean canRead() {
     return !isHidden() && allRefsAreVisible(Collections.emptySet());
   }
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
new file mode 100644
index 0000000..42389d3
--- /dev/null
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2017 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.project;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.change.IncludedInResolver;
+import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Report whether a commit is reachable from a set of commits. This is used for checking if a user
+ * has read permissions on a commit.
+ */
+public class Reachable {
+  private final VisibleRefFilter.Factory refFilter;
+  private static final Logger log = LoggerFactory.getLogger(Reachable.class);
+
+  @Inject
+  Reachable(VisibleRefFilter.Factory refFilter) {
+    this.refFilter = refFilter;
+  }
+
+  /** @return true if a commit is reachable from a given set of refs. */
+  public boolean fromRefs(
+      ProjectState state, Repository repo, RevCommit commit, Map<String, Ref> refs) {
+    try (RevWalk rw = new RevWalk(repo)) {
+      Map<String, Ref> filtered = refFilter.create(state, repo).filter(refs, true);
+      return IncludedInResolver.includedInAny(repo, rw, commit, filtered.values());
+    } catch (IOException e) {
+      log.error(
+          String.format(
+              "Cannot verify permissions to commit object %s in repository %s",
+              commit.name(), state.getNameKey()),
+          e);
+      return false;
+    }
+  }
+
+  /** @return true if a commit is reachable from a repo's branches and tags. */
+  boolean fromHeadsOrTags(ProjectState state, Repository repo, RevCommit commit) {
+    try {
+      RefDatabase refdb = repo.getRefDatabase();
+      Collection<Ref> heads = refdb.getRefs(Constants.R_HEADS).values();
+      Collection<Ref> tags = refdb.getRefs(Constants.R_TAGS).values();
+      Map<String, Ref> refs = Maps.newHashMapWithExpectedSize(heads.size() + tags.size());
+      for (Ref r : Iterables.concat(heads, tags)) {
+        refs.put(r.getName(), r);
+      }
+      return fromRefs(state, repo, commit, refs);
+    } catch (IOException e) {
+      log.error(
+          String.format(
+              "Cannot verify permissions to commit object %s in repository %s",
+              commit.name(), state.getProject().getNameKey()),
+          e);
+      return false;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index ebb3e65..3f41219 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -51,6 +51,7 @@
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.account.VersionedAccountDestinations;
 import com.google.gerrit.server.account.VersionedAccountQueries;
 import com.google.gerrit.server.change.ChangeTriplet;
@@ -59,7 +60,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.strategy.SubmitDryRun;
-import com.google.gerrit.server.group.ListMembers;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -70,7 +70,7 @@
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.project.ListChildProjects;
+import com.google.gerrit.server.project.ChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -210,12 +210,12 @@
     final PatchListCache patchListCache;
     final ProjectCache projectCache;
     final Provider<InternalChangeQuery> queryProvider;
-    final Provider<ListChildProjects> listChildProjects;
-    final Provider<ListMembers> listMembers;
+    final ChildProjects childProjects;
     final Provider<ReviewDb> db;
     final StarredChangesUtil starredChangesUtil;
     final SubmitDryRun submitDryRun;
     final boolean allowsDrafts;
+    final GroupMembers groupMembers;
 
     private final Provider<CurrentUser> self;
 
@@ -240,16 +240,16 @@
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
         ProjectCache projectCache,
-        Provider<ListChildProjects> listChildProjects,
+        ChildProjects childProjects,
         ChangeIndexCollection indexes,
         SubmitDryRun submitDryRun,
         ConflictsCache conflictsCache,
         IndexConfig indexConfig,
-        Provider<ListMembers> listMembers,
         StarredChangesUtil starredChangesUtil,
         AccountCache accountCache,
         @GerritServerConfig Config cfg,
-        NotesMigration notesMigration) {
+        NotesMigration notesMigration,
+        GroupMembers groupMembers) {
       this(
           db,
           queryProvider,
@@ -269,16 +269,16 @@
           patchListCache,
           repoManager,
           projectCache,
-          listChildProjects,
+          childProjects,
           submitDryRun,
           conflictsCache,
           indexes != null ? indexes.getSearchIndex() : null,
           indexConfig,
-          listMembers,
           starredChangesUtil,
           accountCache,
           cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true),
-          notesMigration);
+          notesMigration,
+          groupMembers);
     }
 
     private Arguments(
@@ -300,16 +300,16 @@
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
         ProjectCache projectCache,
-        Provider<ListChildProjects> listChildProjects,
+        ChildProjects childProjects,
         SubmitDryRun submitDryRun,
         ConflictsCache conflictsCache,
         ChangeIndex index,
         IndexConfig indexConfig,
-        Provider<ListMembers> listMembers,
         StarredChangesUtil starredChangesUtil,
         AccountCache accountCache,
         boolean allowsDrafts,
-        NotesMigration notesMigration) {
+        NotesMigration notesMigration,
+        GroupMembers groupMembers) {
       this.db = db;
       this.queryProvider = queryProvider;
       this.rewriter = rewriter;
@@ -327,17 +327,17 @@
       this.patchListCache = patchListCache;
       this.repoManager = repoManager;
       this.projectCache = projectCache;
-      this.listChildProjects = listChildProjects;
+      this.childProjects = childProjects;
       this.submitDryRun = submitDryRun;
       this.conflictsCache = conflictsCache;
       this.index = index;
       this.indexConfig = indexConfig;
-      this.listMembers = listMembers;
       this.starredChangesUtil = starredChangesUtil;
       this.accountCache = accountCache;
       this.allowsDrafts = allowsDrafts;
       this.hasOperands = hasOperands;
       this.notesMigration = notesMigration;
+      this.groupMembers = groupMembers;
     }
 
     Arguments asUser(CurrentUser otherUser) {
@@ -360,16 +360,16 @@
           patchListCache,
           repoManager,
           projectCache,
-          listChildProjects,
+          childProjects,
           submitDryRun,
           conflictsCache,
           index,
           indexConfig,
-          listMembers,
           starredChangesUtil,
           accountCache,
           allowsDrafts,
-          notesMigration);
+          notesMigration,
+          groupMembers);
     }
 
     Arguments asUser(Account.Id otherId) {
@@ -653,7 +653,7 @@
 
   @Operator
   public Predicate<ChangeData> parentproject(String name) {
-    return new ParentProjectPredicate(args.projectCache, args.listChildProjects, args.self, name);
+    return new ParentProjectPredicate(args.projectCache, args.childProjects, name);
   }
 
   @Operator
@@ -775,12 +775,8 @@
     // expand a group predicate into multiple user predicates
     if (group != null) {
       Set<Account.Id> allMembers =
-          args.listMembers
-              .get()
-              .getTransitiveMembers(group)
-              .stream()
-              .map(a -> new Account.Id(a._accountId))
-              .collect(toSet());
+          args.groupMembers.listAccounts(group).stream().map(a -> a.getId()).collect(toSet());
+
       int maxTerms = args.indexConfig.maxTerms();
       if (allMembers.size() > maxTerms) {
         // limit the number of query terms otherwise Gerrit will barf
@@ -906,7 +902,6 @@
     }
 
     // If its not an account, maybe its a group?
-    //
     Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
     if (!suggestions.isEmpty()) {
       HashSet<AccountGroup.UUID> ids = new HashSet<>();
diff --git a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index e3ff21a..a9de2b1 100644
--- a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -18,13 +18,10 @@
 import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ListChildProjects;
+import com.google.gerrit.server.project.ChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.inject.Provider;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -37,19 +34,13 @@
   protected final String value;
 
   public ParentProjectPredicate(
-      ProjectCache projectCache,
-      Provider<ListChildProjects> listChildProjects,
-      Provider<CurrentUser> self,
-      String value) {
-    super(predicates(projectCache, listChildProjects, self, value));
+      ProjectCache projectCache, ChildProjects childProjects, String value) {
+    super(predicates(projectCache, childProjects, value));
     this.value = value;
   }
 
   protected static List<Predicate<ChangeData>> predicates(
-      ProjectCache projectCache,
-      Provider<ListChildProjects> listChildProjects,
-      Provider<CurrentUser> self,
-      String value) {
+      ProjectCache projectCache, ChildProjects childProjects, String value) {
     ProjectState projectState = projectCache.get(new Project.NameKey(value));
     if (projectState == null) {
       return Collections.emptyList();
@@ -58,10 +49,7 @@
     List<Predicate<ChangeData>> r = new ArrayList<>();
     r.add(new ProjectPredicate(projectState.getName()));
     try {
-      ProjectResource proj = new ProjectResource(projectState, self.get());
-      ListChildProjects children = listChildProjects.get();
-      children.setRecursive(true);
-      for (ProjectInfo p : children.apply(proj)) {
+      for (ProjectInfo p : childProjects.list(projectState.getNameKey())) {
         r.add(new ProjectPredicate(p.name));
       }
     } catch (PermissionBackendException e) {
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
new file mode 100644
index 0000000..e0262bb
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -0,0 +1,22 @@
+package(
+    default_visibility = ["//visibility:public"],
+)
+
+java_library(
+    name = "restapi",
+    srcs = glob(["**/*.java"]),
+    deps = [
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/server",
+        "//java/org/eclipse/jgit:server",
+        "//lib:args4j",
+        "//lib:guava",
+        "//lib:gwtorm",
+        "//lib:servlet-api-3_1",
+        "//lib/guice",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/log:api",
+    ],
+)
diff --git a/java/com/google/gerrit/server/config/CachesCollection.java b/java/com/google/gerrit/server/restapi/config/CachesCollection.java
similarity index 95%
rename from java/com/google/gerrit/server/config/CachesCollection.java
rename to java/com/google/gerrit/server/restapi/config/CachesCollection.java
index 7ecfa63..cfdc648 100644
--- a/java/com/google/gerrit/server/config/CachesCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/CachesCollection.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
@@ -28,6 +28,8 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.CacheResource;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/config/CapabilitiesCollection.java b/java/com/google/gerrit/server/restapi/config/CapabilitiesCollection.java
similarity index 91%
rename from java/com/google/gerrit/server/config/CapabilitiesCollection.java
rename to java/com/google/gerrit/server/restapi/config/CapabilitiesCollection.java
index 1124048..ae1278d 100644
--- a/java/com/google/gerrit/server/config/CapabilitiesCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/CapabilitiesCollection.java
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.config.CapabilityResource;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
diff --git a/java/com/google/gerrit/server/config/CheckConsistency.java b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
similarity index 97%
rename from java/com/google/gerrit/server/config/CheckConsistency.java
rename to java/com/google/gerrit/server/restapi/config/CheckConsistency.java
index 38813de..95b20c2 100644
--- a/java/com/google/gerrit/server/config/CheckConsistency.java
+++ b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.CheckAccountExternalIdsResultInfo;
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountsConsistencyChecker;
 import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.group.db.GroupsConsistencyChecker;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
diff --git a/java/com/google/gerrit/server/config/ConfigCollection.java b/java/com/google/gerrit/server/restapi/config/ConfigCollection.java
similarity index 94%
rename from java/com/google/gerrit/server/config/ConfigCollection.java
rename to java/com/google/gerrit/server/restapi/config/ConfigCollection.java
index f268110..934dbc1 100644
--- a/java/com/google/gerrit/server/config/ConfigCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfigCollection.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.RestCollection;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
diff --git a/java/com/google/gerrit/server/config/Module.java b/java/com/google/gerrit/server/restapi/config/ConfigRestModule.java
similarity index 83%
rename from java/com/google/gerrit/server/config/Module.java
rename to java/com/google/gerrit/server/restapi/config/ConfigRestModule.java
index 7bf5ad5..71b2f9c 100644
--- a/java/com/google/gerrit/server/config/Module.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfigRestModule.java
@@ -12,23 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
-import static com.google.gerrit.server.config.CapabilityResource.CAPABILITY_KIND;
 import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
 import static com.google.gerrit.server.config.TaskResource.TASK_KIND;
-import static com.google.gerrit.server.config.TopMenuResource.TOP_MENU_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.config.CapabilityResource;
+import com.google.gerrit.server.config.TopMenuResource;
 
-public class Module extends RestApiModule {
+public class ConfigRestModule extends RestApiModule {
   @Override
   protected void configure() {
-    DynamicMap.mapOf(binder(), CAPABILITY_KIND);
+    DynamicMap.mapOf(binder(), CapabilityResource.CAPABILITY_KIND);
     DynamicMap.mapOf(binder(), CONFIG_KIND);
     DynamicMap.mapOf(binder(), TASK_KIND);
-    DynamicMap.mapOf(binder(), TOP_MENU_KIND);
+    DynamicMap.mapOf(binder(), TopMenuResource.TOP_MENU_KIND);
     child(CONFIG_KIND, "capabilities").to(CapabilitiesCollection.class);
     child(CONFIG_KIND, "tasks").to(TasksCollection.class);
     get(TASK_KIND).to(GetTask.class);
diff --git a/java/com/google/gerrit/server/config/ConfirmEmail.java b/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
similarity index 94%
rename from java/com/google/gerrit/server/config/ConfirmEmail.java
rename to java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
index 1044bbb..f6ceb68b 100644
--- a/java/com/google/gerrit/server/config/ConfirmEmail.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfirmEmail.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -23,8 +23,9 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.config.ConfirmEmail.Input;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
+import com.google.gerrit.server.restapi.config.ConfirmEmail.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
diff --git a/java/com/google/gerrit/server/config/DeleteTask.java b/java/com/google/gerrit/server/restapi/config/DeleteTask.java
similarity index 93%
rename from java/com/google/gerrit/server/config/DeleteTask.java
rename to java/com/google/gerrit/server/restapi/config/DeleteTask.java
index d20589a..a08b036 100644
--- a/java/com/google/gerrit/server/config/DeleteTask.java
+++ b/java/com/google/gerrit/server/restapi/config/DeleteTask.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
 import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.config.TaskResource;
 import com.google.gerrit.server.git.WorkQueue.Task;
 import com.google.inject.Singleton;
 
diff --git a/java/com/google/gerrit/server/config/FlushCache.java b/java/com/google/gerrit/server/restapi/config/FlushCache.java
similarity index 95%
rename from java/com/google/gerrit/server/config/FlushCache.java
rename to java/com/google/gerrit/server/restapi/config/FlushCache.java
index 5d2ec36..55e9dc3 100644
--- a/java/com/google/gerrit/server/config/FlushCache.java
+++ b/java/com/google/gerrit/server/restapi/config/FlushCache.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
 import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.CacheResource;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/config/GetCache.java b/java/com/google/gerrit/server/restapi/config/GetCache.java
similarity index 77%
rename from java/com/google/gerrit/server/config/GetCache.java
rename to java/com/google/gerrit/server/restapi/config/GetCache.java
index 53628cc..5abaf1e 100644
--- a/java/com/google/gerrit/server/config/GetCache.java
+++ b/java/com/google/gerrit/server/restapi/config/GetCache.java
@@ -12,17 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.config.CacheResource;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GetCache implements RestReadView<CacheResource> {
 
   @Override
-  public CacheInfo apply(CacheResource rsrc) {
-    return new CacheInfo(rsrc.getName(), rsrc.getCache());
+  public ListCaches.CacheInfo apply(CacheResource rsrc) {
+    return new ListCaches.CacheInfo(rsrc.getName(), rsrc.getCache());
   }
 }
diff --git a/java/com/google/gerrit/server/config/GetDiffPreferences.java b/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
similarity index 94%
rename from java/com/google/gerrit/server/config/GetDiffPreferences.java
rename to java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
index 8393fb4..6e72503 100644
--- a/java/com/google/gerrit/server/config/GetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
@@ -11,7 +11,7 @@
 // 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;
+package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.server.config.ConfigUtil.loadSection;
 
@@ -20,6 +20,8 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.UserConfigSections;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/config/GetPreferences.java b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
similarity index 94%
rename from java/com/google/gerrit/server/config/GetPreferences.java
rename to java/com/google/gerrit/server/restapi/config/GetPreferences.java
index ed212f4..c8a173f 100644
--- a/java/com/google/gerrit/server/config/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.server.config.ConfigUtil.loadSection;
 
@@ -20,6 +20,8 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.account.GeneralPreferencesLoader;
 import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.UserConfigSections;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
similarity index 95%
rename from java/com/google/gerrit/server/config/GetServerInfo.java
rename to java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 90e2838..cacbd34 100644
--- a/java/com/google/gerrit/server/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import static java.util.stream.Collectors.toList;
 
@@ -48,6 +48,16 @@
 import com.google.gerrit.server.change.AllowedFormats;
 import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.gerrit.server.change.Submit;
+import com.google.gerrit.server.config.AgreementJson;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritOptions;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.documentation.QueryDocumentationExecutor;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
diff --git a/java/com/google/gerrit/server/config/GetSummary.java b/java/com/google/gerrit/server/restapi/config/GetSummary.java
similarity index 97%
rename from java/com/google/gerrit/server/config/GetSummary.java
rename to java/com/google/gerrit/server/restapi/config/GetSummary.java
index 82912c0..26f069c 100644
--- a/java/com/google/gerrit/server/config/GetSummary.java
+++ b/java/com/google/gerrit/server/restapi/config/GetSummary.java
@@ -12,11 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.Task;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/config/GetTask.java b/java/com/google/gerrit/server/restapi/config/GetTask.java
similarity index 79%
rename from java/com/google/gerrit/server/config/GetTask.java
rename to java/com/google/gerrit/server/restapi/config/GetTask.java
index e4b3320..a32f3ba 100644
--- a/java/com/google/gerrit/server/config/GetTask.java
+++ b/java/com/google/gerrit/server/restapi/config/GetTask.java
@@ -12,17 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.config.ListTasks.TaskInfo;
+import com.google.gerrit.server.config.TaskResource;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GetTask implements RestReadView<TaskResource> {
 
   @Override
-  public TaskInfo apply(TaskResource rsrc) {
-    return new TaskInfo(rsrc.getTask());
+  public ListTasks.TaskInfo apply(TaskResource rsrc) {
+    return new ListTasks.TaskInfo(rsrc.getTask());
   }
 }
diff --git a/java/com/google/gerrit/server/config/GetVersion.java b/java/com/google/gerrit/server/restapi/config/GetVersion.java
similarity index 91%
rename from java/com/google/gerrit/server/config/GetVersion.java
rename to java/com/google/gerrit/server/restapi/config/GetVersion.java
index c71cb69..8135719 100644
--- a/java/com/google/gerrit/server/config/GetVersion.java
+++ b/java/com/google/gerrit/server/restapi/config/GetVersion.java
@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.common.Version;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.inject.Singleton;
 
 @Singleton
diff --git a/java/com/google/gerrit/server/config/ListCaches.java b/java/com/google/gerrit/server/restapi/config/ListCaches.java
similarity index 97%
rename from java/com/google/gerrit/server/config/ListCaches.java
rename to java/com/google/gerrit/server/restapi/config/ListCaches.java
index d78f61d..c0a9d71 100644
--- a/java/com/google/gerrit/server/config/ListCaches.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCaches.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.cache.PersistentCache;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collections;
diff --git a/java/com/google/gerrit/server/config/ListCapabilities.java b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
similarity index 94%
rename from java/com/google/gerrit/server/config/ListCapabilities.java
rename to java/com/google/gerrit/server/restapi/config/ListCapabilities.java
index b8d1888..6a1e5f6 100644
--- a/java/com/google/gerrit/server/config/ListCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.CapabilityConstants;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/config/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
similarity index 97%
rename from java/com/google/gerrit/server/config/ListTasks.java
rename to java/com/google/gerrit/server/restapi/config/ListTasks.java
index bbda9eb..71ee5ad 100644
--- a/java/com/google/gerrit/server/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.common.collect.ComparisonChain;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.TaskInfoFactory;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.ProjectTask;
diff --git a/java/com/google/gerrit/server/config/ListTopMenus.java b/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
similarity index 92%
rename from java/com/google/gerrit/server/config/ListTopMenus.java
rename to java/com/google/gerrit/server/restapi/config/ListTopMenus.java
index a7ba938..7a85bcd 100644
--- a/java/com/google/gerrit/server/config/ListTopMenus.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTopMenus.java
@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.webui.TopMenu;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
diff --git a/java/com/google/gerrit/server/config/PostCaches.java b/java/com/google/gerrit/server/restapi/config/PostCaches.java
similarity index 94%
rename from java/com/google/gerrit/server/config/PostCaches.java
rename to java/com/google/gerrit/server/restapi/config/PostCaches.java
index d08f0a9..f21672c 100644
--- a/java/com/google/gerrit/server/config/PostCaches.java
+++ b/java/com/google/gerrit/server/restapi/config/PostCaches.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
 import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
@@ -25,8 +25,10 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.config.PostCaches.Input;
+import com.google.gerrit.server.config.CacheResource;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.config.PostCaches.Input;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
diff --git a/java/com/google/gerrit/server/config/RestCacheAdminModule.java b/java/com/google/gerrit/server/restapi/config/RestCacheAdminModule.java
similarity index 95%
rename from java/com/google/gerrit/server/config/RestCacheAdminModule.java
rename to java/com/google/gerrit/server/restapi/config/RestCacheAdminModule.java
index 992c62e..7283033 100644
--- a/java/com/google/gerrit/server/config/RestCacheAdminModule.java
+++ b/java/com/google/gerrit/server/restapi/config/RestCacheAdminModule.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.server.config.CacheResource.CACHE_KIND;
 import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
diff --git a/java/com/google/gerrit/server/config/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
similarity index 93%
rename from java/com/google/gerrit/server/config/SetDiffPreferences.java
rename to java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
index 80c4625..a61b2aa 100644
--- a/java/com/google/gerrit/server/config/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
@@ -11,12 +11,11 @@
 // 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;
+package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.server.config.ConfigUtil.loadSection;
 import static com.google.gerrit.server.config.ConfigUtil.skipField;
 import static com.google.gerrit.server.config.ConfigUtil.storeSection;
-import static com.google.gerrit.server.config.GetDiffPreferences.readFromGit;
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -24,6 +23,8 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.UserConfigSections;
@@ -65,7 +66,7 @@
     if (!hasSetFields(in)) {
       throw new BadRequestException("unsupported option");
     }
-    return writeToGit(readFromGit(gitManager, allUsersName, in));
+    return writeToGit(GetDiffPreferences.readFromGit(gitManager, allUsersName, in));
   }
 
   private DiffPreferencesInfo writeToGit(DiffPreferencesInfo in)
diff --git a/java/com/google/gerrit/server/config/SetPreferences.java b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
similarity index 94%
rename from java/com/google/gerrit/server/config/SetPreferences.java
rename to java/com/google/gerrit/server/restapi/config/SetPreferences.java
index 55337d5..e2671d1 100644
--- a/java/com/google/gerrit/server/config/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.server.config.ConfigUtil.loadSection;
 import static com.google.gerrit.server.config.ConfigUtil.skipField;
 import static com.google.gerrit.server.config.ConfigUtil.storeSection;
-import static com.google.gerrit.server.config.GetPreferences.readFromGit;
+import static com.google.gerrit.server.restapi.config.GetPreferences.readFromGit;
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -27,6 +27,8 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GeneralPreferencesLoader;
 import com.google.gerrit.server.account.VersionedAccountPreferences;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.UserConfigSections;
diff --git a/java/com/google/gerrit/server/config/TasksCollection.java b/java/com/google/gerrit/server/restapi/config/TasksCollection.java
similarity index 95%
rename from java/com/google/gerrit/server/config/TasksCollection.java
rename to java/com/google/gerrit/server/restapi/config/TasksCollection.java
index fcaee8e..f5b6e56 100644
--- a/java/com/google/gerrit/server/config/TasksCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/TasksCollection.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -21,6 +21,8 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.TaskResource;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.ProjectTask;
 import com.google.gerrit.server.git.WorkQueue.Task;
diff --git a/java/com/google/gerrit/server/config/TopMenuCollection.java b/java/com/google/gerrit/server/restapi/config/TopMenuCollection.java
similarity index 91%
rename from java/com/google/gerrit/server/config/TopMenuCollection.java
rename to java/com/google/gerrit/server/restapi/config/TopMenuCollection.java
index 2fc2dc1..36a1b04 100644
--- a/java/com/google/gerrit/server/config/TopMenuCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/TopMenuCollection.java
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.restapi.config;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.TopMenuResource;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
diff --git a/java/com/google/gerrit/server/update/BatchUpdateReviewDb.java b/java/com/google/gerrit/server/update/BatchUpdateReviewDb.java
index 21e1f92..3b8f871 100644
--- a/java/com/google/gerrit/server/update/BatchUpdateReviewDb.java
+++ b/java/com/google/gerrit/server/update/BatchUpdateReviewDb.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ChangeAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
 import com.google.gwtorm.server.AtomicUpdate;
 
@@ -28,6 +29,14 @@
     changesWrapper = new BatchUpdateChanges(delegate.changes());
   }
 
+  /** @return the underlying delegate. Supports BatchUpdateReviewDb too. */
+  public static ReviewDb unwrap(ReviewDb db) {
+    if (db instanceof BatchUpdateReviewDb) {
+      db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
+    }
+    return ReviewDbUtil.unwrapDb(db);
+  }
+
   public ReviewDb unsafeGetDelegate() {
     return delegate;
   }
diff --git a/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java b/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
index ceef352..c10ae1b 100644
--- a/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
+++ b/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
@@ -61,8 +61,8 @@
  * <p>Used when {@code noteDb.changes.disableReviewDb=true}, at which point ReviewDb is not
  * consulted during updates.
  */
-class NoteDbBatchUpdate extends BatchUpdate {
-  interface AssistedFactory {
+public class NoteDbBatchUpdate extends BatchUpdate {
+  public interface AssistedFactory {
     NoteDbBatchUpdate create(
         ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when);
   }
diff --git a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
index 6835cb4..07ae04d 100644
--- a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
@@ -100,10 +100,10 @@
  * The implementation in this class is well-tested, and it is strongly recommended that you not
  * attempt to reimplement this logic. Use {@code BatchUpdate} if at all possible.
  */
-class ReviewDbBatchUpdate extends BatchUpdate {
+public class ReviewDbBatchUpdate extends BatchUpdate {
   private static final Logger log = LoggerFactory.getLogger(ReviewDbBatchUpdate.class);
 
-  interface AssistedFactory {
+  public interface AssistedFactory {
     ReviewDbBatchUpdate create(
         ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when);
   }
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index a33ce86..3ed1f2f 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -14,6 +14,7 @@
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/git/receive",
         "//java/com/google/gerrit/server/ioutil",
+        "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/util/cli",
         "//java/org/eclipse/jgit:server",
diff --git a/java/com/google/gerrit/sshd/commands/FlushCaches.java b/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 392fd29..2271ece 100644
--- a/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -16,17 +16,17 @@
 
 import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
 import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
-import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH;
-import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH_ALL;
+import static com.google.gerrit.server.restapi.config.PostCaches.Operation.FLUSH;
+import static com.google.gerrit.server.restapi.config.PostCaches.Operation.FLUSH_ALL;
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.config.ListCaches;
-import com.google.gerrit.server.config.ListCaches.OutputFormat;
-import com.google.gerrit.server.config.PostCaches;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.config.ListCaches;
+import com.google.gerrit.server.restapi.config.ListCaches.OutputFormat;
+import com.google.gerrit.server.restapi.config.PostCaches;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/sshd/commands/KillCommand.java b/java/com/google/gerrit/sshd/commands/KillCommand.java
index 3465a9c..a7e751a 100644
--- a/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -22,10 +22,10 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.config.DeleteTask;
 import com.google.gerrit.server.config.TaskResource;
-import com.google.gerrit.server.config.TasksCollection;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.config.DeleteTask;
+import com.google.gerrit.server.restapi.config.TasksCollection;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 1ed7db3..a356f7f 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -26,18 +26,18 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.config.GetSummary;
-import com.google.gerrit.server.config.GetSummary.JvmSummaryInfo;
-import com.google.gerrit.server.config.GetSummary.MemSummaryInfo;
-import com.google.gerrit.server.config.GetSummary.SummaryInfo;
-import com.google.gerrit.server.config.GetSummary.TaskSummaryInfo;
-import com.google.gerrit.server.config.GetSummary.ThreadSummaryInfo;
-import com.google.gerrit.server.config.ListCaches;
-import com.google.gerrit.server.config.ListCaches.CacheInfo;
-import com.google.gerrit.server.config.ListCaches.CacheType;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.config.GetSummary;
+import com.google.gerrit.server.restapi.config.GetSummary.JvmSummaryInfo;
+import com.google.gerrit.server.restapi.config.GetSummary.MemSummaryInfo;
+import com.google.gerrit.server.restapi.config.GetSummary.SummaryInfo;
+import com.google.gerrit.server.restapi.config.GetSummary.TaskSummaryInfo;
+import com.google.gerrit.server.restapi.config.GetSummary.ThreadSummaryInfo;
+import com.google.gerrit.server.restapi.config.ListCaches;
+import com.google.gerrit.server.restapi.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.restapi.config.ListCaches.CacheType;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
diff --git a/java/com/google/gerrit/sshd/commands/ShowQueue.java b/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 0296690..6d2fbb4 100644
--- a/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -23,13 +23,13 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.config.ListTasks;
-import com.google.gerrit.server.config.ListTasks.TaskInfo;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.Task;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.config.ListTasks;
+import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 2715c75..59102a9 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -25,6 +25,7 @@
         "//java/com/google/gerrit/server:module",
         "//java/com/google/gerrit/server/api",
         "//java/com/google/gerrit/server/cache/h2",
+        "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//lib:gwtorm",
         "//lib:h2",
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
index e0fc358..0b7f340 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
@@ -17,10 +17,12 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.restapi.DeprecatedIdentifierException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Project;
 import org.junit.Before;
@@ -119,4 +121,27 @@
     exception.expect(ResourceNotFoundException.class);
     gApi.changes().id("I1234567890");
   }
+
+  @Test
+  @GerritConfig(
+    name = "change.api.allowedIdentifier",
+    values = {"PROJECT_NUMERIC_ID", "NUMERIC_ID"}
+  )
+  public void deprecatedChangeIdReturnsBadRequest() throws Exception {
+    // project~changeNumber still works
+    ChangeApi cApi1 = gApi.changes().id(project.get(), changeInfo._number);
+    assertThat(cApi1.get().changeId).isEqualTo(changeInfo.changeId);
+    // Change number still works
+    ChangeApi cApi2 = gApi.changes().id(changeInfo._number);
+    assertThat(cApi2.get().changeId).isEqualTo(changeInfo.changeId);
+    // IHash throws
+    ChangeInfo ci =
+        gApi.changes().create(new ChangeInput(project.get(), "master", "different message")).get();
+    exception.expect(DeprecatedIdentifierException.class);
+    exception.expectMessage(
+        "The provided change identifier "
+            + ci.changeId
+            + " is deprecated. Use 'project~changeNumber' instead.");
+    gApi.changes().id(ci.changeId);
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/BUILD b/javatests/com/google/gerrit/acceptance/rest/config/BUILD
index 825523d..8550423 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/config/BUILD
@@ -4,4 +4,7 @@
     srcs = glob(["*IT.java"]),
     group = "rest_config",
     labels = ["rest"],
+    deps = [
+        "//java/com/google/gerrit/server/restapi",
+    ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java b/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
index 2ef74b4..65ed7e4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
@@ -15,15 +15,15 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH;
-import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH_ALL;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.restapi.config.PostCaches.Operation.FLUSH;
+import static com.google.gerrit.server.restapi.config.PostCaches.Operation.FLUSH_ALL;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.server.config.ListCaches.CacheInfo;
-import com.google.gerrit.server.config.PostCaches;
+import com.google.gerrit.server.restapi.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.restapi.config.PostCaches;
 import java.util.Arrays;
 import org.junit.Test;
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
index f196684..7133580 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.server.config.ConfirmEmail;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
+import com.google.gerrit.server.restapi.config.ConfirmEmail;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.inject.Inject;
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index bc27fff..caecefa 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.server.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.restapi.config.ListCaches.CacheInfo;
 import org.junit.Test;
 
 public class FlushCacheIT extends AbstractDaemonTest {
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/GetCacheIT.java b/javatests/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
index fe600cc..247d63b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
@@ -18,8 +18,8 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.server.config.ListCaches.CacheInfo;
-import com.google.gerrit.server.config.ListCaches.CacheType;
+import com.google.gerrit.server.restapi.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.restapi.config.ListCaches.CacheType;
 import org.junit.Test;
 
 public class GetCacheIT extends AbstractDaemonTest {
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java b/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
index 900b4be..6d2c6dfa 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.server.config.ListTasks.TaskInfo;
+import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
 import com.google.gson.reflect.TypeToken;
 import java.util.List;
 import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java b/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
index 7cd9584..c19f5d0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
@@ -20,7 +20,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.server.config.ListTasks.TaskInfo;
+import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
 import com.google.gson.reflect.TypeToken;
 import java.util.List;
 import java.util.Optional;
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
index 4d48bf4..ae17be0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
@@ -20,8 +20,8 @@
 import com.google.common.collect.Ordering;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.server.config.ListCaches.CacheInfo;
-import com.google.gerrit.server.config.ListCaches.CacheType;
+import com.google.gerrit.server.restapi.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.restapi.config.ListCaches.CacheType;
 import com.google.gson.reflect.TypeToken;
 import java.util.Arrays;
 import java.util.List;
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ListTasksIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
index ee6411a..674ca79 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.server.config.ListTasks.TaskInfo;
+import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
 import com.google.gson.reflect.TypeToken;
 import java.util.List;
 import org.junit.Test;
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 5bdfe39..a228ed6 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -42,6 +42,7 @@
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/project/testing:project-test-util",
+        "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//java/org/eclipse/jgit:server",
diff --git a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
index 6fe48dc..bcba665 100644
--- a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
+++ b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -22,7 +22,8 @@
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.server.config.ListCapabilities.CapabilityInfo;
+import com.google.gerrit.server.restapi.config.ListCapabilities;
+import com.google.gerrit.server.restapi.config.ListCapabilities.CapabilityInfo;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index e737f47..1723713 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,8 +1,8 @@
 load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "MAVEN_CENTRAL", "maven_jar")
 
-_JGIT_VERS = "4.9.1.201712030800-r.66-gf8eff40ca"
+_JGIT_VERS = "4.9.2.201712150930-r.171-gfdbaa25db"
 
-_DOC_VERS = "4.9.1.201712030800-r"  # Set to _JGIT_VERS unless using a snapshot
+_DOC_VERS = "4.9.2.201712150930-r"  # Set to _JGIT_VERS unless using a snapshot
 
 JGIT_DOC_URL = "http://download.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
 
@@ -26,28 +26,28 @@
         name = "jgit_lib",
         artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "0b974aa9c6c929c39c506ab2705d42f3d7da84c7",
-        src_sha1 = "8884bef0415e092563b60b2167adbb09ac19d131",
+        sha1 = "29b822410b29286a09df728f8379e5cb8b1a486e",
+        src_sha1 = "5106b81910a057470cfd2584d9cb3502bcbebbc2",
         unsign = True,
     )
     maven_jar(
         name = "jgit_servlet",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "19c6bcdf5e0ba1907f6eeb18ae02d6ae04f630e3",
+        sha1 = "01f6718f6b629e28caad38e00190811b38574e74",
         unsign = True,
     )
     maven_jar(
         name = "jgit_archive",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "0063dde3c017e05ee4e84ae16c97cb8817b91782",
+        sha1 = "9c9e9332e7dc724dbe1837e21feccd98bc25e6b4",
     )
     maven_jar(
         name = "jgit_junit",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "a9cb1e58df9bd876a2e81130f61a9bac0f182520",
+        sha1 = "4154c70b78b62035dad446332b24f7816b7a2a1b",
         unsign = True,
     )
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 7c648d4..8cc7465 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -134,10 +134,21 @@
     _computeLabelValues(labelName, _labels) {
       const result = [];
       const labels = _labels.base;
-      const t = labels[labelName];
-      if (!t) { return result; }
-      const approvals = t.all || [];
-      const values = Object.keys(t.values);
+      const labelInfo = labels[labelName];
+      if (!labelInfo) { return result; }
+      if (!labelInfo.values) {
+        if (labelInfo.rejected || labelInfo.approved) {
+          const ok = labelInfo.approved || !labelInfo.rejected;
+          return [{
+            value: ok ? '👍️' : '👎️',
+            className: ok ? 'positive' : 'negative',
+            account: ok ? labelInfo.approved : labelInfo.rejected,
+          }];
+        }
+        return result;
+      }
+      const approvals = labelInfo.all || [];
+      const values = Object.keys(labelInfo.values);
       for (const label of approvals) {
         if (label.value && label.value != labels[labelName].default_value) {
           let labelClassName;
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 7374bb7..868a147 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -664,6 +664,32 @@
     });
 
     suite('label colors', () => {
+      test('valueless label rejected', () => {
+        element.change = {
+          labels: {
+            'Do-Not-Submit': {
+              rejected: {name: 'someone'},
+            },
+          },
+        };
+        flushAsynchronousOperations();
+        const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
+        assert.isTrue(labels[0].classList.contains('negative'));
+      });
+
+      test('valueless label approved', () => {
+        element.change = {
+          labels: {
+            'To-The-Infinity': {
+              approved: {name: 'someone'},
+            },
+          },
+        };
+        flushAsynchronousOperations();
+        const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
+        assert.isTrue(labels[0].classList.contains('positive'));
+      });
+
       test('-2 to +2', () => {
         element.change = {
           labels: {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index 7524abcb..3189cd4 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -45,22 +45,22 @@
         background-color: #fff9c4;
       }
       .patchInfo-header {
+        align-items: center;
         background-color: #fafafa;
         border-top: 1px solid #ddd;
         display: flex;
         padding: 6px var(--default-horizontal-margin);
       }
-      .patchInfo-header-wrapper {
-        align-items: center;
-        display: flex;
-        width: 100%;
-      }
       .patchInfo-left {
+        align-items: baseline;
+        display: flex;
+      }
+      .patchInfoContent {
         align-items: center;
         display: flex;
         flex-wrap: wrap;
       }
-      .patchInfo-header-wrapper .container.latestPatchContainer {
+      .patchInfo-header .container.latestPatchContainer {
         display: none;
       }
       .patchInfoOldPatchSet .container.latestPatchContainer {
@@ -75,7 +75,7 @@
       .mobile {
         display: none;
       }
-      .patchInfo-header-wrapper .container {
+      .patchInfo-header .container {
         align-items: center;
         display: flex;
       }
@@ -137,6 +137,7 @@
         display: initial;
       }
       .editLoaded .showOnEdit.flexContainer {
+        align-items: center;
         display: flex;
       }
       .label {
@@ -150,9 +151,9 @@
       }
     </style>
     <div class$="patchInfo-header [[_computeEditLoadedClass(editLoaded)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]">
-      <div class="patchInfo-header-wrapper">
-        <div class="patchInfo-left">
-          <h3 class="label">Files</h3>
+      <div class="patchInfo-left">
+        <h3 class="label">Files</h3>
+        <div class="patchInfoContent">
           <gr-patch-range-select
               id="rangeSelect"
               change-comments="[[changeComments]]"
@@ -199,62 +200,62 @@
             </template>
           </span>
         </div>
-        <div class$="rightControls [[_computeExpandedClass(filesExpanded)]]">
-          <span class="showOnEdit flexContainer">
-            <gr-edit-controls id="editControls" change="[[change]]"></gr-edit-controls>
-            <span class="separator"></span>
-          </span>
-          <span class="downloadContainer desktop">
-            <gr-button link
-                class="download"
-                on-tap="_handleDownloadTap">Download</gr-button>
-          </span>
-          <template is="dom-if"
-              if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
-            <gr-button
-                id="expandBtn"
-                link
-                on-tap="_expandAllDiffs">Expand All</gr-button>
-            <gr-button
-                id="collapseBtn"
-                link
-                on-tap="_collapseAllDiffs">Collapse All</gr-button>
-          </template>
-          <template is="dom-if"
-              if="[[!_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
-            <div class="warning">
-              Bulk actions disabled because there are too many files.
-            </div>
-          </template>
-          <div class="fileViewActions">
-            <span class="separator"></span>
-            <span>Diff Views:</span>
-            <gr-button
-                id="sideBySideBtn"
-                link
-                has-tooltip
-                title="Side-by-side diff"
-                class$="[[_computeSelectedClass(diffViewMode, _VIEW_MODES.SIDE_BY_SIDE)]]"
-                on-tap="_handleSideBySideTap"><iron-icon icon="gr-icons:side-by-side"></iron-icon></gr-button>
-            <gr-button
-                id="unifiedBtn"
-                link
-                has-tooltip
-                title="Unified dff"
-                class$="[[_computeSelectedClass(diffViewMode, _VIEW_MODES.UNIFIED)]]"
-                on-tap="_handleUnifiedTap"><iron-icon icon="gr-icons:unified"></iron-icon></gr-button>
-            <span id="diffPrefsContainer"
-                class="hideOnEdit"
-                hidden$="[[_computePrefsButtonHidden(diffPrefs, loggedIn)]]"
-                hidden>
-              <gr-button
-                  link
-                  has-tooltip
-                  title="Diff preferences"
-                  class="prefsButton desktop"
-                  on-tap="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
-            </span>
+      </div>
+      <div class$="rightControls [[_computeExpandedClass(filesExpanded)]]">
+        <span class="showOnEdit flexContainer">
+          <gr-edit-controls id="editControls" change="[[change]]"></gr-edit-controls>
+          <span class="separator"></span>
+        </span>
+        <span class="downloadContainer desktop">
+          <gr-button link
+              class="download"
+              on-tap="_handleDownloadTap">Download</gr-button>
+        </span>
+        <template is="dom-if"
+            if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
+          <gr-button
+              id="expandBtn"
+              link
+              on-tap="_expandAllDiffs">Expand All</gr-button>
+          <gr-button
+              id="collapseBtn"
+              link
+              on-tap="_collapseAllDiffs">Collapse All</gr-button>
+        </template>
+        <template is="dom-if"
+            if="[[!_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
+          <div class="warning">
+            Bulk actions disabled because there are too many files.
           </div>
+        </template>
+        <div class="fileViewActions">
+          <span class="separator"></span>
+          <span>Diff Views:</span>
+          <gr-button
+              id="sideBySideBtn"
+              link
+              has-tooltip
+              title="Side-by-side diff"
+              class$="[[_computeSelectedClass(diffViewMode, _VIEW_MODES.SIDE_BY_SIDE)]]"
+              on-tap="_handleSideBySideTap"><iron-icon icon="gr-icons:side-by-side"></iron-icon></gr-button>
+          <gr-button
+              id="unifiedBtn"
+              link
+              has-tooltip
+              title="Unified dff"
+              class$="[[_computeSelectedClass(diffViewMode, _VIEW_MODES.UNIFIED)]]"
+              on-tap="_handleUnifiedTap"><iron-icon icon="gr-icons:unified"></iron-icon></gr-button>
+          <span id="diffPrefsContainer"
+              class="hideOnEdit"
+              hidden$="[[_computePrefsButtonHidden(diffPrefs, loggedIn)]]"
+              hidden>
+            <gr-button
+                link
+                has-tooltip
+                title="Diff preferences"
+                class="prefsButton desktop"
+                on-tap="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
+          </span>
         </div>
       </div>
     </div>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index c4120eb..44c7e2d 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -819,6 +819,12 @@
      * @param {!Array} record The splice record in the expanded paths list.
      */
     _expandedPathsChanged(record) {
+      // Clear content for any diffs that are not open so if they get re-opened
+      // the stale content does not flash before it is cleared and reloaded.
+      const collapsedDiffs = this.diffs.filter(diff =>
+          this._expandedFilePaths.indexOf(diff.path) === -1);
+      this._clearCollapsedDiffs(collapsedDiffs);
+
       if (!record) { return; }
 
       this.filesExpanded = this._computeExpandedFiles(
@@ -843,6 +849,12 @@
       this.$.diffCursor.handleDiffUpdate();
     },
 
+    _clearCollapsedDiffs(collapsedDiffs) {
+      for (const diff of collapsedDiffs) {
+        diff.clearDiffContent();
+      }
+    },
+
     /**
      * Given an array of paths and a NodeList of diff elements, render the diff
      * for each path in order, awaiting the previous render to complete before
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 0a45fe3..4c1326a 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -831,39 +831,44 @@
 
     test('_togglePathExpanded', () => {
       const path = 'path/to/my/file.txt';
-      element.files = [{__path: path}];
-      const renderStub = sandbox.stub(element, '_renderInOrder')
-          .returns(Promise.resolve());
+      element._files = [{__path: path}];
+      const renderSpy = sandbox.spy(element, '_renderInOrder');
+      const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
 
       assert.equal(element._expandedFilePaths.length, 0);
       element._togglePathExpanded(path);
       flushAsynchronousOperations();
+      assert.equal(collapseStub.lastCall.args[0].length, 0);
 
-      assert.equal(renderStub.callCount, 1);
+      assert.equal(renderSpy.callCount, 1);
       assert.include(element._expandedFilePaths, path);
       element._togglePathExpanded(path);
       flushAsynchronousOperations();
 
-      assert.equal(renderStub.callCount, 2);
+      assert.equal(renderSpy.callCount, 2);
       assert.notInclude(element._expandedFilePaths, path);
+      assert.equal(collapseStub.lastCall.args[0].length, 1);
     });
 
-    test('collapseAllDiffs', () => {
-      sandbox.stub(element, '_renderInOrder')
-          .returns(Promise.resolve());
+    test('expandAllDiffs and collapseAllDiffs', () => {
+      const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
       const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
           'handleDiffUpdate');
 
       const path = 'path/to/my/file.txt';
-      element.files = [{__path: path}];
-      element._expandedFilePaths = [path];
-      element._showInlineDiffs = true;
+      element._files = [{__path: path}];
+      element.expandAllDiffs();
+      flushAsynchronousOperations();
+      assert.isTrue(element._showInlineDiffs);
+      assert.isTrue(cursorUpdateStub.calledOnce);
+      assert.equal(collapseStub.lastCall.args[0].length, 0);
 
       element.collapseAllDiffs();
       flushAsynchronousOperations();
       assert.equal(element._expandedFilePaths.length, 0);
       assert.isFalse(element._showInlineDiffs);
-      assert.isTrue(cursorUpdateStub.calledOnce);
+      assert.isTrue(cursorUpdateStub.calledTwice);
+      assert.equal(collapseStub.lastCall.args[0].length, 1);
     });
 
     test('_expandedPathsChanged', done => {
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
index b11c230..8324ab2 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -73,10 +73,10 @@
         --button-background-color: var(--vote-color-min);
       }
       iron-selector > gr-button.iron-selected.negative {
-        --button-background-color: #var(--vote-color-negative);;
+        --button-background-color: var(--vote-color-negative);
       }
       iron-selector > gr-button.iron-selected.neutral {
-        --button-background-color: var(--vote-color-neutral);;
+        --button-background-color: var(--vote-color-neutral);
       }
       .placeholder {
         display: inline-block;
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index fff3580..968766d 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -142,7 +142,9 @@
     },
 
     _computeLabelValueTitle(labels, label, value) {
-      return labels[label] && labels[label].values[value];
+      return labels[label] &&
+        labels[label].values &&
+        labels[label].values[value];
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index d2effdd..335cc49 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -45,6 +45,7 @@
       .titleText::before {
         background-image: var(--header-icon);
         background-size: var(--header-icon-size) var(--header-icon-size);
+        background-repeat: no-repeat;
         content: "";
         display: inline-block;
         height: var(--header-icon-size);
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index b59ad9d..f8c7e74 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -72,14 +72,15 @@
 
       View: {
         ADMIN: 'admin',
-        CHANGE: 'change',
         AGREEMENTS: 'agreements',
+        CHANGE: 'change',
         DASHBOARD: 'dashboard',
         DIFF: 'diff',
         EDIT: 'edit',
+        GROUP: 'group',
+        PLUGIN_SCREEN: 'plugin-screen',
         SEARCH: 'search',
         SETTINGS: 'settings',
-        GROUP: 'group',
       },
 
       GroupDetailView: {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 5d26cb6..6d7e78d 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -130,6 +130,8 @@
     // Matches /c/<changeNum>/ /<URL tail>
     // Catches improperly encoded URLs (context: Issue 7100)
     IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
+
+    PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
   };
 
   /**
@@ -621,6 +623,14 @@
       page((ctx, next) => {
         document.body.scrollTop = 0;
 
+        if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
+          // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen
+          // This is needed to allow plugins to add basic #/x/ screen links to
+          // any location.
+          this._redirect(ctx.hash);
+          return;
+        }
+
         // Fire asynchronously so that the URL is changed by the time the event
         // is processed.
         this.async(() => {
@@ -748,6 +758,8 @@
       this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
           '_handleImproperlyEncodedPlusRoute');
 
+      this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
+
       // Note: this route should appear last so it only catches URLs unmatched
       // by other patterns.
       this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
@@ -1276,6 +1288,13 @@
       this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
     },
 
+    _handlePluginScreen(ctx) {
+      const view = Gerrit.Nav.View.PLUGIN_SCREEN;
+      const plugin = ctx.params[0];
+      const screen = ctx.params[1];
+      this._setParams({view, plugin, screen});
+    },
+
     /**
      * Catchall route for when no other route is matched.
      */
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index 7159e0a..2f29667 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -168,6 +168,7 @@
         '_handleTagListFilterOffsetRoute',
         '_handleTagListFilterRoute',
         '_handleTagListOffsetRoute',
+        '_handlePluginScreen',
       ];
 
       // Handler names that check authentication themselves, and thus don't need
@@ -1332,6 +1333,16 @@
           assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
         });
       });
+
+      test('_handlePluginScreen', () => {
+        const ctx = {params: ['foo', 'bar']};
+        assertDataToParams(ctx, '_handlePluginScreen', {
+          view: Gerrit.Nav.View.PLUGIN_SCREEN,
+          plugin: 'foo',
+          screen: 'bar',
+        });
+        assert.isFalse(redirectStub.called);
+      });
     });
 
     suite('_parseQueryString', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 5f3269a..75ed70c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -176,7 +176,7 @@
       this.clearBlame();
       this._safetyBypass = null;
       this._showWarning = false;
-      this._clearDiffContent();
+      this.clearDiffContent();
 
       const promises = [];
 
@@ -615,7 +615,7 @@
       return this.prefs;
     },
 
-    _clearDiffContent() {
+    clearDiffContent() {
       this.$.diffTable.innerHTML = null;
     },
 
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html
new file mode 100644
index 0000000..b5573ab
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.html
@@ -0,0 +1,43 @@
+<!--
+Copyright (C) 2017 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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-default-editor">
+  <template>
+    <style include="shared-styles">
+      textarea {
+        border: 1px solid #ddd;
+        border-radius: 3px;
+        box-sizing: border-box;
+        font-family: var(--monospace-font-family);
+        min-height: 60vh;
+        resize: none;
+        white-space: pre;
+        width: 100%;
+      }
+      textarea:focus {
+        outline: none;
+      }
+    </style>
+    <textarea
+        id="textarea"
+        value="[[fileContent]]"
+        on-input="_handleTextareaInput"></textarea>
+  </template>
+  <script src="gr-default-editor.js"></script>
+</dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
new file mode 100644
index 0000000..f30f9fa
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
@@ -0,0 +1,35 @@
+// Copyright (C) 2017 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.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-default-editor',
+
+    /**
+     * Fired when the content of the editor changes.
+     *
+     * @event content-change
+     */
+
+    properties: {
+      fileContent: String,
+    },
+
+    _handleTextareaInput(e) {
+      this.dispatchEvent(new CustomEvent('content-change',
+          {detail: {value: e.target.value}, bubbles: true}));
+    },
+  });
+})();
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
new file mode 100644
index 0000000..43ec9e5
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
@@ -0,0 +1,55 @@
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-default-editor</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+
+<link rel="import" href="gr-default-editor.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-default-editor></gr-default-editor>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-default-editor tests', () => {
+    let element;
+
+    setup(() => {
+      element = fixture('basic');
+      element.fileContent = '';
+    });
+
+    test('fires content-change event', done => {
+      const contentChangedHandler = e => {
+        assert.equal(e.detail.value, 'test');
+        done();
+      };
+      const textarea = element.$.textarea;
+      element.addEventListener('content-change', contentChangedHandler);
+      textarea.value = 'test';
+      textarea.dispatchEvent(new CustomEvent('input',
+          {target: textarea, bubbles: true}));
+    });
+  });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
index 3042e76..10da09b 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
@@ -26,9 +26,9 @@
 <link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
 <link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-default-editor/gr-default-editor.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
-
 <dom-module id="gr-editor-view">
   <template>
     <style include="shared-styles">
@@ -54,19 +54,6 @@
       .textareaWrapper {
         margin: var(--default-horizontal-margin);
       }
-      .textareaWrapper textarea {
-        border: 1px solid #ddd;
-        border-radius: 3px;
-        box-sizing: border-box;
-        font-family: var(--monospace-font-family);
-        min-height: 60vh;
-        resize: none;
-        white-space: pre;
-        width: 100%;
-      }
-      .textareaWrapper textarea:focus {
-        outline: none;
-      }
       .textareaWrapper .editButtons {
         display: none;
       }
@@ -96,11 +83,11 @@
       </header>
     </gr-fixed-panel>
     <div class="textareaWrapper">
-      <gr-endpoint-decorator name="editor">
+      <gr-endpoint-decorator id="editorEndpoint" name="editor">
         <gr-endpoint-param name="fileContent" value="[[_newContent]]"></gr-endpoint-param>
         <gr-endpoint-param name="prefs" value="[[_prefs]]"></gr-endpoint-param>
         <gr-endpoint-param name="fileType" value="[[_type]]"></gr-endpoint-param>
-        <textarea value="{{_newContent::input}}" id="file"></textarea>
+        <gr-default-editor id="file" file-content="[[_newContent]]"></gr-default-editor>
       </gr-endpoint-decorator>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index f31608d..447c258 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -53,6 +53,10 @@
       Gerrit.PathListBehavior,
     ],
 
+    listeners: {
+      'content-change': '_handleContentChange',
+    },
+
     attached() {
       this._getEditPrefs().then(prefs => { this._prefs = prefs; });
     },
@@ -132,5 +136,9 @@
       // TODO(kaspern): Add a confirm dialog if there are unsaved changes.
       this._viewEditInChangeView();
     },
+
+    _handleContentChange(e) {
+      if (e.detail.value) { this.set('_newContent', e.detail.value); }
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
index 05a247a..fa9bcec 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -114,6 +114,17 @@
     });
   });
 
+  test('reacts to content-change event', () => {
+    element._newContent = 'test';
+    element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', {
+      bubbles: true,
+      detail: {value: 'new content value'},
+    }));
+    flushAsynchronousOperations();
+
+    assert.equal(element._newContent, 'new content value');
+  });
+
   suite('edit file content', () => {
     const originalText = 'file text';
     const newText = 'file text changed';
@@ -127,7 +138,7 @@
     });
 
     test('initial load', () => {
-      assert.equal(element.$.file.value, originalText);
+      assert.equal(element.$.file.fileContent, originalText);
       assert.isTrue(element.$.save.hasAttribute('disabled'));
     });
 
@@ -137,7 +148,6 @@
       element._newContent = newText;
       flushAsynchronousOperations();
 
-      assert.equal(element.$.file.value, newText);
       assert.isFalse(element.$.save.hasAttribute('disabled'));
 
       MockInteractions.tap(element.$.save);
@@ -157,7 +167,6 @@
       element._newContent = newText;
       flushAsynchronousOperations();
 
-      assert.equal(element.$.file.value, newText);
       assert.isFalse(element.$.save.hasAttribute('disabled'));
 
       MockInteractions.tap(element.$.save);
@@ -174,7 +183,6 @@
       element._newContent = newText;
       flushAsynchronousOperations();
 
-      assert.equal(element.$.file.value, newText);
       assert.isFalse(element.$.save.hasAttribute('disabled'));
 
       MockInteractions.tap(element.$.cancel);
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 035e906..95cddab 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -48,6 +48,7 @@
 <link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
 <link rel="import" href="./edit/gr-editor-view/gr-editor-view.html">
 <link rel="import" href="./plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
+<link rel="import" href="./plugins/gr-endpoint-param/gr-endpoint-param.html">
 <link rel="import" href="./plugins/gr-external-style/gr-external-style.html">
 <link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
 <link rel="import" href="./settings/gr-cla-view/gr-cla-view.html">
@@ -172,6 +173,11 @@
         <gr-admin-view path="[[_path]]"
             params=[[params]]></gr-admin-view>
       </template>
+      <template is="dom-if" if="[[_showPluginScreen]]" restamp="true">
+        <gr-endpoint-decorator name="[[_pluginScreenName]]">
+          <gr-endpoint-param name="token" value="[[params.screen]]"></gr-endpoint-param>
+        </gr-endpoint-decorator>
+      </template>
       <template is="dom-if" if="[[_showCLAView]]" restamp="true">
         <gr-cla-view path="[[_path]]"></gr-cla-view>
       </template>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 637cce5..bb24c74 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -36,7 +36,7 @@
 
     properties: {
       /**
-       * @type {{ query: string, view: string }}
+       * @type {{ query: string, view: string, screen: string }}
        */
       params: Object,
       keyEventTarget: {
@@ -72,6 +72,7 @@
       _showAdminView: Boolean,
       _showCLAView: Boolean,
       _showEditorView: Boolean,
+      _showPluginScreen: Boolean,
       /** @type {?} */
       _viewState: Object,
       /** @type {?} */
@@ -79,6 +80,10 @@
       _lastSearchPage: String,
       _path: String,
       _isShadowDom: Boolean,
+      _pluginScreenName: {
+        type: String,
+        computed: '_computePluginScreenName(params)',
+      },
     },
 
     listeners: {
@@ -160,6 +165,14 @@
           view === Gerrit.Nav.View.GROUP);
       this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS);
       this.set('_showEditorView', view === Gerrit.Nav.View.EDIT);
+      const isPluginScreen = view === Gerrit.Nav.View.PLUGIN_SCREEN;
+      this.set('_showPluginScreen', false);
+      // Navigation within plugin screens does not restamp gr-endpoint-decorator
+      // because _showPluginScreen value does not change. To force restamp,
+      // change _showPluginScreen value between true and false.
+      if (isPluginScreen) {
+        this.async(() => this.set('_showPluginScreen', true), 1);
+      }
       if (this.params.justRegistered) {
         this.$.registration.open();
       }
@@ -282,5 +295,9 @@
         Gerrit.Nav.navigateToStatusSearch(status);
       }
     },
+
+    _computePluginScreenName({plugin, screen}) {
+      return Gerrit._getPluginScreenName(plugin, screen);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 9c11522..504504e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -456,5 +456,33 @@
         assert.isFalse(stub.called);
       });
     });
+
+    suite('screen', () => {
+      test('screenUrl()', () => {
+        sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/base');
+        assert.equal(plugin.screenUrl(), 'http://test.com/base/x/testplugin');
+        assert.equal(
+            plugin.screenUrl('foo'), 'http://test.com/base/x/testplugin/foo');
+      });
+
+      test('deprecated works', () => {
+        const stub = sandbox.stub();
+        const hookStub = {onAttached: sandbox.stub()};
+        sandbox.stub(plugin, 'hook').returns(hookStub);
+        plugin.deprecated.screen('foo', stub);
+        assert.isTrue(plugin.hook.calledWith('testplugin-screen-foo'));
+        const fakeEl = {style: {display: ''}};
+        hookStub.onAttached.callArgWith(0, fakeEl);
+        assert.isTrue(stub.called);
+        assert.equal(fakeEl.style.display, 'none');
+      });
+
+      test('works', () => {
+        sandbox.stub(plugin, 'registerCustomComponent');
+        plugin.screen('foo', 'some-module');
+        assert.isTrue(plugin.registerCustomComponent.calledWith(
+            'testplugin-screen-foo', 'some-module'));
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index af7c155..2950c05 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -23,7 +23,7 @@
    */
   const plugins = {};
 
-  const stubbedMethods = ['_loadedGwt', 'screen', 'settingsScreen', 'panel'];
+  const stubbedMethods = ['_loadedGwt', 'settingsScreen', 'panel'];
   const GWT_PLUGIN_STUB = {};
   for (const name of stubbedMethods) {
     GWT_PLUGIN_STUB[name] = warnNotSupported.bind(null, name);
@@ -92,7 +92,11 @@
           url.href, '— Unable to determine name.');
       return;
     }
-    return pathname.split('/')[2];
+    // Pathname should normally look like this:
+    // /plugins/PLUGINNAME/static/SCRIPTNAME.html
+    // Or, for app/samples:
+    // /plugins/PLUGINNAME.html
+    return pathname.split('/')[2].split('.')[0];
   }
 
   function Plugin(opt_url) {
@@ -105,8 +109,9 @@
     }
     this.deprecated = {
       install: deprecatedAPI.install.bind(this),
-      popup: deprecatedAPI.popup.bind(this),
       onAction: deprecatedAPI.onAction.bind(this),
+      popup: deprecatedAPI.popup.bind(this),
+      screen: deprecatedAPI.screen.bind(this),
     };
 
     this._url = new URL(opt_url);
@@ -159,6 +164,13 @@
         this._name + (opt_path || '/');
   };
 
+  Plugin.prototype.screenUrl = function(opt_screenName) {
+    const origin = this._url.origin;
+    const base = Gerrit.BaseUrlBehavior.getBaseUrl();
+    const tokenPart = opt_screenName ? '/' + opt_screenName : '';
+    return `${origin}${base}/x/${this.getPluginName()}${tokenPart}`;
+  };
+
   Plugin.prototype._send = function(method, url, opt_callback, opt_payload) {
     return send(method, this.url(url), opt_callback, opt_payload);
   };
@@ -237,6 +249,15 @@
     return api.open();
   };
 
+  Plugin.prototype.screen = function(screenName, opt_moduleName) {
+    if (opt_moduleName && typeof opt_moduleName !== 'string') {
+      throw new Error('deprecated, use deprecated.screen');
+    }
+    return this.registerCustomComponent(
+        Gerrit._getPluginScreenName(this.getPluginName(), screenName),
+        opt_moduleName);
+  };
+
   const deprecatedAPI = {
     install() {
       console.log('Installing deprecated APIs is deprecated!');
@@ -277,6 +298,29 @@
       });
     },
 
+    screen(pattern, callback) {
+      console.warn('plugin.deprecated.screen is deprecated,' +
+          ' use plugin.screen instead!');
+      if (pattern instanceof RegExp) {
+        console.error('deprecated.screen() does not support RegExp. ' +
+            'Please use strings for patterns.');
+        return;
+      }
+      this.hook(Gerrit._getPluginScreenName(this.getPluginName(), pattern))
+          .onAttached(el => {
+            el.style.display = 'none';
+            callback({
+              body: el,
+              token: el.token,
+              onUnload: () => {},
+              setTitle: () => {},
+              setWindowTitle: () => {},
+              show: () => {
+                el.style.display = 'initial';
+              },
+            });
+          });
+    },
   };
 
   const Gerrit = window.Gerrit || {};
@@ -420,5 +464,9 @@
     return Gerrit._pluginsPending === 0;
   };
 
+  Gerrit._getPluginScreenName = function(pluginName, screenName) {
+    return `${pluginName}-screen-${screenName}`;
+  };
+
   window.Gerrit = Gerrit;
 })(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 6c052ae..5118d70 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -1344,7 +1344,7 @@
             // X-FYI-Content-Type header of the response.
             const type = res.headers.get('X-FYI-Content-Type');
             return this.getResponseObject(res).then(content => {
-              return {content, type};
+              return {content, type, ok: true};
             });
           });
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index bf238af..6542bb6 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -1199,7 +1199,8 @@
           .returns(Promise.resolve('new content'));
 
       return element.getFileInChangeEdit('1', 'test/path').then(res => {
-        assert.deepEqual(res, {content: 'new content', type: 'text/java'});
+        assert.deepEqual(res,
+            {content: 'new content', type: 'text/java', ok: true});
       });
     });
   });
diff --git a/polygerrit-ui/app/samples/some-screen.html b/polygerrit-ui/app/samples/some-screen.html
new file mode 100644
index 0000000..de29315
--- /dev/null
+++ b/polygerrit-ui/app/samples/some-screen.html
@@ -0,0 +1,49 @@
+<dom-module id="some-screen">
+  <script>
+    Gerrit.install(plugin => {
+      // Recommended approach for screen() API.
+      plugin.screen('main', 'some-screen-main');
+
+      const mainUrl = plugin.screenUrl('main');
+
+      // Support for deprecated screen API.
+      plugin.deprecated.screen('foo', ({token, body, show}) => {
+        body.innerHTML = `This is a plugin screen at ${token}<br/>` +
+            `<a href="${mainUrl}">Go to main plugin screen</a>`;
+        show();
+      });
+
+      // Quick and dirty way to get something on screen.
+      plugin.screen('bar').onAttached(el => {
+        el.innerHTML = `This is a plugin screen at ${el.token}<br/>` +
+            `<a href="${mainUrl}">Go to main plugin screen</a>`;
+      });
+
+      // Add a "Plugin screen" link to the change view screen.
+      plugin.hook('change-metadata-item').onAttached(el => {
+        el.innerHTML = `<a href="${mainUrl}">Plugin screen</a>`;
+      });
+    });
+  </script>
+</dom-module>
+
+<dom-module id="some-screen-main">
+  <template>
+    This is the <b>main</b> plugin screen at [[token]]
+    <ul>
+      <li><a href$="[[rootUrl]]/foo">via deprecated</a></li>
+      <li><a href$="[[rootUrl]]/bar">without component</a></li>
+    </ul>
+  </template>
+  <script>
+    Polymer({
+      is: 'some-screen-main',
+      properties: {
+        rootUrl: String,
+      },
+      attached() {
+        this.rootUrl = `${this.plugin.screenUrl()}`;
+      },
+    });
+  </script>
+</dom-module>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 68dbfd6..f903790 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -104,6 +104,7 @@
     'diff/gr-selection-action-box/gr-selection-action-box_test.html',
     'diff/gr-syntax-layer/gr-syntax-layer_test.html',
     'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html',
+    'edit/gr-default-editor/gr-default-editor_test.html',
     'edit/gr-edit-controls/gr-edit-controls_test.html',
     'edit/gr-edit-file-controls/gr-edit-file-controls_test.html',
     'edit/gr-editor-view/gr-editor-view_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 5be1c60..ece071c 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -200,7 +200,7 @@
 
 // Any path prefixes that should resolve to index.html.
 var (
-	fePaths    = []string{"/q/", "/c/", "/p/", "/dashboard/", "/admin/"}
+	fePaths    = []string{"/q/", "/c/", "/p/", "/x/", "/dashboard/", "/admin/"}
 	issueNumRE = regexp.MustCompile(`^\/\d+\/?$`)
 )