Merge "Merge branch 'stable-2.15' into master"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index de73930..cc485d5 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2441,6 +2441,21 @@
 +
 By default, true.
 
+[[httpd.gracefulStopTimeout]]httpd.gracefulStopTimeout::
++
+Set a graceful stop time. If set, the daemon ensures that all incoming
+calls are preserved for a maximum period of time, before starting
+the graceful shutdown process. Sites behind a workload balancer such as
+HAProxy would need this to be set for avoiding serving errors during
+rolling restarts.
++
+Values should use common unit suffixes to express their setting:
++
+* s, sec, second, seconds
+* m, min, minute, minutes
++
+By default, 0 seconds (immediate shutdown).
+
 [[httpd.inheritChannel]]httpd.inheritChannel::
 +
 If true, permits the daemon to inherit its server socket channel
@@ -2760,7 +2775,7 @@
 simultaneous writes that may cause one of the writes to not be reflected in the
 index. The check to avoid this does consume some resources.
 +
-Defaults to true.
+Defaults to false.
 
 [[index.scheduledIndexer]]
 ==== Subsection index.scheduledIndexer
@@ -4032,7 +4047,7 @@
 Only used when `sendemail.from` is set to `USER`.
 List of allowed domains. If user's email matches one of the domains, emails will
 be sent as USER, otherwise as MIXED mode. Wildcards may be specified by
-including `*` to match any number of characters, for example `*.example.com`
+including `\*` to match any number of characters, for example `*.example.com`
 matches any subdomain of `example.com`.
 +
 By default, `*`.
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index 2d0812e..de5171c 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -424,10 +424,13 @@
 
 As project owner you can administrate the branches of your project in
 the Gerrit Web UI under `Projects` > `List` > <your project> >
-`Branches`. In the Web UI both link:project-configuration.html#branch-creation[
-branch creation] and link:project-configuration.html#branch-deletion[branch
-deletion] are allowed for project owners without requiring any
-additional access rights.
+`Branches`. In the Web UI link:project-configuration.html#branch-creation[
+branch creation] is allowed if you have
+link:access-control.html#category_create[Create Reference] access right and
+link:project-configuration.html#branch-deletion[branch deletion] is allowed if
+you have the link:access-control.html#category_delete[Delete Reference] or the
+link:access-control.html#category_push[Push] access right with the `force`
+option.
 
 By setting `HEAD` on the project you can define its
 link:project-configuration.html#default-branch[default branch]. For convenience
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 2b34185..7360bd4 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -157,7 +157,8 @@
 
 oldRev:: The old value of the ref, prior to the update.
 
-newRev:: The new value the ref was updated to.
+newRev:: The new value the ref was updated to. Zero value (`0000000000000000000000000000000000000000`)
+indicates that the ref was deleted.
 
 refName:: Full ref name within project.
 
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 5c79c1b..f76b5e4 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -257,9 +257,7 @@
 
 To be able to create new branches the user must have the
 link:access-control.html#category_create[Create Reference] access
-right. In addition, project owners and Gerrit administrators can create
-new branches from the Web UI or via REST even without having the
-`Create Reference` access right.
+right.
 
 When using the Web UI, the REST endpoint or the SSH command it is only
 possible to create branches on commits that already exist in the
@@ -295,9 +293,7 @@
 To be able to delete branches, the user must have the
 link:access-control.html#category_delete[Delete Reference] or the
 link:access-control.html#category_push[Push] access right with the
-`force` option. In addition, project owners and Gerrit administrators
-can delete branches from the Web UI or via REST even without having the
-`Force Push` access right.
+`force` option.
 
 [[default-branch]]
 === Default Branch
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 49d5ee5..00fd81f 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -1549,8 +1549,11 @@
 |Field Name    ||Description
 |`id`          ||The URL encoded UUID of the group.
 |`name`        |
