Merge "Add default values in create project dialog and group config"
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 901f15a..47e2505 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -43,6 +43,7 @@
 * `http/server/error_count`: Rate of REST API error responses.
 * `http/server/success_count`: Rate of REST API success responses.
 * `http/server/rest_api/count`: Rate of REST API calls by view.
+* `http/server/rest_api/change_id_type`: Rate of REST API calls by change ID type.
 * `http/server/rest_api/error_count`: Rate of REST API calls by view.
 * `http/server/rest_api/server_latency`: REST API call latency by view.
 * `http/server/rest_api/response_bytes`: Size of REST API response on network
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
index 8a479dd..de25ef0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.api;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
@@ -28,7 +30,6 @@
 import com.google.gwt.user.client.ui.DialogBox;
 import com.google.gwtexpui.progress.client.ProgressBar;
 import java.util.List;
-import java.util.stream.Collectors;
 
 /** Loads JavaScript plugins with a progress meter visible. */
 public class PluginLoader extends DialogBox {
@@ -39,7 +40,7 @@
     if (plugins == null || plugins.isEmpty()) {
       callback.onSuccess(VoidResult.create());
     } else {
-      plugins = plugins.stream().filter(p -> p.endsWith(".js")).collect(Collectors.toList());
+      plugins = plugins.stream().filter(p -> p.endsWith(".js")).collect(toList());
       if (plugins.isEmpty()) {
         callback.onSuccess(VoidResult.create());
       } else {
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 2b899df..dda1e6c 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -84,7 +84,7 @@
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.change.Abandon;
+import com.google.gerrit.server.change.BatchAbandon;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.FileContentUtil;
 import com.google.gerrit.server.change.RevisionResource;
@@ -237,7 +237,7 @@
   @Inject protected SystemGroupBackend systemGroupBackend;
   @Inject protected MutableNotesMigration notesMigration;
   @Inject protected ChangeNotes.Factory notesFactory;
-  @Inject protected Abandon changeAbandoner;
+  @Inject protected BatchAbandon batchAbandon;
 
   protected EventRecorder eventRecorder;
   protected GerritServer server;
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index f42b622..ab887fe 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.extensions.api.changes.RecipientType.BCC;
 import static com.google.gerrit.extensions.api.changes.RecipientType.CC;
 import static com.google.gerrit.extensions.api.changes.RecipientType.TO;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
@@ -48,7 +49,6 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.junit.TestRepository;
 import org.junit.After;
 import org.junit.Before;
@@ -121,7 +121,7 @@
               .stream()
               .map(Address::getEmail)
               .filter(e -> !recipients.get(TO).contains(e) && !recipients.get(CC).contains(e))
-              .collect(Collectors.toList()));
+              .collect(toList()));
       this.users = users;
       if (!message.headers().containsKey("X-Gerrit-MessageType")) {
         fail("a message was sent with X-Gerrit-MessageType header");
@@ -162,7 +162,7 @@
       }
       Truth.assertThat(header).isInstanceOf(AddressList.class);
       AddressList addrList = (AddressList) header;
-      return addrList.getAddressList().stream().map(Address::getEmail).collect(Collectors.toList());
+      return addrList.getAddressList().stream().map(Address::getEmail).collect(toList());
     }
 
     public FakeEmailSenderSubject to(String... emails) {
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 732c1f7..ccdb415 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -59,6 +59,7 @@
     "//java/com/google/gerrit/metrics",
     "//java/com/google/gerrit/reviewdb:server",
     "//java/com/google/gerrit/server",
+    "//java/com/google/gerrit/server/schema",
     "//java/com/google/gerrit/pgm/init",
     "//java/com/google/gerrit/server/git/receive",
     "//lib:gson",
diff --git a/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java b/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java
index 58a9238..7f932c3 100644
--- a/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java
@@ -20,12 +20,16 @@
 import com.google.inject.Inject;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
 
 public class LightweightPluginDaemonTest extends AbstractDaemonTest {
   @Inject private PluginGuiceEnvironment env;
 
   @Inject private PluginUser.Factory pluginUserFactory;
 
+  @Rule public TemporaryFolder tempDataDir = new TemporaryFolder();
+
   private TestServerPlugin plugin;
 
   @Before
@@ -40,7 +44,8 @@
             getClass().getClassLoader(),
             testPlugin.sysModule(),
             testPlugin.httpModule(),
-            testPlugin.sshModule());
+            testPlugin.sshModule(),
+            tempDataDir.getRoot().toPath());
 
     plugin.start(env);
     env.onStartPlugin(plugin);
diff --git a/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java b/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
index 3a43e24..6774ec80 100644
--- a/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
+++ b/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
@@ -29,7 +29,7 @@
  * override the response http status code.
  */
 public class HttpServletResponseRecorder extends HttpServletResponseWrapper {
-  private static final Logger log = LoggerFactory.getLogger(HttpServletResponseWrapper.class);
+  private static final Logger log = LoggerFactory.getLogger(HttpServletResponseRecorder.class);
   private static final String LOCATION_HEADER = "Location";
 
   private int status;
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index 7fc3074..936044d 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/schema",
         "//java/com/google/gerrit/sshd",
         "//lib:guava",
         "//lib:gwtorm",
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index 4dfaf1c..3dce217 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/schema",
         "//java/com/google/gerrit/sshd",
         "//java/com/google/gwtexpui/linker:server",
         "//java/com/google/gwtexpui/server",
diff --git a/java/com/google/gerrit/pgm/init/BUILD b/java/com/google/gerrit/pgm/init/BUILD
index ef3a063..4b53b67 100644
--- a/java/com/google/gerrit/pgm/init/BUILD
+++ b/java/com/google/gerrit/pgm/init/BUILD
@@ -17,6 +17,7 @@
         "//java/com/google/gerrit/pgm/util",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/schema",
         "//lib:guava",
         "//lib:gwtjsonrpc",
         "//lib:gwtorm",
diff --git a/java/com/google/gerrit/pgm/util/BUILD b/java/com/google/gerrit/pgm/util/BUILD
index 42cdf6f..d94211c 100644
--- a/java/com/google/gerrit/pgm/util/BUILD
+++ b/java/com/google/gerrit/pgm/util/BUILD
@@ -13,6 +13,7 @@
         "//java/com/google/gerrit/server:module",
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/git/receive",
+        "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/util/cli",
         "//lib:args4j",
         "//lib:guava",
diff --git a/java/com/google/gerrit/server/ChangeFinder.java b/java/com/google/gerrit/server/ChangeFinder.java
index 0b0a855..4b7cbac 100644
--- a/java/com/google/gerrit/server/ChangeFinder.java
+++ b/java/com/google/gerrit/server/ChangeFinder.java
@@ -20,6 +20,10 @@
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -56,11 +60,20 @@
     };
   }
 
+  private enum ChangeIdType {
+    TRIPLET,
+    NUMERIC_ID,
+    CHANGE_ID,
+    PROJECT_NUMERIC_ID,
+    COMMIT_HASH
+  }
+
   private final IndexConfig indexConfig;
   private final Cache<Change.Id, String> changeIdProjectCache;
   private final Provider<InternalChangeQuery> queryProvider;
   private final Provider<ReviewDb> reviewDb;
   private final ChangeNotes.Factory changeNotesFactory;
