Merge changes I5eb4af9b,I95eb6ec5,Ib05ade0c

* changes:
  EditScreen: Show the base version of the file in a CodeMirror merge view
  Add the "base" parameter to ChangeEditApi
  Add addFinal(HttpCallback) to CallbackGroup
diff --git a/Documentation/cmd-index-activate.txt b/Documentation/cmd-index-activate.txt
index 37783ef..6cb7781 100644
--- a/Documentation/cmd-index-activate.txt
+++ b/Documentation/cmd-index-activate.txt
@@ -5,7 +5,7 @@
 
 == SYNOPSIS
 --
-'ssh' -p @SSH_PORT@ @SSH_HOST@ 'gerrit index activate <index>'
+'ssh' -p <port> <host> 'gerrit index activate <index>'
 --
 
 == DESCRIPTION
diff --git a/Documentation/cmd-index-start.txt b/Documentation/cmd-index-start.txt
index cee283e..0a481e5 100644
--- a/Documentation/cmd-index-start.txt
+++ b/Documentation/cmd-index-start.txt
@@ -5,7 +5,7 @@
 
 == SYNOPSIS
 --
-'ssh' -p @SSH_PORT@ @SSH_HOST@ 'gerrit index start <index>'
+'ssh' -p <port> <host> 'gerrit index start <index>'
 --
 
 == DESCRIPTION
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index 0ff59d4..090781b 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -54,15 +54,17 @@
 
 --current-patch-set::
 	Include information about the current patch set in the results.
+	Note that the information will only be included when the current
+	patch set is visible to the caller.
 
 --patch-sets::
-	Include information about all patch sets.  If combined with
-	the --current-patch-set flag then the current patch set
-	information will be output twice, once in each field.
+	Include information about all patch sets visible to the caller.
+        If combined with the --current-patch-set flag then the current patch
+	set information will be output twice, once in each field.
 
 --all-approvals::
-	Include information about all patch sets along with the
-	approval information for each patch set.  If combined with
+	Include information about all patch sets visible to the caller along
+	with the approval information for each patch set.  If combined with
 	the --current-patch-set flag then the current patch set
 	information will be output twice, once in each field.
 
@@ -76,7 +78,7 @@
 --comments::
 	Include comments for all changes. If combined with the
 	--patch-sets flag then all inline/file comments are included for
-	each patch set.
+	each patch set that is visible to the caller.
 
 --commit-message::
 	Include the full commit message in the change description.
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index aab8aa0..715d589 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -13,6 +13,7 @@
 	[--list-plugins]
 	[--install-plugin=<PLUGIN_NAME>]
 	[--install-all-plugins]
+	[--secure-store-lib]
 	[--dev]
 	[--skip-all-downloads]
 	[--skip-download=<LIBRARY_NAME>]
@@ -63,6 +64,12 @@
 	This option also works in batch mode. This option cannot be supplied
 	alongside --install-plugin.
 
+--secure-store-lib::
+	Path to the jar providing the chosen
+	link:dev-plugins.html#secure-store[SecureStore] implementation class.
+	This option is used the same way as the --new-secure-store-lib option
+	documented in link:pgm-SwitchSecureStore.html[SwitchSecureStore].
+
 --install-plugin::
 	Automatically install plugin with given name without asking.
 	This option also works in batch mode. This option may be supplied
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index fd49487..1955c39 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -496,6 +496,11 @@
     revision(r).submit();
   }
 
+  protected PushOneCommit.Result amendChangeAsDraft(String changeId)
+      throws Exception {
+    return amendChange(changeId, "refs/drafts/master");
+  }
+
   protected ChangeInfo info(String id)
       throws RestApiException {
     return gApi.changes().id(id).info();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index cbaf789..bc206b4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -46,12 +47,14 @@
 import com.google.gerrit.testutil.TestTimeUtil;
 
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PushResult;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import java.util.Collection;
+import java.util.List;
 import java.util.Set;
 
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
@@ -488,4 +491,37 @@
     r.assertErrorStatus(
         "not Signed-off-by author/committer/uploader in commit message footer");
   }
+
+  @Test
+  public void testPushSameCommitTwiceUsingMagicBranchBaseOption()
+      throws Exception {
+    grant(Permission.PUSH, project, "refs/heads/master");
+    PushOneCommit.Result rBase = pushTo("refs/heads/master");
+    rBase.assertOkStatus();
+
+    gApi.projects()
+        .name(project.get())
+        .branch("foo")
+        .create(new BranchInput());
+
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent");
+
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    PushResult pr = GitUtil.pushHead(
+        testRepo, "refs/for/foo%base=" + rBase.getCommit().name(), false, false);
+    assertThat(pr.getMessages()).contains("changes: new: 1, refs: 1, done");
+
+    List<ChangeInfo> changes = query(r.getCommit().name());
+    assertThat(changes).hasSize(2);
+    ChangeInfo c1 = get(changes.get(0).id);
+    ChangeInfo c2 = get(changes.get(1).id);
+    assertThat(c1.project).isEqualTo(c2.project);
+    assertThat(c1.branch).isNotEqualTo(c2.branch);
+    assertThat(c1.changeId).isEqualTo(c2.changeId);
+    assertThat(c1.currentRevision).isEqualTo(c2.currentRevision);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 24d0e097..a6ea4d2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -28,10 +28,11 @@
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.GetServerInfo.ServerInfo;
 
-import java.nio.file.Path;
-import java.nio.file.Files;
 import org.junit.Test;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
+
 public class ServerInfoIT extends AbstractDaemonTest {
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 057d902..9a5dfeb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -14,6 +14,7 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
+
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
index 3733cd1..2865ff87 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -16,11 +16,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.GitUtil.initSsh;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.Side;
@@ -300,13 +302,39 @@
     assertThat(changes.get(0).submitRecords.size()).isEqualTo(1);
   }
 
+  @Test
+  public void testQueryWithNonVisibleCurrentPatchSet() throws Exception {
+    String changeId = createChange().getChangeId();
+    amendChangeAsDraft(changeId);
+    String query = "--current-patch-set --patch-sets " + changeId;
+    List<ChangeAttribute> changes = executeSuccessfulQuery(query);
+    assertThat(changes.size()).isEqualTo(1);
+    assertThat(changes.get(0).patchSets).isNotNull();
+    assertThat(changes.get(0).patchSets).hasSize(2);
+    assertThat(changes.get(0).currentPatchSet).isNotNull();
+
+    SshSession userSession = new SshSession(server, user);
+    initSsh(user);
+    userSession.open();
+    changes = executeSuccessfulQuery(query, userSession);
+    assertThat(changes.size()).isEqualTo(1);
+    assertThat(changes.get(0).patchSets).hasSize(1);
+    assertThat(changes.get(0).currentPatchSet).isNull();
+    userSession.close();
+  }
+
+  private List<ChangeAttribute> executeSuccessfulQuery(String params,
+      SshSession session) throws Exception {
+    String rawResponse =
+        session.exec("gerrit query --format=JSON " + params);
+    assert_().withFailureMessage(session.getError())
+        .that(session.hasError()).isFalse();
+    return getChanges(rawResponse);
+  }
+
   private List<ChangeAttribute> executeSuccessfulQuery(String params)
       throws Exception {
-    String rawResponse =
-        adminSshSession.exec("gerrit query --format=JSON " + params);
-    assert_().withFailureMessage(adminSshSession.getError())
-        .that(adminSshSession.hasError()).isFalse();
-    return getChanges(rawResponse);
+    return executeSuccessfulQuery(params, adminSshSession);
   }
 
   private static List<ChangeAttribute> getChanges(String rawResponse) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index b499530..e29048a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -46,6 +46,8 @@
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyDownEvent;
 import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.event.dom.client.MouseOutEvent;
 import com.google.gwt.event.dom.client.MouseOutHandler;
 import com.google.gwt.event.dom.client.MouseOverEvent;
@@ -139,6 +141,14 @@
         }
       },
       KeyDownEvent.getType());