-not set if returned in a map where the group name is used as map key|
-The name of the group.
+optional, not set if returned in a map where the group name is used as map key|
+The name of the group. +
+For external groups the group name is missing if there is no group
+backend that can resolve the group UUID. E.g. this can happen when a
+plugin that provided a group backend was uninstalled.
 |`url`         |optional|
 URL to information about the group. Typically a URL to a web page that
 permits users to apply to join the group, or manage their membership.
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 0f030a6..2ad8cbd 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -4,9 +4,7 @@
 
 Most basic searches can be viewed by clicking on a link along the top
 menu bar.  The link will prefill the search box with a common search
-query, execute it, and present the results.  If exactly one change
-matches the search, the change will be presented instead of a list.
-
+query, execute it, and present the results.
 
 [options="header"]
 |=================================================
@@ -14,7 +12,7 @@
 |All > Open           | status:open '(or is:open)'
 |All > Merged         | status:merged
 |All > Abandoned      | status:abandoned
-|My > Watched Changes | status:open is:watched
+|My > Watched Changes | is:watched is:open
 |My > Starred Changes | is:starred
 |My > Draft Comments  | has:draft
 |Open changes in Foo  | status:open project:Foo
@@ -36,6 +34,9 @@
 |Approval requirement             | Code-Review>=+2, Verified=1
 |=============================================================
 
+For change searches (i.e. those using a numerical id, Change-Id, or commit
+SHA1), if the search results in a single change that change will be
+presented instead of a list.
 
 [[search-operators]]
 == Search Operators
@@ -601,7 +602,7 @@
 link:#submittable[submittable:ok].)
 
 `is:open (label:Verified-1 OR label:Code-Review-2)`::
-`is:open (label:Verified=reject OR label:Code-Review:reject)`::
+`is:open (label:Verified=reject OR label:Code-Review=reject)`::
 +
 Changes that are blocked from submission due to a blocking score.
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
index b056afa..5e38a14 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
@@ -113,17 +113,19 @@
           GroupInfo member = auditEvent.memberAsGroup();
           if (AccountGroup.isInternalGroup(member.getGroupUUID())) {
             table.setWidget(
-                row, 3, new Hyperlink(member.name(), Dispatcher.toGroup(member.getGroupUUID())));
+                row,
+                3,
+                new Hyperlink(formatGroup(member), Dispatcher.toGroup(member.getGroupUUID())));
             fmt.getElement(row, 3).setTitle(null);
           } else if (member.url() != null) {
             Anchor a = new Anchor();
-            a.setText(member.name());
+            a.setText(formatGroup(member));
             a.setHref(member.url());
             a.setTitle("UUID " + member.getGroupUUID().get());
             table.setWidget(row, 3, a);
             fmt.getElement(row, 3).setTitle(null);
           } else {
-            table.setText(row, 3, member.name());
+            table.setText(row, 3, formatGroup(member));
             fmt.getElement(row, 3).setTitle("UUID " + member.getGroupUUID().get());
           }
           break;
@@ -148,4 +150,10 @@
     b.append(")");
     return b.toString();
   }
+
+  private static String formatGroup(GroupInfo group) {
+    return group.name() != null && !group.name().isEmpty()
+        ? group.name()
+        : group.getGroupUUID().get();
+  }
 }
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index 09ffe9d..b754351 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -35,9 +35,13 @@
 import com.google.inject.Injector;
 import com.google.inject.Module;
 import com.google.inject.Provider;
+import java.io.File;
 import java.util.Arrays;
 import java.util.Collections;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
 import org.junit.Rule;
 import org.junit.rules.RuleChain;
 import org.junit.rules.TemporaryFolder;
@@ -109,8 +113,12 @@
           return new Statement() {
             @Override
             public void evaluate() throws Throwable {
-              beforeTest(description);
-              base.evaluate();
+              try {
+                beforeTest(description);
+                base.evaluate();
+              } finally {
+                afterTest();
+              }
             }
           };
         }
@@ -122,13 +130,65 @@
   protected Account.Id adminId;
 
   private GerritServer.Description serverDesc;