+  private final Counter1<ChangeIdType> changeIdCounter;
 
   @Inject
   ChangeFinder(
@@ -68,12 +81,20 @@
       @Named(CACHE_NAME) Cache<Change.Id, String> changeIdProjectCache,
       Provider<InternalChangeQuery> queryProvider,
       Provider<ReviewDb> reviewDb,
-      ChangeNotes.Factory changeNotesFactory) {
+      ChangeNotes.Factory changeNotesFactory,
+      MetricMaker metricMaker) {
     this.indexConfig = indexConfig;
     this.changeIdProjectCache = changeIdProjectCache;
     this.queryProvider = queryProvider;
     this.reviewDb = reviewDb;
     this.changeNotesFactory = changeNotesFactory;
+    this.changeIdCounter =
+        metricMaker.newCounter(
+            "http/server/rest_api/change_id_type",
+            new Description("Total number of API calls per identifier type.")
+                .setRate()
+                .setUnit("requests"),
+            Field.ofEnum(ChangeIdType.class, "change_id_type"));
   }
 
   /**
@@ -94,6 +115,7 @@
       // Try project~numericChangeId
       Integer n = Ints.tryParse(id.substring(z + 1));
       if (n != null) {
+        changeIdCounter.increment(ChangeIdType.PROJECT_NUMERIC_ID);
         return fromProjectNumber(id.substring(0, z), n.intValue());
       }
     }
@@ -102,6 +124,7 @@
       // Try numeric changeId
       Integer n = Ints.tryParse(id);
       if (n != null) {
+        changeIdCounter.increment(ChangeIdType.NUMERIC_ID);
         return find(new Change.Id(n));
       }
     }
@@ -112,6 +135,7 @@
 
     // Try commit hash
     if (id.matches("^([0-9a-fA-F]{" + RevId.ABBREV_LEN + "," + RevId.LEN + "})$")) {
+      changeIdCounter.increment(ChangeIdType.COMMIT_HASH);
       return asChangeNotes(query.byCommit(id));
     }
 
@@ -120,12 +144,17 @@
       Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id, y, z);
       if (triplet.isPresent()) {
         ChangeTriplet t = triplet.get();
+        changeIdCounter.increment(ChangeIdType.TRIPLET);
         return asChangeNotes(query.byBranchKey(t.branch(), t.id()));
       }
     }
 
     // Try isolated Ihash... format ("Change-Id: Ihash").
-    return asChangeNotes(query.byKeyPrefix(id));
+    List<ChangeNotes> notes = asChangeNotes(query.byKeyPrefix(id));
+    if (!notes.isEmpty()) {
+      changeIdCounter.increment(ChangeIdType.CHANGE_ID);
+    }
+    return notes;
   }
 
   private List<ChangeNotes> fromProjectNumber(String project, int changeNumber)
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 701384b..f368c17 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -64,7 +64,6 @@
 import com.google.gerrit.server.change.GetAssignee;
 import com.google.gerrit.server.change.GetHashtags;
 import com.google.gerrit.server.change.GetPastAssignees;
-import com.google.gerrit.server.change.GetPureRevert;
 import com.google.gerrit.server.change.GetTopic;
 import com.google.gerrit.server.change.Ignore;
 import com.google.gerrit.server.change.Index;
@@ -77,6 +76,7 @@
 import com.google.gerrit.server.change.PostHashtags;
 import com.google.gerrit.server.change.PostPrivate;
 import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.change.PureRevert;
 import com.google.gerrit.server.change.PutAssignee;
 import com.google.gerrit.server.change.PutMessage;
 import com.google.gerrit.server.change.PutTopic;
@@ -147,7 +147,7 @@
   private final SetWorkInProgress setWip;
   private final SetReadyForReview setReady;
   private final PutMessage putMessage;
-  private final GetPureRevert getPureRevert;
+  private final PureRevert pureRevert;
   private final StarredChangesUtil stars;
 
   @Inject
@@ -192,7 +192,7 @@
       SetWorkInProgress setWip,
       SetReadyForReview setReady,
       PutMessage putMessage,
-      GetPureRevert getPureRevert,
+      PureRevert pureRevert,
       StarredChangesUtil stars,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
@@ -235,7 +235,7 @@
     this.setWip = setWip;
     this.setReady = setReady;
     this.putMessage = putMessage;
-    this.getPureRevert = getPureRevert;
+    this.pureRevert = pureRevert;
     this.stars = stars;
     this.change = change;
   }
@@ -702,7 +702,7 @@
   @Override
   public PureRevertInfo pureRevert(@Nullable String claimedOriginal) throws RestApiException {
     try {
-      return getPureRevert.setClaimedOriginal(claimedOriginal).apply(change);
+      return pureRevert.get(change.getNotes(), claimedOriginal);
     } catch (Exception e) {
       throw asRestApiException("Cannot compute pure revert", e);
     }
diff --git a/java/com/google/gerrit/server/change/Abandon.java b/java/com/google/gerrit/server/change/Abandon.java
index c7addff..f07efea 100644
--- a/java/com/google/gerrit/server/change/Abandon.java
+++ b/java/com/google/gerrit/server/change/Abandon.java
@@ -23,18 +23,15 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -44,7 +41,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Collection;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
@@ -132,69 +128,6 @@
     return op.getChange();
   }
 
-  /**
-   * If an extension has more than one changes to abandon that belong to the same project, they
-   * should use the batch instead of abandoning one by one.
-   *
-   * <p>It's the caller's responsibility to ensure that all jobs inside the same batch have the
-   * matching project from its ChangeData. Violations will result in a ResourceConflictException.
-   */
-  public void batchAbandon(
-      BatchUpdate.Factory updateFactory,
-      Project.NameKey project,
-      CurrentUser user,
-      Collection<ChangeData> changes,
-      String msgTxt,
-      NotifyHandling notifyHandling,
-      ListMultimap<RecipientType, Account.Id> accountsToNotify)
-      throws RestApiException, UpdateException {
-    if (changes.isEmpty()) {
-      return;
-    }
-    Account account = user.isIdentifiedUser() ? user.asIdentifiedUser().getAccount() : null;
-    try (BatchUpdate u = updateFactory.create(dbProvider.get(), project, user, TimeUtil.nowTs())) {
-      for (ChangeData change : changes) {
-        if (!project.equals(change.project())) {
-          throw new ResourceConflictException(
-              String.format(
-                  "Project name \"%s\" doesn't match \"%s\"",
-                  change.project().get(), project.get()));
-        }
-        u.addOp(
-            change.getId(),
-            abandonOpFactory.create(account, msgTxt, notifyHandling, accountsToNotify));
-      }
-      u.execute();
-    }
-  }
-
-  public void batchAbandon(
-      BatchUpdate.Factory updateFactory,
-      Project.NameKey project,
-      CurrentUser user,
-      Collection<ChangeData> changes,
-      String msgTxt)
-      throws RestApiException, UpdateException {
-    batchAbandon(
-        updateFactory,
-        project,
-        user,
-        changes,
-        msgTxt,
-        NotifyHandling.ALL,
-        ImmutableListMultimap.of());
-  }
-
-  public void batchAbandon(
-      BatchUpdate.Factory updateFactory,
-      Project.NameKey project,
-      CurrentUser user,
-      Collection<ChangeData> changes)
-      throws RestApiException, UpdateException {
-    batchAbandon(
-        updateFactory, project, user, changes, "", NotifyHandling.ALL, ImmutableListMultimap.of());
-  }
-
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
     Change change = rsrc.getChange();
diff --git a/java/com/google/gerrit/server/change/AbandonUtil.java b/java/com/google/gerrit/server/change/AbandonUtil.java
index 3239813..9866ea9 100644
--- a/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -42,7 +42,7 @@
   private final ChangeCleanupConfig cfg;
   private final Provider<ChangeQueryProcessor> queryProvider;
   private final ChangeQueryBuilder queryBuilder;
-  private final Abandon abandon;
+  private final BatchAbandon batchAbandon;
   private final InternalUser internalUser;
 
   @Inject