+    addDomHandler(
+      new KeyPressHandler() {
+        @Override
+        public void onKeyPress(KeyPressEvent e) {
+          e.stopPropagation();
+        }
+      },
+      KeyPressEvent.getType());
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
index d2b740e..39b85cf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
@@ -42,6 +42,7 @@
 import com.google.gwt.user.client.ui.ImageResourceRenderer;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtorm.client.KeyUtil;
+
 import net.codemirror.lib.CodeMirror;
 
 import java.util.List;
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 5acdc25..b40d46b 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -16,6 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
+import com.google.common.base.Joiner;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.AbstractFuture;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -47,7 +48,6 @@
 import org.apache.lucene.store.AlreadyClosedException;
 import org.apache.lucene.store.Directory;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -69,13 +69,11 @@
     return f.getName() + "_SORT";
   }
 
-  public static void setReady(SitePaths sitePaths, int version, boolean ready)
-      throws IOException {
+  public static void setReady(SitePaths sitePaths, String name, int version,
+      boolean ready) throws IOException {
     try {
-      // TODO(dborowitz): Totally broken for non-change indexes.
-      FileBasedConfig cfg =
-          LuceneVersionManager.loadGerritIndexConfig(sitePaths);
-      LuceneVersionManager.setReady(cfg, version, ready);
+      GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
+      cfg.setReady(name, version, ready);
       cfg.save();
     } catch (ConfigInvalidException e) {
       throw new IOException(e);
@@ -85,6 +83,7 @@
   private final Schema<V> schema;
   private final SitePaths sitePaths;
   private final Directory dir;
+  private final String name;
   private final TrackingIndexWriter writer;
   private final ReferenceManager<IndexSearcher> searcherManager;
   private final ControlledRealTimeReopenThread<IndexSearcher> reopenThread;
@@ -94,12 +93,15 @@
       Schema<V> schema,
       SitePaths sitePaths,
       Directory dir,
-      final String name,
+      String name,
+      String subIndex,
       GerritIndexWriterConfig writerConfig,
       SearcherFactory searcherFactory) throws IOException {
     this.schema = schema;
     this.sitePaths = sitePaths;
     this.dir = dir;
+    this.name = name;
+    final String index = Joiner.on('_').skipNulls().join(name, subIndex);
     IndexWriter delegateWriter;
     long commitPeriod = writerConfig.getCommitWithinMs();
 
@@ -114,7 +116,7 @@
       delegateWriter = autoCommitWriter;
 
       new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder()
-          .setNameFormat("Commit-%d " + name)
+          .setNameFormat("Commit-%d " + index)
           .setDaemon(true)
           .build())
           .scheduleAtFixedRate(new Runnable() {
@@ -126,13 +128,13 @@
                   autoCommitWriter.commit();
                 }
               } catch (IOException e) {
-                log.error("Error committing " + name + " Lucene index", e);
+                log.error("Error committing " + index + " Lucene index", e);
               } catch (OutOfMemoryError e) {
-                log.error("Error committing " + name + " Lucene index", e);
+                log.error("Error committing " + index + " Lucene index", e);
                 try {
                   autoCommitWriter.close();
                 } catch (IOException e2) {
-                  log.error("SEVERE: Error closing " + name
+                  log.error("SEVERE: Error closing " + index
                       + " Lucene index  after OOM; index may be corrupted.", e);
                 }
               }
@@ -181,7 +183,7 @@
 
   @Override
   public void markReady(boolean ready) throws IOException {
-    setReady(sitePaths, schema.getVersion(), ready);
+    setReady(sitePaths, name, schema.getVersion(), ready);
   }
 
   @Override
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java
index ad53493..6f0df0f 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.gerrit.lucene.LuceneChangeIndex.ID_SORT_FIELD;
 import static com.google.gerrit.lucene.LuceneChangeIndex.UPDATED_SORT_FIELD;
+import static com.google.gerrit.server.index.change.ChangeSchemaDefinitions.NAME;
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.SitePaths;
@@ -57,10 +58,11 @@
       Schema<ChangeData> schema,
       SitePaths sitePaths,
       Directory dir,
-      String name,
+      String subIndex,
       GerritIndexWriterConfig writerConfig,
       SearcherFactory searcherFactory) throws IOException {
-    super(schema, sitePaths, dir, name, writerConfig, searcherFactory);
+    super(schema, sitePaths, dir, NAME, subIndex, writerConfig,
+        searcherFactory);
   }
 
   @Override
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java
new file mode 100644
index 0000000..f43e385
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.lucene;
+
+import com.google.common.primitives.Ints;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.IOException;
+
+class GerritIndexStatus {
+  private static final String SECTION = "index";
+  private static final String KEY_READY = "ready";
+
+  private final FileBasedConfig cfg;
+
+  GerritIndexStatus(SitePaths sitePaths)
+      throws ConfigInvalidException, IOException {
+    cfg = new FileBasedConfig(
+        sitePaths.index_dir.resolve("gerrit_index.config").toFile(),
+        FS.detect());
+    cfg.load();
+    convertLegacyConfig();
+  }
+
+  void setReady(String indexName, int version, boolean ready) {
+    cfg.setBoolean(SECTION, indexDirName(indexName, version), KEY_READY, ready);
+  }
+
+  boolean getReady(String indexName, int version) {
+    return cfg.getBoolean(SECTION, indexDirName(indexName, version), KEY_READY,
+        false);
+  }
+
+  void save() throws IOException {
+    cfg.save();
+  }
+
+  private void convertLegacyConfig() throws IOException {
+    boolean dirty = false;
+    // Convert legacy [index "25"] to modern [index "changes_0025"].
+    for (String subsection : cfg.getSubsections(SECTION)) {
+      Integer v = Ints.tryParse(subsection);
+      if (v != null) {
+        String ready = cfg.getString(SECTION, subsection, KEY_READY);
+        if (ready != null) {
+          dirty = false;
+          cfg.unset(SECTION, subsection, KEY_READY);
+          cfg.setString(SECTION,
+              indexDirName(ChangeSchemaDefinitions.NAME, v), KEY_READY, ready);
+        }
+      }
+    }
+    if (dirty) {
+      cfg.save();
+    }
+  }
+
+  private static String indexDirName(String indexName, int version) {
+    return String.format("%s_%04d", indexName, version);
+  }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index f4fa0cb..275fa48 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -126,7 +126,6 @@
     return QueryBuilder.intTerm(LEGACY_ID.getName(), id.get());
   }
 
-  private final SitePaths sitePaths;
   private final FillArgs fillArgs;
   private final ListeningExecutorService executor;
   private final Provider<ReviewDb> db;
@@ -145,7 +144,6 @@
       ChangeData.Factory changeDataFactory,
       FillArgs fillArgs,
       @Assisted Schema<ChangeData> schema) throws IOException {
-    this.sitePaths = sitePaths;
     this.fillArgs = fillArgs;
     this.executor = executor;
     this.db = db;
@@ -252,9 +250,9 @@
 
   @Override
   public void markReady(boolean ready) throws IOException {
-    // Do not delegate to ChangeSubIndex#markReady, since changes have an
-    // additional level of directory nesting.
-    AbstractLuceneIndex.setReady(sitePaths, schema.getVersion(), ready);
+    // Arbitrary done on open index, as ready bit is set
+    // per index and not sub index
+    openIndex.markReady(ready);
   }
 
   private Sort getSort() {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index 6ccf829..b49a9f2 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -34,8 +34,6 @@
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -76,23 +74,6 @@
         prefix, schema.getVersion()));
   }
 
-  static FileBasedConfig loadGerritIndexConfig(SitePaths sitePaths)
-      throws ConfigInvalidException, IOException {
-    FileBasedConfig cfg = new FileBasedConfig(
-        sitePaths.index_dir.resolve("gerrit_index.config").toFile(),
-        FS.detect());
-    cfg.load();
-    return cfg;
-  }
-
-  static void setReady(Config cfg, int version, boolean ready) {
-    cfg.setBoolean("index", Integer.toString(version), "ready", ready);
-  }
-
-  private static boolean getReady(Config cfg, int version) {
-    return cfg.getBoolean("index", Integer.toString(version), "ready", false);
-  }
-
   private final SitePaths sitePaths;
   private final Map<String, IndexDefinition<?, ?, ?>> defs;
   private final Map<String, OnlineReindexer<?, ?, ?>> reindexers;