+  private SystemReader oldSystemReader;
 
   private void beforeTest(Description description) throws Exception {
+    // SystemReader must be overridden before creating any repos, since they read the user/system
+    // configs at initialization time, and are then stored in the RepositoryCache forever.
+    oldSystemReader = setFakeSystemReader(tempSiteDir.getRoot());
+
     serverDesc = GerritServer.Description.forTestMethod(description, configName);
     sitePaths = new SitePaths(tempSiteDir.getRoot().toPath());
     GerritServer.init(serverDesc, baseConfig, sitePaths.site_path);
   }
 
+  private static SystemReader setFakeSystemReader(File tempDir) {
+    SystemReader oldSystemReader = SystemReader.getInstance();
+    SystemReader.setInstance(
+        new SystemReader() {
+          @Override
+          public String getHostname() {
+            return oldSystemReader.getHostname();
+          }
+
+          @Override
+          public String getenv(String variable) {
+            return oldSystemReader.getenv(variable);
+          }
+
+          @Override
+          public String getProperty(String key) {
+            return oldSystemReader.getProperty(key);
+          }
+
+          @Override
+          public FileBasedConfig openUserConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "user.config"), FS.detect());
+          }
+
+          @Override
+          public FileBasedConfig openSystemConfig(Config parent, FS fs) {
+            return new FileBasedConfig(parent, new File(tempDir, "system.config"), FS.detect());
+          }
+
+          @Override
+          public long getCurrentTime() {
+            return oldSystemReader.getCurrentTime();
+          }
+
+          @Override
+          public int getTimezone(long when) {
+            return oldSystemReader.getTimezone(when);
+          }
+        });
+    return oldSystemReader;
+  }
+
+  private void afterTest() throws Exception {
+    SystemReader.setInstance(oldSystemReader);
+    oldSystemReader = null;
+  }
+
   protected ServerContext startServer() throws Exception {
     return startServer(null);
   }
@@ -153,7 +213,10 @@
   }
 
   protected static void runGerrit(String... args) throws Exception {
-    assertThat(GerritLauncher.mainImpl(args))
+    // Use invokeProgram with the current classloader, rather than mainImpl, which would create a
+    // new classloader. This is necessary so that static state, particularly the SystemReader, is
+    // shared with the test method.
+    assertThat(GerritLauncher.invokeProgram(StandaloneSiteTest.class.getClassLoader(), args))
         .named("gerrit.war " + Arrays.stream(args).collect(joining(" ")))
         .isEqualTo(0);
   }
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index e8892be..b7d232d 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -64,6 +64,17 @@
     System.exit(mainImpl(argv));
   }
 
+  /**
+   * Invokes a proram.
+   *
+   * <p>Creates a new classloader to load and run the program class. To reuse a classloader across
+   * calls (e.g. from tests), use {@link #invokeProgram(ClassLoader, String[])}.
+   *
+   * @param argv arguments, as would be passed to {@code gerrit.war}. The first argument is the
+   *     program name.
+   * @return program return code.
+   * @throws Exception if any error occurs.
+   */
   public static int mainImpl(String[] argv) throws Exception {
     if (argv.length == 0) {
       File me;
@@ -164,7 +175,16 @@
     }
   }
 
-  private static int invokeProgram(ClassLoader loader, String[] origArgv) throws Exception {
+  /**
+   * Invokes a proram in the provided {@code ClassLoader}.
+   *
+   * @param loader classloader to load program class from.
+   * @param origArgv arguments, as would be passed to {@code gerrit.war}. The first argument is the
+   *     program name.
+   * @return program return code.
+   * @throws Exception if any error occurs.
+   */
+  public static int invokeProgram(ClassLoader loader, String[] origArgv) throws Exception {
     String name = origArgv[0];
     final String[] argv = new String[origArgv.length - 1];
     System.arraycopy(origArgv, 1, argv, 0, argv.length);
diff --git a/java/com/google/gerrit/pgm/MigrateToNoteDb.java b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
index 22fdb2c..10761c7 100644
--- a/java/com/google/gerrit/pgm/MigrateToNoteDb.java
+++ b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
@@ -29,13 +29,16 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.index.DummyIndexModule;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.notedb.rebuild.GcAllUsers;
 import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import org.kohsuke.args4j.Option;
@@ -99,6 +102,7 @@
   private LifecycleManager dbManager;
   private LifecycleManager sysManager;
 
+  @Inject private GcAllUsers gcAllUsers;
   @Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
 
   @Override
@@ -137,6 +141,9 @@
           migrator.migrate();
         }
       }