@@ -51,11 +51,11 @@
       InternalUser.Factory internalUserFactory,
       Provider<ChangeQueryProcessor> queryProvider,
       ChangeQueryBuilder queryBuilder,
-      Abandon abandon) {
+      BatchAbandon batchAbandon) {
     this.cfg = cfg;
     this.queryProvider = queryProvider;
     this.queryBuilder = queryBuilder;
-    this.abandon = abandon;
+    this.batchAbandon = batchAbandon;
     internalUser = internalUserFactory.create();
   }
 
@@ -85,7 +85,7 @@
       for (Project.NameKey project : abandons.keySet()) {
         Collection<ChangeData> changes = getValidChanges(abandons.get(project), query);
         try {
-          abandon.batchAbandon(updateFactory, project, internalUser, changes, message);
+          batchAbandon.batchAbandon(updateFactory, project, internalUser, changes, message);
           count += changes.size();
         } catch (Throwable e) {
           StringBuilder msg = new StringBuilder("Failed to auto-abandon inactive change(s):");
diff --git a/java/com/google/gerrit/server/change/BatchAbandon.java b/java/com/google/gerrit/server/change/BatchAbandon.java
new file mode 100644
index 0000000..059f110
--- /dev/null
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2012 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.change;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.Collection;
+
+@Singleton
+public class BatchAbandon {
+  private final Provider<ReviewDb> dbProvider;
+  private final AbandonOp.Factory abandonOpFactory;
+
+  @Inject
+  BatchAbandon(Provider<ReviewDb> dbProvider, AbandonOp.Factory abandonOpFactory) {
+    this.dbProvider = dbProvider;
+    this.abandonOpFactory = abandonOpFactory;
+  }
+
+  /**
+   * If an extension has more than one changes to abandon that belong to the same project, they
+   * should use the batch instead of abandoning one by one.
+   *
+   * <p>It's the caller's responsibility to ensure that all jobs inside the same batch have the
+   * matching project from its ChangeData. Violations will result in a ResourceConflictException.
+   */
+  public void batchAbandon(
+      BatchUpdate.Factory updateFactory,
+      Project.NameKey project,
+      CurrentUser user,
+      Collection<ChangeData> changes,
+      String msgTxt,
+      NotifyHandling notifyHandling,
+      ListMultimap<RecipientType, Account.Id> accountsToNotify)
+      throws RestApiException, UpdateException {
+    if (changes.isEmpty()) {
+      return;
+    }
+    Account account = user.isIdentifiedUser() ? user.asIdentifiedUser().getAccount() : null;
+    try (BatchUpdate u = updateFactory.create(dbProvider.get(), project, user, TimeUtil.nowTs())) {
+      for (ChangeData change : changes) {
+        if (!project.equals(change.project())) {
+          throw new ResourceConflictException(
+              String.format(
+                  "Project name \"%s\" doesn't match \"%s\"",
+                  change.project().get(), project.get()));
+        }
+        u.addOp(
+            change.getId(),
+            abandonOpFactory.create(account, msgTxt, notifyHandling, accountsToNotify));
+      }
+      u.execute();
+    }
+  }
+
+  public void batchAbandon(
+      BatchUpdate.Factory updateFactory,
+      Project.NameKey project,
+      CurrentUser user,
+      Collection<ChangeData> changes,
+      String msgTxt)
+      throws RestApiException, UpdateException {
+    batchAbandon(
+        updateFactory,
+        project,
+        user,
+        changes,
+        msgTxt,
+        NotifyHandling.ALL,
+        ImmutableListMultimap.of());
+  }
+
+  public void batchAbandon(
+      BatchUpdate.Factory updateFactory,
+      Project.NameKey project,
+      CurrentUser user,
+      Collection<ChangeData> changes)
+      throws RestApiException, UpdateException {
+    batchAbandon(
+        updateFactory, project, user, changes, "", NotifyHandling.ALL, ImmutableListMultimap.of());
+  }
+}
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index 341ad4a..c8b5bea 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -33,7 +33,7 @@
 import org.slf4j.LoggerFactory;
 
 public class DeleteReviewerByEmailOp implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class);
+  private static final Logger log = LoggerFactory.getLogger(DeleteReviewerByEmailOp.class);
 
   public interface Factory {
     DeleteReviewerByEmailOp create(Address reviewer, DeleteReviewerInput input);
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index ad1cf60..a398195 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -60,7 +60,7 @@
 import org.slf4j.LoggerFactory;
 
 public class DeleteReviewerOp implements BatchUpdateOp {
-  private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class);
+  private static final Logger log = LoggerFactory.getLogger(DeleteReviewerOp.class);
 
   public interface Factory {
     DeleteReviewerOp create(Account reviewerAccount, DeleteReviewerInput input);
diff --git a/java/com/google/gerrit/server/change/GetPureRevert.java b/java/com/google/gerrit/server/change/GetPureRevert.java
index 27c5d49..815ce07 100644
--- a/java/com/google/gerrit/server/change/GetPureRevert.java
+++ b/java/com/google/gerrit/server/change/GetPureRevert.java
@@ -20,38 +20,14 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffFormatter;
-import org.eclipse.jgit.errors.InvalidObjectIdException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.ThreeWayMerger;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.kohsuke.args4j.Option;
 
 public class GetPureRevert implements RestReadView<ChangeResource> {
-  private final MergeUtil.Factory mergeUtilFactory;
-  private final GitRepositoryManager repoManager;
-  private final ProjectCache projectCache;
-  private final ChangeNotes.Factory notesFactory;
-  private final Provider<ReviewDb> dbProvider;
-  private final PatchSetUtil psUtil;
+
+  private final PureRevert pureRevert;
 
   @Option(
     name = "--claimed-original",
@@ -62,93 +38,14 @@
   private String claimedOriginal;
 
   @Inject
-  GetPureRevert(
-      MergeUtil.Factory mergeUtilFactory,
-      GitRepositoryManager repoManager,
-      ProjectCache projectCache,
-      ChangeNotes.Factory notesFactory,
-      Provider<ReviewDb> dbProvider,
-      PatchSetUtil psUtil) {
-    this.mergeUtilFactory = mergeUtilFactory;
-    this.repoManager = repoManager;
-    this.projectCache = projectCache;
-    this.notesFactory = notesFactory;
-    this.dbProvider = dbProvider;
-    this.psUtil = psUtil;
+  GetPureRevert(PureRevert pureRevert) {
+    this.pureRevert = pureRevert;
   }
 
   @Override
   public PureRevertInfo apply(ChangeResource rsrc)
       throws ResourceConflictException, IOException, BadRequestException, OrmException,
           AuthException {
-    PatchSet currentPatchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
-    if (currentPatchSet == null) {
-      throw new ResourceConflictException("current revision is missing");
-    }
-    return getPureRevert(rsrc.getNotes());
-  }
-
-  public PureRevertInfo getPureRevert(ChangeNotes notes)
-      throws OrmException, IOException, BadRequestException, ResourceConflictException {
-    PatchSet currentPatchSet = psUtil.current(dbProvider.get(), notes);
-    if (currentPatchSet == null) {
-      throw new ResourceConflictException("current revision is missing");
-    }
-
-    if (claimedOriginal == null) {
-      if (notes.getChange().getRevertOf() == null) {
-        throw new BadRequestException("no ID was provided and change isn't a revert");
-      }
-      PatchSet ps =
-          psUtil.current(
-              dbProvider.get(),
-              notesFactory.createChecked(
-                  dbProvider.get(), notes.getProjectName(), notes.getChange().getRevertOf()));
-      claimedOriginal = ps.getRevision().get();
-    }
-
-    try (Repository repo = repoManager.openRepository(notes.getProjectName());
-        ObjectInserter oi = repo.newObjectInserter();
-        RevWalk rw = new RevWalk(repo)) {
-      RevCommit claimedOriginalCommit;
-      try {
-        claimedOriginalCommit = rw.parseCommit(ObjectId.fromString(claimedOriginal));
-      } catch (InvalidObjectIdException | MissingObjectException e) {
-        throw new BadRequestException("invalid object ID");
-      }
-      if (claimedOriginalCommit.getParentCount() == 0) {
-        throw new BadRequestException("can't check against initial commit");
-      }
-      RevCommit claimedRevertCommit =
-          rw.parseCommit(ObjectId.fromString(currentPatchSet.getRevision().get()));
-      if (claimedRevertCommit.getParentCount() == 0) {
-        throw new BadRequestException("claimed revert has no parents");
-      }
-      // Rebase claimed revert onto claimed original
-      ThreeWayMerger merger =
-          mergeUtilFactory
-              .create(projectCache.checkedGet(notes.getProjectName()))
-              .newThreeWayMerger(oi, repo.getConfig());
-      merger.setBase(claimedRevertCommit.getParent(0));
-      merger.merge(claimedRevertCommit, claimedOriginalCommit);
-      if (merger.getResultTreeId() == null) {
-        // Merge conflict during rebase
-        return new PureRevertInfo(false);
-      }
-
-      // Any differences between claimed original's parent and the rebase result indicate that the
-      // claimedRevert is not a pure revert but made content changes
-      try (DiffFormatter df = new DiffFormatter(new ByteArrayOutputStream())) {
-        df.setRepository(repo);
-        List<DiffEntry> entries =
-            df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
-        return new PureRevertInfo(entries.isEmpty());
-      }
-    }
-  }
-
-  public GetPureRevert setClaimedOriginal(String claimedOriginal) {
-    this.claimedOriginal = claimedOriginal;
-    return this;
+    return pureRevert.get(rsrc.getNotes(), claimedOriginal);
   }
 }
diff --git a/java/com/google/gerrit/server/change/PureRevert.java b/java/com/google/gerrit/server/change/PureRevert.java
new file mode 100644
index 0000000..850f33a
--- /dev/null
+++ b/java/com/google/gerrit/server/change/PureRevert.java
@@ -0,0 +1,127 @@
+// 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.change;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class PureRevert {
+  private final MergeUtil.Factory mergeUtilFactory;
+  private final GitRepositoryManager repoManager;
+  private final ProjectCache projectCache;
+  private final ChangeNotes.Factory notesFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final PatchSetUtil psUtil;
+
+  @Inject
+  PureRevert(
+      MergeUtil.Factory mergeUtilFactory,
+      GitRepositoryManager repoManager,
+      ProjectCache projectCache,
+      ChangeNotes.Factory notesFactory,
+      Provider<ReviewDb> dbProvider,
+      PatchSetUtil psUtil) {
+    this.mergeUtilFactory = mergeUtilFactory;
+    this.repoManager = repoManager;
+    this.projectCache = projectCache;
+    this.notesFactory = notesFactory;
+    this.dbProvider = dbProvider;
+    this.psUtil = psUtil;
+  }
+
+  public PureRevertInfo get(ChangeNotes notes, @Nullable String claimedOriginal)
+      throws OrmException, IOException, BadRequestException, ResourceConflictException {
+    PatchSet currentPatchSet = psUtil.current(dbProvider.get(), notes);
+    if (currentPatchSet == null) {
+      throw new ResourceConflictException("current revision is missing");
+    }
+
+    if (claimedOriginal == null) {
+      if (notes.getChange().getRevertOf() == null) {
+        throw new BadRequestException("no ID was provided and change isn't a revert");
+      }
+      PatchSet ps =
+          psUtil.current(
+              dbProvider.get(),
+              notesFactory.createChecked(
+                  dbProvider.get(), notes.getProjectName(), notes.getChange().getRevertOf()));
+      claimedOriginal = ps.getRevision().get();
+    }
+
+    try (Repository repo = repoManager.openRepository(notes.getProjectName());
+        ObjectInserter oi = repo.newObjectInserter();
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit claimedOriginalCommit;
+      try {
+        claimedOriginalCommit = rw.parseCommit(ObjectId.fromString(claimedOriginal));
+      } catch (InvalidObjectIdException | MissingObjectException e) {
+        throw new BadRequestException("invalid object ID");
+      }
+      if (claimedOriginalCommit.getParentCount() == 0) {
+        throw new BadRequestException("can't check against initial commit");
+      }
+      RevCommit claimedRevertCommit =
+          rw.parseCommit(ObjectId.fromString(currentPatchSet.getRevision().get()));
+      if (claimedRevertCommit.getParentCount() == 0) {
+        throw new BadRequestException("claimed revert has no parents");
+      }
+      // Rebase claimed revert onto claimed original
+      ThreeWayMerger merger =
+          mergeUtilFactory
+              .create(projectCache.checkedGet(notes.getProjectName()))
+              .newThreeWayMerger(oi, repo.getConfig());
+      merger.setBase(claimedRevertCommit.getParent(0));
+      merger.merge(claimedRevertCommit, claimedOriginalCommit);
+      if (merger.getResultTreeId() == null) {
+        // Merge conflict during rebase
+        return new PureRevertInfo(false);
+      }
+
+      // Any differences between claimed original's parent and the rebase result indicate that the
+      // claimedRevert is not a pure revert but made content changes
+      try (DiffFormatter df = new DiffFormatter(new ByteArrayOutputStream())) {
+        df.setRepository(repo);
+        List<DiffEntry> entries =
+            df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
+        return new PureRevertInfo(entries.isEmpty());
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java b/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
index 3fc786b..1e5088be 100644
--- a/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
+++ b/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.fixes;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.groupingBy;
 
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -33,7 +34,6 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 
@@ -73,9 +73,7 @@
     checkNotNull(fixReplacements, "Fix replacements must not be null");
 
     Map<String, List<FixReplacement>> fixReplacementsPerFilePath =
-        fixReplacements
-            .stream()
-            .collect(Collectors.groupingBy(fixReplacement -> fixReplacement.path));
+        fixReplacements.stream().collect(groupingBy(fixReplacement -> fixReplacement.path));
 
     List<TreeModification> treeModifications = new ArrayList<>();
     for (Map.Entry<String, List<FixReplacement>> entry : fixReplacementsPerFilePath.entrySet()) {
diff --git a/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java b/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
index c76a59a..3a954fb 100644
--- a/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
+++ b/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git.strategy;
 
+import static java.util.stream.Collectors.toSet;
+
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Streams;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -31,7 +33,6 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -68,7 +69,7 @@
             repo.getRefDatabase().getRefs(Constants.R_TAGS).values().stream())
         .map(Ref::getObjectId)
         .filter(o -> o != null)
-        .collect(Collectors.toSet());
+        .collect(toSet());
   }
 
   public static Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw) throws IOException {
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index c528f8e..e57f5ce 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.stream.Collectors.joining;
 
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
@@ -38,7 +39,6 @@
 import java.util.StringJoiner;
 import java.util.function.Function;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -307,7 +307,7 @@
   private <E> void saveToFile(
       String filePath, ImmutableSet<E> elements, Function<E, String> toStringFunction)
       throws IOException {
-    String fileContent = elements.stream().map(toStringFunction).collect(Collectors.joining("\n"));
+    String fileContent = elements.stream().map(toStringFunction).collect(joining("\n"));
     saveUTF8(filePath, fileContent);
   }
 
diff --git a/java/com/google/gerrit/server/mail/ListMailFilter.java b/java/com/google/gerrit/server/mail/ListMailFilter.java
index a88a0e4..21347cb 100644
--- a/java/com/google/gerrit/server/mail/ListMailFilter.java
+++ b/java/com/google/gerrit/server/mail/ListMailFilter.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.server.mail;
 
+import static java.util.stream.Collectors.joining;
+
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.mail.receive.MailMessage;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Arrays;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -42,7 +43,7 @@
   ListMailFilter(@GerritServerConfig Config cfg) {
     this.mode = cfg.getEnum("receiveemail", "filter", "mode", ListFilterMode.OFF);
     String[] addresses = cfg.getStringList("receiveemail", "filter", "patterns");
-    String concat = Arrays.asList(addresses).stream().collect(Collectors.joining("|"));
+    String concat = Arrays.asList(addresses).stream().collect(joining("|"));
     this.mailPattern = Pattern.compile(concat);
   }
 
diff --git a/java/com/google/gerrit/server/mail/receive/MetadataParser.java b/java/com/google/gerrit/server/mail/receive/MetadataParser.java
index 7085051..88c54f9 100644
--- a/java/com/google/gerrit/server/mail/receive/MetadataParser.java
+++ b/java/com/google/gerrit/server/mail/receive/MetadataParser.java
@@ -29,7 +29,7 @@
 
 /** Parse metadata from inbound email */
 public class MetadataParser {
-  private static final Logger log = LoggerFactory.getLogger(MailProcessor.class);
+  private static final Logger log = LoggerFactory.getLogger(MetadataParser.class);
 
   public static MailMetadata parse(MailMessage m) {
     MailMetadata metadata = new MailMetadata();
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 1c66e89..abdb517 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -126,7 +126,7 @@
     this.accountCache = accountCache;
     this.serverIdent = serverIdent;
     this.serverId = serverId;
-    this.writeJson = config.getBoolean("notedb", "writeJson", false);
+    this.writeJson = config.getBoolean("notedb", "writeJson", true);
   }
 
   @VisibleForTesting
diff --git a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
index 3264be2..db7b86a 100644
--- a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED;
 import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -31,7 +32,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
@@ -139,7 +139,7 @@
         .values()
         .stream()
         .flatMap(n -> n.getComments().stream())
-        .collect(Collectors.toMap(c -> c.key.uuid, c -> c));
+        .collect(toMap(c -> c.key.uuid, c -> c));
   }
 
   /**
diff --git a/java/com/google/gerrit/server/patch/IntraLineDiff.java b/java/com/google/gerrit/server/patch/IntraLineDiff.java
index ee8b88b..a182335 100644
--- a/java/com/google/gerrit/server/patch/IntraLineDiff.java
+++ b/java/com/google/gerrit/server/patch/IntraLineDiff.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeEnum;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.client.CodedEnum;
@@ -30,7 +31,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.ReplaceEdit;
 
@@ -109,7 +109,7 @@
         for (int j = 0; j < innerCount; j++) {
           inner[j] = readEdit(in);
         }
-        editArray[i] = new ReplaceEdit(editArray[i], toList(inner));
+        editArray[i] = new ReplaceEdit(editArray[i], asList(inner));
       }
     }
     edits = ImmutableList.copyOf(editArray);
@@ -128,7 +128,7 @@
 
   private static ReplaceEdit copy(ReplaceEdit edit) {
     List<Edit> internalEdits =
-        edit.getInternalEdits().stream().map(IntraLineDiff::copy).collect(Collectors.toList());
+        edit.getInternalEdits().stream().map(IntraLineDiff::copy).collect(toList());
     return new ReplaceEdit(
         edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB(), internalEdits);
   }
@@ -148,7 +148,7 @@
     return new Edit(beginA, endA, beginB, endB);
   }
 
-  private static List<Edit> toList(Edit[] l) {
+  private static List<Edit> asList(Edit[] l) {
     return Collections.unmodifiableList(Arrays.asList(l));
   }
 }
diff --git a/java/com/google/gerrit/server/plugins/ListPlugins.java b/java/com/google/gerrit/server/plugins/ListPlugins.java
index 9a42bb9..989e839 100644
--- a/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.plugins;
 
 import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toMap;
 
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -30,7 +31,6 @@
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.kohsuke.args4j.Option;
 
@@ -137,7 +137,7 @@
     if (limit > 0) {
       s = s.limit(limit);
     }
-    return new TreeMap<>(s.collect(Collectors.toMap(p -> p.getName(), p -> toPluginInfo(p))));
+    return new TreeMap<>(s.collect(toMap(p -> p.getName(), p -> toPluginInfo(p))));
   }
 
   private void checkMatchOptions(boolean cond) throws BadRequestException {
diff --git a/java/com/google/gerrit/server/plugins/TestServerPlugin.java b/java/com/google/gerrit/server/plugins/TestServerPlugin.java
index 6897b9a..dbdc576 100644
--- a/java/com/google/gerrit/server/plugins/TestServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/TestServerPlugin.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.plugins;
 
 import com.google.gerrit.server.PluginUser;
+import java.nio.file.Path;
 
 public class TestServerPlugin extends ServerPlugin {
   private final ClassLoader classLoader;
@@ -29,9 +30,10 @@
       ClassLoader classloader,
       String sysName,
       String httpName,
-      String sshName)
+      String sshName,
+      Path dataDir)
       throws InvalidPluginException {
-    super(name, pluginCanonicalWebUrl, user, null, null, null, null, classloader);
+    super(name, pluginCanonicalWebUrl, user, null, null, null, dataDir, classloader);
     this.classLoader = classloader;
     this.sysName = sysName;
     this.httpName = httpName;
diff --git a/java/com/google/gerrit/server/project/ProjectJson.java b/java/com/google/gerrit/server/project/ProjectJson.java
index 6f15c7d..f2a93d3 100644
--- a/java/com/google/gerrit/server/project/ProjectJson.java
+++ b/java/com/google/gerrit/server/project/ProjectJson.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import static java.util.stream.Collectors.toMap;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
@@ -28,7 +30,6 @@
 import com.google.inject.Singleton;
 import java.util.HashMap;
 import java.util.List;
-import java.util.stream.Collectors;
 
 @Singleton
 public class ProjectJson {
@@ -48,9 +49,7 @@
     for (LabelType t : projectState.getLabelTypes().getLabelTypes()) {
       LabelTypeInfo labelInfo = new LabelTypeInfo();
       labelInfo.values =
-          t.getValues()
-              .stream()
-              .collect(Collectors.toMap(LabelValue::formatValue, LabelValue::getText));
+          t.getValues().stream().collect(toMap(LabelValue::formatValue, LabelValue::getText));
       labelInfo.defaultValue = t.getDefaultValue();
       info.labels.put(t.getName(), labelInfo);
     }
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index f07ae88..e73db1a 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -59,8 +59,8 @@
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.StarredChangesUtil.StarRef;
-import com.google.gerrit.server.change.GetPureRevert;
 import com.google.gerrit.server.change.MergeabilityCache;
+import com.google.gerrit.server.change.PureRevert;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -346,7 +346,7 @@
   private final PatchSetUtil psUtil;
   private final ProjectCache projectCache;
   private final TrackingFooters trackingFooters;
-  private final GetPureRevert pureRevert;
+  private final PureRevert pureRevert;
   private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
 
   // Required assisted injected fields.
@@ -415,7 +415,7 @@
       PatchSetUtil psUtil,
       ProjectCache projectCache,
       TrackingFooters trackingFooters,
-      GetPureRevert pureRevert,
+      PureRevert pureRevert,
       SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
       @Assisted ReviewDb db,
       @Assisted Project.NameKey project,
@@ -1178,7 +1178,7 @@
       return null;
     }
     try {
-      return pureRevert.getPureRevert(notes()).isPureRevert;
+      return pureRevert.get(notes(), null).isPureRevert;
     } catch (IOException | BadRequestException | ResourceConflictException e) {
       throw new OrmException("could not compute pure revert", e);
     }
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index bcfd53c..ebb3e65 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -89,7 +89,6 @@
 import java.util.Set;
 import java.util.function.Function;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
@@ -1244,7 +1243,7 @@
     }
 
     List<Predicate<ChangeData>> predicates =
-        parts.stream().map(fullPredicateFunc).collect(Collectors.toList());
+        parts.stream().map(fullPredicateFunc).collect(toList());
     return Predicate.and(predicates);
   }
 
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
new file mode 100644
index 0000000..2292234
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -0,0 +1,27 @@
+java_library(
+    name = "schema",
+    srcs = glob(
+        ["**/*.java"],
+    ),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/metrics",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/server",
+        "//java/org/eclipse/jgit:server",
+        "//lib:guava",
+        "//lib:gwtorm",
+        "//lib/auto:auto-value",
+        "//lib/commons:dbcp",
+        "//lib/guice",
+        "//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/log:api",
+        "//lib/log:jsonevent-layout",
+        "//lib/log:log4j",
+    ],
+)
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index 0c71dae..a33ce86 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/schema",
         "//java/com/google/gerrit/util/cli",
         "//java/org/eclipse/jgit:server",
         "//lib:args4j",
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 8e13b61..2715c75 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/schema",
         "//lib:gwtorm",
         "//lib:h2",
         "//lib:truth",
diff --git a/java/com/google/gerrit/testing/SchemaUpgradeTestEnvironment.java b/java/com/google/gerrit/testing/SchemaUpgradeTestEnvironment.java
index 62487bf..23f66bf 100644
--- a/java/com/google/gerrit/testing/SchemaUpgradeTestEnvironment.java
+++ b/java/com/google/gerrit/testing/SchemaUpgradeTestEnvironment.java
@@ -30,11 +30,14 @@
 import com.google.inject.Injector;
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
+import org.eclipse.jgit.lib.Config;
+import org.junit.rules.MethodRule;
+import org.junit.runners.model.FrameworkMethod;
 import org.junit.runners.model.Statement;
 
-public final class SchemaUpgradeTestEnvironment implements TestRule {
+public final class SchemaUpgradeTestEnvironment implements MethodRule {
+  private final Provider<Config> configProvider;
+
   @Inject private AccountManager accountManager;
   @Inject private IdentifiedUser.GenericFactory userFactory;
   @Inject private SchemaFactory<ReviewDb> schemaFactory;
@@ -44,17 +47,33 @@
   @Inject private InMemoryDatabase inMemoryDatabase;
 
   private ReviewDb db;
-  private Injector injector;
   private LifecycleManager lifecycle;
 
+  /** Create a test environment using an empty base config. */
+  public SchemaUpgradeTestEnvironment() {
+    this(Config::new);
+  }
+
+  /**
+   * Create a test environment using the specified base config.
+   *
+   * <p>The config is passed as a provider so it can be lazily initialized after this rule is
+   * instantiated, for example using {@link ConfigSuite}.
+   *
+   * @param configProvider possibly-lazy provider for the base config.
+   */
+  public SchemaUpgradeTestEnvironment(Provider<Config> configProvider) {
+    this.configProvider = configProvider;
+  }
+
   @Override
-  public Statement apply(Statement statement, Description description) {
+  public Statement apply(Statement base, FrameworkMethod method, Object target) {
     return new Statement() {
       @Override
       public void evaluate() throws Throwable {
         try {
-          setUp();
-          statement.evaluate();
+          setUp(target);
+          base.evaluate();
         } finally {
           tearDown();
         }
@@ -62,14 +81,6 @@
     };
   }
 
-  public ReviewDb getDb() {
-    return db;
-  }
-
-  public Injector getInjector() {
-    return injector;
-  }
-
   public void setApiUser(Account.Id id) {
     IdentifiedUser user = userFactory.create(id);
     requestContext.setContext(
@@ -86,8 +97,12 @@
         });
   }
 
-  private void setUp() throws Exception {
-    injector = Guice.createInjector(new InMemoryModule());
+  private void setUp(Object target) throws Exception {
+    Config cfg = configProvider.get();
+    InMemoryModule.setDefaults(cfg);
+
+    Injector injector =
+        Guice.createInjector(new InMemoryModule(cfg, NoteDbMode.newNotesMigrationFromEnv()));
     injector.injectMembers(this);
     lifecycle = new LifecycleManager();
     lifecycle.add(injector);
@@ -98,6 +113,9 @@
     }
     db = schemaFactory.open();
     setApiUser(accountManager.authenticate(AuthRequest.forUser("user")).getAccountId());
+
+    // Inject target members after setting API user, so it can @Inject a ReviewDb if it wants.
+    injector.injectMembers(target);
   }
 
   private void tearDown() {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index 637d5f9..05eca2a 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -66,8 +66,7 @@
     PushOneCommit.Result a = createChange();
     PushOneCommit.Result b = createChange();
     List<ChangeData> list = ImmutableList.of(a.getChange(), b.getChange());
-    changeAbandoner.batchAbandon(
-        batchUpdateFactory, a.getChange().project(), user, list, "deadbeef");
+    batchAbandon.batchAbandon(batchUpdateFactory, a.getChange().project(), user, list, "deadbeef");
 
     ChangeInfo info = get(a.getChangeId(), MESSAGES);
     assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
@@ -96,7 +95,7 @@
     exception.expect(ResourceConflictException.class);
     exception.expectMessage(
         String.format("Project name \"%s\" doesn't match \"%s\"", project2Name, project1Name));
-    changeAbandoner.batchAbandon(batchUpdateFactory, new Project.NameKey(project1Name), user, list);
+    batchAbandon.batchAbandon(batchUpdateFactory, new Project.NameKey(project1Name), user, list);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 2b1b313..cc4e2a7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -49,6 +49,7 @@
 import static com.google.gerrit.server.project.testing.Util.value;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
@@ -154,7 +155,6 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -461,7 +461,7 @@
     assertThat(result.reviewers).isNotEmpty();
     ChangeInfo info = gApi.changes().id(changeId).get();
     Function<Collection<AccountInfo>, Collection<String>> toEmails =
-        ais -> ais.stream().map(ai -> ai.email).collect(Collectors.toSet());
+        ais -> ais.stream().map(ai -> ai.email).collect(toSet());
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
         .containsExactly(
             admin.email, user1.email, user2.email, "byemail1@example.com", "byemail2@example.com");
@@ -3230,7 +3230,7 @@
   @Test
   public void putTopicExceedLimitFails() throws Exception {
     String changeId = createChange().getChangeId();
-    String topic = Stream.generate(() -> "t").limit(2049).collect(Collectors.joining());
+    String topic = Stream.generate(() -> "t").limit(2049).collect(joining());
 
     exception.expect(BadRequestException.class);
     exception.expectMessage("topic length exceeds the limit");
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index f3e9a19..060cef5 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -19,6 +19,8 @@
 import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.FileInfoSubject.assertThat;
 import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toMap;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableMap;
@@ -44,7 +46,6 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import javax.imageio.ImageIO;
 import org.eclipse.jgit.lib.Config;
@@ -64,7 +65,7 @@
   private static final String FILE_CONTENT =
       IntStream.rangeClosed(1, 100)
           .mapToObj(number -> String.format("Line %d\n", number))
-          .collect(Collectors.joining());
+          .collect(joining());
   private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n";
 
   private boolean intraline;
@@ -1286,7 +1287,7 @@
     testRepo.reset(parentCommit);
     Map<String, String> files =
         Arrays.stream(removedFilePaths)
-            .collect(Collectors.toMap(Function.identity(), path -> "Irrelevant content"));
+            .collect(toMap(Function.identity(), path -> "Irrelevant content"));
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), testRepo, "Remove files from repo", files);
     PushOneCommit.Result result = push.rm("refs/for/master");
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 7b86a96..9670b34 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -93,7 +93,6 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.junit.TestRepository;
@@ -353,14 +352,14 @@
 
   @Test
   public void pushForMasterWithTopicInRefExceedLimitFails() throws Exception {
-    String topic = Stream.generate(() -> "t").limit(2049).collect(Collectors.joining());
+    String topic = Stream.generate(() -> "t").limit(2049).collect(joining());
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
     r.assertErrorStatus("topic length exceeds the limit (2048)");
   }
 
   @Test
   public void pushForMasterWithTopicAsOptionExceedLimitFails() throws Exception {
-    String topic = Stream.generate(() -> "t").limit(2049).collect(Collectors.joining());
+    String topic = Stream.generate(() -> "t").limit(2049).collect(joining());
     PushOneCommit.Result r = pushTo("refs/for/master%topic=" + topic);
     r.assertErrorStatus("topic length exceeds the limit (2048)");
   }
diff --git a/javatests/com/google/gerrit/acceptance/pgm/BUILD b/javatests/com/google/gerrit/acceptance/pgm/BUILD
index a61ca4b..3550a99 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/BUILD
+++ b/javatests/com/google/gerrit/acceptance/pgm/BUILD
@@ -5,7 +5,10 @@
     group = "pgm",
     labels = ["pgm"],
     vm_args = ["-Xmx512m"],
-    deps = [":util"],
+    deps = [
+        ":util",
+        "//java/com/google/gerrit/server/schema",
+    ],
 )
 
 java_library(
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 4c5f231..7b36126 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -18,6 +18,8 @@
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
@@ -68,7 +70,6 @@
 import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -962,7 +963,7 @@
         .values()
         .stream()
         .flatMap(List::stream)
-        .collect(Collectors.toList());
+        .collect(toList());
   }
 
   private CommentInput addComment(String changeId, String message) throws Exception {
@@ -976,7 +977,7 @@
   private void addComments(String changeId, String revision, CommentInput... commentInputs)
       throws Exception {
     ReviewInput input = new ReviewInput();
-    input.comments = Arrays.stream(commentInputs).collect(Collectors.groupingBy(c -> c.path));
+    input.comments = Arrays.stream(commentInputs).collect(groupingBy(c -> c.path));
     gApi.changes().id(changeId).revision(revision).review(input);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/BUILD b/javatests/com/google/gerrit/acceptance/server/notedb/BUILD
index 57454bb..20c256f 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/BUILD
@@ -7,4 +7,5 @@
         "notedb",
         "server",
     ],
+    deps = ["//java/com/google/gerrit/server/schema"],
 )
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
index 7d33845..d4990af 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance.server.notedb;
 
+import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
@@ -24,6 +25,7 @@
 import static com.google.gerrit.server.notedb.NotesMigrationState.REVIEW_DB;
 import static com.google.gerrit.server.notedb.NotesMigrationState.WRITE;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Comparator.naturalOrder;
 import static org.easymock.EasyMock.createStrictMock;
 import static org.easymock.EasyMock.expectLastCall;
 import static org.easymock.EasyMock.replay;
@@ -31,6 +33,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -59,16 +62,12 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
-import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.stream.Stream;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -545,22 +544,15 @@
     addedListeners.add(listeners.add(listener));
   }
 
-  private SortedSet<String> getObjectFiles(Project.NameKey project) throws Exception {
-    SortedSet<String> files = new TreeSet<>();
-    try (Repository repo = repoManager.openRepository(project)) {
-      Files.walkFileTree(
-          ((FileRepository) repo).getObjectDatabase().getDirectory().toPath(),
-          new SimpleFileVisitor<Path>() {
-            @Override
-            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
-              String name = file.getFileName().toString();
-              if (!attrs.isDirectory() && !name.endsWith(".pack") && !name.endsWith(".idx")) {
-                files.add(name);
-              }
-              return FileVisitResult.CONTINUE;
-            }
-          });
+  private ImmutableSortedSet<String> getObjectFiles(Project.NameKey project) throws Exception {
+    try (Repository repo = repoManager.openRepository(project);
+        Stream<Path> paths =
+            Files.walk(((FileRepository) repo).getObjectDatabase().getDirectory().toPath())) {
+      return paths
+          .filter(path -> !Files.isDirectory(path))
+          .map(Path::toString)
+          .filter(name -> !name.endsWith(".pack") && !name.endsWith(".idx"))
+          .collect(toImmutableSortedSet(naturalOrder()));
     }
-    return files;
   }
 }
diff --git a/javatests/com/google/gerrit/gpg/BUILD b/javatests/com/google/gerrit/gpg/BUILD
index 0c82c09..9beb0ff 100644
--- a/javatests/com/google/gerrit/gpg/BUILD
+++ b/javatests/com/google/gerrit/gpg/BUILD
@@ -15,6 +15,7 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/project/testing:project-test-util",
+        "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib:gwtorm",
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 2eaf4f5..5bdfe39 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/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//java/org/eclipse/jgit:server",
         "//lib:grappa",
diff --git a/javatests/com/google/gerrit/server/query/account/BUILD b/javatests/com/google/gerrit/server/query/account/BUILD
index f582bbd..0127fa5 100644
--- a/javatests/com/google/gerrit/server/query/account/BUILD
+++ b/javatests/com/google/gerrit/server/query/account/BUILD
@@ -13,6 +13,7 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:truth",
         "//lib/guice",
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index 35ba46b..5ade4ef 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -16,6 +16,7 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:gwtorm",
         "//lib:truth",
diff --git a/javatests/com/google/gerrit/server/query/group/BUILD b/javatests/com/google/gerrit/server/query/group/BUILD
index 3173d45..e9e206e 100644
--- a/javatests/com/google/gerrit/server/query/group/BUILD
+++ b/javatests/com/google/gerrit/server/query/group/BUILD
@@ -13,6 +13,7 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:truth",
         "//lib/guice",
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index aac64c6..760aa36 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -12,6 +12,7 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:truth",
         "//lib/guice",
diff --git a/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java b/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
index dc685fd..ec6f1ea 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
@@ -48,8 +48,8 @@
 
   @Inject private CreateGroup.Factory createGroupFactory;
   @Inject private Schema_151 schema151;
+  @Inject private ReviewDb db;
 
-  private ReviewDb db;
   private Connection connection;
   private PreparedStatement createdOnRetrieval;
   private PreparedStatement createdOnUpdate;
@@ -57,8 +57,6 @@
 
   @Before
   public void setUp() throws Exception {
-    testEnv.getInjector().injectMembers(this);
-    db = testEnv.getDb();
     assume().that(db instanceof JdbcSchema).isTrue();
 
     connection = ((JdbcSchema) db).getConnection();
diff --git a/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java b/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
index 5beeec1..0bf9399 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
@@ -53,15 +53,13 @@
   @Inject private GerritApi gApi;
   @Inject private GitRepositoryManager repoManager;
   @Inject private Provider<IdentifiedUser> userProvider;
+  @Inject private ReviewDb db;
   @Inject private Schema_160 schema160;
 
-  private ReviewDb db;
   private Account.Id accountId;
 
   @Before
   public void setUp() throws Exception {
-    testEnv.getInjector().injectMembers(this);
-    db = testEnv.getDb();
     accountId = userProvider.get().getAccountId();
   }
 
diff --git a/javatests/com/google/gerrit/server/schema/Schema_161_to_162_Test.java b/javatests/com/google/gerrit/server/schema/Schema_161_to_162_Test.java
index 7391d49..c428d26 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_161_to_162_Test.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_161_to_162_Test.java
@@ -35,7 +35,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
@@ -47,16 +46,9 @@
   @Inject private GerritApi gApi;
   @Inject private GitRepositoryManager repoManager;
   @Inject private Schema_162 schema162;
+  @Inject private ReviewDb db;
   @Inject @GerritPersonIdent private PersonIdent serverUser;
 
-  private ReviewDb db;
-
-  @Before
-  public void setUp() throws Exception {
-    testEnv.getInjector().injectMembers(this);
-    db = testEnv.getDb();
-  }
-
   @Test
   public void skipCorrectInheritance() throws Exception {
     assertThatAllUsersInheritsFrom(allProjectsName.get());
diff --git a/javatests/com/google/gerrit/server/update/BUILD b/javatests/com/google/gerrit/server/update/BUILD
index 80c2d72..0cd3234 100644
--- a/javatests/com/google/gerrit/server/update/BUILD
+++ b/javatests/com/google/gerrit/server/update/BUILD
@@ -29,6 +29,7 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib:gwtorm",
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
index 152ef3d..27d3a42 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -22,6 +22,7 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -101,11 +102,17 @@
               visible-change-table-columns="[[visibleChangeTableColumns]]"
               show-number="[[showNumber]]"
               show-star="[[showStar]]"
+              tabindex="0"
               label-names="[[labelNames]]"></gr-change-list-item>
         </template>
       </template>
     </table>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-cursor-manager
+        id="cursor"
+        index="{{selectedIndex}}"
+        scroll-behavior="keep-visible"
+        focus-on-move></gr-cursor-manager>
   </template>
   <script src="gr-change-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 262d1dc..f66d8c8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -113,6 +113,10 @@
       keydown: '_scopedKeydownHandler',
     },
 
+    observers: [
+      '_sectionsChanged(sections.*)',
+    ],
+
     /**
      * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
      * events must be scoped to a component level (e.g. `enter`) in order to not
@@ -194,7 +198,7 @@
     },
 
     _sectionHref(query) {
-      return `${this.getBaseUrl()}/q/${this.encodeURL(query, true)}`;
+      return Gerrit.Nav.getUrlForSearchQuery(query);
     },
 
     /**
@@ -234,10 +238,7 @@
           this.modifierPressed(e)) { return; }
 
       e.preventDefault();
-      // Compute absolute index of item that would come after final item.
-      const len = this._computeItemAbsoluteIndex(this.sections.length, 0);
-      if (this.selectedIndex === len - 1) { return; }
-      this.selectedIndex += 1;
+      this.$.cursor.next();
     },
 
     _handleKKey(e) {
@@ -245,8 +246,7 @@
           this.modifierPressed(e)) { return; }
 
       e.preventDefault();
-      if (this.selectedIndex === 0) { return; }
-      this.selectedIndex -= 1;
+      this.$.cursor.previous();
     },
 
     _handleOKey(e) {
@@ -317,5 +317,12 @@
     _getListItems() {
       return Polymer.dom(this.root).querySelectorAll('gr-change-list-item');
     },
+
+    _sectionsChanged() {
+      // Flush DOM operations so that the list item elements will be loaded.
+      Polymer.dom.flush();
+      this.$.cursor.stops = this._getListItems();
+      this.$.cursor.moveToStart();
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index bded5f6..7b9fadf 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -473,16 +473,6 @@
       }
     });
 
-    test('_sectionHref', () => {
-      assert.equal(
-          element._sectionHref('is:open owner:self'),
-          '/q/is:open+owner:self');
-      assert.equal(
-          element._sectionHref(
-              'is:open ((reviewer:self -is:ignored) OR assignee:self)'),
-          '/q/is:open+((reviewer:self+-is:ignored)+OR+assignee:self)');
-    });
-
     test('_computeItemAbsoluteIndex', () => {
       sandbox.stub(element, '_computeLabelNames');
       element.sections = [
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index d699167..3bf68f0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -219,6 +219,9 @@
       .text {
         white-space: pre;
       }
+      gr-commit-info {
+        display: inline-block;
+      }
       @media screen and (min-width: 80em) {
         .commitMessage {
           max-width: var(--commit-message-max-width, 100ch);
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
index c51a538..aac5c33 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
@@ -21,22 +21,24 @@
 <dom-module id="gr-commit-info">
   <template>
     <style include="shared-styles">
-      :host {
+      .container {
         align-items: center;
         display: flex;
       }
     </style>
-    <template is="dom-if" if="[[_showWebLink]]">
-      <a target="_blank" rel="noopener"
-         href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
-    </template>
-    <template is="dom-if" if="[[!_showWebLink]]">
-      [[_computeShortHash(commitInfo)]]
-    </template>
-    <gr-copy-clipboard
-        hide-input
-        text="[[commitInfo.commit]]">
-    </gr-copy-clipboard>
+    <div class="container">
+      <template is="dom-if" if="[[_showWebLink]]">
+        <a target="_blank" rel="noopener"
+            href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
+      </template>
+      <template is="dom-if" if="[[!_showWebLink]]">
+        [[_computeShortHash(commitInfo)]]
+      </template>
+      <gr-copy-clipboard
+          hide-input
+          text="[[commitInfo.commit]]">
+      </gr-copy-clipboard>
+    </div>
   </template>
   <script src="gr-commit-info.js"></script>
 </dom-module>
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 319c92d..63021db 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
@@ -60,7 +60,7 @@
           box-shadow: none;
           padding: .2em .85em;
         }
-        --gr-button-background: var(--button-background-color, #d9d9d9);
+        --gr-button-background: var(--button-background-color, #f5f5f5);
         --gr-button-color: black;
       }
       iron-selector > gr-button.iron-selected.max {
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 f30cc1c..b59ad9d 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -26,6 +26,9 @@
     //    - `changeNum`, required, String: the numeric ID of the change.
     //
     // - Gerrit.Nav.View.SEARCH:
+    //    - `query`, optional, String: the literal search query. If provided,
+    //        the string will be used as the query, and all other params will be
+    //        ignored.
     //    - `owner`, optional, String: the owner name.
     //    - `project`, optional, String: the project name.
     //    - `branch`, optional, String: the branch name.
@@ -136,6 +139,13 @@
         return this._generateUrl(params);
       },
 
+      getUrlForSearchQuery(query) {
+        return this._getUrlFor({
+          view: Gerrit.Nav.View.SEARCH,
+          query,
+        });
+      },
+
       /**
        * @param {!string} project The name of the project.
        * @param {boolean=} opt_openOnly When true, only search open changes in
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 60fd109..5d26cb6 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -322,6 +322,10 @@
      * @return {string}
      */
     _generateSearchUrl(params) {
+      if (params.query) {
+        return '/q/' + this.encodeURL(params.query, true);
+      }
+
       const operators = [];
       if (params.owner) {
         operators.push('owner:' + this.encodeURL(params.owner, false));
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 909235b..7159e0a 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
@@ -226,6 +226,10 @@
             '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
             'topic:"g%2525h"+status:op%2525en');
 
+        // The presence of the query param overrides other params.
+        params.query = 'foo$bar';
+        assert.equal(element._generateUrl(params), '/q/foo%2524bar');
+
         params = {
           view: Gerrit.Nav.View.SEARCH,
           statuses: ['a', 'b', 'c'],