@@ -120,9 +101,9 @@
 
   @Override
   public void start() {
-    FileBasedConfig cfg;
+    GerritIndexStatus cfg;
     try {
-      cfg = loadGerritIndexConfig(sitePaths);
+      cfg = new GerritIndexStatus(sitePaths);
     } catch (ConfigInvalidException | IOException e) {
       throw fail(e);
     }
@@ -140,7 +121,7 @@
   }
 
   private <K, V, I extends Index<K, V>> void initIndex(
-      IndexDefinition<K, V, I> def, FileBasedConfig cfg) {
+      IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
     TreeMap<Integer, Version<V>> versions = scanVersions(def, cfg);
     // Search from the most recent ready version.
     // Write to the most recent ready version and the most recent version.
@@ -179,8 +160,7 @@
       }
     }
 
-    // TODO: include index name.
-    markNotReady(cfg, versions.values(), write);
+    markNotReady(cfg, def.getName(), versions.values(), write);
 
     int latest = write.get(0).version;
     if (onlineUpgrade && latest != search.version) {
@@ -245,7 +225,7 @@
   }
 
   private <K, V, I extends Index<K, V>> TreeMap<Integer, Version<V>>
-      scanVersions(IndexDefinition<K, V, I> def, Config cfg) {
+      scanVersions(IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
     TreeMap<Integer, Version<V>> versions = Maps.newTreeMap();
     for (Schema<V> schema : def.getSchemas().values()) {
       // This part is Lucene-specific.
@@ -255,7 +235,8 @@
         log.warn("Not a directory: %s", p.toAbsolutePath());
       }
       int v = schema.getVersion();
-      versions.put(v, new Version<>(schema, v, isDir, getReady(cfg, v)));
+      versions.put(v, new Version<>(
+          schema, v, isDir, cfg.getReady(def.getName(), v)));
     }
 
     String prefix = def.getName() + "_";
@@ -274,7 +255,8 @@
           continue;
         }
         if (!versions.containsKey(v)) {
-          versions.put(v, new Version<V>(null, v, true, getReady(cfg, v)));
+          versions.put(v, new Version<V>(
+              null, v, true, cfg.getReady(def.getName(), v)));
         }
       }
     } catch (IOException e) {
@@ -283,12 +265,12 @@
     return versions;
   }
 