+      try (PrintWriter w = new PrintWriter(System.out, true)) {
+        gcAllUsers.run(w);
+      }
     } finally {
       stop();
     }
@@ -190,6 +197,7 @@
             install(dbInjector.getInstance(BatchProgramModule.class));
             install(new DummyIndexModule());
             factory(ChangeResource.Factory.class);
+            factory(GarbageCollection.Factory.class);
           }
         });
   }
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 3895d16..b6eac05 100644
--- a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -39,6 +39,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import javax.servlet.DispatcherType;
 import javax.servlet.Filter;
 import org.eclipse.jetty.http.HttpScheme;
@@ -56,6 +57,7 @@
 import org.eclipse.jetty.server.handler.ContextHandler;
 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.server.handler.RequestLogHandler;
+import org.eclipse.jetty.server.handler.StatisticsHandler;
 import org.eclipse.jetty.server.session.SessionHandler;
 import org.eclipse.jetty.servlet.DefaultServlet;
 import org.eclipse.jetty.servlet.FilterHolder;
@@ -149,6 +151,15 @@
       httpd.addBean(mbean);
     }
 
+    long gracefulStopTimeout =
+        cfg.getTimeUnit("httpd", null, "gracefulStopTimeout", 0L, TimeUnit.MILLISECONDS);
+    if (gracefulStopTimeout > 0) {
+      StatisticsHandler statsHandler = new StatisticsHandler();
+      statsHandler.setHandler(app);
+      app = statsHandler;
+      httpd.setStopTimeout(gracefulStopTimeout);
+    }
+
     httpd.setHandler(app);
     httpd.setStopAtShutdown(false);
   }
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index cf51197..1ea115a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -152,7 +152,7 @@
   }
 
   private static boolean autoReindexIfStale(Config cfg) {
-    return cfg.getBoolean("index", null, "autoReindexIfStale", true);
+    return cfg.getBoolean("index", null, "autoReindexIfStale", false);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java b/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java
new file mode 100644
index 0000000..0653192
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/rebuild/GcAllUsers.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2018 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.notedb.rebuild;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_GC_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_AUTO;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GarbageCollectionResult;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GarbageCollection;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class GcAllUsers {
+  private static final Logger log = LoggerFactory.getLogger(GcAllUsers.class);
+
+  private final AllUsersName allUsers;
+  private final GarbageCollection.Factory gcFactory;
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  GcAllUsers(
+      AllUsersName allUsers,
+      GarbageCollection.Factory gcFactory,
+      GitRepositoryManager repoManager) {
+    this.allUsers = allUsers;
+    this.gcFactory = gcFactory;
+    this.repoManager = repoManager;
+  }
+
+  public void runWithLogger() {
+    // Print log messages using logger, and skip progress.
+    run(s -> log.info(s), null);
+  }
+
+  public void run(PrintWriter writer) {
+    // Print both log messages and progress to given writer.
+    run(checkNotNull(writer)::println, writer);
+  }
+
+  private void run(Consumer<String> logOneLine, @Nullable PrintWriter progressWriter) {
+    if (!(repoManager instanceof LocalDiskRepositoryManager)) {
+      logOneLine.accept("Skipping GC of " + allUsers + "; not a local disk repo");
+      return;
+    }
+    if (!enableAutoGc(logOneLine)) {
+      logOneLine.accept(
+          "Skipping GC of "
+              + allUsers
+              + " due to disabling "
+              + CONFIG_GC_SECTION
+              + "."
+              + CONFIG_KEY_AUTO);
+      logOneLine.accept(
+          "If loading accounts is slow after the NoteDb migration, run `git gc` on "
+              + allUsers
+              + " manually");
+      return;
+    }
+
+    if (progressWriter == null) {
+      // Mimic log line from GarbageCollection.
+      logOneLine.accept("collecting garbage for \"" + allUsers + "\":\n");
+    }
+    GarbageCollectionResult result =
+        gcFactory.create().run(ImmutableList.of(allUsers), progressWriter);
+    if (!result.hasErrors()) {
+      return;
+    }
+    for (GarbageCollectionResult.Error e : result.getErrors()) {
+      switch (e.getType()) {
+        case GC_ALREADY_SCHEDULED:
+          logOneLine.accept("GC already scheduled for " + e.getProjectName());
+          break;
+        case GC_FAILED:
+          logOneLine.accept("GC failed for " + e.getProjectName());
+          break;
+        case REPOSITORY_NOT_FOUND:
+          logOneLine.accept(e.getProjectName() + " repo not found");
+          break;
+        default:
+          logOneLine.accept("GC failed for " + e.getProjectName() + ": " + e.getType());
+          break;
+      }
+    }
+  }
+
+  private boolean enableAutoGc(Consumer<String> logOneLine) {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      return repo.getConfig().getInt(CONFIG_GC_SECTION, CONFIG_KEY_AUTO, -1) != 0;
+    } catch (IOException e) {
+      logOneLine.accept(
+          "Error reading config for " + allUsers + ":\n" + Throwables.getStackTraceAsString(e));
+      return false;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
index 535736d..65755ed 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
@@ -50,19 +50,22 @@
     }
   }
 
-  private Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
+  private final GcAllUsers gcAllUsers;
   private final OnlineUpgrader indexUpgrader;
+  private final Provider<NoteDbMigrator.Builder> migratorBuilderProvider;
   private final boolean upgradeIndex;
   private final boolean trial;
 
   @Inject
   OnlineNoteDbMigrator(
       @GerritServerConfig Config cfg,
-      Provider<NoteDbMigrator.Builder> migratorBuilderProvider,
+      GcAllUsers gcAllUsers,
       OnlineUpgrader indexUpgrader,
+      Provider<NoteDbMigrator.Builder> migratorBuilderProvider,
       @Named(TRIAL) boolean trial) {
-    this.migratorBuilderProvider = migratorBuilderProvider;
+    this.gcAllUsers = gcAllUsers;
     this.indexUpgrader = indexUpgrader;
+    this.migratorBuilderProvider = migratorBuilderProvider;
     this.upgradeIndex = VersionManager.getOnlineUpgrade(cfg);
     this.trial = trial || NoteDbMigrator.getTrialMode(cfg);
   }
@@ -88,6 +91,7 @@
     } catch (Exception e) {
       log.error("Error in online NoteDb migration", e);
     }
+    gcAllUsers.runWithLogger();
     log.info("Online NoteDb migration completed in {}s", sw.elapsed(TimeUnit.SECONDS));
 
     if (upgradeIndex) {
diff --git a/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java b/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
index 35fcc94..1bb23fb 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.pgm;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
@@ -28,7 +29,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.index.GerritIndexStatus;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
@@ -41,6 +44,11 @@
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -72,6 +80,16 @@
     gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
     // Unlike in the running server, for tests, we don't stack notedb.config on gerrit.config.
     noteDbConfig = new FileBasedConfig(sitePaths.notedb_config.toFile(), FS.detect());
+
+    // Set gc.pruneExpire=now so GC prunes all unreachable objects from All-Users, which allows us
+    // to reliably test that it behaves as expected.
+    Path cfgPath = sitePaths.site_path.resolve("git").resolve("All-Users.git").resolve("config");
+    assertWithMessage("Expected All-Users config at %s", cfgPath)
+        .that(Files.isRegularFile(cfgPath))
+        .isTrue();
+    FileBasedConfig cfg = new FileBasedConfig(cfgPath.toFile(), FS.detect());
+    cfg.setString("gc", null, "pruneExpire", "now");
+    cfg.save();
   }
 
   @Test
@@ -114,11 +132,17 @@
     migrate();
     assertNotesMigrationState(NotesMigrationState.NOTE_DB);
 