-  private <V> void markNotReady(FileBasedConfig cfg, Iterable<Version<V>> versions,
-      Collection<Version<V>> inUse) {
+  private <V> void markNotReady(GerritIndexStatus cfg, String name,
+      Iterable<Version<V>> versions, Collection<Version<V>> inUse) {
     boolean dirty = false;
     for (Version<V> v : versions) {
       if (!inUse.contains(v) && v.exists) {
-        setReady(cfg, v.version, false);
+        cfg.setReady(name, v.version, false);
         dirty = true;
       }
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
index 28b7b17..77c466e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -59,9 +59,8 @@
 
     IndexType type = index.select("Type", "type", IndexType.LUCENE);
     for (SchemaDefinitions<?> def : IndexModule.ALL_SCHEMA_DEFS) {
-      // TODO(dborowitz): Totally broken for non-change indexes.
       AbstractLuceneIndex.setReady(
-          site, def.getLatest().getVersion(), true);
+          site, def.getName(), def.getLatest().getVersion(), true);
     }
     if ((site.isNew || isEmptySite()) && type == IndexType.LUCENE) {
       // Do nothing
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index fb7bfad..a081197 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -327,6 +327,7 @@
     return effectiveGroups;
   }
 
+  @SuppressWarnings("deprecation")
   @Override
   public Set<Change.Id> getStarredChanges() {
     if (starredChanges == null) {
@@ -354,6 +355,7 @@
     starredChanges = null;
   }
 
+  @SuppressWarnings("deprecation")
   public void asyncStarredChanges() {
     if (starredChanges == null && starredChangesUtil != null) {
       starredQuery = starredChangesUtil.queryFromIndex(accountId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 0e2f811..b8bd905 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -16,10 +16,10 @@
 
 import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
 
+import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ChildProjectApi;
-import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.PutDescriptionInput;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index efa6174..7984c76b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -1503,8 +1503,8 @@
   private void selectNewAndReplacedChangesFromMagicBranch() {
     newChanges = Lists.newArrayList();
 
-    SetMultimap<ObjectId, Ref> existing = changeRefsById();
-    GroupCollector groupCollector = GroupCollector.create(refsById, db, psUtil,
+    SetMultimap<ObjectId, Ref> existing = HashMultimap.create();
+    GroupCollector groupCollector = GroupCollector.create(changeRefsById(), db, psUtil,
         notesFactory, project.getNameKey());
 
     rp.getRevWalk().reset();
@@ -1525,6 +1525,7 @@
       } else {
         markHeadsAsUninteresting(
             rp.getRevWalk(),
+            existing,
             magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
       }
 
@@ -1681,15 +1682,23 @@
     }
   }
 
-  private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) {
+  private void markHeadsAsUninteresting(
+      final RevWalk walk,
+      SetMultimap<ObjectId, Ref> existing,
+      @Nullable String forRef) {
     for (Ref ref : allRefs.values()) {
-      if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef))
-          && ref.getObjectId() != null) {
+      if (ref.getObjectId() == null) {
+        continue;
+      } else if (ref.getName().startsWith(REFS_CHANGES)) {
+        existing.put(ref.getObjectId(), ref);
+      } else if (ref.getName().startsWith(R_HEADS)
+          || (forRef != null && forRef.equals(ref.getName()))) {
         try {
-          rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
+          walk.markUninteresting(walk.parseCommit(ref.getObjectId()));
         } catch (IOException e) {
           log.warn(String.format("Invalid ref %s in %s",
               ref.getName(), project.getName()), e);
+          continue;
         }
       }
     }
@@ -2332,11 +2341,11 @@
       if (!(parsedObject instanceof RevCommit)) {
         return;
       }
+      SetMultimap<ObjectId, Ref> existing = HashMultimap.create();
       walk.markStart((RevCommit)parsedObject);
-      markHeadsAsUninteresting(walk, cmd.getRefName());
-      Set<ObjectId> existing = changeRefsById().keySet();
+      markHeadsAsUninteresting(walk, existing, cmd.getRefName());
       for (RevCommit c; (c = walk.next()) != null;) {
-        if (existing.contains(c)) {
+        if (existing.keySet().contains(c)) {
           continue;
         } else if (!validCommit(walk, ctl, cmd, c)) {
           break;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index ce34a53..1e3fdbe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
index 2aa2bdb..8ee1ced 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
@@ -17,14 +17,23 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.base.Stopwatch;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
 
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.util.io.NullOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.OutputStream;
 import java.io.PrintWriter;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public abstract class SiteIndexer<K, V, I extends Index<K, V>> {
+  private static final Logger log = LoggerFactory.getLogger(SiteIndexer.class);
+
   public static class Result {
     private final long elapsedNanos;
     private final boolean success;
@@ -73,4 +82,60 @@
   }
 
   public abstract Result indexAll(I index);
+
+  protected final void addErrorListener(ListenableFuture<?> future,
+      String desc, ProgressMonitor progress, AtomicBoolean ok) {
+    future.addListener(
+        new ErrorListener(future, desc, progress, ok),
+        MoreExecutors.directExecutor());
+  }
+
+  private static class ErrorListener implements Runnable {
+    private final ListenableFuture<?> future;
+    private final String desc;
+    private final ProgressMonitor progress;
+    private final AtomicBoolean ok;
+
+    private ErrorListener(ListenableFuture<?> future, String desc,
+        ProgressMonitor progress, AtomicBoolean ok) {
+      this.future = future;
+      this.desc = desc;
+      this.progress = progress;
+      this.ok = ok;
+    }
+
+    @Override
+    public void run() {
+      try {
+        future.get();
+      } catch (ExecutionException | InterruptedException e) {
+        fail(e);
+      } catch (RuntimeException e) {
+        failAndThrow(e);
+      } catch (Error e) {
+        // Can't join with RuntimeException because "RuntimeException |
+        // Error" becomes Throwable, which messes with signatures.
+        failAndThrow(e);
+      } finally {
+        synchronized (progress) {
+          progress.update(1);
+        }
+      }
+    }
+
+    private void fail(Throwable t) {
+      log.error("Failed to index " + desc, t);
+      ok.set(false);
+    }
+
+    private void failAndThrow(RuntimeException e) {
+      fail(e);
+      throw e;
+    }
+
+    private void failAndThrow(Error e) {
+      fail(e);
+      throw e;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
new file mode 100644
index 0000000..cb7b3ef
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.IndexDefinition;
+
+public interface AccountIndex extends Index<Account.Id, AccountState> {
+  public interface Factory extends
+      IndexDefinition.IndexFactory<Account.Id, AccountState, AccountIndex> {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
new file mode 100644
index 0000000..9f4cca8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.inject.Singleton;
+
+@Singleton
+public class AccountIndexCollection extends
+    IndexCollection<Account.Id, AccountState, AccountIndex> {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java
new file mode 100644
index 0000000..ea16e13
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.IndexDefinition;
+import com.google.inject.Inject;
+
+public class AccountIndexDefinition
+    extends IndexDefinition<Account.Id, AccountState, AccountIndex> {
+
+  @Inject
+  AccountIndexDefinition(
+      AccountIndexCollection indexCollection,
+      AccountIndex.Factory indexFactory,
+      AllAccountsIndexer allAccountsIndexer) {
+    super(AccountSchemaDefinitions.INSTANCE, indexCollection, indexFactory,
+        allAccountsIndexer);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
new file mode 100644
index 0000000..0c5af2c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index.account;
+
+import static com.google.gerrit.server.index.SchemaUtil.schema;
+
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.SchemaDefinitions;
+
+public class AccountSchemaDefinitions extends SchemaDefinitions<AccountState> {
+  static final Schema<AccountState> V1 = schema(
+      AccountField.ID,
+      AccountField.ACTIVE,
+      AccountField.EMAIL,
+      AccountField.EXTERNAL_ID,
+      AccountField.NAME_PART,
+      AccountField.REGISTERED,
+      AccountField.USERNAME);
+
+  public static final AccountSchemaDefinitions INSTANCE =
+      new AccountSchemaDefinitions();
+
+  private AccountSchemaDefinitions() {
+    super("accounts", AccountState.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemas.java
deleted file mode 100644
index 1c94706..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemas.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.index.account;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.index.FieldDef;
-import com.google.gerrit.server.index.Schema;
-import com.google.gerrit.server.index.SchemaUtil;
-
-import java.util.Collection;
-
-public class AccountSchemas {
-  static final Schema<AccountState> V1 = schema(
-      AccountField.ID,
-      AccountField.ACTIVE,
-      AccountField.EMAIL,
-      AccountField.EXTERNAL_ID,
-      AccountField.NAME_PART,
-      AccountField.REGISTERED,
-      AccountField.USERNAME);
-
-  private static Schema<AccountState> schema(
-      Collection<FieldDef<AccountState, ?>> fields) {
-    return new Schema<>(ImmutableList.copyOf(fields));
-  }
-
-  @SafeVarargs
-  private static Schema<AccountState> schema(
-      FieldDef<AccountState, ?>... fields) {
-    return schema(ImmutableList.copyOf(fields));
-  }
-
-  public static final ImmutableMap<Integer, Schema<AccountState>> ALL =
-      SchemaUtil.schemasFromClass(AccountSchemas.class, AccountState.class);
-
-  public static Schema<AccountState> get(int version) {
-    Schema<AccountState> schema = ALL.get(version);
-    checkArgument(schema != null, "Unrecognized schema version: %s", version);
-    return schema;
-  }
-
-  public static Schema<AccountState> getLatest() {
-    return Iterables.getLast(ALL.values());
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
new file mode 100644
index 0000000..1c008b46
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
@@ -0,0 +1,138 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index.account;
+
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.SiteIndexer;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Singleton
+public class AllAccountsIndexer
+    extends SiteIndexer<Account.Id, AccountState, AccountIndex> {
+  private static final Logger log =
+      LoggerFactory.getLogger(AllAccountsIndexer.class);
+
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ListeningExecutorService executor;
+  private final AccountCache accountCache;
+
+  @Inject
+  AllAccountsIndexer(
+      SchemaFactory<ReviewDb> schemaFactory,
+      @IndexExecutor(BATCH) ListeningExecutorService executor,
+      AccountCache accountCache) {
+    this.schemaFactory = schemaFactory;
+    this.executor = executor;
+    this.accountCache = accountCache;
+  }
+
+  @Override
+  public SiteIndexer.Result indexAll(final AccountIndex index) {
+    ProgressMonitor progress =
+        new TextProgressMonitor(new PrintWriter(progressOut));
+    progress.start(2);
+    Stopwatch sw = Stopwatch.createStarted();
+    List<Account.Id> ids;
+    try {
+      ids = collectAccounts(progress);
+    } catch (OrmException e) {
+      log.error("Error collecting accounts", e);
+      return new Result(sw, false, 0, 0);
+    }
+    return reindexAccounts(index, ids, progress);
+  }
+
+  private SiteIndexer.Result reindexAccounts(final AccountIndex index,
+      List<Account.Id> ids, ProgressMonitor progress) {
+    progress.beginTask("Reindexing accounts", ids.size());
+    List<ListenableFuture<?>> futures = new ArrayList<>(ids.size());
+    AtomicBoolean ok = new AtomicBoolean(true);
+    final AtomicInteger done = new AtomicInteger();
+    final AtomicInteger failed = new AtomicInteger();
+    Stopwatch sw = Stopwatch.createStarted();
+    for (final Account.Id id : ids) {
+      final String desc = "account " + id;
+      ListenableFuture<?> future = executor.submit(
+          new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+              try {
+                accountCache.evict(id);
+                index.replace(accountCache.get(id));
+                if (verboseWriter != null) {
+                  verboseWriter.println("Reindexed " + desc);
+                }
+                done.incrementAndGet();
+              } catch (Exception e) {
+                failed.incrementAndGet();
+                throw e;
+              }
+              return null;
+            }
+          });
+      addErrorListener(future, desc, progress, ok);
+      futures.add(future);
+    }
+
+    try {
+      Futures.successfulAsList(futures).get();
+    } catch (ExecutionException | InterruptedException e) {
+      log.error("Error waiting on account futures", e);
+      return new Result(sw, false, 0, 0);
+    }
+
+    progress.endTask();
+    return new Result(sw, ok.get(), done.get(), failed.get());
+  }
+
+  private List<Account.Id> collectAccounts(ProgressMonitor progress)
+      throws OrmException {
+    progress.beginTask("Collecting accounts", ProgressMonitor.UNKNOWN);
+    List<Account.Id> ids = new ArrayList<>();
+    try (ReviewDb db = schemaFactory.open()) {
+      for (Account account : db.accounts().all()) {
+        ids.add(account.getId());
+      }
+    }
+    progress.endTask();
+    return ids;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index c336f4e..3ca7c6a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -27,7 +27,6 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -152,43 +151,11 @@
     final AtomicBoolean ok = new AtomicBoolean(true);
 
     for (final Project.NameKey project : projects) {
-      final ListenableFuture<?> future = executor.submit(reindexProject(
+      ListenableFuture<?> future = executor.submit(reindexProject(
           indexerFactory.create(executor, index), project, doneTask, failedTask,
           verboseWriter));
+      addErrorListener(future, "project " + project, projTask, ok);
       futures.add(future);
-      future.addListener(new Runnable() {
-        @Override
-        public void run() {
-          try {
-            future.get();
-          } catch (ExecutionException | InterruptedException e) {
-            fail(project, e);
-          } catch (RuntimeException e) {
-            failAndThrow(project, e);
-          } catch (Error e) {
-            // Can't join with RuntimeException because "RuntimeException |
-            // Error" becomes Throwable, which messes with signatures.
-            failAndThrow(project, e);
-          } finally {
-            projTask.update(1);
-          }
-        }
-
-        private void fail(Project.NameKey project, Throwable t) {
-          log.error("Failed to index project " + project, t);
-          ok.set(false);
-        }
-
-        private void failAndThrow(Project.NameKey project, RuntimeException e) {
-          fail(project, e);
-          throw e;
-        }
-
-        private void failAndThrow(Project.NameKey project, Error e) {
-          fail(project, e);
-          throw e;
-        }
-      }, MoreExecutors.directExecutor());
     }
 
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 8fb9000..1e59cdb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -70,10 +70,11 @@
   static final Schema<ChangeData> V29 =
       schema(V28, ChangeField.HASHTAG_CASE_AWARE);
 
+  public static final String NAME = "changes";
   public static final ChangeSchemaDefinitions INSTANCE =
       new ChangeSchemaDefinitions();
 
   private ChangeSchemaDefinitions() {
-    super("changes", ChangeData.class);
+    super(NAME, ChangeData.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index efd0a4e..55b8556 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -26,6 +26,7 @@
 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.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -35,7 +36,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 
@@ -76,7 +76,7 @@
 
   @AutoValue
   public abstract static class LoadHandle implements AutoCloseable {
-    public static LoadHandle create(RevWalk walk, ObjectId id) {
+    public static LoadHandle create(ChangeNotesRevWalk walk, ObjectId id) {
       return new AutoValue_AbstractChangeNotes_LoadHandle(
           checkNotNull(walk), id != null ? id.copy() : null);
     }
@@ -85,7 +85,7 @@
       return new AutoValue_AbstractChangeNotes_LoadHandle(null, null);
     }
 
-    @Nullable public abstract RevWalk walk();
+    @Nullable public abstract ChangeNotesRevWalk walk();
     @Nullable public abstract ObjectId id();
 
     @Override
@@ -145,7 +145,7 @@
   }
 
   protected LoadHandle openHandle(Repository repo) throws IOException {
-    return LoadHandle.create(new RevWalk(repo), readRef(repo));
+    return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), readRef(repo));
   }
 
   public T reload() throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 24256bf..b685485 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -69,7 +69,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -642,7 +641,8 @@
         return super.openHandle(repo); // May be null in tests.
       }
       repo.scanForRepoChanges();
-      return LoadHandle.create(new RevWalk(repo), newState.getChangeMetaId());
+      return LoadHandle.create(
+          ChangeNotesCommit.newRevWalk(repo), newState.getChangeMetaId());
     } catch (NoSuchChangeException e) {
       return super.openHandle(repo);
     } catch (OrmException | ConfigInvalidException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
new file mode 100644
index 0000000..5d28454
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Commit implementation with some optimizations for change notes parsing.
+ * <p>
+ * <ul>
+ *   <li>Caches the result of {@link #getFooterLines()}, which is
+ *     otherwise very wasteful with allocations.</li>
+ * </ul>
+ */
+public class ChangeNotesCommit extends RevCommit {
+  public static ChangeNotesRevWalk newRevWalk(Repository repo) {
+    return new ChangeNotesRevWalk(repo);
+  }
+
+  public static class ChangeNotesRevWalk extends RevWalk {
+    private ChangeNotesRevWalk(Repository repo) {
+      super(repo);
+    }
+
+    @Override
+    protected ChangeNotesCommit createCommit(AnyObjectId id) {
+      return new ChangeNotesCommit(id);
+    }
+
+    @Override
+    public ChangeNotesCommit next() throws MissingObjectException,
+         IncorrectObjectTypeException, IOException {
+      return (ChangeNotesCommit) super.next();
+    }
+
+    @Override
+    public void markStart(RevCommit c) throws MissingObjectException,
+        IncorrectObjectTypeException, IOException {
+      checkArgument(c instanceof ChangeNotesCommit);
+      super.markStart(c);
+    }
+
+    @Override
+    public void markUninteresting(RevCommit c) throws MissingObjectException,
+        IncorrectObjectTypeException, IOException {
+      checkArgument(c instanceof ChangeNotesCommit);
+      super.markUninteresting(c);
+    }
+
+    @Override
+    public ChangeNotesCommit lookupCommit(AnyObjectId id) {
+      return (ChangeNotesCommit) super.lookupCommit(id);
+    }
+
+    @Override
+    public ChangeNotesCommit parseCommit(AnyObjectId id)
+        throws MissingObjectException, IncorrectObjectTypeException,
+        IOException {
+      return (ChangeNotesCommit) super.parseCommit(id);
+    }
+  }
+
+  private ListMultimap<String, String> footerLines;
+
+  public ChangeNotesCommit(AnyObjectId id) {
+    super(id);
+  }
+
+  public List<String> getFooterLineValues(FooterKey key) {
+    if (footerLines == null) {
+      List<FooterLine> src = getFooterLines();
+      footerLines = ArrayListMultimap.create(src.size(), 1);
+      for (FooterLine fl : src) {
+        footerLines.put(fl.getKey().toLowerCase(), fl.getValue());
+      }
+    }
+    return footerLines.get(key.getName().toLowerCase());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 69209b9..604c866 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -62,6 +62,7 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.util.LabelVote;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -73,8 +74,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.FooterKey;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.RawParseUtils;
 
 import java.io.IOException;
@@ -123,7 +122,7 @@
   private final NoteDbMetrics metrics;
   private final Change.Id id;
   private final ObjectId tip;
-  private final RevWalk walk;
+  private final ChangeNotesRevWalk walk;
   private final Repository repo;
   private final Map<PatchSet.Id,
       Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>>> approvals;
@@ -131,7 +130,7 @@
   private final Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet;
 
   ChangeNotesParser(Project.NameKey project, Change.Id changeId, ObjectId tip,
-      RevWalk walk, GitRepositoryManager repoManager,
+      ChangeNotesRevWalk walk, GitRepositoryManager repoManager,
       ChangeNoteUtil noteUtil, NoteDbMetrics metrics)
       throws RepositoryNotFoundException, IOException {
     this.id = changeId;
@@ -163,7 +162,8 @@
     walk.markStart(walk.parseCommit(tip));
 
     try (Timer1.Context timer = metrics.parseLatency.start(CHANGES)) {
-      for (RevCommit commit : walk) {
+      ChangeNotesCommit commit;
+      while ((commit = walk.next()) != null) {
         parse(commit);
       }
       parseNotes();
@@ -203,7 +203,7 @@
     return ImmutableListMultimap.copyOf(changeMessagesByPatchSet);
   }
 
-  private void parse(RevCommit commit) throws ConfigInvalidException {
+  private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
     Timestamp ts =
         new Timestamp(commit.getCommitterIdent().getWhen().getTime());
 
@@ -275,17 +275,17 @@
     if (submitRecords.isEmpty()) {
       // Only parse the most recent set of submit records; any older ones are
       // still there, but not currently used.
-      parseSubmitRecords(commit.getFooterLines(FOOTER_SUBMITTED_WITH));
+      parseSubmitRecords(commit.getFooterLineValues(FOOTER_SUBMITTED_WITH));
       updateTs |= !submitRecords.isEmpty();
     }
 
-    for (String line : commit.getFooterLines(FOOTER_LABEL)) {
+    for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
       parseApproval(psId, accountId, ts, line);
       updateTs = true;
     }
 
     for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
-      for (String line : commit.getFooterLines(state.getFooterKey())) {
+      for (String line : commit.getFooterLineValues(state.getFooterKey())) {
         parseReviewer(state, line);
       }
       // Don't update timestamp when a reviewer was added, matching RevewDb
@@ -299,31 +299,35 @@
     }
   }
 
-  private String parseSubmissionId(RevCommit commit)
+  private String parseSubmissionId(ChangeNotesCommit commit)
       throws ConfigInvalidException {
     return parseOneFooter(commit, FOOTER_SUBMISSION_ID);
   }
 
-  private String parseBranch(RevCommit commit) throws ConfigInvalidException {
+  private String parseBranch(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
     String branch = parseOneFooter(commit, FOOTER_BRANCH);
     return branch != null ? RefNames.fullName(branch) : null;
   }
 
-  private String parseChangeId(RevCommit commit) throws ConfigInvalidException {
+  private String parseChangeId(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
     return parseOneFooter(commit, FOOTER_CHANGE_ID);
   }
 
-  private String parseSubject(RevCommit commit) throws ConfigInvalidException {
+  private String parseSubject(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
     return parseOneFooter(commit, FOOTER_SUBJECT);
   }
 
-  private String parseTopic(RevCommit commit) throws ConfigInvalidException {
+  private String parseTopic(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
     return parseOneFooter(commit, FOOTER_TOPIC);
   }
 
-  private String parseOneFooter(RevCommit commit, FooterKey footerKey)
+  private String parseOneFooter(ChangeNotesCommit commit, FooterKey footerKey)
       throws ConfigInvalidException {
-    List<String> footerLines = commit.getFooterLines(footerKey);
+    List<String> footerLines = commit.getFooterLineValues(footerKey);
     if (footerLines.isEmpty()) {
       return null;
     } else if (footerLines.size() > 1) {
@@ -332,8 +336,8 @@
     return footerLines.get(0);
   }
 
-  private String parseExactlyOneFooter(RevCommit commit, FooterKey footerKey)
-      throws ConfigInvalidException {
+  private String parseExactlyOneFooter(ChangeNotesCommit commit,
+      FooterKey footerKey) throws ConfigInvalidException {
     String line = parseOneFooter(commit, footerKey);
     if (line == null) {
       throw expectedOneFooter(footerKey, Collections.<String> emptyList());
@@ -341,7 +345,7 @@
     return line;
   }
 
-  private ObjectId parseRevision(RevCommit commit)
+  private ObjectId parseRevision(ChangeNotesCommit commit)
       throws ConfigInvalidException {
     String sha = parseOneFooter(commit, FOOTER_COMMIT);
     if (sha == null) {
@@ -377,7 +381,7 @@
     ps.setCreatedOn(ts);
   }
 
-  private void parseGroups(PatchSet.Id psId, RevCommit commit)
+  private void parseGroups(PatchSet.Id psId, ChangeNotesCommit commit)
       throws ConfigInvalidException {
     String groupsStr = parseOneFooter(commit, FOOTER_GROUPS);
     if (groupsStr == null) {
@@ -394,12 +398,14 @@
     ps.setGroups(PatchSet.splitGroups(groupsStr));
   }
 
-  private void parseHashtags(RevCommit commit) throws ConfigInvalidException {
-    // Commits are parsed in reverse order and only the last set of hashtags should be used.
+  private void parseHashtags(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
+    // Commits are parsed in reverse order and only the last set of hashtags
+    // should be used.
     if (hashtags != null) {
       return;
     }
-    List<String> hashtagsLines = commit.getFooterLines(FOOTER_HASHTAGS);
+    List<String> hashtagsLines = commit.getFooterLineValues(FOOTER_HASHTAGS);
     if (hashtagsLines.isEmpty()) {
       return;
     } else if (hashtagsLines.size() > 1) {
@@ -411,9 +417,10 @@
     }
   }
 
-  private void parseTag(RevCommit commit) throws ConfigInvalidException {
+  private void parseTag(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
     tag = null;
-    List<String> tagLines = commit.getFooterLines(FOOTER_TAG);
+    List<String> tagLines = commit.getFooterLineValues(FOOTER_TAG);
     if (tagLines.isEmpty()) {
       return;
     } else if (tagLines.size() == 1) {
@@ -423,9 +430,9 @@
     }
   }
 
-  private Change.Status parseStatus(RevCommit commit)
+  private Change.Status parseStatus(ChangeNotesCommit commit)
       throws ConfigInvalidException {
-    List<String> statusLines = commit.getFooterLines(FOOTER_STATUS);
+    List<String> statusLines = commit.getFooterLineValues(FOOTER_STATUS);
     if (statusLines.isEmpty()) {
       return null;
     } else if (statusLines.size() > 1) {
@@ -439,7 +446,7 @@
     return status.get();
   }
 
-  private PatchSet.Id parsePatchSetId(RevCommit commit)
+  private PatchSet.Id parsePatchSetId(ChangeNotesCommit commit)
       throws ConfigInvalidException {
     String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
     int s = psIdLine.indexOf(' ');
@@ -451,7 +458,7 @@
     return new PatchSet.Id(id, psId);
   }
 
-  private PatchSetState parsePatchSetState(RevCommit commit)
+  private PatchSetState parsePatchSetState(ChangeNotesCommit commit)
       throws ConfigInvalidException {
     String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
     int s = psIdLine.indexOf(' ');
@@ -470,7 +477,7 @@
   }
 
   private ChangeMessage parseChangeMessage(PatchSet.Id psId,
-      Account.Id accountId, RevCommit commit, Timestamp ts) {
+      Account.Id accountId, ChangeNotesCommit commit, Timestamp ts) {
     byte[] raw = commit.getRawBuffer();
     int size = raw.length;
     Charset enc = RawParseUtils.parseEncoding(raw);
@@ -532,7 +539,7 @@
   private void parseNotes()
       throws IOException, ConfigInvalidException {
     ObjectReader reader = walk.getObjectReader();
-    RevCommit tipCommit = walk.parseCommit(tip);
+    ChangeNotesCommit tipCommit = walk.parseCommit(tip);
     revisionNoteMap = RevisionNoteMap.parse(
         noteUtil, id, reader, NoteMap.read(reader, tipCommit), false);
     Map<RevId, RevisionNote> rns = revisionNoteMap.revisionNotes;
@@ -705,7 +712,7 @@
     }
   }
 
-  private Account.Id parseIdent(RevCommit commit)
+  private Account.Id parseIdent(ChangeNotesCommit commit)
       throws ConfigInvalidException {
     // Check if the author name/email is the same as the committer name/email,
     // i.e. was the server ident at the time this commit was made.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index ba824a0..43f58a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -35,7 +35,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 
@@ -169,7 +168,7 @@
       }
       ObjectId draftsId = newState.getDraftIds().get(author);
       repo.scanForRepoChanges();
-      return LoadHandle.create(new RevWalk(repo), draftsId);
+      return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), draftsId);
     } catch (NoSuchChangeException e) {
       return super.openHandle(repo);
     } catch (OrmException | ConfigInvalidException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index a4c10ee..76dd030 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -21,6 +21,8 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
@@ -833,13 +835,29 @@
     return patchSets;
   }
 
-  public void setPatchSets(List<PatchSet> patchSets) {
+  /**
+   * @return patches for the change visible to the current user.
+   * @throws OrmException an error occurred reading the database.
+   */
+  public Collection<PatchSet> visiblePatchSets() throws OrmException {
+    return FluentIterable.from(patchSets()).filter(new Predicate<PatchSet>() {
+      @Override
+      public boolean apply(PatchSet input) {
+        try {
+          return changeControl().isPatchVisible(input, db);
+        } catch (OrmException e) {
+          return false;
+        }
+      }}).toList();
+  }
+
+public void setPatchSets(Collection<PatchSet> patchSets) {
     this.currentPatchSet = null;
     this.patchSets = patchSets;
   }
 
   /**
-   * @return patch set with the given ID, or null if it does not exist.
+   * @return patch with the given ID, or null if it does not exist.
    * @throws OrmException an error occurred reading the database.
    */
   public PatchSet patchSet(PatchSet.Id psId) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 2679f29..230bf31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -660,6 +660,7 @@
     return Predicate.or(p);
   }
 
+  @SuppressWarnings("deprecation")
   private Predicate<ChangeData> starredby(Account.Id who)
       throws QueryParseException {
     return args.getSchema().hasField(ChangeField.STARREDBY)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 83364c3..2e2454d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -141,10 +141,10 @@
     List<Predicate<ChangeData>> r =
         Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
     for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
-      r.add(not(equalsLabelPredicate(args, label, i)));
-      r.add(not(equalsLabelPredicate(args, label, -i)));
+      r.add(equalsLabelPredicate(args, label, i));
+      r.add(equalsLabelPredicate(args, label, -i));
     }
-    return and(r);
+    return not(or(r));
   }
 
   private static Predicate<ChangeData> equalsLabelPredicate(Args args,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 8f1e48f..00ecdb2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -275,14 +275,14 @@
     }
 
     if (includePatchSets) {
-      eventFactory.addPatchSets(db, rw, c, d.patchSets(),
+      eventFactory.addPatchSets(db, rw, c, d.visiblePatchSets(),
           includeApprovals ? d.approvals().asMap() : null,
           includeFiles, d.change(), labelTypes);
     }
 
     if (includeCurrentPatchSet) {
       PatchSet current = d.currentPatchSet();
-      if (current != null) {
+      if (current != null && cc.isPatchVisible(current, d.db())) {
         c.currentPatchSet =
             eventFactory.asPatchSetAttribute(db, rw, d.change(), current);
         eventFactory.addApprovals(c.currentPatchSet,
@@ -302,7 +302,7 @@
     if (includeComments) {
       eventFactory.addComments(c, d.messages());
       if (includePatchSets) {
-        eventFactory.addPatchSets(db, rw, c, d.patchSets(),
+        eventFactory.addPatchSets(db, rw, c, d.visiblePatchSets(),
             includeApprovals ? d.approvals().asMap() : null,
             includeFiles, d.change(), labelTypes);
         for (PatchSetAttribute attribute : c.patchSets) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index dc00eaa..d4d7d19 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -27,19 +28,18 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 public class ChangeNotesParserTest extends AbstractChangeNotesTest {
   private TestRepository<InMemoryRepository> testRepo;
-  private RevWalk walk;
+  private ChangeNotesRevWalk walk;
 
   @Before
   public void setUpTestRepo() throws Exception {
     testRepo = new TestRepository<>(repo);
-    walk = new RevWalk(repo);
+    walk = ChangeNotesCommit.newRevWalk(repo);
   }
 
   @After
@@ -53,16 +53,16 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseFails(writeCommit("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n",
+        + "Patch-set: 1\n",
         new PersonIdent("Change Owner", "owner@example.com",
           serverIdent.getWhen(), serverIdent.getTimeZone())));
     assertParseFails(writeCommit("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n",
+        + "Patch-set: 1\n",
         new PersonIdent("Change Owner", "x@gerrit",
           serverIdent.getWhen(), serverIdent.getTimeZone())));
   }
@@ -73,23 +73,23 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Status: NEW\n"
         + "Subject: This is a test change\n");
     assertParseSucceeds("Update change\n"
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Status: new\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Status: OOPS\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Status: NEW\n"
         + "Status: NEW\n");
   }
@@ -100,23 +100,23 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
-        + "Patch-Set: 1\n");
+        + "Patch-set: 1\n"
+        + "Patch-set: 1\n");
     assertParseSucceeds("Update change\n"
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: x\n");
+        + "Patch-set: x\n");
   }
 
   @Test
@@ -125,7 +125,7 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: Label1=+1\n"
         + "Label: Label2=1\n"
         + "Label: Label3=0\n"
@@ -135,33 +135,33 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: -Label1\n"
         + "Label: -Label1 Other Account <2@gerrit>\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: Label1=X\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: Label1 = 1\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: X+Y\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: Label1 Other Account <2@gerrit>\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: -Label!1\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Label: -Label!1 Other Account <2@gerrit>\n");
   }
 
@@ -171,7 +171,7 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n"
         + "Submitted-with: NOT_READY\n"
         + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
@@ -181,19 +181,19 @@
         + "Submitted-with: NEED: Alternative-Code-Review\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Submitted-with: OOPS\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Submitted-with: NEED: X+Y\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Submitted-with: OK: X+Y: Change Owner <1@gerrit>\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Submitted-with: OK: Code-Review: 1@gerrit\n");
   }
 
@@ -203,12 +203,12 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n"
         + "Submission-id: 1-1453387607626-96fabc25");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Submission-id: 1-1453387607626-96fabc25\n"
         + "Submission-id: 1-1453387901516-5d1e2450");
   }
@@ -219,13 +219,13 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Reviewer: Change Owner <1@gerrit>\n"
         + "CC: Other Account <2@gerrit>\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Reviewer: 1@gerrit\n");
   }
 
@@ -235,19 +235,19 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Topic: Some Topic\n"
         + "Subject: This is a test change\n");
     assertParseSucceeds("Update change\n"
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Topic:\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Topic: Some Topic\n"
         + "Topic: Other Topic");
   }
@@ -258,17 +258,17 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseSucceeds("Update change\n"
         + "\n"
         + "Branch: master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Branch: refs/heads/master\n"
         + "Branch: refs/heads/stable");
   }
@@ -279,11 +279,11 @@
         + "\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: This is a test change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
         + "Change-id: I159532ef4844d7c18f7f3fd37a0b275590d41b1b");
   }
@@ -292,13 +292,13 @@
   public void parseSubject() throws Exception {
     assertParseSucceeds("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
         + "Subject: Some subject of a change\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Subject: Some subject of a change\n"
         + "Subject: Some other subject\n");
   }
@@ -418,21 +418,21 @@
   public void parseTag() throws Exception {
     assertParseSucceeds("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
         + "Subject: Change subject\n"
         + "Tag:\n");
     assertParseSucceeds("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
         + "Subject: Change subject\n"
         + "Tag: jenkins\n");
     assertParseFails("Update change\n"
         + "\n"
-        + "Patch-Set: 1\n"
+        + "Patch-set: 1\n"
         + "Branch: refs/heads/master\n"
         + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
         + "Subject: Change subject\n"
@@ -440,6 +440,16 @@
         + "Tag: jenkins\n");
   }
 
+  @Test
+  public void caseInsensitiveFooters() throws Exception {
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "BRaNch: refs/heads/master\n"
+        + "Change-ID: I577fb248e474018276351785930358ec0450e9f7\n"
+        + "patcH-set: 1\n"
+        + "subject: This is a test change\n");
+  }
+
   private RevCommit writeCommit(String body) throws Exception {
     return writeCommit(body, noteUtil.newIdent(
         changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 44fa6f5..4d80866 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -46,6 +46,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
@@ -1020,7 +1021,7 @@
     RevCommit commitWithComments = commitWithApprovals.getParent(0);
     assertThat(commitWithComments).isNotNull();
 
-    try (RevWalk rw = new RevWalk(repo)) {
+    try (ChangeNotesRevWalk rw = ChangeNotesCommit.newRevWalk(repo)) {
       try (ChangeNotesParser notesWithComments = new ChangeNotesParser(
           project, c.getId(), commitWithComments.copy(), rw, repoManager,
           noteUtil, args.metrics)) {
@@ -1032,7 +1033,7 @@
       }
     }
 
-    try (RevWalk rw = new RevWalk(repo)) {
+    try (ChangeNotesRevWalk rw = ChangeNotesCommit.newRevWalk(repo)) {
       try (ChangeNotesParser notesWithApprovals = new ChangeNotesParser(project,
           c.getId(), commitWithApprovals.copy(), rw, repoManager,
           noteUtil, args.metrics)) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 6e2f9c9..97776f1 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -95,6 +95,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
@@ -536,47 +537,97 @@
   public void byLabel() throws Exception {
     accountManager.authenticate(AuthRequest.forUser("anotheruser"));
     TestRepository<Repo> repo = createProject("repo");
-    ChangeInserter ins = newChange(repo);
-    Change change = insert(repo, ins);
+    ChangeInserter ins = newChange(repo, null, null, null, null);
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    ChangeInserter ins3 = newChange(repo, null, null, null, null);
+    ChangeInserter ins4 = newChange(repo, null, null, null, null);
+    ChangeInserter ins5 = newChange(repo, null, null, null, null);
 
-    gApi.changes().id(change.getId().get()).current()
-      .review(new ReviewInput().label("Code-Review", 1));
+    Change reviewMinus2Change = insert(repo, ins);
+    gApi.changes().id(reviewMinus2Change.getId().get()).current()
+        .review(ReviewInput.reject());
+
+    Change reviewMinus1Change = insert(repo, ins2);
+    gApi.changes().id(reviewMinus1Change.getId().get()).current()
+        .review(ReviewInput.dislike());
+
+    Change noLabelChange = insert(repo, ins3);
+
+    Change reviewPlus1Change = insert(repo, ins4);
+    gApi.changes().id(reviewPlus1Change.getId().get()).current()
+        .review(ReviewInput.recommend());
+
+    Change reviewPlus2Change = insert(repo, ins5);
+    gApi.changes().id(reviewPlus2Change.getId().get()).current()
+        .review(ReviewInput.approve());
+
     Map<String, Short> m = gApi.changes()
-        .id(change.getId().get())
+        .id(reviewPlus1Change.getId().get())
         .reviewer(user.getAccountId().toString())
         .votes();
     assertThat(m).hasSize(1);
     assertThat(m).containsEntry("Code-Review", new Short((short)1));
 
-    assertQuery("label:Code-Review=-2");
-    assertQuery("label:Code-Review-2");
-    assertQuery("label:Code-Review=-1");
-    assertQuery("label:Code-Review-1");
-    assertQuery("label:Code-Review=0");
-    assertQuery("label:Code-Review=+1", change);
-    assertQuery("label:Code-Review=1", change);
-    assertQuery("label:Code-Review+1", change);
-    assertQuery("label:Code-Review=+2");
-    assertQuery("label:Code-Review=2");
-    assertQuery("label:Code-Review+2");
+    Map<Integer, Change> changes = new LinkedHashMap<>(5);
+    changes.put(2, reviewPlus2Change);
+    changes.put(1, reviewPlus1Change);
+    changes.put(0, noLabelChange);
+    changes.put(-1, reviewMinus1Change);
+    changes.put(-2, reviewMinus2Change);
 
-    assertQuery("label:Code-Review>=0", change);
-    assertQuery("label:Code-Review>0", change);
-    assertQuery("label:Code-Review>=1", change);
-    assertQuery("label:Code-Review>1");
-    assertQuery("label:Code-Review>=2");
+    assertQuery("label:Code-Review=-2", reviewMinus2Change);
+    assertQuery("label:Code-Review-2", reviewMinus2Change);
+    assertQuery("label:Code-Review=-1", reviewMinus1Change);
+    assertQuery("label:Code-Review-1", reviewMinus1Change);
+    assertQuery("label:Code-Review=0", noLabelChange);
+    assertQuery("label:Code-Review=+1", reviewPlus1Change);
+    assertQuery("label:Code-Review=1", reviewPlus1Change);
+    assertQuery("label:Code-Review+1", reviewPlus1Change);
+    assertQuery("label:Code-Review=+2", reviewPlus2Change);
+    assertQuery("label:Code-Review=2", reviewPlus2Change);
+    assertQuery("label:Code-Review+2", reviewPlus2Change);
 
-    assertQuery("label: Code-Review<=2", change);
-    assertQuery("label: Code-Review<2", change);
-    assertQuery("label: Code-Review<=1", change);
-    assertQuery("label:Code-Review<1");
-    assertQuery("label:Code-Review<=0");
+    assertQuery("label:Code-Review>-3", codeReviewInRange(changes, -2, 2));
+    assertQuery("label:Code-Review>=-2", codeReviewInRange(changes, -2, 2));
+    assertQuery("label:Code-Review>-2", codeReviewInRange(changes, -1, 2));
+    assertQuery("label:Code-Review>=-1", codeReviewInRange(changes, -1, 2));
+    assertQuery("label:Code-Review>-1", codeReviewInRange(changes, 0, 2));
+    assertQuery("label:Code-Review>=0", codeReviewInRange(changes, 0, 2));
+    assertQuery("label:Code-Review>0", codeReviewInRange(changes, 1, 2));
+    assertQuery("label:Code-Review>=1", codeReviewInRange(changes, 1, 2));
+    assertQuery("label:Code-Review>1", reviewPlus2Change);
+    assertQuery("label:Code-Review>=2", reviewPlus2Change);
+    assertQuery("label:Code-Review>2");
+
+    assertQuery("label:Code-Review<=2", codeReviewInRange(changes, -2, 2));
+    assertQuery("label:Code-Review<2", codeReviewInRange(changes, -2, 1));
+    assertQuery("label:Code-Review<=1", codeReviewInRange(changes, -2, 1));
+    assertQuery("label:Code-Review<1", codeReviewInRange(changes, -2, 0));
+    assertQuery("label:Code-Review<=0", codeReviewInRange(changes, -2, 0));
+    assertQuery("label:Code-Review<0", codeReviewInRange(changes, -2, -1));
+    assertQuery("label:Code-Review<=-1", codeReviewInRange(changes, -2, -1));
+    assertQuery("label:Code-Review<-1", reviewMinus2Change);
+    assertQuery("label:Code-Review<=-2", reviewMinus2Change);
+    assertQuery("label:Code-Review<-2");
 
     assertQuery("label:Code-Review=+1,anotheruser");
-    assertQuery("label:Code-Review=+1,user", change);
-    assertQuery("label:Code-Review=+1,user=user", change);
-    assertQuery("label:Code-Review=+1,Administrators", change);
-    assertQuery("label:Code-Review=+1,group=Administrators", change);
+    assertQuery("label:Code-Review=+1,user", reviewPlus1Change);
+    assertQuery("label:Code-Review=+1,user=user", reviewPlus1Change);
+    assertQuery("label:Code-Review=+1,Administrators", reviewPlus1Change);
+    assertQuery("label:Code-Review=+1,group=Administrators", reviewPlus1Change);
+  }
+
+  private Change[] codeReviewInRange(Map<Integer, Change> changes, int start,
+      int end) {
+    int size = 0;
+    Change[] range = new Change[end - start + 1];
+    for (int i : changes.keySet()) {
+      if (i >= start && i <= end) {
+        range[size] = changes.get(i);
+        size++;
+      }
+    }
+    return range;
   }
 
   private String createGroup(String name, String owner) throws Exception {
diff --git a/plugins/replication b/plugins/replication
index d5cd908..b80cd81 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit d5cd908c0d7938a5d253d49f68ed352bbc7449cf
+Subproject commit b80cd8168ae8ba065c0186b1ddfec366a6368cb6
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index affbd06..f23d50b 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -301,7 +301,14 @@
 
     getChangeRevisionActions: function(changeNum, patchNum) {
       return this.fetchJSON(
-          this.getChangeActionURL(changeNum, patchNum, '/actions'));
+          this.getChangeActionURL(changeNum, patchNum, '/actions')).then(
+              function(revisionActions) {
+                // The rebase button on change screen is always enabled.
+                if (revisionActions.rebase) {
+                  revisionActions.rebase.enabled = true;
+                }
+                return revisionActions;
+              });
     },
 
     getReviewedFiles: function(changeNum, patchNum) {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 752d718..a5994c9 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -222,5 +222,20 @@
               element._specialFilePathCompare),
           ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
     });
+
+    test('rebase always enabled', function(done) {
+      var resolveFetchJSON;
+      var fetchJSONStub = sinon.stub(element, 'fetchJSON').returns(
+          new Promise(function(resolve) {
+            resolveFetchJSON = resolve;
+          }));
+      element.getChangeRevisionActions('42', '1337').then(
+          function(response) {
+            assert.isTrue(response.rebase.enabled);
+            fetchJSONStub.restore();
+            done();
+          });
+      resolveFetchJSON({rebase:{}});
+    });
   });
 </script>