+    File allUsersDir;
     try (ServerContext ctx = startServer()) {
       GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class);
       try (Repository repo = repoManager.openRepository(project)) {
         assertThat(repo.exactRef(RefNames.changeMetaRef(changeId))).isNotNull();
       }
+      assertThat(repoManager).isInstanceOf(LocalDiskRepositoryManager.class);
+      try (Repository repo =
+          repoManager.openRepository(ctx.getInjector().getInstance(AllUsersName.class))) {
+        allUsersDir = repo.getDirectory();
+      }
 
       try (ReviewDb db = openUnderlyingReviewDb(ctx)) {
         Change c = db.changes().get(changeId);
@@ -137,6 +161,15 @@
     }
     assertNoAutoMigrateConfig(gerritConfig);
     assertAutoMigrateConfig(noteDbConfig, false);
+
+    try (FileRepository repo = new FileRepository(allUsersDir)) {
+      try (Stream<Path> paths = Files.walk(repo.getObjectsDirectory().toPath())) {
+        assertThat(paths.filter(p -> !p.toString().contains("pack") && Files.isRegularFile(p)))
+            .named("loose object files in All-Users")
+            .isEmpty();
+      }
+      assertThat(repo.getObjectDatabase().getPacks()).named("packfiles in All-Users").hasSize(1);
+    }
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index ddda473..c3b5af9 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -51,6 +51,7 @@
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -228,8 +229,7 @@
             "Add Email",
             userId,
             u -> u.addExternalId(ExternalId.createEmail(userId, email)).setPreferredEmail(email));
-    user = userFactory.create(userId);
-    requestContext.setContext(newRequestContext(userId));
+    resetUser();
   }
 
   protected RequestContext newRequestContext(Account.Id requestUserId) {
@@ -247,6 +247,11 @@
     };
   }
 
+  protected void resetUser() {
+    user = userFactory.create(userId);
+    requestContext.setContext(newRequestContext(userId));
+  }
+
   @After
   public void tearDownInjector() {
     if (lifecycle != null) {
@@ -385,6 +390,20 @@
   }
 
   @Test
+  public void byStatusAbandoned() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.MERGED);
+    insert(repo, ins1);
+    ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.ABANDONED);
+    Change change1 = insert(repo, ins2);
+    insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
+
+    assertQuery("status:abandoned", change1);
+    assertQuery("status:ABANDONED", change1);
+    assertQuery("is:abandoned", change1);
+  }
+
+  @Test
   public void byStatusPrefix() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
@@ -1502,6 +1521,8 @@
     Change change1 = insert(repo, newChange(repo));
     Change change2 = insert(repo, newChange(repo));
 
+    assertQuery("has:draft");
+
     DraftInput in = new DraftInput();
     in.line = 1;
     in.message = "nit: trailing whitespace";
@@ -1517,6 +1538,7 @@
     int user2 =
         accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId().get();
 
+    assertQuery("has:draft", change2, change1);
     assertQuery("draftby:" + userId.get(), change2, change1);
     assertQuery("draftby:" + user2);
   }
@@ -2156,6 +2178,35 @@
   }
 
   @Test
+  public void watched() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
+    Change change1 = insert(repo, ins1);
+
+    TestRepository<Repo> repo2 = createProject("repo2");
+
+    ChangeInserter ins2 = newChangeWithStatus(repo2, Change.Status.NEW);
+    insert(repo2, ins2);
+
+    assertQuery("is:watched");
+    assertQuery("watchedby:self");
+
+    List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = "repo";
+    pwi.filter = null;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+    resetUser();
+
+    assertQuery("is:watched", change1);
+    assertQuery("watchedby:self", change1);
+  }
+
+  @Test
   public void selfAndMe() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));