Merge "Asynchronous diff rendering"
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 2a58041..0b275c1 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -633,7 +633,7 @@
 @Singleton
 public class SampleOperator
     implements ChangeQueryBuilder.ChangeOperatorFactory {
-  public static class MyPredicate extends OperatorPredicate<ChangeData> {
+  public static class MyPredicate extends OperatorChangePredicate<ChangeData> {
     ...
   }
 
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 73bb72d..59fb282 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -7,19 +7,71 @@
 [[account-endpoints]]
 == Account Endpoints
 
-[[suggest-account]]
-=== Suggest Account
+[[query-account]]
+=== Query Account
 --
 'GET /accounts/'
 --
 
-Suggest users for a given query `q` and result limit `n`. If result
-limit is not passed, then the default 10 is used. Returns a list of
-matching link:#account-info[AccountInfo] entities.
+Queries accounts visible to the caller. The
+link:user-search-accounts.html#_search_operators[query string] must be
+provided by the `q` parameter. The `n` parameter can be used to limit
+the returned results.
+
+As result a list of link:#account-info[AccountInfo] entities is
+returned.
 
 .Request
 ----
-  GET /accounts/?q=John HTTP/1.0
+  GET /accounts/?q=name:John+email:example.com&n=2 HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "_account_id": 1000096,
+    },
+    {
+      "_account_id": 1001439,
+      "_more_accounts": true
+    }
+  ]
+----
+
+If the number of accounts matching the query exceeds either the
+internal limit or a supplied `n` query parameter, the last account
+object has a `_more_accounts: true` JSON field set.
+
+The `S` or `start` query parameter can be supplied to skip a number
+of accounts from the list.
+
+Additional fields can be obtained by adding `o` parameters, each
+option slows down the query response time to the client so they are
+generally disabled by default. Optional fields are:
+
+[[details]]
+--
+* `DETAILS`: Includes full name, preferred email, username and avatars
+for each account.
+--
+
+[[suggest-account]]
+To get account suggestions set the parameter `suggest` and provide the
+typed substring as query `q`. If a result limit `n` is not specified,
+then the default 10 is used.
+
+For account suggestions link:#details[account details] are always
+returned.
+
+.Request
+----
+  GET /accounts/?suggest&q=John HTTP/1.0
 ----
 
 .Response
@@ -1879,7 +1931,7 @@
 
 [[account-detail-info]]
 === AccountDetailInfo
-The `AccountDetailInfo` entity contains detailled information about an
+The `AccountDetailInfo` entity contains detailed information about an
 account.
 
 `AccountDetailInfo` has the same fields as link:#account-info[
@@ -1898,20 +1950,29 @@
 The `AccountInfo` entity contains information about an account.
 
 [options="header",cols="1,^1,5"]
-|===========================
-|Field Name    ||Description
-|`_account_id` ||The numeric ID of the account.
-|`name`        |optional|The full name of the user. +
-Only set if link:rest-api-changes.html#detailed-accounts[detailed
-account information] is requested.
-|`email`       |optional|
+|=============================
+|Field Name      ||Description
+|`_account_id`   ||The numeric ID of the account.
+|`name`          |optional|The full name of the user. +
+Only set if detailed account information is requested. +
+See option link:rest-api-changes.html#detailed-accounts[
+DETAILED_ACCOUNTS] for change queries +
+and option link:#detaileds[DETAILS] for account queries.
+|`email`         |optional|
 The email address the user prefers to be contacted through. +
-Only set if link:rest-api-changes.html#detailed-accounts[detailed
-account information] is requested.
-|`username`    |optional|The username of the user. +
-Only set if link:rest-api-changes.html#detailed-accounts[detailed
-account information] is requested.
-|===========================
+Only set if detailed account information is requested. +
+See option link:rest-api-changes.html#detailed-accounts[
+DETAILED_ACCOUNTS] for change queries +
+and option link:#detaileds[DETAILS] for account queries.
+|`username`      |optional|The username of the user. +
+Only set if detailed account information is requested. +
+See option link:rest-api-changes.html#detailed-accounts[
+DETAILED_ACCOUNTS] for change queries +
+and option link:#detaileds[DETAILS] for account queries.
+|`_more_accounts`|optional, not set if `false`|
+Whether the query would deliver more results if not limited. +
+Only set on the last account that is returned.
+|=============================
 
 [[account-input]]
 === AccountInput
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 039a05f..aad6113 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -353,6 +353,7 @@
       "current_revision": "184ebe53805e102605d11f6b143486d15c23a09c",
       "revisions": {
         "184ebe53805e102605d11f6b143486d15c23a09c": {
+          "kind": "REWORK",
           "_number": 1,
           "ref": "refs/changes/97/97/1",
           "fetch": {
@@ -936,6 +937,7 @@
     "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
     "revisions": {
       "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
+        "kind": "REWORK",
         "_number": 2,
         "ref": "refs/changes/99/4799/2",
         "fetch": {
@@ -1314,6 +1316,7 @@
       "current_revision": "9adb9f4c7b40eeee0646e235de818d09164d7379",
       "revisions": {
         "9adb9f4c7b40eeee0646e235de818d09164d7379": {
+          "kind": "REWORK",
           "_number": 1,
           "created": "2015-05-01 15:39:57.979000000",
           "uploader": {
@@ -1432,6 +1435,7 @@
       "current_revision": "1bd7c12a38854a2c6de426feec28800623f492c4",
       "revisions": {
         "1bd7c12a38854a2c6de426feec28800623f492c4": {
+          "kind": "REWORK",
           "_number": 1,
           "created": "2015-05-01 15:39:57.979000000",
           "uploader": {
@@ -2659,12 +2663,14 @@
     "current_revision": "674ac754f91e64a0efb8087e59a176484bd534d1",
     "revisions": {
       "674ac754f91e64a0efb8087e59a176484bd534d1": {
-      "_number": 2,
-      "ref": "refs/changes/65/3965/2",
-      "fetch": {
-        "http": {
-          "url": "http://gerrit/myProject",
-          "ref": "refs/changes/65/3965/2"
+        "kind": "REWORK",
+        "_number": 2,
+        "ref": "refs/changes/65/3965/2",
+        "fetch": {
+          "http": {
+            "url": "http://gerrit/myProject",
+            "ref": "refs/changes/65/3965/2"
+          }
         }
       }
     }
@@ -2863,6 +2869,7 @@
     "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
     "revisions": {
       "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
+        "kind": "REWORK",
         "_number": 2,
         "ref": "refs/changes/99/4799/2",
         "fetch": {
@@ -4974,6 +4981,8 @@
 |===========================
 |Field Name    ||Description
 |`draft`       |not set if `false`|Whether the patch set is a draft.
+|`kind`        ||The change kind. Valid values are `REWORK`, `TRIVIAL_REBASE`,
+`MERGE_FIRST_PARENT_UPDATE`, `NO_CODE_CHANGE`, and `NO_CHANGE`.
 |`_number`     ||The patch set number.
 |`created`     ||
 The link:rest-api.html#timestamp[timestamp] of when the patch set was
diff --git a/Documentation/user-search-accounts.txt b/Documentation/user-search-accounts.txt
new file mode 100644
index 0000000..15d87b0
--- /dev/null
+++ b/Documentation/user-search-accounts.txt
@@ -0,0 +1,83 @@
+= Gerrit Code Review - Searching Accounts
+
+== Basic Change Search
+
+Similar to many popular search engines on the web, just enter some
+text and let Gerrit figure out the meaning:
+
+[options="header"]
+|=============================================================
+|Description                      | Examples
+|Name                             | John
+|Email address                    | jdoe@example.com
+|Username                         | jdoe
+|Account-Id                       | 1000096
+|Own account                      | self
+|=============================================================
+
+[[search-operators]]
+== Search Operators
+
+Operators act as restrictions on the search. As more operators
+are added to the same query string, they further restrict the
+returned results. Search can also be performed by typing only a
+text with no operator, which will match against a variety of fields.
+
+[[email]]
+email:'EMAIL'::
++
+Matches accounts that have the email address 'EMAIL' or an email
+address that starts with 'EMAIL'.
+
+[[is]]
+[[is-active]]
+is:active::
++
+Matches accounts that are active.
+
+[[is-inactive]]
+is:inactive::
++
+Matches accounts that are inactive.
+
+[[name]]
+name:'NAME'::
++
+Matches accounts that have any name part 'NAME'. The name parts consist
+of any part of the full name and the email addresses.
+
+[[username]]
+username:'USERNAME'::
++
+Matches accounts that have the username 'USERNAME'.
+
+== Magical Operators
+
+[[is-visible]]
+is:visible::
++
+Magical internal flag to prove the current user has access to read
+the change. This flag is always added to any query.
+
+[[is-active-magic]]
+is:active::
++
+Matches accounts that are active. If neither link:#is-active[is:active]
+nor link:#is-inactive[is:inactive] is contained in a query, `is:active`
+is automatically added so that by default only active accounts are
+matched.
+
+[[limit]]
+limit:'CNT'::
++
+Limit the returned results to no more than 'CNT' records. This is
+automatically set to the page size configured in the current user's
+preferences. Including it in a web query may lead to unpredictable
+results with regards to pagination.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
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 1b325ec..488f9f9 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
@@ -867,6 +867,11 @@
     }
   }
 
+  protected RevCommit getRemoteHead(String project, String branch)
+      throws Exception {
+    return getRemoteHead(new Project.NameKey(project), branch);
+  }
+
   protected RevCommit getRemoteHead() throws Exception {
     return getRemoteHead(project, "master");
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 1a17305..b12b9be 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -653,6 +653,35 @@
   }
 
   @Test
+  public void removeReviewerNoVotes() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    gApi.changes()
+        .id(changeId)
+        .addReviewer(user.getId().toString());
+
+    // ReviewerState will vary between ReviewDb and NoteDb; we just care that it
+    // shows up somewhere.
+    Iterable<AccountInfo> reviewers = Iterables.concat(
+        gApi.changes().id(changeId).get().reviewers.values());
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(user.getId().get());
+
+    gApi.changes()
+        .id(changeId)
+        .reviewer(user.getId().toString())
+        .remove();
+    assertThat(gApi.changes().id(changeId).get().reviewers.isEmpty());
+
+    exception.expect(ResourceNotFoundException.class);
+    gApi.changes()
+        .id(changeId)
+        .reviewer(user.getId().toString())
+        .remove();
+  }
+
+  @Test
   public void removeReviewer() throws Exception {
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 4be4d52..a3de2b4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -36,23 +36,58 @@
 public class ProjectIT extends AbstractDaemonTest  {
 
   @Test
-  public void createProjectFoo() throws Exception {
+  public void createProject() throws Exception {
     String name = name("foo");
     assertThat(name).isEqualTo(
         gApi.projects()
             .create(name)
             .get()
             .name);
+
+    RevCommit head = getRemoteHead(name, "refs/meta/config");
+    eventRecorder.assertRefUpdatedEvents(name, "refs/meta/config",
+        null, head);
+
+    eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master",
+        new String[]{});
   }
 
   @Test
-  public void createProjectFooWithGitSuffix() throws Exception {
+  public void createProjectWithGitSuffix() throws Exception {
     String name = name("foo");
     assertThat(name).isEqualTo(
         gApi.projects()
             .create(name + ".git")
             .get()
             .name);
+
+    RevCommit head = getRemoteHead(name, "refs/meta/config");
+    eventRecorder.assertRefUpdatedEvents(name, "refs/meta/config",
+        null, head);
+
+    eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master",
+        new String[]{});
+  }
+
+  @Test
+  public void createProjectWithInitialCommit() throws Exception {
+    String name = name("foo");
+    ProjectInput input = new ProjectInput();
+    input.name = name;
+    input.createEmptyCommit = true;
+    assertThat(name).isEqualTo(
+        gApi.projects()
+            .create(input)
+            .get()
+            .name);
+
+    RevCommit head = getRemoteHead(name, "refs/meta/config");
+    eventRecorder.assertRefUpdatedEvents(name, "refs/meta/config",
+        null, head);
+
+    head = getRemoteHead(name, "refs/heads/master");
+    eventRecorder.assertRefUpdatedEvents(name, "refs/heads/master",
+        null, head);
   }
 
   @Test
@@ -67,6 +102,15 @@
   }
 
   @Test
+  public void createProjectNoNameInInput() throws Exception {
+    ProjectInput in = new ProjectInput();
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("input.name is required");
+    gApi.projects()
+        .create(in);
+  }
+
+  @Test
   public void createProjectDuplicate() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = name("baz");
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 0fc0712..975dc2b 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
@@ -16,6 +16,8 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
@@ -33,6 +35,9 @@
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.group.SystemGroupBackend;
 
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
@@ -355,6 +360,61 @@
         .containsNoneIn(accessSectionInfo.permissions.keySet());
   }
 
+  @Test
+  public void unknownPermissionRemainsUnchanged() throws Exception {
+    String access = "access";
+    String unknownPermission = "unknownPermission";
+    String registeredUsers = "group Registered Users";
+    String refsFor = "refs/for/*";
+    // Clone repository to forcefully add permission
+    TestRepository<InMemoryRepository> allProjectsRepo =
+        cloneProject(allProjects, admin);
+
+    // Fetch permission ref
+    GitUtil.fetch(allProjectsRepo, "refs/meta/config:cfg");
+    allProjectsRepo.reset("cfg");
+
+    // Load current permissions
+    String config = gApi.projects()
+        .name(allProjects.get())
+        .branch("refs/meta/config").file("project.config").asString();
+
+    // Append and push unknown permission
+    Config cfg = new Config();
+    cfg.fromText(config);
+    cfg.setString(access, refsFor, unknownPermission, registeredUsers);
+    config = cfg.toText();
+    PushOneCommit push = pushFactory.create(
+        db, admin.getIdent(), allProjectsRepo, "Subject", "project.config",
+        config);
+    push.to("refs/meta/config").assertOkStatus();
+
+    // Verify that unknownPermission is present
+    config = gApi.projects()
+        .name(allProjects.get())
+        .branch("refs/meta/config").file("project.config").asString();
+    cfg.fromText(config);
+    assertThat(cfg.getString(access, refsFor, unknownPermission))
+        .isEqualTo(registeredUsers);
+
+    // Make permission change through API
+    ProjectAccessInput accessInput = newProjectAccessInput();
+    AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
+    accessInput.add.put(refsFor, accessSectionInfo);
+    gApi.projects().name(allProjects.get()).access(accessInput);
+    accessInput.add.clear();
+    accessInput.remove.put(refsFor, accessSectionInfo);
+    gApi.projects().name(allProjects.get()).access(accessInput);
+
+    // Verify that unknownPermission is still present
+    config = gApi.projects()
+        .name(allProjects.get())
+        .branch("refs/meta/config").file("project.config").asString();
+    cfg.fromText(config);
+    assertThat(cfg.getString(access, refsFor, unknownPermission))
+        .isEqualTo(registeredUsers);
+  }
+
   private ProjectAccessInput newProjectAccessInput() {
     ProjectAccessInput p = new ProjectAccessInput();
     p.add = new HashMap<>();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index d5f9b6e..d8f2885 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -45,6 +45,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.change.PostReview;
@@ -60,7 +61,6 @@
 import com.google.gerrit.server.notedb.NoteDbChangeState;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager;
 import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
-import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.NoteDbChecker;
 import com.google.gerrit.testutil.NoteDbMode;
@@ -264,13 +264,13 @@
 
     // First write doesn't create the ref, but rebuilding works.
     checker.assertNoChangeRef(project, id);
-    assertThat(unwrapDb().changes().get(id).getNoteDbState()).isNull();
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isNull();
     checker.rebuildAndCheckChanges(id);
 
     // Now that there is a ref, writes are "turned on" for this change, and
     // NoteDb stays up to date without explicit rebuilding.
     gApi.changes().id(id.get()).topic(name("new-topic"));
-    assertThat(unwrapDb().changes().get(id).getNoteDbState()).isNotNull();
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isNotNull();
     checker.checkChanges(id);
   }
 
@@ -323,13 +323,13 @@
     Change.Id id = r.getPatchSetId().getParentKey();
 
     ObjectId changeMetaId = getMetaRef(project, changeMetaRef(id));
-    assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name());
 
     putDraft(user, id, 1, "comment by user");
     ObjectId userDraftsId = getMetaRef(
         allUsers, refsDraftComments(id, user.getId()));
-    assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name()
         + "," + user.getId() + "=" + userDraftsId.name());
 
@@ -337,7 +337,7 @@
     ObjectId adminDraftsId = getMetaRef(
         allUsers, refsDraftComments(id, admin.getId()));
     assertThat(admin.getId().get()).isLessThan(user.getId().get());
-    assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name()
         + "," + admin.getId() + "=" + adminDraftsId.name()
         + "," + user.getId() + "=" + userDraftsId.name());
@@ -345,7 +345,7 @@
     putDraft(admin, id, 2, "revised comment by admin");
     adminDraftsId = getMetaRef(
         allUsers, refsDraftComments(id, admin.getId()));
-    assertThat(unwrapDb().changes().get(id).getNoteDbState()).isEqualTo(
+    assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo(
         changeMetaId.name()
         + "," + admin.getId() + "=" + adminDraftsId.name()
         + "," + user.getId() + "=" + userDraftsId.name());
@@ -374,7 +374,7 @@
     // Check that the bundles are equal.
     ChangeBundle actual = ChangeBundle.fromNotes(
         plcUtil, notesFactory.create(dbProvider.get(), project, id));
-    ChangeBundle expected = ChangeBundle.fromReviewDb(unwrapDb(), id);
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
     assertThat(actual.differencesFrom(expected)).isEmpty();
   }
 
@@ -425,7 +425,7 @@
     // Check that the bundles are equal.
     ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
     ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
-    ChangeBundle expected = ChangeBundle.fromReviewDb(unwrapDb(), id);
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
     assertThat(actual.differencesFrom(expected)).isEmpty();
     assertThat(
             Iterables.transform(
@@ -464,7 +464,7 @@
     // Check that the bundles are equal.
     ChangeBundle actual = ChangeBundle.fromNotes(
         plcUtil, notesFactory.create(dbProvider.get(), project, id));
-    ChangeBundle expected = ChangeBundle.fromReviewDb(unwrapDb(), id);
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
     assertThat(actual.differencesFrom(expected)).isEmpty();
   }
 
@@ -494,7 +494,7 @@
     assertChangeUpToDate(false, id);
     assertThat(getMetaRef(project, changeMetaRef(id))).isEqualTo(oldMetaId);
     ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
-    ChangeBundle expected = ChangeBundle.fromReviewDb(unwrapDb(), id);
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
     assertThat(actual.differencesFrom(expected)).isEmpty();
     assertChangeUpToDate(false, id);
 
@@ -536,7 +536,7 @@
     // Not up to date, but the actual returned state matches anyway.
     assertDraftsUpToDate(false, id, user);
     ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
-    ChangeBundle expected = ChangeBundle.fromReviewDb(unwrapDb(), id);
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
     assertThat(actual.differencesFrom(expected)).isEmpty();
 
     // Another rebuild attempt succeeds
@@ -564,7 +564,7 @@
     setNotesMigration(false, false);
     putDraft(user, id, 1, "second comment by user");
 
-    ReviewDb db = unwrapDb();
+    ReviewDb db = getUnwrappedDb();
     Change c = db.changes().get(id);
     // Leave change meta ID alone so DraftCommentNotes does the rebuild.
     NoteDbChangeState bogusState = new NoteDbChangeState(
@@ -591,7 +591,7 @@
     assertChangeUpToDate(true, id);
     assertDraftsUpToDate(false, id, user);
     ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
-    ChangeBundle expected = ChangeBundle.fromReviewDb(unwrapDb(), id);
+    ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
     assertThat(actual.differencesFrom(expected)).isEmpty();
 
     // Another rebuild attempt succeeds
@@ -950,7 +950,7 @@
   }
 
   private void setInvalidNoteDbState(Change.Id id) throws Exception {
-    ReviewDb db = unwrapDb();
+    ReviewDb db = getUnwrappedDb();
     Change c = db.changes().get(id);
     // In reality we would have NoteDb writes enabled, which would write a real
     // state into this field. For tests however, we turn NoteDb writes off, so
@@ -963,7 +963,7 @@
   private void assertChangeUpToDate(boolean expected, Change.Id id)
       throws Exception {
     try (Repository repo = repoManager.openRepository(project)) {
-      Change c = unwrapDb().changes().get(id);
+      Change c = getUnwrappedDb().changes().get(id);
       assertThat(c).isNotNull();
       assertThat(c.getNoteDbState()).isNotNull();
       assertThat(NoteDbChangeState.parse(c).isChangeUpToDate(
@@ -975,7 +975,7 @@
   private void assertDraftsUpToDate(boolean expected, Change.Id changeId,
       TestAccount account) throws Exception {
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      Change c = unwrapDb().changes().get(changeId);
+      Change c = getUnwrappedDb().changes().get(changeId);
       assertThat(c).isNotNull();
       assertThat(c.getNoteDbState()).isNotNull();
       NoteDbChangeState state = NoteDbChangeState.parse(c);
@@ -1052,11 +1052,8 @@
     return msg;
   }
 
-  private ReviewDb unwrapDb() {
+  private ReviewDb getUnwrappedDb() {
     ReviewDb db = dbProvider.get();
-    if (db instanceof DisabledChangesReviewDbWrapper) {
-      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
-    }
-    return db;
+    return ReviewDbUtil.unwrapDb(db);
   }
 }
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 726a5f1..838f42c 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.server.cache.h2;
 
+import com.google.common.base.Throwables;
 import com.google.common.cache.AbstractLoadingCache;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.CacheStats;
 import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.hash.BloomFilter;
 import com.google.common.hash.Funnel;
 import com.google.common.hash.Funnels;
@@ -81,6 +83,9 @@
     PersistentCache {
   private static final Logger log = LoggerFactory.getLogger(H2CacheImpl.class);
 
+  private static final ImmutableSet<String> OLD_CLASS_NAMES = ImmutableSet.of(
+      "com.google.gerrit.server.change.ChangeKind");
+
   private final Executor executor;
   private final SqlStore<K, V> store;
   private final TypeLiteral<K> keyType;
@@ -472,7 +477,9 @@
           c.get.clearParameters();
         }
       } catch (SQLException e) {
-        log.warn("Cannot read cache " + url + " for " + key, e);
+        if (!isOldClassNameError(e)) {
+          log.warn("Cannot read cache " + url + " for " + key, e);
+        }
         c = close(c);
         return null;
       } finally {
@@ -480,6 +487,16 @@
       }
     }
 
+    private static boolean isOldClassNameError(Throwable t) {
+      for (Throwable c : Throwables.getCausalChain(t)) {
+        if (c instanceof ClassNotFoundException
+            && OLD_CLASS_NAMES.contains(c.getMessage())) {
+          return true;
+        }
+      }
+      return false;
+    }
+
     private boolean expired(Timestamp created) {
       if (expireAfterWrite == 0) {
         return false;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index 0d2e025..fe8a1d8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.extensions.api.accounts;
 
+import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
+import java.util.Arrays;
+import java.util.EnumSet;
 import java.util.List;
 
 public interface Accounts {
@@ -70,6 +73,25 @@
     throws RestApiException;
 
   /**
+   * Queries users.
+   * <p>
+   * Example code:
+   * {@code query().withQuery("name:John email:example.com").withLimit(5).get()}
+   *
+   * @return API for setting parameters and getting result.
+   */
+  QueryRequest query() throws RestApiException;
+
+  /**
+   * Queries users.
+   * <p>
+   * Shortcut API for {@code query().withQuery(String)}.
+   *
+   * @see #query()
+   */
+  QueryRequest query(String query) throws RestApiException;
+
+  /**
    * API for setting parameters and getting result.
    * Used for {@code suggestAccounts()}.
    *
@@ -113,6 +135,84 @@
   }
 
   /**
+   * API for setting parameters and getting result.
+   * Used for {@code query()}.
+   *
+   * @see #query()
+   */
+  abstract class QueryRequest {
+    private String query;
+    private int limit;
+    private int start;
+    private EnumSet<ListAccountsOption> options =
+        EnumSet.noneOf(ListAccountsOption.class);
+
+    /**
+     * Executes query and returns a list of accounts.
+     */
+    public abstract List<AccountInfo> get() throws RestApiException;
+
+    /**
+     * Set query.
+     *
+     * @param query needs to be in human-readable form.
+     */
+    public QueryRequest withQuery(String query) {
+      this.query = query;
+      return this;
+    }
+
+    /**
+     * Set limit for returned list of accounts.
+     * Optional; server-default is used when not provided.
+     */
+    public QueryRequest withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    /**
+     * Set number of accounts to skip.
+     * Optional; no accounts are skipped when not provided.
+     */
+    public QueryRequest withStart(int start) {
+      this.start = start;
+      return this;
+    }
+
+    public QueryRequest withOption(ListAccountsOption options) {
+      this.options.add(options);
+      return this;
+    }
+
+    public QueryRequest withOptions(ListAccountsOption... options) {
+      this.options.addAll(Arrays.asList(options));
+      return this;
+    }
+
+    public QueryRequest withOptions(EnumSet<ListAccountsOption> options) {
+      this.options = options;
+      return this;
+    }
+
+    public String getQuery() {
+      return query;
+    }
+
+    public int getLimit() {
+      return limit;
+    }
+
+    public int getStart() {
+      return start;
+    }
+
+    public EnumSet<ListAccountsOption> getOptions() {
+      return options;
+    }
+  }
+
+  /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
@@ -142,5 +242,15 @@
       throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public QueryRequest query() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public QueryRequest query(String query) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeKind.java
similarity index 95%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeKind.java
index 63f6095..e58e005 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeKind.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.change;
+package com.google.gerrit.extensions.client;
 
 /** Operation performed by a change relative to its parent. */
 public enum ChangeKind {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListAccountsOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListAccountsOption.java
new file mode 100644
index 0000000..bc3679c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListAccountsOption.java
@@ -0,0 +1,59 @@
+// 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.extensions.client;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+/** Output options available for retrieval of account details. */
+public enum ListAccountsOption {
+  /** Return detailed account properties. */
+  DETAILS(0);
+
+  private final int value;
+
+  ListAccountsOption(int v) {
+    this.value = v;
+  }
+
+  public int getValue() {
+    return value;
+  }
+
+  public static EnumSet<ListAccountsOption> fromBits(int v) {
+    EnumSet<ListAccountsOption> r = EnumSet.noneOf(ListAccountsOption.class);
+    for (ListAccountsOption o : ListAccountsOption.values()) {
+      if ((v & (1 << o.value)) != 0) {
+        r.add(o);
+        v &= ~(1 << o.value);
+      }
+      if (v == 0) {
+        return r;
+      }
+    }
+    if (v != 0) {
+      throw new IllegalArgumentException("unknown " + Integer.toHexString(v));
+    }
+    return r;
+  }
+
+  public static int toBits(Set<ListAccountsOption> set) {
+    int r = 0;
+    for (ListAccountsOption o : set) {
+      r |= 1 << o.value;
+    }
+    return r;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
index 39d98de..8322eaf 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -22,6 +22,7 @@
   public String email;
   public String username;
   public List<AvatarInfo> avatars;
+  public Boolean _moreAccounts;
 
   public AccountInfo(Integer id) {
     this._accountId = id;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
index 025c623..34a1e63 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.extensions.common;
 
+import com.google.gerrit.extensions.client.ChangeKind;
+
 import java.sql.Timestamp;
 import java.util.Map;
 
 public class RevisionInfo {
   public transient boolean isCurrent;
   public Boolean draft;
+  public ChangeKind kind;
   public int _number;
   public Timestamp created;
   public AccountInfo uploader;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index a822cfc..acd2e78 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -53,6 +53,7 @@
   public static void suggest(String query, int limit,
       AsyncCallback<JsArray<AccountInfo>> cb) {
     new RestApi("/accounts/")
+      .addParameterTrue("suggest")
       .addParameter("q", query)
       .addParameter("n", limit)
       .background()
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 bc8efc6..4747455 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
@@ -451,6 +451,12 @@
       cd.setChangedLines(
           added.numericValue().intValue(),
           deleted.numericValue().intValue());
+    } else {
+      // No ChangedLines stored, likely due to failure during reindexing, for
+      // example due to LargeObjectException. But we know the field was
+      // requested, so update ChangeData to prevent callers from trying to
+      // lazily load it, as that would probably also fail.
+      cd.setNoChangedLines();
     }
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
index 9991a76..0adb1af 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm;
 
+import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
 import com.google.common.base.Function;
@@ -48,7 +49,6 @@
 import com.google.gerrit.server.index.change.ReindexAfterUpdate;
 import com.google.gerrit.server.notedb.ChangeRebuilder;
 import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -153,7 +153,7 @@
             new Callable<Boolean>() {
               @Override
               public Boolean call() {
-                try (ReviewDb db = unwrap(schemaFactory.open())) {
+                try (ReviewDb db = unwrapDb(schemaFactory.open())) {
                   return rebuilder.rebuildProject(
                       db, changesByProject, project, allUsersRepo);
                 } catch (Exception e) {
@@ -234,7 +234,7 @@
         ArrayListMultimap.create();
     try (ReviewDb db = schemaFactory.open()) {
       if (projects.isEmpty() && !changes.isEmpty()) {
-        Iterable<Change> todo = unwrap(db).changes().get(
+        Iterable<Change> todo = unwrapDb(db).changes().get(
             Iterables.transform(changes, new Function<Integer, Change.Id>() {
               @Override
               public Change.Id apply(Integer in) {
@@ -245,7 +245,7 @@
           changesByProject.put(c.getProject(), c.getId());
         }
       } else {
-        for (Change c : unwrap(db).changes().all()) {
+        for (Change c : unwrapDb(db).changes().all()) {
           boolean include = false;
           if (projects.isEmpty() && changes.isEmpty()) {
             include = true;
@@ -263,11 +263,4 @@
       return ImmutableMultimap.copyOf(changesByProject);
     }
   }
-
-  private static ReviewDb unwrap(ReviewDb db) {
-    if (db instanceof DisabledChangesReviewDbWrapper) {
-      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
-    }
-    return db;
-  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
index 540ba0b..36163b6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
+import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
 import static com.google.inject.Scopes.SINGLETON;
 import static com.google.inject.Stage.PRODUCTION;
@@ -399,7 +400,7 @@
           System.err.flush();
 
         } else if (ui.yesno(true, "%s\nExecute now", msg)) {
-          try (JdbcSchema db = (JdbcSchema) schema.open();
+          try (JdbcSchema db = (JdbcSchema) unwrapDb(schema.open());
               JdbcExecutor e = new JdbcExecutor(db)) {
             for (String sql : pruneList) {
               e.execute(sql);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
index 3476de5..ee20d99 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.base.MoreObjects;
-import com.google.common.collect.Ordering;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -30,12 +30,9 @@
 import java.io.IOException;
 import java.net.URL;
 import java.net.URLClassLoader;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
@@ -116,23 +113,12 @@
   }
 
   private List<Path> scanJarsInPluginsDirectory() {
-    if (pluginsDir == null || !Files.isDirectory(pluginsDir)) {
-      return Collections.emptyList();
-    }
-    DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
-      @Override
-      public boolean accept(Path entry) throws IOException {
-        return entry.getFileName().toString().endsWith(".jar")
-            && Files.isRegularFile(entry);
-      }
-    };
-    try (DirectoryStream<Path> paths =
-        Files.newDirectoryStream(pluginsDir, filter)) {
-      return Ordering.natural().sortedCopy(paths);
+    try {
+      return PluginLoader.listPlugins(pluginsDir, ".jar");
     } catch (IOException e) {
       ui.message("WARN: Cannot list %s: %s", pluginsDir.toAbsolutePath(),
           e.getMessage());
-      return Collections.emptyList();
+      return ImmutableList.of();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DisabledChangesReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisabledChangesReviewDbWrapper.java
similarity index 93%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/DisabledChangesReviewDbWrapper.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisabledChangesReviewDbWrapper.java
index 658f3bb..b70778e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DisabledChangesReviewDbWrapper.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisabledChangesReviewDbWrapper.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.reviewdb.server;
 
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.reviewdb.client.Account;
@@ -21,13 +21,6 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.server.ChangeAccess;
-import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
-import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
-import com.google.gerrit.reviewdb.server.PatchSetAccess;
-import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 
@@ -40,7 +33,7 @@
   private final DisabledPatchSetAccess patchSets;
   private final DisabledPatchLineCommentAccess patchComments;
 
-  DisabledChangesReviewDbWrapper(ReviewDb db) {
+  public DisabledChangesReviewDbWrapper(ReviewDb db) {
     super(db);
     changes = new DisabledChangeAccess(delegate.changes());
     patchSetApprovals =
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
index 5d782dd..42d0993 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
@@ -49,6 +49,13 @@
     return CHANGE_ID_FUNCTION;
   }
 
+  public static ReviewDb unwrapDb(ReviewDb db) {
+    if (db instanceof DisabledChangesReviewDbWrapper) {
+      return ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
+    }
+    return db;
+  }
+
   private ReviewDbUtil() {
   }
 }
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 6aa9e2c..4fc578c 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -190,6 +190,7 @@
     ['src/test/java/**/*.java'],
     excludes = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS
   ),
+  resources = glob(['src/test/resources/com/google/gerrit/server/mail/*']),
   deps = TESTUTIL_DEPS + [
     ':testutil',
     '//gerrit-antlr:query_exception',
@@ -202,6 +203,7 @@
     '//lib:guava',
     '//lib:guava-retrying',
     '//lib:protobuf',
+    '//lib/commons:validator',
     '//lib/dropwizard:dropwizard-core',
     '//lib/guice:guice-assistedinject',
     '//lib/prolog:runtime',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index f4aa4c2..a3a7734 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -21,11 +21,11 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Table;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.ChangeKind;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LabelNormalizer;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index d54a999..06cf255 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -40,7 +40,7 @@
   private final AccountResolver resolver;
   private final AccountControl.Factory accountControlFactory;
   private final IdentifiedUser.GenericFactory userFactory;
-  private final Provider<SuggestAccounts> list;
+  private final Provider<QueryAccounts> list;
   private final DynamicMap<RestView<AccountResource>> views;
   private final CreateAccount.Factory createAccountFactory;
 
@@ -49,7 +49,7 @@
       AccountResolver resolver,
       AccountControl.Factory accountControlFactory,
       IdentifiedUser.GenericFactory userFactory,
-      Provider<SuggestAccounts> list,
+      Provider<QueryAccounts> list,
       DynamicMap<RestView<AccountResource>> views,
       CreateAccount.Factory createAccountFactory) {
     this.self = self;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java
new file mode 100644
index 0000000..7c3ef1e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java
@@ -0,0 +1,265 @@
+// Copyright (C) 2014 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.account;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.client.ListAccountsOption;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.api.accounts.AccountInfoComparator;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.account.AccountIndex;
+import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.QueryResult;
+import com.google.gerrit.server.query.account.AccountQueryBuilder;
+import com.google.gerrit.server.query.account.AccountQueryProcessor;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+import org.kohsuke.args4j.Option;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class QueryAccounts implements RestReadView<TopLevelResource> {
+  private static final int MAX_SUGGEST_RESULTS = 100;
+  private static final String MAX_SUFFIX = "\u9fa5";
+
+  private final AccountControl accountControl;
+  private final AccountLoader.Factory accountLoaderFactory;
+  private final AccountCache accountCache;
+  private final AccountIndexCollection indexes;
+  private final AccountQueryBuilder queryBuilder;
+  private final AccountQueryProcessor queryProcessor;
+  private final ReviewDb db;
+  private final boolean suggestConfig;
+  private final int suggestFrom;
+
+  private AccountLoader accountLoader;
+  private boolean suggest;
+  private int suggestLimit = 10;
+  private String query;
+  private Integer start;
+  private EnumSet<ListAccountsOption> options;
+
+  @Option(name = "--suggest", metaVar = "SUGGEST", usage = "suggest users")
+  public void setSuggest(boolean suggest) {
+    this.suggest = suggest;
+  }
+
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of users to return")
+  public void setLimit(int n) {
+    queryProcessor.setLimit(n);
+
+    if (n < 0) {
+      suggestLimit = 10;
+    } else if (n == 0) {
+      suggestLimit = MAX_SUGGEST_RESULTS;
+    } else {
+      suggestLimit = Math.min(n, MAX_SUGGEST_RESULTS);
+    }
+  }
+
+  @Option(name = "-o", usage = "Output options per account")
+  public void addOption(ListAccountsOption o) {
+    options.add(o);
+  }
+
+  @Option(name = "-O", usage = "Output option flags, in hex")
+  void setOptionFlagsHex(String hex) {
+    options.addAll(ListAccountsOption.fromBits(Integer.parseInt(hex, 16)));
+  }
+
+  @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "match users")
+  public void setQuery(String query) {
+    this.query = query;
+  }
+
+  @Option(name = "--start", aliases = {"-S"}, metaVar = "CNT",
+      usage = "Number of accounts to skip")
+  public void setStart(int start) {
+    this.start = start;
+  }
+
+  @Inject
+  QueryAccounts(AccountControl.Factory accountControlFactory,
+      AccountLoader.Factory accountLoaderFactory,
+      AccountCache accountCache,
+      AccountIndexCollection indexes,
+      AccountQueryBuilder queryBuilder,
+      AccountQueryProcessor queryProcessor,
+      ReviewDb db,
+      @GerritServerConfig Config cfg) {
+    this.accountControl = accountControlFactory.get();
+    this.accountLoaderFactory = accountLoaderFactory;
+    this.accountCache = accountCache;
+    this.indexes = indexes;
+    this.queryBuilder = queryBuilder;
+    this.queryProcessor = queryProcessor;
+    this.db = db;
+    this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
+    this.options = EnumSet.noneOf(ListAccountsOption.class);
+
+    if ("off".equalsIgnoreCase(cfg.getString("suggest", null, "accounts"))) {
+      suggestConfig = false;
+    } else {
+      boolean suggest;
+      try {
+        AccountVisibility av =
+            cfg.getEnum("suggest", null, "accounts", AccountVisibility.ALL);
+        suggest = (av != AccountVisibility.NONE);
+      } catch (IllegalArgumentException err) {
+        suggest = cfg.getBoolean("suggest", null, "accounts", true);
+      }
+      this.suggestConfig = suggest;
+    }
+  }
+
+  @Override
+  public List<AccountInfo> apply(TopLevelResource rsrc)
+      throws OrmException, BadRequestException, MethodNotAllowedException {
+    if (Strings.isNullOrEmpty(query)) {
+      throw new BadRequestException("missing query field");
+    }
+
+    if (suggest && (!suggestConfig || query.length() < suggestFrom)) {
+      return Collections.emptyList();
+    }
+
+    accountLoader = accountLoaderFactory
+        .create(suggest || options.contains(ListAccountsOption.DETAILS));
+
+    AccountIndex searchIndex = indexes.getSearchIndex();
+    if (searchIndex != null) {
+      return queryFromIndex();
+    }
+
+    if (!suggest) {
+      throw new MethodNotAllowedException();
+    }
+    if (start != null) {
+      throw new MethodNotAllowedException("option start not allowed");
+    }
+    return queryFromDb();
+  }
+
+  public List<AccountInfo> queryFromIndex()
+      throws BadRequestException, MethodNotAllowedException, OrmException {
+    if (queryProcessor.isDisabled()) {
+      throw new MethodNotAllowedException("query disabled");
+    }
+
+    if (start != null) {
+      queryProcessor.setStart(start);
+    }
+
+    Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
+    try {
+      Predicate<AccountState> queryPred;
+      if (suggest) {
+        queryPred = queryBuilder.defaultField(query);
+        queryProcessor.setLimit(suggestLimit);
+      } else {
+        queryPred = queryBuilder.parse(query);
+      }
+      QueryResult<AccountState> result = queryProcessor.query(queryPred);
+      for (AccountState accountState : result.entities()) {
+        Account.Id id = accountState.getAccount().getId();
+        matches.put(id, accountLoader.get(id));
+      }
+
+      accountLoader.fill();
+
+      List<AccountInfo> sorted =
+          AccountInfoComparator.ORDER_NULLS_LAST.sortedCopy(matches.values());
+      if (!sorted.isEmpty() && result.more()) {
+        sorted.get(sorted.size() - 1)._moreAccounts = true;
+      }
+      return sorted;
+    } catch (QueryParseException e) {
+      if (suggest) {
+        return ImmutableList.of();
+      }
+      throw new BadRequestException(e.getMessage());
+    }
+  }
+
+  public List<AccountInfo> queryFromDb() throws OrmException {
+    String a = query;
+    String b = a + MAX_SUFFIX;
+
+    Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
+    Map<Account.Id, String> queryEmail = new HashMap<>();
+
+    for (Account p : db.accounts().suggestByFullName(a, b, suggestLimit)) {
+      addSuggestion(matches, p);
+    }
+    if (matches.size() < suggestLimit) {
+      for (Account p : db.accounts()
+          .suggestByPreferredEmail(a, b, suggestLimit - matches.size())) {
+        addSuggestion(matches, p);
+      }
+    }
+    if (matches.size() < suggestLimit) {
+      for (AccountExternalId e : db.accountExternalIds()
+          .suggestByEmailAddress(a, b, suggestLimit - matches.size())) {
+        if (addSuggestion(matches, e.getAccountId())) {
+          queryEmail.put(e.getAccountId(), e.getEmailAddress());
+        }
+      }
+    }
+
+    accountLoader.fill();
+    for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
+      AccountInfo info = matches.get(p.getKey());
+      if (info != null) {
+        info.email = p.getValue();
+      }
+    }
+
+    return AccountInfoComparator.ORDER_NULLS_LAST.sortedCopy(matches.values());
+  }
+
+  private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account a) {
+    if (!a.isActive()) {
+      return false;
+    }
+    Account.Id id = a.getId();
+    if (!map.containsKey(id) && accountControl.canSee(id)) {
+      map.put(id, accountLoader.get(id));
+      return true;
+    }
+    return false;
+  }
+
+  private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account.Id id) {
+    Account a = accountCache.get(id).getAccount();
+    return addSuggestion(map, a);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
deleted file mode 100644
index 1cc3c94..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
+++ /dev/null
@@ -1,161 +0,0 @@
-// Copyright (C) 2014 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.account;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.api.accounts.AccountInfoComparator;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-import org.eclipse.jgit.lib.Config;
-import org.kohsuke.args4j.Option;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-public class SuggestAccounts implements RestReadView<TopLevelResource> {
-  private static final int MAX_RESULTS = 100;
-  private static final String MAX_SUFFIX = "\u9fa5";
-
-  private final AccountControl accountControl;
-  private final AccountLoader accountLoader;
-  private final AccountCache accountCache;
-  private final ReviewDb db;
-  private final boolean suggest;
-  private final int suggestFrom;
-
-  private int limit = 10;
-  private String query;
-
-  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of users to return")
-  public void setLimit(int n) {
-    if (n < 0) {
-      limit = 10;
-    } else if (n == 0) {
-      limit = MAX_RESULTS;
-    } else {
-      limit = Math.min(n, MAX_RESULTS);
-    }
-  }
-
-  @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "match users")
-  public void setQuery(String query) {
-    this.query = query;
-  }
-
-  @Inject
-  SuggestAccounts(AccountControl.Factory accountControlFactory,
-      AccountLoader.Factory accountLoaderFactory,
-      AccountCache accountCache,
-      ReviewDb db,
-      @GerritServerConfig Config cfg) {
-    accountControl = accountControlFactory.get();
-    accountLoader = accountLoaderFactory.create(true);
-    this.accountCache = accountCache;
-    this.db = db;
-    this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
-
-    if ("off".equalsIgnoreCase(cfg.getString("suggest", null, "accounts"))) {
-      suggest = false;
-    } else {
-      boolean suggest;
-      try {
-        AccountVisibility av =
-            cfg.getEnum("suggest", null, "accounts", AccountVisibility.ALL);
-        suggest = (av != AccountVisibility.NONE);
-      } catch (IllegalArgumentException err) {
-        suggest = cfg.getBoolean("suggest", null, "accounts", true);
-      }
-      this.suggest = suggest;
-    }
-  }
-
-  @Override
-  public List<AccountInfo> apply(TopLevelResource rsrc)
-      throws OrmException, BadRequestException {
-    if (Strings.isNullOrEmpty(query)) {
-      throw new BadRequestException("missing query field");
-    }
-
-    if (!suggest || query.length() < suggestFrom) {
-      return Collections.emptyList();
-    }
-
-    String a = query;
-    String b = a + MAX_SUFFIX;
-
-    Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
-    Map<Account.Id, String> queryEmail = new HashMap<>();
-
-    for (Account p : db.accounts().suggestByFullName(a, b, limit)) {
-      addSuggestion(matches, p);
-    }
-    if (matches.size() < limit) {
-      for (Account p : db.accounts()
-          .suggestByPreferredEmail(a, b, limit - matches.size())) {
-        addSuggestion(matches, p);
-      }
-    }
-    if (matches.size() < limit) {
-      for (AccountExternalId e : db.accountExternalIds()
-          .suggestByEmailAddress(a, b, limit - matches.size())) {
-        if (addSuggestion(matches, e.getAccountId())) {
-          queryEmail.put(e.getAccountId(), e.getEmailAddress());
-        }
-      }
-    }
-
-    accountLoader.fill();
-    for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
-      AccountInfo info = matches.get(p.getKey());
-      if (info != null) {
-        info.email = p.getValue();
-      }
-    }
-
-    List<AccountInfo> m = new ArrayList<>(matches.values());
-    Collections.sort(m, AccountInfoComparator.ORDER_NULLS_LAST);
-    return m;
-  }
-
-  private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account a) {
-    if (!a.isActive()) {
-      return false;
-    }
-    Account.Id id = a.getId();
-    if (!map.containsKey(id) && accountControl.canSee(id)) {
-      map.put(id, accountLoader.get(id));
-      return true;
-    }
-    return false;
-  }
-
-  private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account.Id id) {
-    Account a = accountCache.get(id).getAccount();
-    return addSuggestion(map, a);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index 3bd7634..39e8b51 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.accounts.Accounts;
+import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -24,7 +25,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsCollection;
-import com.google.gerrit.server.account.SuggestAccounts;
+import com.google.gerrit.server.account.QueryAccounts;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -37,17 +38,17 @@
   private final AccountsCollection accounts;
   private final AccountApiImpl.Factory api;
   private final Provider<CurrentUser> self;
-  private final Provider<SuggestAccounts> suggestAccountsProvider;
+  private final Provider<QueryAccounts> queryAccountsProvider;
 
   @Inject
   AccountsImpl(AccountsCollection accounts,
       AccountApiImpl.Factory api,
       Provider<CurrentUser> self,
-      Provider<SuggestAccounts> suggestAccountsProvider) {
+      Provider<QueryAccounts> queryAccountsProvider) {
     this.accounts = accounts;
     this.api = api;
     this.self = self;
-    this.suggestAccountsProvider = suggestAccountsProvider;
+    this.queryAccountsProvider = queryAccountsProvider;
   }
 
   @Override
@@ -92,10 +93,42 @@
   private List<AccountInfo> suggestAccounts(SuggestAccountsRequest r)
     throws RestApiException {
     try {
-      SuggestAccounts mySuggestAccounts = suggestAccountsProvider.get();
-      mySuggestAccounts.setQuery(r.getQuery());
-      mySuggestAccounts.setLimit(r.getLimit());
-      return mySuggestAccounts.apply(TopLevelResource.INSTANCE);
+      QueryAccounts myQueryAccounts = queryAccountsProvider.get();
+      myQueryAccounts.setSuggest(true);
+      myQueryAccounts.setQuery(r.getQuery());
+      myQueryAccounts.setLimit(r.getLimit());
+      return myQueryAccounts.apply(TopLevelResource.INSTANCE);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve suggested accounts", e);
+    }
+  }
+
+  @Override
+  public QueryRequest query() throws RestApiException {
+    return new QueryRequest() {
+      @Override
+      public List<AccountInfo> get() throws RestApiException {
+        return AccountsImpl.this.query(this);
+      }
+    };
+  }
+
+  @Override
+  public QueryRequest query(String query) throws RestApiException {
+    return query().withQuery(query);
+  }
+
+  private List<AccountInfo> query(QueryRequest r)
+    throws RestApiException {
+    try {
+      QueryAccounts myQueryAccounts = queryAccountsProvider.get();
+      myQueryAccounts.setQuery(r.getQuery());
+      myQueryAccounts.setLimit(r.getLimit());
+      myQueryAccounts.setStart(r.getStart());
+      for (ListAccountsOption option : r.getOptions()) {
+        myQueryAccounts.addOption(option);
+      }
+      return myQueryAccounts.apply(TopLevelResource.INSTANCE);
     } catch (OrmException e) {
       throw new RestApiException("Cannot retrieve suggested accounts", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index db31d42..5d21c45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -66,6 +66,9 @@
 
   @Override
   public ProjectApi create(ProjectInput in) throws RestApiException {
+    if (in.name == null) {
+      throw new BadRequestException("input.name is required");
+    }
     return name(in.name).create(in);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 322d455..ec145d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -163,6 +163,7 @@
   private final GpgApiAdapter gpgApi;
   private final ChangeNotes.Factory notesFactory;
   private final ChangeResource.Factory changeResourceFactory;
+  private final ChangeKindCache changeKindCache;
 
   private AccountLoader accountLoader;
   private Map<Change.Id, List<SubmitRecord>> submitRecords;
@@ -190,6 +191,7 @@
       GpgApiAdapter gpgApi,
       ChangeNotes.Factory notesFactory,
       ChangeResource.Factory changeResourceFactory,
+      ChangeKindCache changeKindCache,
       @Assisted Set<ListChangesOption> options) {
     this.db = db;
     this.labelNormalizer = ln;
@@ -211,6 +213,7 @@
     this.gpgApi = gpgApi;
     this.notesFactory = notesFactory;
     this.changeResourceFactory = changeResourceFactory;
+    this.changeKindCache = changeKindCache;
     this.options = options.isEmpty()
         ? EnumSet.noneOf(ListChangesOption.class)
         : EnumSet.copyOf(options);
@@ -409,10 +412,10 @@
       out.mergeable = cd.isMergeable();
     }
     out.submittable = Submit.submittable(cd);
-    ChangedLines changedLines = cd.changedLines();
-    if (changedLines != null) {
-      out.insertions = changedLines.insertions;
-      out.deletions = changedLines.deletions;
+    Optional<ChangedLines> changedLines = cd.changedLines();
+    if (changedLines.isPresent()) {
+      out.insertions = changedLines.get().insertions;
+      out.deletions = changedLines.get().deletions;
     }
     out.subject = in.getSubject();
     out.status = in.getStatus().asChangeStatus();
@@ -472,7 +475,7 @@
     finish(out);
 
     if (needRevisions) {
-      out.revisions = revisions(ctl, src);
+      out.revisions = revisions(ctl, cd, src);
       if (out.revisions != null) {
         for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
           if (entry.getValue().isCurrent) {
@@ -887,18 +890,21 @@
         .toSortedList(AccountInfoComparator.ORDER_NULLS_FIRST);
   }
 
-  private Map<String, RevisionInfo> revisions(ChangeControl ctl,
+  private Map<String, RevisionInfo> revisions(ChangeControl ctl, ChangeData cd,
       Map<PatchSet.Id, PatchSet> map) throws PatchListNotAvailableException,
       GpgException, OrmException, IOException {
     Map<String, RevisionInfo> res = new LinkedHashMap<>();
-    for (PatchSet in : map.values()) {
-      if ((has(ALL_REVISIONS)
-          || in.getId().equals(ctl.getChange().currentPatchSetId()))
-          && ctl.isPatchVisible(in, db.get())) {
-        res.put(in.getRevision().get(), toRevisionInfo(ctl, in));
+    try (Repository repo =
+        repoManager.openRepository(ctl.getProject().getNameKey())) {
+      for (PatchSet in : map.values()) {
+        if ((has(ALL_REVISIONS)
+            || in.getId().equals(ctl.getChange().currentPatchSetId()))
+            && ctl.isPatchVisible(in, db.get())) {
+          res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, repo));
+        }
       }
+      return res;
     }
-    return res;
   }
 
   private Map<PatchSet.Id, PatchSet> loadPatchSets(ChangeData cd,
@@ -933,12 +939,17 @@
       throws PatchListNotAvailableException, GpgException, OrmException,
       IOException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-    RevisionInfo rev = toRevisionInfo(ctl, in);
-    accountLoader.fill();
-    return rev;
+    try (Repository repo =
+        repoManager.openRepository(ctl.getProject().getNameKey())) {
+      RevisionInfo rev = toRevisionInfo(
+          ctl, changeDataFactory.create(db.get(), ctl), in, repo);
+      accountLoader.fill();
+      return rev;
+    }
   }
 
-  private RevisionInfo toRevisionInfo(ChangeControl ctl, PatchSet in)
+  private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd,
+      PatchSet in, Repository repo)
       throws PatchListNotAvailableException, GpgException, OrmException,
       IOException {
     Change c = ctl.getChange();
@@ -950,14 +961,14 @@
     out.uploader = accountLoader.get(in.getUploader());
     out.draft = in.isDraft() ? true : null;
     out.fetch = makeFetchMap(ctl, in);
+    out.kind = changeKindCache.getChangeKind(repo, cd, in);
 
     boolean setCommit = has(ALL_COMMITS)
         || (out.isCurrent && has(CURRENT_COMMIT));
     boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS);
     if (setCommit || addFooters) {
       Project.NameKey project = c.getProject();
-      try (Repository repo = repoManager.openRepository(project);
-          RevWalk rw = new RevWalk(repo)) {
+      try (RevWalk rw = new RevWalk(repo)) {
         String rev = in.getRevision().get();
         RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
         rw.parseBody(commit);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
index 5148f9a..2302b70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.ChangeData;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -33,4 +35,6 @@
       ObjectId prior, ObjectId next);
 
   ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch);
+
+  ChangeKind getChangeKind(Repository repo, ChangeData cd, PatchSet patch);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index 2f660c3..edc1b12 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -22,6 +22,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.Weigher;
 import com.google.common.collect.FluentIterable;
+import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -117,6 +118,12 @@
       return getChangeKindInternal(this, db, change, patch, changeDataFactory,
           projectCache, repoManager);
     }
+
+    @Override
+    public ChangeKind getChangeKind(Repository repo, ChangeData cd,
+        PatchSet patch) {
+      return getChangeKindInternal(this, repo, cd, patch, projectCache);
+    }
   }
 
   public static class Key implements Serializable {
@@ -332,23 +339,25 @@
         projectCache, repoManager);
   }
 
+  @Override
+  public ChangeKind getChangeKind(Repository repo, ChangeData cd,
+      PatchSet patch) {
+    return getChangeKindInternal(this, repo, cd, patch, projectCache);
+  }
+
   private static ChangeKind getChangeKindInternal(
       ChangeKindCache cache,
-      ReviewDb db,
-      Change change,
+      Repository repo,
+      ChangeData change,
       PatchSet patch,
-      ChangeData.Factory changeDataFactory,
-      ProjectCache projectCache,
-      GitRepositoryManager repoManager) {
-    // TODO - dborowitz: add NEW_CHANGE type for default.
+      ProjectCache projectCache) {
     ChangeKind kind = ChangeKind.REWORK;
-    // Trivial case: if we're on the first patch, we don't need to open
+    // Trivial case: if we're on the first patch, we don't need to use
     // the repository.
     if (patch.getId().get() > 1) {
-      try (Repository repo = repoManager.openRepository(change.getProject())) {
-        ProjectState projectState = projectCache.checkedGet(change.getProject());
-        ChangeData cd = changeDataFactory.create(db, change);
-        Collection<PatchSet> patchSetCollection = cd.patchSets();
+      try {
+        ProjectState projectState = projectCache.checkedGet(change.project());
+        Collection<PatchSet> patchSetCollection = change.patchSets();
         PatchSet priorPs = patch;
         for (PatchSet ps : patchSetCollection) {
           if (ps.getId().get() < patch.getId().get() &&
@@ -372,6 +381,32 @@
       } catch (IOException | OrmException e) {
         // Do nothing; assume we have a complex change
         log.warn("Unable to get change kind for patchSet " + patch.getPatchSetId() +
+            "of change " + change.getId(), e);
+      }
+    }
+    return kind;
+  }
+
+  private static ChangeKind getChangeKindInternal(
+      ChangeKindCache cache,
+      ReviewDb db,
+      Change change,
+      PatchSet patch,
+      ChangeData.Factory changeDataFactory,
+      ProjectCache projectCache,
+      GitRepositoryManager repoManager) {
+    // TODO - dborowitz: add NEW_CHANGE type for default.
+    ChangeKind kind = ChangeKind.REWORK;
+    // Trivial case: if we're on the first patch, we don't need to open
+    // the repository.
+    if (patch.getId().get() > 1) {
+      try (Repository repo = repoManager.openRepository(change.getProject())) {
+        kind = getChangeKindInternal(cache, repo,
+            changeDataFactory.create(db, change), patch,
+            projectCache);
+      } catch (IOException e) {
+        // Do nothing; assume we have a complex change
+        log.warn("Unable to get change kind for patchSet " + patch.getPatchSetId() +
             "of change " + change.getChangeId(), e);
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
index 9f8411f..3ca0e1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -33,7 +34,6 @@
 import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.BatchUpdateReviewDb;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
@@ -57,10 +57,7 @@
     if (db instanceof BatchUpdateReviewDb) {
       db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
     }
-    if (db instanceof DisabledChangesReviewDbWrapper) {
-      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
-    }
-    return db;
+    return ReviewDbUtil.unwrapDb(db);
   }
 
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index 93cb01b..1cd8726 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -146,6 +146,7 @@
       psUtil.delete(ctx.getDb(), ctx.getUpdate(patchSet.getId()), patchSet);
 
       accountPatchReviewStore.get().clearReviewed(psId);
+      // Use the unwrap from DeleteDraftChangeOp to handle BatchUpdateReviewDb.
       ReviewDb db = DeleteDraftChangeOp.unwrap(ctx.getDb());
       db.changeMessages().delete(db.changeMessages().byPatchSet(psId));
       db.patchComments().delete(db.patchComments().byPatchSet(psId));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index 28d7dcd8..a2a59e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -128,6 +128,10 @@
     public boolean updateChange(ChangeContext ctx)
         throws AuthException, ResourceNotFoundException, OrmException {
       Account.Id reviewerId = reviewer.getId();
+      if (!approvalsUtil.getReviewers(ctx.getDb(), ctx.getNotes()).all()
+          .contains(reviewerId)) {
+        throw new ResourceNotFoundException();
+      }
       currChange = ctx.getChange();
       currPs = psUtil.current(dbProvider.get(), ctx.getNotes());
 
@@ -156,9 +160,6 @@
           throw new AuthException("delete not permitted");
         }
       }
-      if (del.isEmpty()) {
-        throw new ResourceNotFoundException();
-      }
       ctx.getDb().patchSetApprovals().delete(del);
       ChangeUpdate update = ctx.getUpdate(currPs.getId());
       update.removeReviewer(reviewerId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index 842e8bb..08ef76e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
@@ -32,7 +33,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -186,13 +186,10 @@
       throws OrmException {
     // Empty update of Change to bump rowVersion, changing its ETag.
     // TODO(dborowitz): Include cache info in ETag somehow instead.
-    if (db instanceof DisabledChangesReviewDbWrapper) {
-      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
-    }
+    db = ReviewDbUtil.unwrapDb(db);
     Change c = db.changes().get(id);
     if (c != null) {
       db.changes().update(Collections.singleton(c));
     }
   }
-
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
index 58a6c60..95a0f36 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -45,7 +46,7 @@
   private static final String EXTENSION = ".config";
 
   private final SitePaths site;
-  private final GerritServerConfigProvider cfgProvider;
+  private final Provider<Config> cfgProvider;
   private final ProjectCache projectCache;
   private final ProjectState.Factory projectStateFactory;
   private final Map<String, Config> pluginConfigs;
@@ -54,8 +55,11 @@
   private volatile Config cfg;
 
   @Inject
-  PluginConfigFactory(SitePaths site, GerritServerConfigProvider cfgProvider,
-      ProjectCache projectCache, ProjectState.Factory projectStateFactory) {
+  PluginConfigFactory(
+      SitePaths site,
+      @GerritServerConfig Provider<Config> cfgProvider,
+      ProjectCache projectCache,
+      ProjectState.Factory projectStateFactory) {
     this.site = site;
     this.cfgProvider = cfgProvider;
     this.projectCache = projectCache;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
index c669e26..824d800 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.data;
 
-import com.google.gerrit.server.change.ChangeKind;
+import com.google.gerrit.extensions.client.ChangeKind;
 
 import java.util.List;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index a2145e6..6811056 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Optional;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -30,7 +31,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.change.ChangeKind;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.BatchUpdate;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index eb76350..598ed71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -50,7 +50,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.NoSuchRefException;
-import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -856,7 +855,7 @@
         RevWalk rw, Change.Id id) throws Exception {
       Change c = newChanges.get(id);
       if (c == null) {
-        c = unwrap(db).changes().get(id);
+        c = ReviewDbUtil.unwrapDb(db).changes().get(id);
       }
       // Pass in preloaded change to controlFor, to avoid:
       //  - reading from a db that does not belong to this update
@@ -903,11 +902,4 @@
       op.postUpdate(ctx);
     }
   }
-
-  private static ReviewDb unwrap(ReviewDb db) {
-    if (db instanceof DisabledChangesReviewDbWrapper) {
-      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
-    }
-    return db;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index b94efce..6d9d759 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -177,6 +177,7 @@
   private long maxObjectSizeLimit;
   private Map<String, Config> pluginConfigs;
   private boolean checkReceivedObjects;
+  private Set<String> sectionsWithUnknownPermissions;
 
   public static ProjectConfig read(MetaDataUpdate update) throws IOException,
       ConfigInvalidException {
@@ -281,7 +282,13 @@
 
   public void remove(AccessSection section) {
     if (section != null) {
-      accessSections.remove(section.getName());
+      String name = section.getName();
+      if (sectionsWithUnknownPermissions.contains(name)) {
+        AccessSection a = accessSections.get(name);
+        a.setPermissions(new ArrayList<Permission>());
+      } else {
+        accessSections.remove(name);
+      }
     }
   }
 
@@ -609,6 +616,7 @@
   private void loadAccessSections(
       Config rc, Map<String, GroupReference> groupsByName) {
     accessSections = new HashMap<>();
+    sectionsWithUnknownPermissions = new HashSet<>();
     for (String refName : rc.getSubsections(ACCESS)) {
       if (RefConfigSection.isValid(refName) && isValidRegex(refName)) {
         AccessSection as = getAccessSection(refName, true);
@@ -626,6 +634,8 @@
             Permission perm = as.getPermission(varName, true);
             loadPermissionRules(rc, ACCESS, refName, varName, groupsByName,
                 perm, Permission.hasRange(varName));
+          } else {
+            sectionsWithUnknownPermissions.add(as.getName());
           }
         }
       }
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 bbb325e..66637fb 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
@@ -37,6 +37,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.BiMap;
+import com.google.common.collect.Collections2;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.HashMultimap;
@@ -50,8 +51,6 @@
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.SortedSetMultimap;
-import com.google.common.util.concurrent.CheckedFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.Capable;
@@ -291,7 +290,6 @@
   private final TagCache tagCache;
   private final AccountCache accountCache;
   private final ChangeInserter.Factory changeInserterFactory;
-  private final ListeningExecutorService changeUpdateExector;
   private final RequestScopePropagator requestScopePropagator;
   private final SshInfo sshInfo;
   private final AllProjectsName allProjectsName;
@@ -355,7 +353,6 @@
       ChangeInserter.Factory changeInserterFactory,
       CommitValidators.Factory commitValidatorsFactory,
       @CanonicalWebUrl String canonicalWebUrl,
-      @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
       RequestScopePropagator requestScopePropagator,
       SshInfo sshInfo,
       AllProjectsName allProjectsName,
@@ -392,7 +389,6 @@
     this.accountCache = accountCache;
     this.changeInserterFactory = changeInserterFactory;
     this.commitValidatorsFactory = commitValidatorsFactory;
-    this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
     this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
@@ -1892,17 +1888,19 @@
   }
 
   private void readChangesForReplace() throws OrmException {
-    List<CheckedFuture<ChangeNotes, OrmException>> futures =
-        Lists.newArrayListWithCapacity(replaceByChange.size());
-    for (ReplaceRequest request : replaceByChange.values()) {
-      futures.add(notesFactory.createAsync(changeUpdateExector, db,
-          project.getNameKey(), request.ontoChange));
-    }
-    for (CheckedFuture<ChangeNotes, OrmException> f : futures) {
-      ChangeNotes notes = f.checkedGet();
-      if (notes.getChange() != null) {
-        replaceByChange.get(notes.getChangeId()).notes = notes;
-      }
+    Collection<ChangeNotes> allNotes =
+        notesFactory.create(
+            db,
+            Collections2.transform(
+                replaceByChange.values(),
+                new Function<ReplaceRequest, Change.Id>() {
+                  @Override
+                  public Change.Id apply(ReplaceRequest in) {
+                    return in.ontoChange;
+                  }
+                }));
+    for (ChangeNotes notes : allNotes) {
+      replaceByChange.get(notes.getChangeId()).notes = notes;
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index ef785f2..c0bfccb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -35,7 +36,6 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.change.ChangeKind;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.extensions.events.CommentAdded;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
index d3b9e95..ff9ff03 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
@@ -20,7 +20,7 @@
 public abstract class IndexPredicate<I> extends OperatorPredicate<I> {
   private final FieldDef<I, ?> def;
 
-  public IndexPredicate(FieldDef<I, ?> def, String value) {
+  protected IndexPredicate(FieldDef<I, ?> def, String value) {
     super(def.getName(), value);
     this.def = def;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedQuery.java
index 3040ca6..65097b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedQuery.java
@@ -14,10 +14,8 @@
 
 package com.google.gerrit.server.index;
 
-import com.google.common.base.Function;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.gerrit.server.query.DataSource;
 import com.google.gerrit.server.query.Paginated;
 import com.google.gerrit.server.query.Predicate;
@@ -26,10 +24,7 @@
 import com.google.gwtorm.server.ResultSet;
 
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Wrapper combining an {@link IndexPredicate} together with a
@@ -48,8 +43,7 @@
 
   private QueryOptions opts;
   private final Predicate<T> pred;
-  private DataSource<T> source;
-  private final Map<T, DataSource<T>> fromSource;
+  protected DataSource<T> source;
 
   public IndexedQuery(Index<I, T> index, Predicate<T> pred,
       QueryOptions opts) throws QueryParseException {
@@ -57,7 +51,6 @@
     this.opts = opts;
     this.pred = pred;
     this.source = index.getSource(pred, this.opts);
-    this.fromSource = new HashMap<>();
   }
 
   @Override
@@ -90,38 +83,7 @@
 
   @Override
   public ResultSet<T> read() throws OrmException {
-    final DataSource<T> currSource = source;
-    final ResultSet<T> rs = currSource.read();
-
-    return new ResultSet<T>() {
-      @Override
-      public Iterator<T> iterator() {
-        return Iterables.transform(
-            rs,
-            new Function<T, T>() {
-              @Override
-              public
-              T apply(T t) {
-                fromSource.put(t, currSource);
-                return t;
-              }
-            }).iterator();
-      }
-
-      @Override
-      public List<T> toList() {
-        List<T> r = rs.toList();
-        for (T t : r) {
-          fromSource.put(t, currSource);
-        }
-        return r;
-      }
-
-      @Override
-      public void close() {
-        rs.close();
-      }
-    };
+    return source.read();
   }
 
   @Override
@@ -146,19 +108,6 @@
   }
 
   @Override
-  public boolean match(T t) throws OrmException {
-    return (source != null && fromSource.get(t) == source) || pred.match(t);
-  }
-
-  @Override
-  public int getCost() {
-    // Index queries are assumed to be cheaper than any other type of query, so
-    // so try to make sure they get picked. Note that pred's cost may be higher
-    // because it doesn't know whether it's being used in an index query or not.
-    return 1;
-  }
-
-  @Override
   public int hashCode() {
     return pred.hashCode();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IntegerRangePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IntegerRangePredicate.java
index 1259951..e62a685 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IntegerRangePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IntegerRangePredicate.java
@@ -31,11 +31,13 @@
     }
   }
 
-  protected abstract int getValueInt(T object) throws OrmException;
+  protected abstract Integer getValueInt(T object) throws OrmException;
 
-  @Override
   public boolean match(T object) throws OrmException {
-    int valueInt = getValueInt(object);
+    Integer valueInt = getValueInt(object);
+    if (valueInt == null) {
+      return false;
+    }
     return valueInt >= range.min && valueInt <= range.max;
   }
 
@@ -48,9 +50,4 @@
   public int getMaximumValue() {
     return range.max;
   }
-
-  @Override
-  public int getCost() {
-    return 1;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
index 8ba7df9..1e2e80b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
@@ -38,9 +38,4 @@
 
   public abstract Date getMinTimestamp();
   public abstract Date getMaxTimestamp();
-
-  @Override
-  public int getCost() {
-    return 1;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
index 8627e3a..0de17d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
@@ -77,7 +77,7 @@
           // Additional values not currently added by getPersonParts.
           // TODO(dborowitz): Move to getPersonParts and remove this hack.
           if (fullName != null) {
-            parts.add(fullName);
+            parts.add(fullName.toLowerCase());
           }
           return parts;
         }
@@ -114,7 +114,8 @@
                   public String apply(String in) {
                     return in.toLowerCase();
                   }
-                });
+                })
+            .toSet();
         }
       };
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java
index 74ba1c3..65e9b09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexRewriter.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.index.QueryOptions;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.account.AccountPredicates;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -37,6 +38,9 @@
   @Override
   public Predicate<AccountState> rewrite(Predicate<AccountState> in,
       QueryOptions opts) throws QueryParseException {
+    if (!AccountPredicates.hasActive(in)) {
+      in = Predicate.and(in, AccountPredicates.isActive());
+    }
     AccountIndex index = indexes.getSearchIndex();
     checkNotNull(index, "no active search index configured for accounts");
     return new IndexedAccountQuery(index, in, opts);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index 074c81a..fe448c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -19,6 +19,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
+import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -595,9 +596,8 @@
         @Override
         public Integer get(ChangeData input, FillArgs args)
             throws OrmException {
-
-          return input.changedLines() != null
-              ? input.changedLines().insertions
+          return input.changedLines().isPresent()
+              ? input.changedLines().get().insertions
               : null;
         }
       };
@@ -609,8 +609,8 @@
         @Override
         public Integer get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.changedLines() != null
-              ? input.changedLines().deletions
+          return input.changedLines().isPresent()
+              ? input.changedLines().get().deletions
               : null;
         }
       };
@@ -622,9 +622,9 @@
         @Override
         public Integer get(ChangeData input, FillArgs args)
             throws OrmException {
-          ChangedLines changedLines = input.changedLines();
-          return changedLines != null
-              ? changedLines.insertions + changedLines.deletions
+          Optional<ChangedLines> changedLines = input.changedLines();
+          return changedLines.isPresent()
+              ? changedLines.get().insertions + changedLines.get().deletions
               : null;
         }
       };
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index 8fe02f4..996caa7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -14,22 +14,33 @@
 
 package com.google.gerrit.server.index.change;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.index.change.ChangeField.CHANGE;
 import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.IndexedQuery;
 import com.google.gerrit.server.index.QueryOptions;
+import com.google.gerrit.server.query.DataSource;
+import com.google.gerrit.server.query.Matchable;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -41,7 +52,7 @@
  * {@link ChangeDataSource} to be chosen by the query processor.
  */
 public class IndexedChangeQuery extends IndexedQuery<Change.Id, ChangeData>
-    implements ChangeDataSource {
+    implements ChangeDataSource, Matchable<ChangeData> {
   public static QueryOptions oneResult() {
     return createOptions(IndexConfig.createDefault(), 0, 1,
         ImmutableSet.<String> of());
@@ -65,9 +76,68 @@
         opts.limit(), opts.fields());
   }
 
+  private final Map<ChangeData, DataSource<ChangeData>> fromSource;
+
   public IndexedChangeQuery(ChangeIndex index, Predicate<ChangeData> pred,
       QueryOptions opts) throws QueryParseException {
     super(index, pred, convertOptions(opts));
+    this.fromSource = new HashMap<>();
+  }
+
+  @Override
+  public ResultSet<ChangeData> read() throws OrmException {
+    final DataSource<ChangeData> currSource = source;
+    final ResultSet<ChangeData> rs = currSource.read();
+
+    return new ResultSet<ChangeData>() {
+      @Override
+      public Iterator<ChangeData> iterator() {
+        return Iterables.transform(
+            rs,
+            new Function<ChangeData, ChangeData>() {
+              @Override
+              public ChangeData apply(ChangeData cd) {
+                fromSource.put(cd, currSource);
+                return cd;
+              }
+            }).iterator();
+      }
+
+      @Override
+      public List<ChangeData> toList() {
+        List<ChangeData> r = rs.toList();
+        for (ChangeData cd : r) {
+          fromSource.put(cd, currSource);
+        }
+        return r;
+      }
+
+      @Override
+      public void close() {
+        rs.close();
+      }
+    };
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    if (source != null && fromSource.get(cd) == source) {
+      return true;
+    }
+
+    Predicate<ChangeData> pred = getChild(0);
+    checkState(pred.isMatchable(),
+        "match invoked, but child predicate %s " + "doesn't implement %s", pred,
+        Matchable.class.getName());
+    return pred.asMatchable().match(cd);
+  }
+
+  @Override
+  public int getCost() {
+    // Index queries are assumed to be cheaper than any other type of query, so
+    // so try to make sure they get picked. Note that pred's cost may be higher
+    // because it doesn't know whether it's being used in an index query or not.
+    return 1;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java
index 5db8f64..e446f9a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java
@@ -151,10 +151,14 @@
         throws OrmException, IOException, NoSuchChangeException {
       // Reload change, as some time may have passed since GetChanges.
       ReviewDb db = ctx.getReviewDbProvider().get();
-      Change c = notesFactory
-          .createChecked(db, new Project.NameKey(event.getProjectName()), id)
-          .getChange();
-      indexerFactory.create(executor, indexes).index(db, c);
+      try {
+        Change c = notesFactory
+            .createChecked(db, new Project.NameKey(event.getProjectName()), id)
+            .getChange();
+        indexerFactory.create(executor, indexes).index(db, c);
+      } catch (NoSuchChangeException e) {
+        indexerFactory.create(executor, indexes).delete(id);
+      }
       return null;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
index 8a80bfe..6e4e1d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -211,6 +211,6 @@
         p = Predicate.and(filterPredicate, p);
       }
     }
-    return p == null || p.match(changeData);
+    return p == null || p.asMatchable().match(changeData);
   }
 }
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 60c7d25..82ed02a 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
@@ -34,11 +34,6 @@
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Ordering;
-import com.google.common.util.concurrent.AsyncFunction;
-import com.google.common.util.concurrent.CheckedFuture;
-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.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.metrics.Timer1;
@@ -52,6 +47,7 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.git.RefCache;
 import com.google.gerrit.server.git.RepoRefCache;
@@ -59,7 +55,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -80,7 +75,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.Callable;
 
 /** View of a single {@link Change} based on the log of its notes branch. */
 public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
@@ -128,20 +122,16 @@
 
     public ChangeNotes createChecked(ReviewDb db, Change c)
         throws OrmException, NoSuchChangeException {
-      ChangeNotes notes = create(db, c.getProject(), c.getId());
-      if (notes.getChange() == null) {
-        throw new NoSuchChangeException(c.getId());
-      }
-      return notes;
+      return createChecked(db, c.getProject(), c.getId());
     }
 
     public ChangeNotes createChecked(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException, NoSuchChangeException {
-      ChangeNotes notes = create(db, project, changeId);
-      if (notes.getChange() == null) {
+      Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId);
+      if (change == null || !change.getProject().equals(project)) {
         throw new NoSuchChangeException(changeId);
       }
-      return notes;
+      return new ChangeNotes(args, change).load();
     }
 
     public ChangeNotes createChecked(Change.Id changeId)
@@ -161,7 +151,7 @@
 
     public ChangeNotes create(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException {
-      Change change = unwrap(db).changes().get(changeId);
+      Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId);
       checkNotNull(change,
           "change %s not found in ReviewDb", changeId);
       checkArgument(change.getProject().equals(project),
@@ -194,7 +184,7 @@
         ReviewDb db, Change.Id changeId) throws OrmException {
       checkState(!args.migration.readChanges(), "do not call"
           + " createFromIdOnlyWhenNoteDbDisabled when NoteDb is enabled");
-      Change change = unwrap(db).changes().get(changeId);
+      Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId);
       checkNotNull(change,
           "change %s not found in ReviewDb", changeId);
       return new ChangeNotes(args, change).load();
@@ -217,41 +207,6 @@
       return new ChangeNotes(args, change).load();
     }
 
-    public CheckedFuture<ChangeNotes, OrmException> createAsync(
-        final ListeningExecutorService executorService, final ReviewDb db,
-        final Project.NameKey project, final Change.Id changeId) {
-      return Futures.makeChecked(
-          Futures.transformAsync(unwrap(db).changes().getAsync(changeId),
-              new AsyncFunction<Change, ChangeNotes>() {
-                @Override
-                public ListenableFuture<ChangeNotes> apply(
-                    final Change change) {
-                  return executorService.submit(new Callable<ChangeNotes>() {
-                    @Override
-                    public ChangeNotes call() throws Exception {
-                      checkArgument(change.getProject().equals(project),
-                          "passed project %s when creating ChangeNotes for %s,"
-                              + " but actual project is %s",
-                          project, changeId, change.getProject());
-                      // Disable auto-rebuilding. This may be called async for a
-                      // large number of changes in one project, increasing
-                      // write contention. Plus, we haven't propagated the
-                      // request scope for the rebuilder to open the db.
-                      return new ChangeNotes(args, change, false, null).load();
-                    }
-                  });
-                }
-              }), new Function<Exception, OrmException>() {
-                @Override
-                public OrmException apply(Exception e) {
-                  if (e instanceof OrmException) {
-                    return (OrmException) e;
-                  }
-                  return new OrmException(e);
-                }
-              });
-    }
-
     public List<ChangeNotes> create(ReviewDb db,
         Collection<Change.Id> changeIds) throws OrmException {
       List<ChangeNotes> notes = new ArrayList<>();
@@ -266,7 +221,7 @@
         return notes;
       }
 
-      for (Change c : unwrap(db).changes().get(changeIds)) {
+      for (Change c : ReviewDbUtil.unwrapDb(db).changes().get(changeIds)) {
         notes.add(createFromChangeOnlyWhenNoteDbDisabled(c));
       }
       return notes;
@@ -286,7 +241,7 @@
         return notes;
       }
 
-      for (Change c : unwrap(db).changes().get(changeIds)) {
+      for (Change c : ReviewDbUtil.unwrapDb(db).changes().get(changeIds)) {
         if (c != null && project.equals(c.getDest().getParentKey())) {
           ChangeNotes cn = createFromChangeOnlyWhenNoteDbDisabled(c);
           if (predicate.apply(cn)) {
@@ -312,7 +267,7 @@
           }
         }
       } else {
-        for (Change change : unwrap(db).changes().all()) {
+        for (Change change : ReviewDbUtil.unwrapDb(db).changes().all()) {
           ChangeNotes notes = createFromChangeOnlyWhenNoteDbDisabled(change);
           if (predicate.apply(notes)) {
             m.put(change.getProject(), notes);
@@ -338,7 +293,7 @@
       // A batch size of N may overload get(Iterable), so use something smaller,
       // but still >1.
       for (List<Change.Id> batch : Iterables.partition(ids, 30)) {
-        for (Change change : unwrap(db).changes().get(batch)) {
+        for (Change change : ReviewDbUtil.unwrapDb(db).changes().get(batch)) {
           notes.add(createFromChangeOnlyWhenNoteDbDisabled(change));
         }
       }
@@ -349,7 +304,7 @@
         Project.NameKey project) throws OrmException, IOException {
       Set<Change.Id> ids = scan(repo);
       List<ChangeNotes> changeNotes = new ArrayList<>(ids.size());
-      db = unwrap(db);
+      db = ReviewDbUtil.unwrapDb(db);
       for (Change.Id id : ids) {
         Change change = db.changes().get(id);
         if (change == null) {
@@ -385,13 +340,6 @@
     }
   }
 
-  private static ReviewDb unwrap(ReviewDb db) {
-    if (db instanceof DisabledChangesReviewDbWrapper) {
-      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
-    }
-    return db;
-  }
-
   private final RefCache refs;
 
   private Change change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
index 8f805d9..a181431 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
@@ -61,7 +61,6 @@
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
@@ -157,7 +156,7 @@
   public Result rebuild(ReviewDb db, Change.Id changeId)
       throws NoSuchChangeException, IOException, OrmException,
       ConfigInvalidException {
-    db = unwrapDb(db);
+    db = ReviewDbUtil.unwrapDb(db);
     Change change = db.changes().get(changeId);
     if (change == null) {
       throw new NoSuchChangeException(changeId);
@@ -188,7 +187,7 @@
   @Override
   public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
       throws NoSuchChangeException, IOException, OrmException {
-    db = unwrapDb(db);
+    db = ReviewDbUtil.unwrapDb(db);
     Change change = db.changes().get(changeId);
     if (change == null) {
       throw new NoSuchChangeException(changeId);
@@ -204,7 +203,7 @@
   public Result execute(ReviewDb db, Change.Id changeId,
       NoteDbUpdateManager manager) throws NoSuchChangeException, OrmException,
       IOException {
-    db = unwrapDb(db);
+    db = ReviewDbUtil.unwrapDb(db);
     Change change = db.changes().get(changeId);
     if (change == null) {
       throw new NoSuchChangeException(changeId);
@@ -1021,11 +1020,4 @@
       }
     }
   }
-
-  private ReviewDb unwrapDb(ReviewDb db) {
-    if (db instanceof DisabledChangesReviewDbWrapper) {
-      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
-    }
-    return db;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 4e8d2a0..e170510 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -25,6 +26,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
+import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.annotations.PluginName;
@@ -58,7 +60,6 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -135,6 +136,34 @@
     }
   }
 
+  public static List<Path> listPlugins(Path pluginsDir, final String suffix)
+      throws IOException {
+    if (pluginsDir == null || !Files.exists(pluginsDir)) {
+      return ImmutableList.of();
+    }
+    DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
+      @Override
+      public boolean accept(Path entry) throws IOException {
+        String n = entry.getFileName().toString();
+        boolean accept = !n.startsWith(".last_")
+            && !n.startsWith(".next_")
+            && Files.isRegularFile(entry);
+        if (!Strings.isNullOrEmpty(suffix)) {
+          accept &= n.endsWith(suffix);
+        }
+        return accept;
+      }
+    };
+    try (DirectoryStream<Path> files = Files.newDirectoryStream(
+        pluginsDir, filter)) {
+      return Ordering.natural().sortedCopy(files);
+    }
+  }
+
+  public static List<Path> listPlugins(Path pluginsDir) throws IOException {
+    return listPlugins(pluginsDir, null);
+  }
+
   public boolean isRemoteAdminEnabled() {
     return remoteAdmin;
   }
@@ -681,20 +710,8 @@
   }
 
   private List<Path> scanPathsInPluginsDirectory(Path pluginsDir) {
-    if (pluginsDir == null || !Files.exists(pluginsDir)) {
-      return Collections.emptyList();
-    }
-    DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
-      @Override
-      public boolean accept(Path entry) throws IOException {
-        String n = entry.getFileName().toString();
-        return !n.startsWith(".last_")
-            && !n.startsWith(".next_");
-      }
-    };
-    try (DirectoryStream<Path> files
-        = Files.newDirectoryStream(pluginsDir, filter)) {
-      return ImmutableList.copyOf(files);
+    try {
+      return listPlugins(pluginsDir);
     } catch (IOException e) {
       log.error("Cannot list " + pluginsDir.toAbsolutePath(), e);
       return ImmutableList.of();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
index 39b0fa3..899e789 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.query;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.gwtorm.server.OrmException;
 
 import java.util.ArrayList;
@@ -23,7 +25,7 @@
 import java.util.List;
 
 /** Requires all predicates to be true. */
-public class AndPredicate<T> extends Predicate<T> {
+public class AndPredicate<T> extends Predicate<T> implements Matchable<T> {
   private final List<Predicate<T>> children;
   private final int cost;
 
@@ -39,11 +41,11 @@
       if (getClass() == p.getClass()) {
         for (Predicate<T> gp : p.getChildren()) {
           t.add(gp);
-          c += gp.getCost();
+          c += gp.estimateCost();
         }
       } else {
         t.add(p);
-        c += p.getCost();
+        c += p.estimateCost();
       }
     }
     children = t;
@@ -71,9 +73,21 @@
   }
 
   @Override
+  public boolean isMatchable() {
+    for (Predicate<T> c : children) {
+      if (!c.isMatchable()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
   public boolean match(final T object) throws OrmException {
-    for (final Predicate<T> c : children) {
-      if (!c.match(object)) {
+    for (Predicate<T> c : children) {
+      checkState(c.isMatchable(), "match invoked, but child predicate %s "
+          + "doesn't implement %s", c, Matchable.class.getName());
+      if (!c.asMatchable().match(object)) {
         return false;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
index 4b62333..168be5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
@@ -36,17 +36,29 @@
     implements DataSource<T>, Comparator<Predicate<T>> {
   protected final DataSource<T> source;
 
+  private final IsVisibleToPredicate<T> isVisibleToPredicate;
   private final int start;
   private final int cardinality;
 
-
   public AndSource(Collection<? extends Predicate<T>> that) {
-    this(that, 0);
+    this(that, null, 0);
   }
 
-  public AndSource(Collection<? extends Predicate<T>> that, int start) {
+  public AndSource(Predicate<T> that,
+      IsVisibleToPredicate<T> isVisibleToPredicate) {
+    this(that, isVisibleToPredicate, 0);
+  }
+
+  public AndSource(Predicate<T> that,
+      IsVisibleToPredicate<T> isVisibleToPredicate, int start) {
+    this(ImmutableList.of(that), isVisibleToPredicate, start);
+  }
+
+  public AndSource(Collection<? extends Predicate<T>> that,
+      IsVisibleToPredicate<T> isVisibleToPredicate, int start) {
     super(that);
     checkArgument(start >= 0, "negative start: %s", start);
+    this.isVisibleToPredicate = isVisibleToPredicate;
     this.start = start;
 
     int c = Integer.MAX_VALUE;
@@ -56,9 +68,10 @@
       if (p instanceof DataSource) {
         c = Math.min(c, ((DataSource<?>) p).getCardinality());
 
-        if (p.getCost() < minCost) {
+        int cost = p.estimateCost();
+        if (cost < minCost) {
           s = toDataSource(p);
-          minCost = p.getCost();
+          minCost = cost;
         }
       }
     }
@@ -85,7 +98,7 @@
     int nextStart = 0;
     boolean skipped = false;
     for (T data : buffer(source.read())) {
-      if (match(data)) {
+      if (!isMatchable() || match(data)) {
         r.add(data);
       } else {
         skipped = true;
@@ -124,6 +137,24 @@
     return new ListResultSet<>(r);
   }
 
+  @Override
+  public boolean isMatchable() {
+    return isVisibleToPredicate != null || super.isMatchable();
+  }
+
+  @Override
+  public boolean match(T object) throws OrmException {
+    if (isVisibleToPredicate != null && !isVisibleToPredicate.match(object)) {
+      return false;
+    }
+
+    if (super.isMatchable() && !super.match(object)) {
+      return false;
+    }
+
+    return true;
+  }
+
   private Iterable<T> buffer(ResultSet<T> scanner) {
     return FluentIterable.from(Iterables.partition(scanner, 50))
         .transformAndConcat(new Function<List<T>, List<T>>() {
@@ -156,7 +187,7 @@
     int cmp = ai - bi;
 
     if (cmp == 0) {
-      cmp = a.getCost() - b.getCost();
+      cmp = a.estimateCost() - b.estimateCost();
     }
 
     if (cmp == 0
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/IsVisibleToPredicate.java
new file mode 100644
index 0000000..38411e3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/IsVisibleToPredicate.java
@@ -0,0 +1,22 @@
+// 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.query;
+
+public abstract class IsVisibleToPredicate<T> extends OperatorPredicate<T>
+    implements Matchable<T> {
+  public IsVisibleToPredicate(String name, String value) {
+    super(name, value);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/LimitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/LimitPredicate.java
index d4e7440..7c38e5a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/LimitPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/LimitPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query;
 
-public class LimitPredicate<T> extends IntPredicate<T> {
+public class LimitPredicate<T> extends IntPredicate<T> implements Matchable<T> {
   @SuppressWarnings("unchecked")
   public static Integer getLimit(String fieldName, Predicate<?> p) {
     IntPredicate<?> ip = QueryBuilder.find(p, IntPredicate.class, fieldName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Matchable.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Matchable.java
new file mode 100644
index 0000000..b37e112
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Matchable.java
@@ -0,0 +1,29 @@
+// 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.query;
+
+import com.google.gwtorm.server.OrmException;
+
+public interface Matchable<T> {
+  /**
+   * Does this predicate match this object?
+   *
+   * @throws OrmException
+   */
+  boolean match(T object) throws OrmException;
+
+  /** @return a cost estimate to run this predicate, higher figures cost more. */
+  int getCost();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
index 248fb9c..8ffba72 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.query;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.gwtorm.server.OrmException;
 
 import java.util.Collection;
@@ -21,7 +23,7 @@
 import java.util.List;
 
 /** Negates the result of another predicate. */
-public class NotPredicate<T> extends Predicate<T> {
+public class NotPredicate<T> extends Predicate<T> implements Matchable<T> {
   private final Predicate<T> that;
 
   protected NotPredicate(final Predicate<T> that) {
@@ -58,13 +60,20 @@
   }
 
   @Override
+  public boolean isMatchable() {
+    return that.isMatchable();
+  }
+
+  @Override
   public boolean match(final T object) throws OrmException {
-    return !that.match(object);
+    checkState(that.isMatchable(), "match invoked, but child predicate %s "
+        + "doesn't implement %s", that, Matchable.class.getName());
+    return !that.asMatchable().match(object);
   }
 
   @Override
   public int getCost() {
-    return that.getCost();
+    return that.estimateCost();
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java
index e9fea02..2cb70af 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java
@@ -22,7 +22,7 @@
   private final String name;
   private final String value;
 
-  public OperatorPredicate(final String name, final String value) {
+  protected OperatorPredicate(final String name, final String value) {
     this.name = name;
     this.value = value;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
index 2432a41..ad15286 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.query;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.gwtorm.server.OrmException;
 
 import java.util.ArrayList;
@@ -23,7 +25,7 @@
 import java.util.List;
 
 /** Requires one predicate to be true. */
-public class OrPredicate<T> extends Predicate<T> {
+public class OrPredicate<T> extends Predicate<T> implements Matchable<T> {
   private final List<Predicate<T>> children;
   private final int cost;
 
@@ -39,11 +41,11 @@
       if (getClass() == p.getClass()) {
         for (Predicate<T> gp : p.getChildren()) {
           t.add(gp);
-          c += gp.getCost();
+          c += gp.estimateCost();
         }
       } else {
         t.add(p);
-        c += p.getCost();
+        c += p.estimateCost();
       }
     }
     children = t;
@@ -71,9 +73,21 @@
   }
 
   @Override
+  public boolean isMatchable() {
+    for (Predicate<T> c : children) {
+      if (!c.isMatchable()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
   public boolean match(final T object) throws OrmException {
     for (final Predicate<T> c : children) {
-      if (c.match(object)) {
+      checkState(c.isMatchable(), "match invoked, but child predicate %s "
+          + "doesn't implement %s", c, Matchable.class.getName());
+      if (c.asMatchable().match(object)) {
         return true;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index f4be013..3a38da6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.server.query;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.common.collect.Iterables;
-import com.google.gwtorm.server.OrmException;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -113,15 +114,23 @@
   /** Create a copy of this predicate, with new children. */
   public abstract Predicate<T> copy(Collection<? extends Predicate<T>> children);
 
-  /**
-   * Does this predicate match this object?
-   *
-   * @throws OrmException
-   */
-  public abstract boolean match(T object) throws OrmException;
+  public boolean isMatchable() {
+    return this instanceof Matchable;
+  }
+
+  @SuppressWarnings("unchecked")
+  public Matchable<T> asMatchable() {
+    checkState(isMatchable(), "not matchable");
+    return (Matchable<T>) this;
+  }
 
   /** @return a cost estimate to run this predicate, higher figures cost more. */
-  public abstract int getCost();
+  public int estimateCost() {
+    if (!isMatchable()) {
+      return 1;
+    }
+    return asMatchable().getCost();
+  }
 
   @Override
   public abstract int hashCode();
@@ -129,7 +138,7 @@
   @Override
   public abstract boolean equals(Object other);
 
-  private static class Any<T> extends Predicate<T> {
+  private static class Any<T> extends Predicate<T> implements Matchable<T> {
     private static final Any<Object> INSTANCE = new Any<>();
 
     private Any() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIdPredicate.java
deleted file mode 100644
index 659f5d8..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIdPredicate.java
+++ /dev/null
@@ -1,42 +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.query.account;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.index.account.AccountField;
-import com.google.gwtorm.server.OrmException;
-
-public class AccountIdPredicate extends IndexPredicate<AccountState> {
-  private final Account.Id accountId;
-
-  public AccountIdPredicate(Account.Id accountId) {
-    super(AccountField.ID, AccountQueryBuilder.FIELD_ACCOUNT,
-        accountId.toString());
-    this.accountId = accountId;
-  }
-
-  @Override
-  public boolean match(AccountState accountState) throws OrmException {
-    return accountId.equals(accountState.getAccount().getId());
-  }
-
-  @Override
-  public int getCost() {
-    return 1;
-  }
-
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
similarity index 88%
rename from gerrit-server/src/main/java/com/google/gerrit/server/query/account/IsVisibleToPredicate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
index 8fbe4cf..dc68a61 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/IsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
@@ -17,11 +17,12 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.IsVisibleToPredicate;
 import com.google.gerrit.server.query.change.SingleGroupUser;
 import com.google.gwtorm.server.OrmException;
 
-public class IsVisibleToPredicate extends OperatorPredicate<AccountState> {
+public class AccountIsVisibleToPredicate
+    extends IsVisibleToPredicate<AccountState> {
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
       return user.getAccountId().toString();
@@ -35,7 +36,7 @@
 
   private final AccountControl accountControl;
 
-  IsVisibleToPredicate(AccountControl accountControl) {
+  AccountIsVisibleToPredicate(AccountControl accountControl) {
     super(AccountQueryBuilder.FIELD_VISIBLETO,
         describe(accountControl.getUser()));
     this.accountControl = accountControl;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
new file mode 100644
index 0000000..7a9f5bd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -0,0 +1,71 @@
+// 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.query.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.account.AccountField;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryBuilder;
+
+public class AccountPredicates {
+  public static boolean hasActive(Predicate<AccountState> p) {
+    return QueryBuilder.find(p, AccountPredicate.class,
+        AccountField.ACTIVE.getName()) != null;
+  }
+
+  static Predicate<AccountState> id(Account.Id accountId) {
+    return new AccountPredicate(AccountField.ID,
+        AccountQueryBuilder.FIELD_ACCOUNT, accountId.toString());
+  }
+
+  static Predicate<AccountState> email(String email) {
+    return new AccountPredicate(AccountField.EMAIL,
+        AccountQueryBuilder.FIELD_EMAIL, email.toLowerCase());
+  }
+
+  static Predicate<AccountState> equalsName(String name) {
+    return new AccountPredicate(AccountField.NAME_PART,
+        AccountQueryBuilder.FIELD_NAME, name.toLowerCase());
+  }
+
+  public static Predicate<AccountState> isActive() {
+    return new AccountPredicate(AccountField.ACTIVE, "1");
+  }
+
+  static Predicate<AccountState> isInactive() {
+    return new AccountPredicate(AccountField.ACTIVE, "0");
+  }
+
+  static Predicate<AccountState> username(String username) {
+    return new AccountPredicate(AccountField.USERNAME,
+        AccountQueryBuilder.FIELD_USERNAME, username.toLowerCase());
+  }
+
+  static class AccountPredicate extends IndexPredicate<AccountState> {
+    AccountPredicate(FieldDef<AccountState, ?> def, String value) {
+      super(def, value);
+    }
+
+    AccountPredicate(FieldDef<AccountState, ?> def, String name, String value) {
+      super(def, name, value);
+    }
+  }
+
+  private AccountPredicates() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 4082e08..b10d4c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.account;
 
+import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -28,6 +29,8 @@
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 
+import java.util.List;
+
 /**
  * Parses a query string meant to be applied to account objects.
  */
@@ -37,7 +40,10 @@
   }
 
   public static final String FIELD_ACCOUNT = "account";
+  public static final String FIELD_EMAIL = "email";
   public static final String FIELD_LIMIT = "limit";
+  public static final String FIELD_NAME = "name";
+  public static final String FIELD_USERNAME = "username";
   public static final String FIELD_VISIBLETO = "visibleto";
 
   private static final QueryBuilder.Definition<AccountState, AccountQueryBuilder> mydef =
@@ -81,15 +87,19 @@
   }
 
   @Operator
-  public Predicate<AccountState> account(String query)
-      throws QueryParseException {
-    if ("self".equals(query)) {
-      return new AccountIdPredicate(self());
+  public Predicate<AccountState> email(String email) {
+    return AccountPredicates.email(email);
+  }
+
+  @Operator
+  public Predicate<AccountState> is(String value) throws QueryParseException {
+    if ("active".equalsIgnoreCase(value)) {
+      return AccountPredicates.isActive();
     }
-    if (query.matches("^[1-9][0-9]*$")) {
-      return new AccountIdPredicate(Account.Id.parse(query));
+    if ("inactive".equalsIgnoreCase(value)) {
+      return AccountPredicates.isInactive();
     }
-    throw error("User " + query + " not found");
+    throw error("Invalid query");
   }
 
   @Operator
@@ -102,10 +112,31 @@
     return new LimitPredicate<>(FIELD_LIMIT, limit);
   }
 
+  @Operator
+  public Predicate<AccountState> name(String name) {
+    return AccountPredicates.equalsName(name);
+  }
+
+  @Operator
+  public Predicate<AccountState> username(String username) {
+    return AccountPredicates.username(username);
+  }
+
   @Override
-  protected Predicate<AccountState> defaultField(String query)
+  public Predicate<AccountState> defaultField(String query)
       throws QueryParseException {
-    return account(query);
+    if ("self".equalsIgnoreCase(query)) {
+      return AccountPredicates.id(self());
+    }
+
+    List<Predicate<AccountState>> preds = Lists.newArrayListWithCapacity(3);
+    Integer id = Ints.tryParse(query);
+    if (id != null) {
+      preds.add(AccountPredicates.id(new Account.Id(id)));
+    }
+    preds.add(name(query));
+    preds.add(username(query));
+    return Predicate.or(preds);
   }
 
   private Account.Id self() throws QueryParseException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
index b2be7b5..48d0897 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.server.query.account;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.query.account.AccountQueryBuilder.FIELD_LIMIT;
 
-import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.index.account.AccountIndexRewriter;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
@@ -33,6 +34,13 @@
 public class AccountQueryProcessor extends QueryProcessor<AccountState> {
   private final AccountControl.Factory accountControlFactory;
 
+  static {
+    // It is assumed that basic rewrites do not touch visibleto predicates.
+    checkState(
+        !AccountIsVisibleToPredicate.class.isAssignableFrom(IndexPredicate.class),
+        "AccountQueryProcessor assumes visibleto is not used by the index rewriter.");
+  }
+
   @Inject
   protected AccountQueryProcessor(Provider<CurrentUser> userProvider,
       Metrics metrics,
@@ -48,7 +56,7 @@
   @Override
   protected Predicate<AccountState> enforceVisibility(
       Predicate<AccountState> pred) {
-    return new AndSource<>(ImmutableList.of(pred,
-        new IsVisibleToPredicate(accountControlFactory.get())));
+    return new AndSource<>(pred,
+        new AccountIsVisibleToPredicate(accountControlFactory.get()), start);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java
index b20a194..b3cdd6a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java
@@ -14,18 +14,17 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.IntegerRangePredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 
-public class AddedPredicate extends IntegerRangePredicate<ChangeData> {
+public class AddedPredicate extends IntegerRangeChangePredicate {
   AddedPredicate(String value) throws QueryParseException {
     super(ChangeField.ADDED, value);
   }
 
   @Override
-  protected int getValueInt(ChangeData changeData) throws OrmException {
-    return changeData.changedLines().insertions;
+  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+    return ChangeField.ADDED.get(changeData, null);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java
index 5ed871a..477bf16 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.TimestampRangePredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 
 import java.util.Date;
 
-public class AfterPredicate extends TimestampRangePredicate<ChangeData> {
+public class AfterPredicate extends TimestampRangeChangePredicate {
   private final Date cut;
 
   AfterPredicate(String value) throws QueryParseException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
index 2b140d3..fd6cbee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -20,13 +20,12 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.index.TimestampRangePredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
 import java.sql.Timestamp;
 
-public class AgePredicate extends TimestampRangePredicate<ChangeData> {
+public class AgePredicate extends TimestampRangeChangePredicate {
   private final long cut;
 
   AgePredicate(String value) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndChangeSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndChangeSource.java
index d40e53f..bd7daed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndChangeSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndChangeSource.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.server.query.AndSource;
+import com.google.gerrit.server.query.IsVisibleToPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
@@ -25,13 +26,13 @@
 public class AndChangeSource extends AndSource<ChangeData>
     implements ChangeDataSource {
 
-  public AndChangeSource(Collection<? extends Predicate<ChangeData>> that) {
-    super(that, 0);
+  public AndChangeSource(Collection<Predicate<ChangeData>> that) {
+    super(that);
   }
 
-  public AndChangeSource(Collection<? extends Predicate<ChangeData>> that,
-      int start) {
-    super(that, start);
+  public AndChangeSource(Predicate<ChangeData> that,
+      IsVisibleToPredicate<ChangeData> isVisibleToPredicate, int start) {
+    super(that, isVisibleToPredicate, start);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
index ae192ab..ebaaab9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
@@ -17,11 +17,10 @@
 import static com.google.gerrit.server.index.change.ChangeField.AUTHOR;
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_AUTHOR;
 
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-public class AuthorPredicate extends IndexPredicate<ChangeData>  {
+public class AuthorPredicate extends ChangeIndexPredicate {
   AuthorPredicate(String value) {
     super(AUTHOR, FIELD_AUTHOR, value.toLowerCase());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java
index 0618cc2..f36a1631 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.TimestampRangePredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 
 import java.util.Date;
 
-public class BeforePredicate extends TimestampRangePredicate<ChangeData> {
+public class BeforePredicate extends TimestampRangeChangePredicate {
   private final Date cut;
 
   BeforePredicate(String value) throws QueryParseException {
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 1794ee3..1929833 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
@@ -331,13 +331,14 @@
   private Collection<PatchSet> patchSets;
   private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals;
   private List<PatchSetApproval> currentApprovals;
-  private Map<Integer, List<String>> files = new HashMap<>();
+  private Map<Integer, List<String>> files;
+  private Map<Integer, Optional<PatchList>> patchLists;
   private Collection<PatchLineComment> publishedComments;
   private CurrentUser visibleTo;
   private ChangeControl changeControl;
   private List<ChangeMessage> messages;
   private List<SubmitRecord> submitRecords;
-  private ChangedLines changedLines;
+  private Optional<ChangedLines> changedLines;
   private SubmitTypeRecord submitTypeRecord;
   private Boolean mergeable;
   private Set<String> hashtags;
@@ -549,10 +550,17 @@
     return db;
   }
 
+  private Map<Integer, List<String>> initFiles() {
+    if (files == null) {
+      files = new HashMap<>();
+    }
+    return files;
+  }
+
   public void setCurrentFilePaths(List<String> filePaths) throws OrmException {
     PatchSet ps = currentPatchSet();
     if (ps != null) {
-      files.put(ps.getPatchSetId(), ImmutableList.copyOf(filePaths));
+      initFiles().put(ps.getPatchSetId(), ImmutableList.copyOf(filePaths));
     }
   }
 
@@ -565,23 +573,23 @@
   }
 
   public List<String> filePaths(PatchSet ps) throws OrmException {
-    if (!files.containsKey(ps.getPatchSetId())) {
+    Integer psId = ps.getPatchSetId();
+    List<String> r = initFiles().get(psId);
+    if (r == null) {
       Change c = change();
       if (c == null) {
         return null;
       }
 
-      PatchList p;
-      try {
-        p = patchListCache.get(c, ps);
-      } catch (PatchListNotAvailableException e) {
+      Optional<PatchList> p = getPatchList(c, ps);
+      if (!p.isPresent()) {
         List<String> emptyFileList = Collections.emptyList();
         files.put(ps.getPatchSetId(), emptyFileList);
         return emptyFileList;
       }
 
-      List<String> r = new ArrayList<>(p.getPatches().size());
-      for (PatchListEntry e : p.getPatches()) {
+      r = new ArrayList<>(p.get().getPatches().size());
+      for (PatchListEntry e : p.get().getPatches()) {
         if (Patch.COMMIT_MSG.equals(e.getNewName())) {
           continue;
         }
@@ -601,37 +609,59 @@
         }
       }
       Collections.sort(r);
-      files.put(ps.getPatchSetId(), Collections.unmodifiableList(r));
+      r = Collections.unmodifiableList(r);
+      files.put(psId, r);
     }
-    return files.get(ps.getPatchSetId());
+    return r;
   }
 
-  public ChangedLines changedLines() throws OrmException {
-    if (changedLines == null) {
-      Change c = change();
-      if (c == null) {
-        return null;
-      }
-
-      PatchSet ps = currentPatchSet();
-      if (ps == null) {
-        return null;
-      }
-
-      PatchList p;
+  private Optional<PatchList> getPatchList(Change c, PatchSet ps) {
+    Integer psId = ps.getId().get();
+    if (patchLists == null) {
+      patchLists = new HashMap<>();
+    }
+    Optional<PatchList> r = patchLists.get(psId);
+    if (r == null) {
       try {
-        p = patchListCache.get(c, ps);
+        r = Optional.of(patchListCache.get(c, ps));
       } catch (PatchListNotAvailableException e) {
-        return null;
+        r = Optional.absent();
       }
+      patchLists.put(psId, r);
+    }
+    return r;
+  }
 
-      changedLines = new ChangedLines(p.getInsertions(), p.getDeletions());
+  private Optional<ChangedLines> computeChangedLines() throws OrmException {
+    Change c = change();
+    if (c == null) {
+      return Optional.absent();
+    }
+    PatchSet ps = currentPatchSet();
+    if (ps == null) {
+      return Optional.absent();
+    }
+    Optional<PatchList> p = getPatchList(c, ps);
+    if (!p.isPresent()) {
+      return Optional.absent();
+    }
+    return Optional.of(
+        new ChangedLines(p.get().getInsertions(), p.get().getDeletions()));
+  }
+
+  public Optional<ChangedLines> changedLines() throws OrmException {
+    if (changedLines == null) {
+      changedLines = computeChangedLines();
     }
     return changedLines;
   }
 
   public void setChangedLines(int insertions, int deletions) {
-    changedLines = new ChangedLines(insertions, deletions);
+    changedLines = Optional.of(new ChangedLines(insertions, deletions));
+  }
+
+  public void setNoChangedLines() {
+    changedLines = Optional.absent();
   }
 
   public Change.Id getId() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
index 1fbeb86..85d433a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -15,12 +15,11 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
 /** Predicate over Change-Id strings (aka Change.Key). */
-class ChangeIdPredicate extends IndexPredicate<ChangeData> {
+class ChangeIdPredicate extends ChangeIndexPredicate {
   ChangeIdPredicate(String id) {
     super(ChangeField.ID, ChangeQueryBuilder.FIELD_CHANGE, id);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
new file mode 100644
index 0000000..80951fd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
@@ -0,0 +1,31 @@
+// 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.query.change;
+
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.query.Matchable;
+
+public abstract class ChangeIndexPredicate extends IndexPredicate<ChangeData>
+    implements Matchable<ChangeData> {
+  protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String value) {
+    super(def, value);
+  }
+
+  protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String name,
+      String value) {
+    super(def, name, value);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
similarity index 91%
rename from gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index c4214f2..303c9f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -20,11 +20,11 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.IsVisibleToPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class IsVisibleToPredicate extends OperatorPredicate<ChangeData> {
+class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> {
   private static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
       return user.getAccountId().toString();
@@ -41,7 +41,8 @@
   private final ChangeControl.GenericFactory changeControl;
   private final CurrentUser user;
 
-  IsVisibleToPredicate(Provider<ReviewDb> db, ChangeNotes.Factory notesFactory,
+  ChangeIsVisibleToPredicate(Provider<ReviewDb> db,
+      ChangeNotes.Factory notesFactory,
       ChangeControl.GenericFactory changeControlFactory, CurrentUser user) {
     super(ChangeQueryBuilder.FIELD_VISIBLETO, describe(user));
     this.db = db;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeOperatorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeOperatorPredicate.java
new file mode 100644
index 0000000..6bec598
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeOperatorPredicate.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.query.change;
+
+import com.google.gerrit.server.query.Matchable;
+import com.google.gerrit.server.query.OperatorPredicate;
+
+public abstract class ChangeOperatorPredicate extends
+    OperatorPredicate<ChangeData> implements Matchable<ChangeData> {
+
+  protected ChangeOperatorPredicate(String name, String value) {
+    super(name, value);
+  }
+}
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 b99f024..a150c93 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
@@ -479,7 +479,7 @@
       // not status: alias?
     }
 
-    throw new IllegalArgumentException();
+    throw error("Invalid query");
   }
 
   @Operator
@@ -768,7 +768,7 @@
   }
 
   public Predicate<ChangeData> visibleto(CurrentUser user) {
-    return new IsVisibleToPredicate(args.db, args.notesFactory,
+    return new ChangeIsVisibleToPredicate(args.db, args.notesFactory,
         args.changeControlGenericFactory, user);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index 4e534c8..44e5e7e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -17,7 +17,6 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_LIMIT;
 
-import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.index.IndexConfig;
@@ -44,7 +43,7 @@
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
     checkState(
-        !IsVisibleToPredicate.class.isAssignableFrom(IndexPredicate.class),
+        !ChangeIsVisibleToPredicate.class.isAssignableFrom(IndexPredicate.class),
         "ChangeQueryProcessor assumes visibleto is not used by the index rewriter.");
   }
 
@@ -80,7 +79,7 @@
   @Override
   protected Predicate<ChangeData> enforceVisibility(
       Predicate<ChangeData> pred) {
-    return new AndChangeSource(ImmutableList.of(pred, new IsVisibleToPredicate(db,
-        notesFactory, changeControlFactory, userProvider.get())), start);
+    return new AndChangeSource(pred, new ChangeIsVisibleToPredicate(db,
+        notesFactory, changeControlFactory, userProvider.get()), start);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeRegexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeRegexPredicate.java
new file mode 100644
index 0000000..747d72d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeRegexPredicate.java
@@ -0,0 +1,31 @@
+// 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.query.change;
+
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.query.Matchable;
+
+public abstract class ChangeRegexPredicate extends RegexPredicate<ChangeData>
+    implements Matchable<ChangeData> {
+  protected ChangeRegexPredicate(FieldDef<ChangeData, ?> def, String value) {
+    super(def, value);
+  }
+
+  protected ChangeRegexPredicate(FieldDef<ChangeData, ?> def, String name,
+      String value) {
+    super(def, name, value);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 4d42c33..1c92ecf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gwtorm.server.OrmException;
@@ -36,7 +35,7 @@
  * <p>
  * Status names are looked up by prefix case-insensitively.
  */
-public final class ChangeStatusPredicate extends IndexPredicate<ChangeData> {
+public final class ChangeStatusPredicate extends ChangeIndexPredicate {
   private static final TreeMap<String, Predicate<ChangeData>> PREDICATES;
   private static final Predicate<ChangeData> CLOSED;
   private static final Predicate<ChangeData> OPEN;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
index 1967a06..48d6e05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
@@ -17,13 +17,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
 import java.util.Objects;
 
-class CommentByPredicate extends IndexPredicate<ChangeData> {
+class CommentByPredicate extends ChangeIndexPredicate {
   private final Account.Id id;
 
   CommentByPredicate(Account.Id id) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
index 5e3fa3d..b351740 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
@@ -22,7 +21,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 
-class CommentPredicate extends IndexPredicate<ChangeData> {
+class CommentPredicate extends ChangeIndexPredicate {
   private final ChangeIndex index;
 
   CommentPredicate(ChangeIndex index, String value) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
index 91b2c58..aa3dde3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -20,10 +20,9 @@
 
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.index.FieldDef;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 
-class CommitPredicate extends IndexPredicate<ChangeData> {
+class CommitPredicate extends ChangeIndexPredicate {
   static FieldDef<ChangeData, ?> commitField(String id) {
     if (id.length() == OBJECT_ID_STRING_LENGTH) {
       return EXACT_COMMIT;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
index f923d00..06f5379 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
@@ -17,11 +17,10 @@
 import static com.google.gerrit.server.index.change.ChangeField.COMMITTER;
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_COMMITTER;
 
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-public class CommitterPredicate extends IndexPredicate<ChangeData>  {
+public class CommitterPredicate extends ChangeIndexPredicate {
   CommitterPredicate(String value) {
     super(COMMITTER, FIELD_COMMITTER, value.toLowerCase());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 74865ec..69bc2ca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gerrit.server.query.OrPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
@@ -84,7 +83,7 @@
       predicatesForOneChange.add(or(or(filePredicates),
           new IsMergePredicate(args, value)));
 
-      predicatesForOneChange.add(new OperatorPredicate<ChangeData>(
+      predicatesForOneChange.add(new ChangeOperatorPredicate(
           ChangeQueryBuilder.FIELD_CONFLICTS, value) {
 
         @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java
index 8e9ac73..9e49269 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java
@@ -14,18 +14,17 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.IntegerRangePredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 
-public class DeletedPredicate extends IntegerRangePredicate<ChangeData> {
+public class DeletedPredicate extends IntegerRangeChangePredicate {
   DeletedPredicate(String value) throws QueryParseException {
     super(ChangeField.DELETED, value);
   }
 
   @Override
-  protected int getValueInt(ChangeData changeData) throws OrmException {
-    return changeData.changedLines().deletions;
+  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+    return ChangeField.DELETED.get(changeData, null);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java
index a3eaa8a..ce33225 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java
@@ -14,20 +14,17 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.IntegerRangePredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
 import com.google.gwtorm.server.OrmException;
 
-public class DeltaPredicate extends IntegerRangePredicate<ChangeData> {
+public class DeltaPredicate extends IntegerRangeChangePredicate {
   DeltaPredicate(String value) throws QueryParseException {
     super(ChangeField.DELTA, value);
   }
 
   @Override
-  protected int getValueInt(ChangeData changeData) throws OrmException {
-    ChangedLines changedLines = changeData.changedLines();
-    return changedLines.insertions + changedLines.deletions;
+  protected Integer getValueInt(ChangeData changeData) throws OrmException {
+    return ChangeField.DELTA.get(changeData, null);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java
index 25fa09f..7e573dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java
@@ -16,12 +16,11 @@
 
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gwtorm.server.OrmException;
 
 import java.util.Set;
 
-class DestinationPredicate extends OperatorPredicate<ChangeData> {
+class DestinationPredicate extends ChangeOperatorPredicate {
   Set<Branch.NameKey> destinations;
 
   DestinationPredicate(Set<Branch.NameKey> destinations, String value) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java
index f1fa000..8be5235 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java
@@ -15,11 +15,10 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class EditByPredicate extends IndexPredicate<ChangeData> {
+class EditByPredicate extends ChangeIndexPredicate {
   private final Account.Id id;
 
   EditByPredicate(Account.Id id) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
index 8c98b6f..6877761 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -14,13 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.OrmException;
 
-class EqualsFilePredicate extends IndexPredicate<ChangeData> {
+class EqualsFilePredicate extends ChangeIndexPredicate {
   static Predicate<ChangeData> create(Arguments args, String value) {
     Predicate<ChangeData> eqPath =
         new EqualsPathPredicate(ChangeQueryBuilder.FIELD_FILE, value);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index b01fdbe..e752b05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -32,7 +31,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class EqualsLabelPredicate extends IndexPredicate<ChangeData> {
+class EqualsLabelPredicate extends ChangeIndexPredicate {
   private final ProjectCache projectCache;
   private final ChangeControl.GenericFactory ccFactory;
   private final IdentifiedUser.GenericFactory userFactory;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
index 85c3cd5..5edd06c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
 import java.util.Collections;
 import java.util.List;
 
-class EqualsPathPredicate extends IndexPredicate<ChangeData> {
+class EqualsPathPredicate extends ChangeIndexPredicate {
   private final String value;
 
   EqualsPathPredicate(String fieldName, String value) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
index 6658577..510910e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
@@ -17,10 +17,9 @@
 import static com.google.gerrit.server.index.change.ChangeField.EXACT_TOPIC;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gwtorm.server.OrmException;
 
-class ExactTopicPredicate extends IndexPredicate<ChangeData> {
+class ExactTopicPredicate extends ChangeIndexPredicate {
   ExactTopicPredicate(String topic) {
     super(EXACT_TOPIC, topic);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
index 23b3ee6..5651544 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
@@ -18,14 +18,13 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 
-class FuzzyTopicPredicate extends IndexPredicate<ChangeData> {
+class FuzzyTopicPredicate extends ChangeIndexPredicate {
   private final ChangeIndex index;
 
   FuzzyTopicPredicate(String topic, ChangeIndex index) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java
index ff9c544..9e9bc8d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java
@@ -15,13 +15,12 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
 import java.util.List;
 
-class GroupPredicate extends IndexPredicate<ChangeData> {
+class GroupPredicate extends ChangeIndexPredicate {
   GroupPredicate(String group) {
     super(ChangeField.GROUP, group);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByLegacyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByLegacyPredicate.java
index f85a9ed..45a00c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByLegacyPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByLegacyPredicate.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.OrmException;
@@ -29,8 +28,8 @@
 import java.util.Set;
 
 @Deprecated
-class HasDraftByLegacyPredicate extends OperatorPredicate<ChangeData> implements
-    ChangeDataSource {
+class HasDraftByLegacyPredicate extends ChangeOperatorPredicate
+    implements ChangeDataSource {
   private final Arguments args;
   private final Account.Id accountId;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
index c18e19c..244589c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -15,11 +15,10 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class HasDraftByPredicate extends IndexPredicate<ChangeData> {
+class HasDraftByPredicate extends ChangeIndexPredicate {
   private final Account.Id accountId;
 
   HasDraftByPredicate(Account.Id accountId) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
index 83990bc..eb3a137 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
@@ -15,11 +15,10 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-public class HasStarsPredicate extends IndexPredicate<ChangeData> {
+public class HasStarsPredicate extends ChangeIndexPredicate {
   private final Account.Id accountId;
 
   HasStarsPredicate(Account.Id accountId) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java
index 3f952d3..185a539 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java
@@ -15,11 +15,10 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.server.change.HashtagsUtil;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class HashtagPredicate extends IndexPredicate<ChangeData> {
+class HashtagPredicate extends ChangeIndexPredicate {
   HashtagPredicate(String hashtag) {
     super(ChangeField.HASHTAG, HashtagsUtil.cleanupHashtag(hashtag));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IntegerRangeChangePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IntegerRangeChangePredicate.java
new file mode 100644
index 0000000..a272fbb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IntegerRangeChangePredicate.java
@@ -0,0 +1,34 @@
+// 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.query.change;
+
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.IntegerRangePredicate;
+import com.google.gerrit.server.query.Matchable;
+import com.google.gerrit.server.query.QueryParseException;
+
+public abstract class IntegerRangeChangePredicate
+    extends IntegerRangePredicate<ChangeData> implements Matchable<ChangeData> {
+
+  protected IntegerRangeChangePredicate(FieldDef<ChangeData, Integer> type,
+      String value) throws QueryParseException {
+    super(type, value);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java
index 3c02bab..376ad84 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.OrmException;
 
@@ -26,7 +25,7 @@
 
 import java.io.IOException;
 
-public class IsMergePredicate extends OperatorPredicate<ChangeData> {
+public class IsMergePredicate extends ChangeOperatorPredicate {
   private final Arguments args;
 
   public IsMergePredicate(Arguments args, String value) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
index 5dd7dd2..d998fa3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
@@ -15,11 +15,10 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.server.index.FieldDef.FillArgs;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class IsMergeablePredicate extends IndexPredicate<ChangeData> {
+class IsMergeablePredicate extends ChangeIndexPredicate {
   private final FillArgs args;
 
   IsMergeablePredicate(FillArgs args) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
index 604f84b..24fcd6b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.server.index.change.ChangeField.REVIEWEDBY;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gwtorm.server.OrmException;
@@ -27,7 +26,7 @@
 import java.util.List;
 import java.util.Set;
 
-class IsReviewedPredicate extends IndexPredicate<ChangeData> {
+class IsReviewedPredicate extends ChangeIndexPredicate {
   private static final Account.Id NOT_REVIEWED =
       new Account.Id(ChangeField.NOT_REVIEWED);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
index 634bc4a6..929ed18 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
@@ -15,12 +15,11 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
 @Deprecated
-class IsStarredByPredicate extends IndexPredicate<ChangeData> {
+class IsStarredByPredicate extends ChangeIndexPredicate {
   private final Account.Id accountId;
 
   IsStarredByPredicate(Account.Id accountId) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index 656eca0..425eb00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -17,10 +17,9 @@
 import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.IndexPredicate;
 
 /** Predicate over change number (aka legacy ID or Change.Id). */
-public class LegacyChangeIdPredicate extends IndexPredicate<ChangeData> {
+public class LegacyChangeIdPredicate extends ChangeIndexPredicate {
   private final Change.Id id;
 
   LegacyChangeIdPredicate(Change.Id id) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyReviewerPredicate.java
index db10670..cd93ed3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyReviewerPredicate.java
@@ -15,12 +15,11 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
 @Deprecated
-class LegacyReviewerPredicate extends IndexPredicate<ChangeData> {
+class LegacyReviewerPredicate extends ChangeIndexPredicate {
   private final Account.Id id;
 
   LegacyReviewerPredicate(Account.Id id) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 021b6d7..722a8ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
@@ -26,7 +25,7 @@
  * Predicate to match changes that contains specified text in commit messages
  * body.
  */
-class MessagePredicate extends IndexPredicate<ChangeData> {
+class MessagePredicate extends ChangeIndexPredicate {
   private final ChangeIndex index;
 
   MessagePredicate(ChangeIndex index, String value) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
index bb7cb403a..dfaac08 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
@@ -16,11 +16,10 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class OwnerPredicate extends IndexPredicate<ChangeData> {
+class OwnerPredicate extends ChangeIndexPredicate {
   private final Account.Id id;
 
   OwnerPredicate(Account.Id id) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
index 467e4c5..72327ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
@@ -17,10 +17,9 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gwtorm.server.OrmException;
 
-class OwnerinPredicate extends OperatorPredicate<ChangeData> {
+class OwnerinPredicate extends ChangeOperatorPredicate {
   private final IdentifiedUser.GenericFactory userFactory;
   private final AccountGroup.UUID uuid;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
index 0bb5650..644870d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
@@ -16,11 +16,10 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class ProjectPredicate extends IndexPredicate<ChangeData> {
+class ProjectPredicate extends ChangeIndexPredicate {
   ProjectPredicate(String id) {
     super(ChangeField.PROJECT, id);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
index 400a204..4c06d1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
@@ -15,11 +15,10 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class ProjectPrefixPredicate extends IndexPredicate<ChangeData> {
+class ProjectPrefixPredicate extends ChangeIndexPredicate {
   ProjectPrefixPredicate(String prefix) {
     super(ChangeField.PROJECTS, prefix);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
index e62855f..491aed9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -15,11 +15,10 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class RefPredicate extends IndexPredicate<ChangeData> {
+class RefPredicate extends ChangeIndexPredicate {
   RefPredicate(String ref) {
     super(ChangeField.REF, ref);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
index d1e0f02..67efd69 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.RegexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.util.RegexListSearcher;
 import com.google.gwtorm.server.OrmException;
 
 import java.util.List;
 
-class RegexPathPredicate extends RegexPredicate<ChangeData> {
+class RegexPathPredicate extends ChangeRegexPredicate {
   RegexPathPredicate(String re) {
     super(ChangeField.PATH, re);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
index 48c815f..007566e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
@@ -16,14 +16,13 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.index.RegexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
-class RegexProjectPredicate extends RegexPredicate<ChangeData> {
+class RegexProjectPredicate extends ChangeRegexPredicate {
   private final RunAutomaton pattern;
 
   RegexProjectPredicate(String re) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
index 00c1dfe..c6d1577 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
@@ -15,14 +15,13 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.RegexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
-class RegexRefPredicate extends RegexPredicate<ChangeData> {
+class RegexRefPredicate extends ChangeRegexPredicate {
   private final RunAutomaton pattern;
 
   RegexRefPredicate(String re) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index d51aaa4..2d65670 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -17,13 +17,12 @@
 import static com.google.gerrit.server.index.change.ChangeField.FUZZY_TOPIC;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.RegexPredicate;
 import com.google.gwtorm.server.OrmException;
 
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
-class RegexTopicPredicate extends RegexPredicate<ChangeData> {
+class RegexTopicPredicate extends ChangeRegexPredicate {
   private final RunAutomaton pattern;
 
   RegexTopicPredicate(String re) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index d655a5f..1c4fbbb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.query.Predicate;
@@ -26,7 +25,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-class ReviewerPredicate extends IndexPredicate<ChangeData> {
+class ReviewerPredicate extends ChangeIndexPredicate {
   @SuppressWarnings("deprecation")
   static Predicate<ChangeData> create(Arguments args, Account.Id id) {
     List<Predicate<ChangeData>> and = new ArrayList<>(2);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
index 76a02432..34c10e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
@@ -17,10 +17,9 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gwtorm.server.OrmException;
 
-class ReviewerinPredicate extends OperatorPredicate<ChangeData> {
+class ReviewerinPredicate extends ChangeOperatorPredicate {
   private final IdentifiedUser.GenericFactory userFactory;
   private final AccountGroup.UUID uuid;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java
index 2facdb7..a31254f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java
@@ -16,11 +16,10 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-public class StarPredicate extends IndexPredicate<ChangeData> {
+public class StarPredicate extends ChangeIndexPredicate {
   private final Account.Id accountId;
   private final String label;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
index 35a5a29..d8d5258 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
@@ -15,11 +15,10 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class SubmissionIdPredicate extends IndexPredicate<ChangeData> {
+class SubmissionIdPredicate extends ChangeIndexPredicate {
 
   SubmissionIdPredicate(String changeSet) {
     super(ChangeField.SUBMISSIONID, changeSet);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TimestampRangeChangePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TimestampRangeChangePredicate.java
new file mode 100644
index 0000000..9242d9d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TimestampRangeChangePredicate.java
@@ -0,0 +1,34 @@
+// 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.query.change;
+
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.TimestampRangePredicate;
+import com.google.gerrit.server.query.Matchable;
+
+import java.sql.Timestamp;
+
+public abstract class TimestampRangeChangePredicate extends
+    TimestampRangePredicate<ChangeData> implements Matchable<ChangeData> {
+  protected TimestampRangeChangePredicate(FieldDef<ChangeData, Timestamp> def,
+      String name, String value) {
+    super(def, name, value);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
index aed8831..e9be4cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.TrackingFooters;
-import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
@@ -27,7 +26,7 @@
 import java.io.IOException;
 import java.util.List;
 
-class TrackingIdPredicate extends IndexPredicate<ChangeData> {
+class TrackingIdPredicate extends ChangeIndexPredicate {
   private static final Logger log = LoggerFactory.getLogger(TrackingIdPredicate.class);
 
   private final TrackingFooters trackingFooters;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
index 6f39a12..f38ddfa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.reviewdb.server.DisabledChangesReviewDbWrapper;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gwtorm.server.OrmException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
index cc3dc5d..0b7e8b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
@@ -92,7 +93,7 @@
   }
 
   public void update(final UpdateUI ui) throws OrmException {
-    try (ReviewDb db = unwrap(schema.open())) {
+    try (ReviewDb db = ReviewDbUtil.unwrapDb(schema.open())) {
 
       final SchemaVersion u = updater.get();
       final CurrentSchemaVersion version = getSchemaVersion(db);
@@ -115,13 +116,6 @@
     }
   }
 
-  private static ReviewDb unwrap(ReviewDb db) {
-    if (db instanceof DisabledChangesReviewDbWrapper) {
-      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
-    }
-    return db;
-  }
-
   private CurrentSchemaVersion getSchemaVersion(final ReviewDb db) {
     try {
       return db.schemaVersion().get(new CurrentSchemaVersion.Key());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 5316bcb..0b5ed32 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gwtorm.server.OrmException;
 
 import org.junit.Ignore;
 
@@ -44,16 +43,6 @@
   }
 
   private Predicate<ChangeData> predicate(String name, String value) {
-    return new OperatorPredicate<ChangeData>(name, value) {
-      @Override
-      public boolean match(ChangeData object) throws OrmException {
-        return false;
-      }
-
-      @Override
-      public int getCost() {
-        return 0;
-      }
-    };
+    return new OperatorPredicate<ChangeData>(name, value) {};
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java
new file mode 100644
index 0000000..6707c9f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java
@@ -0,0 +1,61 @@
+// 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.mail;
+
+import static com.google.common.truth.Truth.assert_;
+
+import org.apache.commons.validator.routines.EmailValidator;
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class ValidatorTest {
+  private static final String UNSUPPORTED_PREFIX = "#! ";
+
+  @Test
+  public void validateTopLevelDomains() throws Exception {
+    try (InputStream in =
+        this.getClass().getResourceAsStream("tlds-alpha-by-domain.txt")) {
+      if (in == null) {
+        throw new Exception("TLD list not found");
+      }
+      BufferedReader r = new BufferedReader(new InputStreamReader(in));
+      String tld;
+      EmailValidator validator = EmailValidator.getInstance();
+      while ((tld = r.readLine()) != null) {
+        if (tld.startsWith("# ") || tld.startsWith("XN--")) {
+          // Ignore comments and non-latin domains
+          continue;
+        }
+        if (tld.startsWith(UNSUPPORTED_PREFIX)) {
+          String test = "test@example."
+              + tld.toLowerCase().substring(UNSUPPORTED_PREFIX.length());
+          assert_()
+            .withFailureMessage("expected invalid TLD \"" + test + "\"")
+            .that(validator.isValid(test))
+            .isFalse();
+        } else {
+          String test = "test@example." + tld.toLowerCase();
+          assert_()
+            .withFailureMessage("failed to validate TLD \"" + test + "\"")
+            .that(validator.isValid(test))
+            .isTrue();
+        }
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java
index a0a6e40..7762e50 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java
@@ -24,16 +24,6 @@
     protected TestPredicate(String name, String value) {
       super(name, value);
     }
-
-    @Override
-    public boolean match(String object) {
-      return false;
-    }
-
-    @Override
-    public int getCost() {
-      return 0;
-    }
   }
 
   protected static TestPredicate f(String name, String value) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
new file mode 100644
index 0000000..355edaf
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -0,0 +1,427 @@
+// 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.query.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.accounts.Accounts.QueryRequest;
+import com.google.gerrit.extensions.client.ListAccountsOption;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.GerritServerTests;
+import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+@Ignore
+public abstract class AbstractQueryAccountsTest extends GerritServerTests {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    Config cfg = new Config();
+    cfg.setInt("index", null, "maxPages", 10);
+    return cfg;
+  }
+
+  @Rule
+  public final TestName testName = new TestName();
+
+  @Inject
+  protected AccountCache accountCache;
+
+  @Inject
+  protected AccountManager accountManager;
+
+  @Inject
+  protected GerritApi gApi;
+
+  @Inject
+  protected IdentifiedUser.GenericFactory userFactory;
+
+  @Inject
+  protected InMemoryDatabase schemaFactory;
+
+  @Inject
+  protected InternalChangeQuery internalChangeQuery;
+
+  @Inject
+  protected SchemaCreator schemaCreator;
+
+  @Inject
+  protected ThreadLocalRequestContext requestContext;
+
+  protected LifecycleManager lifecycle;
+  protected ReviewDb db;
+  protected AccountInfo currentUserInfo;
+  protected CurrentUser user;
+
+  protected abstract Injector createInjector();
+
+  @Before
+  public void setUpInjector() throws Exception {
+    lifecycle = new LifecycleManager();
+    Injector injector = createInjector();
+    lifecycle.add(injector);
+    injector.injectMembers(this);
+    lifecycle.start();
+
+    db = schemaFactory.open();
+    schemaCreator.create(db);
+
+    Account.Id userId = createAccount("user", "User", "user@example.com", true);
+    user = userFactory.create(userId);
+    requestContext.setContext(newRequestContext(userId));
+    currentUserInfo = gApi.accounts().id(userId.get()).get();
+  }
+
+  protected RequestContext newRequestContext(Account.Id requestUserId) {
+    final CurrentUser requestUser =
+        userFactory.create(requestUserId);
+    return new RequestContext() {
+      @Override
+      public CurrentUser getUser() {
+        return requestUser;
+      }
+
+      @Override
+      public Provider<ReviewDb> getReviewDbProvider() {
+        return Providers.of(db);
+      }
+    };
+  }
+
+  @After
+  public void tearDownInjector() {
+    if (lifecycle != null) {
+      lifecycle.stop();
+    }
+    requestContext.setContext(null);
+    if (db != null) {
+      db.close();
+    }
+    InMemoryDatabase.drop(schemaFactory);
+  }
+
+  @Test
+  public void byId() throws Exception {
+    AccountInfo user = newAccount("user");
+
+    assertQuery("9999999");
+    assertQuery(currentUserInfo._accountId, currentUserInfo);
+    assertQuery(user._accountId, user);
+  }
+
+  @Test
+  public void bySelf() throws Exception {
+    assertQuery("self", currentUserInfo);
+  }
+
+  @Test
+  public void byEmail() throws Exception {
+    AccountInfo user1 = newAccountWithEmail("user1", name("user1@example.com"));
+
+    String domain = name("test.com");
+    AccountInfo user2 = newAccountWithEmail("user2", "user2@" + domain);
+    AccountInfo user3 = newAccountWithEmail("user3", "user3@" + domain);
+
+    String prefix = name("prefix");
+    AccountInfo user4 =
+        newAccountWithEmail("user4", prefix + "user4@example.com");
+
+    AccountInfo user5 =
+        newAccountWithEmail("user5", name("user5MixedCase@example.com"));
+
+    assertQuery("notexisting@test.com");
+
+    assertQuery(currentUserInfo.email, currentUserInfo);
+    assertQuery("email:" + currentUserInfo.email, currentUserInfo);
+
+    assertQuery(user1.email, user1);
+    assertQuery("email:" + user1.email, user1);
+
+    assertQuery(domain, user2, user3);
+
+    assertQuery("email:" + prefix, user4);
+
+    assertQuery(user5.email, user5);
+    assertQuery("email:" + user5.email, user5);
+    assertQuery("email:" + user5.email.toUpperCase(), user5);
+  }
+
+  @Test
+  public void byUsername() throws Exception {
+    AccountInfo user1 = newAccount("myuser");
+
+    assertQuery("notexisting");
+    assertQuery("Not Existing");
+
+    assertQuery(user1.username, user1);
+    assertQuery("username:" + user1.username, user1);
+    assertQuery("username:" + user1.username.toUpperCase(), user1);
+  }
+
+  @Test
+  public void isActive() throws Exception {
+    String domain = name("test.com");
+    AccountInfo user1 = newAccountWithEmail("user1", "user1@" + domain);
+    AccountInfo user2 = newAccountWithEmail("user2", "user2@" + domain);
+    AccountInfo user3 = newAccount("user3", "user3@" + domain, false);
+    AccountInfo user4 = newAccount("user4", "user4@" + domain, false);
+
+    // by default only active accounts are returned
+    assertQuery(domain, user1, user2);
+    assertQuery("name:" + domain, user1, user2);
+
+    assertQuery("is:active name:" + domain, user1, user2);
+
+    assertQuery("is:inactive name:" + domain, user3, user4);
+  }
+
+  @Test
+  public void byName() throws Exception {
+    AccountInfo user1 = newAccountWithFullName("jdoe", "John Doe");
+    AccountInfo user2 = newAccountWithFullName("jroe", "Jane Roe");
+
+    assertQuery("notexisting");
+    assertQuery("Not Existing");
+
+    assertQuery(quote(user1.name), user1);
+    assertQuery("name:" + quote(user1.name), user1);
+    assertQuery("John", user1);
+    assertQuery("john", user1);
+    assertQuery("Doe", user1);
+    assertQuery("doe", user1);
+    assertQuery("DOE", user1);
+    assertQuery("name:John", user1);
+    assertQuery("name:john", user1);
+    assertQuery("name:Doe", user1);
+    assertQuery("name:doe", user1);
+    assertQuery("name:DOE", user1);
+
+    assertQuery(quote(user2.name), user2);
+    assertQuery("name:" + quote(user2.name), user2);
+  }
+
+  @Test
+  public void withLimit() throws Exception {
+    String domain = name("test.com");
+    AccountInfo user1 = newAccountWithEmail("user1", "user1@" + domain);
+    AccountInfo user2 = newAccountWithEmail("user2", "user2@" + domain);
+    AccountInfo user3 = newAccountWithEmail("user3", "user3@" + domain);
+
+    List<AccountInfo> result = assertQuery(domain, user1, user2, user3);
+    assertThat(result.get(result.size() - 1)._moreAccounts).isNull();
+
+    result = assertQuery(newQuery(domain).withLimit(2), user1, user2);
+    assertThat(result.get(result.size() - 1)._moreAccounts).isTrue();
+  }
+
+  @Test
+  public void withStart() throws Exception {
+    String domain = name("test.com");
+    AccountInfo user1 = newAccountWithEmail("user1", "user1@" + domain);
+    AccountInfo user2 = newAccountWithEmail("user2", "user2@" + domain);
+    AccountInfo user3 = newAccountWithEmail("user3", "user3@" + domain);
+
+    assertQuery(domain, user1, user2, user3);
+    assertQuery(newQuery(domain).withStart(1), user2, user3);
+  }
+
+  @Test
+  public void withDetails() throws Exception {
+    AccountInfo user1 =
+        newAccount("myuser", "My User", "my.user@example.com", true);
+
+    List<AccountInfo> result = assertQuery(user1.username, user1);
+    AccountInfo ai = result.get(0);
+    assertThat(ai._accountId).isEqualTo(user1._accountId);
+    assertThat(ai.name).isNull();
+    assertThat(ai.username).isNull();
+    assertThat(ai.email).isNull();
+    assertThat(ai.avatars).isNull();
+
+    result = assertQuery(
+        newQuery(user1.username).withOption(ListAccountsOption.DETAILS), user1);
+    ai = result.get(0);
+    assertThat(ai._accountId).isEqualTo(user1._accountId);
+    assertThat(ai.name).isEqualTo(user1.name);
+    assertThat(ai.username).isEqualTo(user1.username);
+    assertThat(ai.email).isEqualTo(user1.email);
+    assertThat(ai.avatars).isNull();
+  }
+
+  protected AccountInfo newAccount(String username) throws Exception {
+    return newAccountWithEmail(username, null);
+  }
+
+  protected AccountInfo newAccountWithEmail(String username, String email)
+      throws Exception {
+    return newAccount(username, email, true);
+  }
+
+  protected AccountInfo newAccountWithFullName(String username, String fullName)
+      throws Exception {
+    return newAccount(username, fullName, null, true);
+  }
+
+  protected AccountInfo newAccount(String username, String email,
+      boolean active) throws Exception {
+    return newAccount(username, null, email, active);
+  }
+
+  protected AccountInfo newAccount(String username, String fullName,
+      String email, boolean active) throws Exception {
+    String uniqueName = name(username);
+
+    try {
+      gApi.accounts().id(uniqueName).get();
+      fail("user " + uniqueName + " already exists");
+    } catch (ResourceNotFoundException e) {
+      // expected: user does not exist yet
+    }
+
+    Account.Id id = createAccount(uniqueName, fullName, email, active);
+    return gApi.accounts().id(id.get()).get();
+  }
+
+  protected String quote(String s) {
+    return "\"" + s + "\"";
+  }
+
+  protected String name(String name) {
+    if (name == null) {
+      return null;
+    }
+    String suffix = testName.getMethodName().toLowerCase();
+    if (name.contains("@")) {
+      return name + "." + suffix;
+    }
+    return name + "_" + suffix;
+  }
+
+  private Account.Id createAccount(String username, String fullName,
+      String email, boolean active) throws Exception {
+    Account.Id id =
+        accountManager.authenticate(AuthRequest.forUser(username)).getAccountId();
+    if (email != null) {
+      accountManager.link(id, AuthRequest.forEmail(email));
+    }
+    Account a = db.accounts().get(id);
+    a.setFullName(fullName);
+    a.setPreferredEmail(email);
+    a.setActive(active);
+    db.accounts().update(ImmutableList.of(a));
+    accountCache.evict(id);
+    return id;
+  }
+
+  protected QueryRequest newQuery(Object query) throws RestApiException {
+    return gApi.accounts().query(query.toString());
+  }
+
+  protected List<AccountInfo> assertQuery(Object query, AccountInfo... accounts)
+      throws Exception {
+    return assertQuery(newQuery(query), accounts);
+  }
+
+  protected List<AccountInfo> assertQuery(QueryRequest query, AccountInfo... accounts)
+      throws Exception {
+    List<AccountInfo> result = query.get();
+    Iterable<Integer> ids = ids(result);
+    assertThat(ids).named(format(query, result, accounts))
+        .containsExactlyElementsIn(ids(accounts)).inOrder();
+    return result;
+  }
+
+  private String format(QueryRequest query, Iterable<AccountInfo> actualIds,
+      AccountInfo... expectedAccounts) {
+    StringBuilder b = new StringBuilder();
+    b.append("query '").append(query.getQuery())
+        .append("' with expected accounts ");
+    b.append(format(Arrays.asList(expectedAccounts)));
+    b.append(" and result ");
+    b.append(format(actualIds));
+    return b.toString();
+  }
+
+  private String format(Iterable<AccountInfo> accounts) {
+    StringBuilder b = new StringBuilder();
+    b.append("[");
+    Iterator<AccountInfo> it = accounts.iterator();
+    while (it.hasNext()) {
+      AccountInfo a = it.next();
+      b.append("{").append(a._accountId).append(", ").append("name=")
+          .append(a.name).append(", ").append("email=").append(a.email)
+          .append(", ").append("username=").append(a.username).append("}");
+      if (it.hasNext()) {
+        b.append(", ");
+      }
+    }
+    b.append("]");
+    return b.toString();
+  }
+
+  protected static Iterable<Integer> ids(AccountInfo... accounts) {
+    return FluentIterable.from(Arrays.asList(accounts)).transform(
+        new Function<AccountInfo, Integer>() {
+          @Override
+          public Integer apply(AccountInfo in) {
+            return in._accountId;
+          }
+        });
+  }
+
+  protected static Iterable<Integer> ids(Iterable<AccountInfo> accounts) {
+    return FluentIterable.from(accounts).transform(
+        new Function<AccountInfo, Integer>() {
+          @Override
+          public Integer apply(AccountInfo in) {
+            return in._accountId;
+          }
+        });
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
new file mode 100644
index 0000000..857b661
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
@@ -0,0 +1,31 @@
+// 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.query.account;
+
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.lib.Config;
+
+public class LuceneQueryAccountsTest extends AbstractQueryAccountsTest {
+  @Override
+  protected Injector createInjector() {
+    Config luceneConfig = new Config(config);
+    InMemoryModule.setDefaults(luceneConfig);
+    return Guice.createInjector(
+        new InMemoryModule(luceneConfig, notesMigration));
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
index 66eed8b..61bfe78 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.notedb.ChangeBundle;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeRebuilder;
-import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -71,7 +70,7 @@
   public void rebuildAndCheckAllChanges() throws Exception {
     rebuildAndCheckChanges(
         Iterables.transform(
-            unwrapDb().changes().all(),
+            getUnwrappedDb().changes().all(),
             ReviewDbUtil.changeIdFunction()));
   }
 
@@ -81,7 +80,7 @@
 
   public void rebuildAndCheckChanges(Iterable<Change.Id> changeIds)
       throws Exception {
-    ReviewDb db = unwrapDb();
+    ReviewDb db = getUnwrappedDb();
 
     List<ChangeBundle> allExpected = readExpected(changeIds);
 
@@ -124,7 +123,7 @@
 
   private List<ChangeBundle> readExpected(Iterable<Change.Id> changeIds)
       throws Exception {
-    ReviewDb db = unwrapDb();
+    ReviewDb db = getUnwrappedDb();
     boolean old = notesMigration.readChanges();
     try {
       notesMigration.setReadChanges(false);
@@ -142,7 +141,7 @@
 
   private void checkActual(List<ChangeBundle> allExpected, List<String> msgs)
       throws Exception {
-    ReviewDb db = unwrapDb();
+    ReviewDb db = getUnwrappedDb();
     boolean oldRead = notesMigration.readChanges();
     boolean oldWrite = notesMigration.writeChanges();
     try {
@@ -179,11 +178,8 @@
     }
   }
 
-  private ReviewDb unwrapDb() {
+  private ReviewDb getUnwrappedDb() {
     ReviewDb db = dbProvider.get();
-    if (db instanceof DisabledChangesReviewDbWrapper) {
-      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
-    }
-    return db;
+    return  ReviewDbUtil.unwrapDb(db);
   }
 }
diff --git a/gerrit-server/src/test/resources/com/google/gerrit/server/mail/tlds-alpha-by-domain.txt b/gerrit-server/src/test/resources/com/google/gerrit/server/mail/tlds-alpha-by-domain.txt
new file mode 100644
index 0000000..9edf6a4
--- /dev/null
+++ b/gerrit-server/src/test/resources/com/google/gerrit/server/mail/tlds-alpha-by-domain.txt
@@ -0,0 +1,1329 @@
+# Version 2016060601, Last Updated Tue Jun  7 07:07:01 2016 UTC
+# From http://data.iana.org/TLD/tlds-alpha-by-domain.txt
+AAA
+AARP
+ABB
+ABBOTT
+ABBVIE
+ABOGADO
+ABUDHABI
+AC
+ACADEMY
+ACCENTURE
+ACCOUNTANT
+ACCOUNTANTS
+ACO
+ACTIVE
+ACTOR
+AD
+ADAC
+ADS
+ADULT
+AE
+AEG
+AERO
+#! AETNA
+AF
+AFL
+AG
+AGAKHAN
+AGENCY
+AI
+AIG
+AIRFORCE
+AIRTEL
+AKDN
+AL
+ALIBABA
+ALIPAY
+ALLFINANZ
+ALLY
+ALSACE
+AM
+AMICA
+AMSTERDAM
+ANALYTICS
+ANDROID
+ANQUAN
+AO
+APARTMENTS
+APP
+APPLE
+AQ
+AQUARELLE
+AR
+ARAMCO
+ARCHI
+ARMY
+ARPA
+ARTE
+AS
+ASIA
+ASSOCIATES
+AT
+ATTORNEY
+AU
+AUCTION
+AUDI
+AUDIO
+AUTHOR
+AUTO
+AUTOS
+AVIANCA
+AW
+AWS
+AX
+AXA
+AZ
+AZURE
+BA
+BABY
+BAIDU
+BAND
+BANK
+BAR
+BARCELONA
+BARCLAYCARD
+BARCLAYS
+BAREFOOT
+BARGAINS
+BAUHAUS
+BAYERN
+BB
+BBC
+BBVA
+BCG
+BCN
+BD
+BE
+BEATS
+BEER
+BENTLEY
+BERLIN
+BEST
+BET
+BF
+BG
+BH
+BHARTI
+BI
+BIBLE
+BID
+BIKE
+BING
+BINGO
+BIO
+BIZ
+BJ
+BLACK
+BLACKFRIDAY
+#! BLOG
+BLOOMBERG
+BLUE
+BM
+BMS
+BMW
+BN
+BNL
+BNPPARIBAS
+BO
+BOATS
+BOEHRINGER
+BOM
+BOND
+BOO
+BOOK
+BOOTS
+BOSCH
+BOSTIK
+BOT
+BOUTIQUE
+BR
+BRADESCO
+BRIDGESTONE
+BROADWAY
+BROKER
+BROTHER
+BRUSSELS
+BS
+BT
+BUDAPEST
+BUGATTI
+BUILD
+BUILDERS
+BUSINESS
+BUY
+BUZZ
+BV
+BW
+BY
+BZ
+BZH
+CA
+CAB
+CAFE
+CAL
+CALL
+CAMERA
+CAMP
+CANCERRESEARCH
+CANON
+CAPETOWN
+CAPITAL
+CAR
+CARAVAN
+CARDS
+CARE
+CAREER
+CAREERS
+CARS
+CARTIER
+CASA
+CASH
+CASINO
+CAT
+CATERING
+CBA
+CBN
+CC
+CD
+CEB
+CENTER
+CEO
+CERN
+CF
+CFA
+CFD
+CG
+CH
+CHANEL
+CHANNEL
+CHASE
+CHAT
+CHEAP
+CHLOE
+CHRISTMAS
+CHROME
+CHURCH
+CI
+CIPRIANI
+CIRCLE
+CISCO
+CITIC
+CITY
+CITYEATS
+CK
+CL
+CLAIMS
+CLEANING
+CLICK
+CLINIC
+CLINIQUE
+CLOTHING
+CLOUD
+CLUB
+CLUBMED
+CM
+CN
+CO
+COACH
+CODES
+COFFEE
+COLLEGE
+COLOGNE
+COM
+COMMBANK
+COMMUNITY
+COMPANY
+COMPARE
+COMPUTER
+COMSEC
+CONDOS
+CONSTRUCTION
+CONSULTING
+CONTACT
+CONTRACTORS
+COOKING
+COOL
+COOP
+CORSICA
+COUNTRY
+COUPON
+COUPONS
+COURSES
+CR
+CREDIT
+CREDITCARD
+CREDITUNION
+CRICKET
+CROWN
+CRS
+CRUISES
+CSC
+CU
+CUISINELLA
+CV
+CW
+CX
+CY
+CYMRU
+CYOU
+CZ
+DABUR
+DAD
+DANCE
+DATE
+DATING
+DATSUN
+DAY
+DCLK
+#! DDS
+DE
+DEALER
+DEALS
+DEGREE
+DELIVERY
+DELL
+DELOITTE
+DELTA
+DEMOCRAT
+DENTAL
+DENTIST
+DESI
+DESIGN
+DEV
+#! DHL
+DIAMONDS
+DIET
+DIGITAL
+DIRECT
+DIRECTORY
+DISCOUNT
+DJ
+DK
+DM
+DNP
+DO
+DOCS
+DOG
+DOHA
+DOMAINS
+#! DOT
+DOWNLOAD
+DRIVE
+#! DTV
+DUBAI
+DURBAN
+DVAG
+DZ
+EARTH
+EAT
+EC
+EDEKA
+EDU
+EDUCATION
+EE
+EG
+EMAIL
+EMERCK
+ENERGY
+ENGINEER
+ENGINEERING
+ENTERPRISES
+EPSON
+EQUIPMENT
+ER
+ERNI
+ES
+ESQ
+ESTATE
+ET
+EU
+EUROVISION
+EUS
+EVENTS
+EVERBANK
+EXCHANGE
+EXPERT
+EXPOSED
+EXPRESS
+EXTRASPACE
+FAGE
+FAIL
+FAIRWINDS
+FAITH
+FAMILY
+FAN
+FANS
+FARM
+FASHION
+FAST
+FEEDBACK
+FERRERO
+FI
+FILM
+FINAL
+FINANCE
+FINANCIAL
+FIRESTONE
+FIRMDALE
+FISH
+FISHING
+FIT
+FITNESS
+FJ
+FK
+FLICKR
+FLIGHTS
+#! FLIR
+FLORIST
+FLOWERS
+FLSMIDTH
+FLY
+FM
+FO
+FOO
+FOOTBALL
+FORD
+FOREX
+FORSALE
+FORUM
+FOUNDATION
+FOX
+FR
+FRESENIUS
+FRL
+FROGANS
+FRONTIER
+FTR
+FUND
+FURNITURE
+FUTBOL
+FYI
+GA
+GAL
+GALLERY
+GALLO
+GALLUP
+GAME
+#! GAMES
+GARDEN
+GB
+GBIZ
+GD
+GDN
+GE
+GEA
+GENT
+GENTING
+GF
+GG
+GGEE
+GH
+GI
+GIFT
+GIFTS
+GIVES
+GIVING
+GL
+GLASS
+GLE
+GLOBAL
+GLOBO
+GM
+GMAIL
+GMBH
+GMO
+GMX
+GN
+GOLD
+GOLDPOINT
+GOLF
+GOO
+GOOG
+GOOGLE
+GOP
+GOT
+GOV
+GP
+GQ
+GR
+GRAINGER
+GRAPHICS
+GRATIS
+GREEN
+GRIPE
+GROUP
+GS
+GT
+GU
+#! GUARDIAN
+GUCCI
+GUGE
+GUIDE
+GUITARS
+GURU
+GW
+GY
+HAMBURG
+HANGOUT
+HAUS
+HDFCBANK
+HEALTH
+HEALTHCARE
+HELP
+HELSINKI
+HERE
+HERMES
+HIPHOP
+#! HISAMITSU
+HITACHI
+HIV
+HK
+#! HKT
+HM
+HN
+HOCKEY
+HOLDINGS
+HOLIDAY
+HOMEDEPOT
+HOMES
+HONDA
+HORSE
+HOST
+HOSTING
+HOTELES
+HOTMAIL
+HOUSE
+HOW
+HR
+HSBC
+HT
+HTC
+HU
+HYUNDAI
+IBM
+ICBC
+ICE
+ICU
+ID
+IE
+IFM
+IINET
+IL
+IM
+IMAMAT
+IMMO
+IMMOBILIEN
+IN
+INDUSTRIES
+INFINITI
+INFO
+ING
+INK
+INSTITUTE
+INSURANCE
+INSURE
+INT
+INTERNATIONAL
+INVESTMENTS
+IO
+IPIRANGA
+IQ
+IR
+IRISH
+IS
+ISELECT
+ISMAILI
+IST
+ISTANBUL
+IT
+ITAU
+IWC
+JAGUAR
+JAVA
+JCB
+JCP
+JE
+JETZT
+JEWELRY
+JLC
+JLL
+JM
+JMP
+JNJ
+JO
+JOBS
+JOBURG
+JOT
+JOY
+JP
+JPMORGAN
+JPRS
+JUEGOS
+KAUFEN
+KDDI
+KE
+KERRYHOTELS
+KERRYLOGISTICS
+KERRYPROPERTIES
+KFH
+KG
+KH
+KI
+KIA
+KIM
+KINDER
+KITCHEN
+KIWI
+KM
+KN
+KOELN
+KOMATSU
+KP
+KPMG
+KPN
+KR
+KRD
+KRED
+KUOKGROUP
+KW
+KY
+KYOTO
+KZ
+LA
+LACAIXA
+LAMBORGHINI
+LAMER
+LANCASTER
+LAND
+LANDROVER
+LANXESS
+LASALLE
+LAT
+LATROBE
+LAW
+LAWYER
+LB
+LC
+LDS
+LEASE
+LECLERC
+LEGAL
+LEXUS
+LGBT
+LI
+LIAISON
+LIDL
+LIFE
+LIFEINSURANCE
+LIFESTYLE
+LIGHTING
+LIKE
+LIMITED
+LIMO
+LINCOLN
+LINDE
+LINK
+#! LIPSY
+LIVE
+LIVING
+LIXIL
+LK
+LOAN
+LOANS
+#! LOCKER
+LOCUS
+LOL
+LONDON
+LOTTE
+LOTTO
+LOVE
+LR
+LS
+LT
+LTD
+LTDA
+LU
+LUPIN
+LUXE
+LUXURY
+LV
+LY
+MA
+MADRID
+MAIF
+MAISON
+MAKEUP
+MAN
+MANAGEMENT
+MANGO
+MARKET
+MARKETING
+MARKETS
+MARRIOTT
+#! MATTEL
+MBA
+MC
+MD
+ME
+MED
+MEDIA
+MEET
+MELBOURNE
+MEME
+MEMORIAL
+MEN
+MENU
+MEO
+#! METLIFE
+MG
+MH
+MIAMI
+MICROSOFT
+MIL
+MINI
+MK
+ML
+#! MLB
+MLS
+MM
+MMA
+MN
+MO
+MOBI
+MOBILY
+MODA
+MOE
+MOI
+MOM
+MONASH
+MONEY
+MONTBLANC
+MORMON
+MORTGAGE
+MOSCOW
+MOTORCYCLES
+MOV
+MOVIE
+MOVISTAR
+MP
+MQ
+MR
+MS
+MT
+MTN
+MTPC
+MTR
+MU
+MUSEUM
+MUTUAL
+MUTUELLE
+MV
+MW
+MX
+MY
+MZ
+NA
+NADEX
+NAGOYA
+NAME
+NATURA
+NAVY
+NC
+NE
+NEC
+NET
+NETBANK
+#! NETFLIX
+NETWORK
+NEUSTAR
+NEW
+NEWS
+#! NEXT
+#! NEXTDIRECT
+NEXUS
+NF
+NG
+NGO
+NHK
+NI
+NICO
+NIKON
+NINJA
+NISSAN
+NISSAY
+NL
+NO
+NOKIA
+NORTHWESTERNMUTUAL
+NORTON
+NOWRUZ
+#! NOWTV
+NP
+NR
+NRA
+NRW
+NTT
+NU
+NYC
+NZ
+OBI
+OFFICE
+OKINAWA
+#! OLAYAN
+#! OLAYANGROUP
+#! OLLO
+OM
+OMEGA
+ONE
+ONG
+ONL
+ONLINE
+OOO
+ORACLE
+ORANGE
+ORG
+ORGANIC
+ORIGINS
+OSAKA
+OTSUKA
+#! OTT
+OVH
+PA
+PAGE
+PAMPEREDCHEF
+PANERAI
+PARIS
+PARS
+PARTNERS
+PARTS
+PARTY
+PASSAGENS
+#! PCCW
+PE
+PET
+PF
+PG
+PH
+PHARMACY
+PHILIPS
+PHOTO
+PHOTOGRAPHY
+PHOTOS
+PHYSIO
+PIAGET
+PICS
+PICTET
+PICTURES
+PID
+PIN
+PING
+PINK
+#! PIONEER
+PIZZA
+PK
+PL
+PLACE
+PLAY
+PLAYSTATION
+PLUMBING
+PLUS
+PM
+PN
+POHL
+POKER
+PORN
+POST
+PR
+PRAXI
+PRESS
+PRO
+PROD
+PRODUCTIONS
+PROF
+PROGRESSIVE
+PROMO
+PROPERTIES
+PROPERTY
+PROTECTION
+PS
+PT
+PUB
+PW
+PWC
+PY
+QA
+QPON
+QUEBEC
+QUEST
+RACING
+RE
+READ
+#! REALESTATE
+REALTOR
+REALTY
+RECIPES
+RED
+REDSTONE
+REDUMBRELLA
+REHAB
+REISE
+REISEN
+REIT
+REN
+RENT
+RENTALS
+REPAIR
+REPORT
+REPUBLICAN
+REST
+RESTAURANT
+REVIEW
+REVIEWS
+REXROTH
+RICH
+#! RICHARDLI
+RICOH
+RIO
+RIP
+RO
+ROCHER
+ROCKS
+RODEO
+ROOM
+RS
+RSVP
+RU
+RUHR
+RUN
+RW
+RWE
+RYUKYU
+SA
+SAARLAND
+SAFE
+SAFETY
+SAKURA
+SALE
+SALON
+SAMSUNG
+SANDVIK
+SANDVIKCOROMANT
+SANOFI
+SAP
+SAPO
+SARL
+SAS
+SAXO
+SB
+SBI
+SBS
+SC
+SCA
+SCB
+SCHAEFFLER
+SCHMIDT
+SCHOLARSHIPS
+SCHOOL
+SCHULE
+SCHWARZ
+SCIENCE
+SCOR
+SCOT
+SD
+SE
+SEAT
+SECURITY
+SEEK
+SELECT
+SENER
+SERVICES
+SEVEN
+SEW
+SEX
+SEXY
+SFR
+SG
+SH
+SHARP
+SHAW
+SHELL
+SHIA
+SHIKSHA
+SHOES
+#! SHOP
+SHOUJI
+SHOW
+SHRIRAM
+SI
+SINA
+SINGLES
+SITE
+SJ
+SK
+SKI
+SKIN
+SKY
+SKYPE
+SL
+SM
+SMILE
+SN
+SNCF
+SO
+SOCCER
+SOCIAL
+SOFTBANK
+SOFTWARE
+SOHU
+SOLAR
+SOLUTIONS
+SONG
+SONY
+SOY
+SPACE
+SPIEGEL
+SPOT
+SPREADBETTING
+SR
+SRL
+ST
+STADA
+STAR
+STARHUB
+STATEBANK
+STATEFARM
+STATOIL
+STC
+STCGROUP
+STOCKHOLM
+STORAGE
+STORE
+STREAM
+STUDIO
+STUDY
+STYLE
+SU
+SUCKS
+SUPPLIES
+SUPPLY
+SUPPORT
+SURF
+SURGERY
+SUZUKI
+SV
+SWATCH
+SWISS
+SX
+SY
+SYDNEY
+SYMANTEC
+SYSTEMS
+SZ
+TAB
+TAIPEI
+TALK
+TAOBAO
+TATAMOTORS
+TATAR
+TATTOO
+TAX
+TAXI
+TC
+TCI
+TD
+TEAM
+TECH
+TECHNOLOGY
+TEL
+TELECITY
+TELEFONICA
+TEMASEK
+TENNIS
+TEVA
+TF
+TG
+TH
+THD
+THEATER
+THEATRE
+TICKETS
+TIENDA
+TIFFANY
+TIPS
+TIRES
+TIROL
+TJ
+TK
+TL
+TM
+TMALL
+TN
+TO
+TODAY
+TOKYO
+TOOLS
+TOP
+TORAY
+TOSHIBA
+TOTAL
+TOURS
+TOWN
+TOYOTA
+TOYS
+TR
+TRADE
+TRADING
+TRAINING
+TRAVEL
+TRAVELERS
+TRAVELERSINSURANCE
+TRUST
+TRV
+TT
+TUBE
+TUI
+TUNES
+TUSHU
+TV
+TVS
+TW
+TZ
+UA
+UBS
+UG
+UK
+UNICOM
+UNIVERSITY
+UNO
+UOL
+#! UPS
+US
+UY
+UZ
+VA
+VACATIONS
+VANA
+VC
+VE
+VEGAS
+VENTURES
+VERISIGN
+VERSICHERUNG
+VET
+VG
+VI
+VIAJES
+VIDEO
+VIG
+VIKING
+VILLAS
+VIN
+VIP
+VIRGIN
+VISION
+VISTA
+VISTAPRINT
+VIVA
+VLAANDEREN
+VN
+VODKA
+VOLKSWAGEN
+VOTE
+VOTING
+VOTO
+VOYAGE
+VU
+VUELOS
+WALES
+WALTER
+WANG
+WANGGOU
+#! WARMAN
+WATCH
+WATCHES
+WEATHER
+WEATHERCHANNEL
+WEBCAM
+WEBER
+WEBSITE
+WED
+WEDDING
+WEIBO
+WEIR
+WF
+WHOSWHO
+WIEN
+WIKI
+WILLIAMHILL
+WIN
+WINDOWS
+WINE
+WME
+WOLTERSKLUWER
+WORK
+WORKS
+WORLD
+WS
+WTC
+WTF
+XBOX
+XEROX
+XIHUAN
+XIN
+XN--11B4C3D
+XN--1CK2E1B
+XN--1QQW23A
+XN--30RR7Y
+XN--3BST00M
+XN--3DS443G
+XN--3E0B707E
+XN--3PXU8K
+XN--42C2D9A
+XN--45BRJ9C
+XN--45Q11C
+XN--4GBRIM
+XN--55QW42G
+XN--55QX5D
+XN--5TZM5G
+XN--6FRZ82G
+XN--6QQ986B3XL
+XN--80ADXHKS
+XN--80AO21A
+XN--80ASEHDB
+XN--80ASWG
+XN--8Y0A063A
+XN--90A3AC
+XN--90AIS
+XN--9DBQ2A
+XN--9ET52U
+XN--9KRT00A
+XN--B4W605FERD
+XN--BCK1B9A5DRE4C
+XN--C1AVG
+XN--C2BR7G
+XN--CCK2B3B
+XN--CG4BKI
+XN--CLCHC0EA0B2G2A9GCD
+XN--CZR694B
+XN--CZRS0T
+XN--CZRU2D
+XN--D1ACJ3B
+XN--D1ALF
+XN--E1A4C
+XN--ECKVDTC9D
+XN--EFVY88H
+XN--ESTV75G
+XN--FCT429K
+XN--FHBEI
+XN--FIQ228C5HS
+XN--FIQ64B
+XN--FIQS8S
+XN--FIQZ9S
+XN--FJQ720A
+XN--FLW351E
+XN--FPCRJ9C3D
+XN--FZC2C9E2C
+XN--FZYS8D69UVGM
+XN--G2XX48C
+XN--GCKR3F0F
+XN--GECRJ9C
+XN--H2BRJ9C
+XN--HXT814E
+XN--I1B6B1A6A2E
+XN--IMR513N
+XN--IO0A7I
+XN--J1AEF
+XN--J1AMH
+XN--J6W193G
+XN--JLQ61U9W7B
+XN--JVR189M
+XN--KCRX77D1X4A
+XN--KPRW13D
+XN--KPRY57D
+XN--KPU716F
+XN--KPUT3I
+XN--L1ACC
+XN--LGBBAT1AD8J
+XN--MGB9AWBF
+XN--MGBA3A3EJT
+XN--MGBA3A4F16A
+XN--MGBA7C0BBN0A
+XN--MGBAAM7A8H
+XN--MGBAB2BD
+XN--MGBAYH7GPA
+XN--MGBB9FBPOB
+XN--MGBBH1A71E
+XN--MGBC0A9AZCG
+XN--MGBCA7DZDO
+XN--MGBERP4A5D4AR
+XN--MGBPL2FH
+XN--MGBT3DHD
+XN--MGBTX2B
+XN--MGBX4CD0AB
+XN--MIX891F
+XN--MK1BU44C
+XN--MXTQ1M
+XN--NGBC5AZD
+XN--NGBE9E0A
+XN--NODE
+XN--NQV7F
+XN--NQV7FS00EMA
+XN--NYQY26A
+XN--O3CW4H
+XN--OGBPF8FL
+XN--P1ACF
+XN--P1AI
+XN--PBT977C
+XN--PGBS0DH
+XN--PSSY2U
+XN--Q9JYB4C
+XN--QCKA1PMC
+XN--QXAM
+XN--RHQV96G
+XN--ROVU88B
+XN--S9BRJ9C
+XN--SES554G
+XN--T60B56A
+XN--TCKWE
+XN--UNUP4Y
+XN--VERMGENSBERATER-CTB
+XN--VERMGENSBERATUNG-PWB
+XN--VHQUV
+XN--VUQ861B
+XN--W4R85EL8FHU5DNRA
+XN--W4RS40L
+XN--WGBH1C
+XN--WGBL6A
+XN--XHQ521B
+XN--XKC2AL3HYE2A
+XN--XKC2DL3A5EE0H
+XN--Y9A3AQ
+XN--YFRO4I67O
+XN--YGBI2AMMX
+XN--ZFR164B
+XPERIA
+XXX
+XYZ
+YACHTS
+YAHOO
+YAMAXUN
+YANDEX
+YE
+YODOBASHI
+YOGA
+YOKOHAMA
+YOU
+YOUTUBE
+YT
+YUN
+ZA
+#! ZAPPOS
+ZARA
+ZERO
+ZIP
+ZM
+ZONE
+ZUERICH
+ZW
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index f9bf064..7c27477 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -72,10 +72,15 @@
   exclude = ['META-INF/LICENSE'],
 )
 
+# When updating the version of commons-validator, also update the
+# list of supported TLDs in:
+#    gerrit-server/src/test/resources/com/google/gerrit/server/mail/tlds-alpha-by-domain.txt
+# from:
+#    http://data.iana.org/TLD/tlds-alpha-by-domain.txt
 maven_jar(
   name = 'validator',
-  id = 'commons-validator:commons-validator:1.4.1',
-  sha1 = '2231238e391057a53f92bde5bbc588622c1956c3',
+  id = 'commons-validator:commons-validator:1.5.1',
+  sha1 = '86d05a46e8f064b300657f751b5a98c62807e2a0',
   license = 'Apache2.0',
 )
 
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index 6eecd42..69b8f9f 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 6eecd42fd629c700409826273d9ed02499a1d12c
+Subproject commit 69b8f9f413ce83a71593a4068a3b8e81f684cbad
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index d2026f6..b741784 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -59,7 +59,7 @@
       }
     </style>
     <div>
-      <section hidden$="[[!_keyCount(actions)]]" hidden>
+      <section hidden$="[[!_actionCount(actions.*, _additionalActions.*)]]">
         <div class="groupLabel">Change</div>
         <template is="dom-repeat" items="[[_changeActionValues]]" as="action">
           <gr-button title$="[[action.title]]"
@@ -72,7 +72,7 @@
               on-tap="_handleActionTap"></gr-button>
         </template>
       </section>
-      <section hidden$="[[!_keyCount(_revisionActions)]]" hidden>
+      <section hidden$="[[!_actionCount(_revisionActions.*, _additionalActions.*)]]">
         <div class="groupLabel">Revision</div>
         <template is="dom-repeat" items="[[_revisionActionValues]]" as="action">
           <gr-button title$="[[action.title]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index d195241..9783831 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -59,7 +59,10 @@
      */
 
     properties: {
-      actions: Object,
+      actions: {
+        type: Object,
+        value: function() { return {}; },
+      },
       primaryActionKeys: {
         type: Array,
         value: function() {
@@ -77,7 +80,10 @@
         type: Boolean,
         value: true,
       },
-      _revisionActions: Object,
+      _revisionActions: {
+        type: Object,
+        value: function() { return {}; },
+      },
       _revisionActionValues: {
         type: Array,
         computed: '_computeRevisionActionValues(_revisionActions.*, ' +
@@ -103,7 +109,7 @@
     ],
 
     observers: [
-      '_actionsChanged(actions, _revisionActions, _additionalActions)',
+      '_actionsChanged(actions.*, _revisionActions.*, _additionalActions.*)',
     ],
 
     ready: function() {
@@ -129,7 +135,7 @@
       }.bind(this));
     },
 
-    addActionButton: function(key, type, label) {
+    addActionButton: function(type, label) {
       if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
         throw Error('Invalid action type: ' + type);
       }
@@ -137,39 +143,59 @@
         enabled: true,
         label: label,
         __type: type,
-        __key: ADDITIONAL_ACTION_KEY_PREFIX + key + Math.random().toString(36),
+        __key: ADDITIONAL_ACTION_KEY_PREFIX + Math.random().toString(36),
       };
       this.push('_additionalActions', action);
       return action.__key;
     },
 
     removeActionButton: function(key) {
-      var idx = -1;
-      for (var i = 0; i < this._additionalActions.length; i++) {
-        if (this._additionalActions[i].__key === key) {
-          idx = i;
-          break;
-        }
-      }
+      var idx = this._indexOfActionButtonWithKey(key);
       if (idx === -1) {
-        console.error('Could not find action button with key:', key);
+        return;
       }
       this.splice('_additionalActions', idx, 1);
     },
 
+    setActionButtonProp: function(key, prop, value) {
+      this.set([
+        '_additionalActions',
+        this._indexOfActionButtonWithKey(key),
+        prop,
+      ], value);
+    },
+
+    _indexOfActionButtonWithKey: function(key) {
+      for (var i = 0; i < this._additionalActions.length; i++) {
+        if (this._additionalActions[i].__key === key) {
+          return i;
+        }
+      }
+      return -1;
+    },
+
     _getRevisionActions: function() {
       return this.$.restAPI.getChangeRevisionActions(this.changeNum,
           this.patchNum);
     },
 
-    _keyCount: function(obj) {
-      return Object.keys(obj).length;
+    _actionCount: function(actionsChangeRecord, additionalActionsChangeRecord) {
+      var additionalActions = (additionalActionsChangeRecord &&
+          additionalActionsChangeRecord.base) || [];
+      return this._keyCount(actionsChangeRecord) + additionalActions.length;
     },
 
-    _actionsChanged: function(actions, revisionActions, additionalActions) {
-      this.hidden = this._keyCount(actions) === 0 &&
-          this._keyCount(revisionActions) === 0 &&
-              this._keyCount(additionalActions) === 0;
+    _keyCount: function(changeRecord) {
+      return Object.keys((changeRecord && changeRecord.base) || {}).length;
+    },
+
+    _actionsChanged: function(actionsChangeRecord, revisionActionsChangeRecord,
+        additionalActionsChangeRecord) {
+      var additionalActions = (additionalActionsChangeRecord &&
+          additionalActionsChangeRecord.base) || [];
+      this.hidden = this._keyCount(actionsChangeRecord) === 0 &&
+          this._keyCount(revisionActionsChangeRecord) === 0 &&
+              additionalActions.length === 0;
     },
 
     _getValuesFor: function(obj) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index fe04843..462b01b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -203,8 +203,7 @@
     test('custom actions', function(done) {
       // Add a button with the same key as a server-based one to ensure
       // collisions are taken care of.
-      var key = element.addActionButton('submit', element.ActionType.REVISION,
-          'Bork!');
+      var key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
       element.addEventListener(key + '-tap', function(e) {
         assert.equal(e.detail.node.getAttribute('data-action-key'), key);
         element.removeActionButton(key);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 6af4493..f19d528 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -93,6 +93,10 @@
       Gerrit.RESTClientBehavior,
     ],
 
+    observers: [
+      '_labelsChanged(_change.labels.*)',
+    ],
+
     ready: function() {
       this._headerEl = this.$$('.header');
     },
@@ -485,6 +489,13 @@
       }
     },
 
+    _labelsChanged: function(changeRecord) {
+      if (!changeRecord) { return; }
+      this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, {
+        change: this._change,
+      });
+    },
+
     _openReplyDialog: function() {
       this.$.replyOverlay.open().then(function() {
         this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index a364c79..556691a 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -19,6 +19,7 @@
 <link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-reply-dialog">
@@ -154,6 +155,7 @@
             on-tap="_cancelTapHandler">Cancel</gr-button>
       </section>
     </div>
+    <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-reply-dialog.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index a702899..0ee779e 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -59,6 +59,10 @@
       }.bind(this));
     },
 
+    ready: function() {
+      this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
+    },
+
     focus: function() {
       this.async(function() {
         this.$.textarea.textarea.focus();
@@ -72,6 +76,48 @@
       };
     },
 
+    setLabelValue: function(label, value) {
+      var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
+      // The selector may not be present if it’s not at the latest patch set.
+      if (!selectorEl) { return; }
+      var item = selectorEl.$$('gr-button[data-value="' + value + '"]');
+      if (!item) { return; }
+      selectorEl.selectIndex(selectorEl.indexOf(item));
+    },
+
+    send: function() {
+      var obj = {
+        drafts: 'PUBLISH_ALL_REVISIONS',
+        labels: {},
+      };
+      for (var label in this.permittedLabels) {
+        if (!this.permittedLabels.hasOwnProperty(label)) { continue; }
+
+        var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
+
+        // The selector may not be present if it’s not at the latest patch set.
+        if (!selectorEl) { continue; }
+
+        var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
+        selectedVal = parseInt(selectedVal, 10);
+        obj.labels[label] = selectedVal;
+      }
+      if (this.draft != null) {
+        obj.message = this.draft;
+      }
+      this.disabled = true;
+      return this._saveReview(obj).then(function(response) {
+        this.disabled = false;
+        if (!response.ok) { return response; }
+
+        this.draft = '';
+        this.fire('send', null, {bubbles: false});
+      }.bind(this)).catch(function(err) {
+        this.disabled = false;
+        throw err;
+      }.bind(this));
+    },
+
     _computeShowLabels: function(patchNum, revisions) {
       var num = parseInt(patchNum, 10);
       for (var rev in revisions) {
@@ -147,34 +193,7 @@
 
     _sendTapHandler: function(e) {
       e.preventDefault();
-      var obj = {
-        drafts: 'PUBLISH_ALL_REVISIONS',
-        labels: {},
-      };
-      for (var label in this.permittedLabels) {
-        var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
-
-        // The selector may not be present if it’s not at the latest patch set.
-        if (!selectorEl) { continue; }
-
-        var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
-        selectedVal = parseInt(selectedVal, 10);
-        obj.labels[label] = selectedVal;
-      }
-      if (this.draft != null) {
-        obj.message = this.draft;
-      }
-      this.disabled = true;
-      this._saveReview(obj).then(function(response) {
-        this.disabled = false;
-        if (!response.ok) { return response; }
-
-        this.draft = '';
-        this.fire('send', null, {bubbles: false});
-      }.bind(this)).catch(function(err) {
-        this.disabled = false;
-        throw err;
-      }.bind(this));
+      this.send();
     },
 
     _saveReview: function(review) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 67aa0ba..832665b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -100,6 +100,12 @@
           return null;
         },
 
+        getLineNumberByChild: function(node) {
+          var lineEl = this.getLineElByChild(node);
+          return lineEl ?
+              parseInt(lineEl.getAttribute('data-value'), 10) : null;
+        },
+
         renderLineRange: function(startLine, endLine, opt_side) {
           var groups =
               this._builder.getGroupsByLineRange(startLine, endLine, opt_side);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
index c8ed041..bc3b23f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
@@ -20,7 +20,7 @@
 <dom-module id="gr-diff-highlight">
   <template>
     <style>
-      :host {
+      .contentWrapper ::content {
         position: relative;
       }
       .contentWrapper ::content .range {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index db092e6..1c3572d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -59,6 +59,11 @@
     },
 
     _enabledChanged: function() {
+      if (this.enabled) {
+        this.listen(document, 'selectionchange', '_handleSelectionChange');
+      } else {
+        this.unlisten(document, 'selectionchange', '_handleSelectionChange');
+      }
       for (var eventName in this._enabledListeners) {
         var methodName = this._enabledListeners[eventName];
         if (this.enabled) {
@@ -88,6 +93,14 @@
       }
     },
 
+    _handleSelectionChange: function() {
+      // Can't use up or down events to handle selection started and/or ended in
+      // in comment threads or outside of diff.
+      // Debounce removeActionBox to give it a chance to react to click/tap.
+      this._removeActionBoxDebounced();
+      this.debounce('selectionChange', this._handleSelection, 200);
+    },
+
     _handleRender: function() {
       this._applyAllHighlights();
     },
@@ -129,6 +142,111 @@
       }, this);
     },
 
+    /**
+     * Convert DOM Range selection to concrete numbers (line, column, side).
+     * Moves range end if it's not inside td.content.
+     * Returns null if selection end is not valid (outside of diff).
+     *
+     * @param {Node} node td.content child
+     * @param {number} offset offset within node
+     * @return {{
+     *   node: Node,
+     *   side: string,
+     *   line: Number,
+     *   column: Number
+     * }}
+     */
+    _normalizeSelectionSide: function(node, offset) {
+      var column;
+      if (!this.contains(node)) {
+        return;
+      }
+      var lineEl = this.diffBuilder.getLineElByChild(node);
+      if (!lineEl) {
+        return;
+      }
+      var side = this.diffBuilder.getSideByLineEl(lineEl);
+      if (!side) {
+        return;
+      }
+      var line = this.diffBuilder.getLineNumberByChild(lineEl);
+      if (!line) {
+        return;
+      }
+      var content = this.diffBuilder.getContentByLineEl(lineEl);
+      if (!content) {
+        return;
+      }
+      if (!content.contains(node)) {
+        node = content;
+        column = 0;
+      } else {
+        var thread = content.querySelector('gr-diff-comment-thread');
+        if (thread && thread.contains(node)) {
+          column = this._getLength(content);
+          node = content;
+        } else {
+          column = this._convertOffsetToColumn(node, offset);
+        }
+      }
+
+      return {
+        node: node,
+        side: side,
+        line: line,
+        column: column,
+      };
+    },
+
+    _handleSelection: function() {
+      var selection = window.getSelection();
+      if (selection.rangeCount != 1) {
+        return;
+      }
+      var range = selection.getRangeAt(0);
+      if (range.collapsed) {
+        return;
+      }
+      var start =
+          this._normalizeSelectionSide(range.startContainer, range.startOffset);
+      if (!start) {
+        return;
+      }
+      var end =
+          this._normalizeSelectionSide(range.endContainer, range.endOffset);
+      if (!end) {
+        return;
+      }
+      if (start.side !== end.side ||
+          end.line < start.line ||
+          (start.line === end.line && start.column === end.column)) {
+        return;
+      }
+
+      // TODO (viktard): Drop empty first and last lines from selection.
+
+      var actionBox = document.createElement('gr-selection-action-box');
+      Polymer.dom(this.root).appendChild(actionBox);
+      actionBox.range = {
+        startLine: start.line,
+        startChar: start.column,
+        endLine: end.line,
+        endChar: end.column,
+      };
+      actionBox.side = start.side;
+      if (start.line === end.line) {
+        actionBox.placeAbove(range);
+      } else if (start.node instanceof Text) {
+        actionBox.placeAbove(start.node.splitText(start.column));
+        start.node.parentElement.normalize(); // Undo splitText from above.
+      } else if (start.node.classList.contains('content') &&
+                 start.node.firstChild) {
+        actionBox.placeAbove(start.node.firstChild);
+      } else {
+        actionBox.placeAbove(start.node);
+      }
+    },
+
     _renderCommentRange: function(comment, el) {
       var lineEl = this.diffBuilder.getLineElByChild(el);
       if (!lineEl) {
@@ -181,6 +299,10 @@
           range.endLine, range.endChar, side);
     },
 
+    _removeActionBoxDebounced: function() {
+      this.debounce('removeActionBox', this._removeActionBox, 10);
+    },
+
     _removeActionBox: function() {
       var actionBox = this.$$('gr-selection-action-box');
       if (actionBox) {
@@ -188,8 +310,24 @@
       }
     },
 
+    _convertOffsetToColumn: function(el, offset) {
+      if (el instanceof Element && el.classList.contains('content')) {
+        return offset;
+      }
+      while (el.previousSibling ||
+          !el.parentElement.classList.contains('content')) {
+        if (el.previousSibling) {
+          el = el.previousSibling;
+          offset += this._getLength(el);
+        } else {
+          el = el.parentElement;
+        }
+      }
+      return offset;
+    },
+
     /**
-     * Traverse diff content from right to left, call callback for each node.
+     * Traverse Element from right to left, call callback for each node.
      * Stops if callback returns true.
      *
      * @param {!Node} startNode
@@ -200,7 +338,9 @@
       var travelLeft = opt_flags && opt_flags.left;
       var node = startNode;
       while (node) {
-        if (node instanceof Element && node.tagName !== 'HL') {
+        if (node instanceof Element &&
+            node.tagName !== 'HL' &&
+            node.tagName !== 'SPAN') {
           break;
         }
         var nextNode = travelLeft ? node.previousSibling : node.nextSibling;
@@ -222,7 +362,6 @@
         node = node.firstChild;
         var length = 0;
         while (node) {
-          // Only measure Text nodes and <hl>
           if (node instanceof Text || node.tagName == 'HL') {
             length += this._getLength(node);
           }
@@ -242,10 +381,16 @@
      * @return {!Element} Wrapped node.
      */
     _wrapInHighlight: function(node, cssClass) {
-      var hl = document.createElement('hl');
-      hl.className = cssClass;
-      Polymer.dom(node.parentElement).replaceChild(hl, node);
-      hl.appendChild(node);
+      var hl;
+      if (node.tagName === 'HL') {
+        hl = node;
+        hl.classList.add(cssClass);
+      } else {
+        hl = document.createElement('hl');
+        hl.className = cssClass;
+        Polymer.dom(node.parentElement).replaceChild(hl, node);
+        hl.appendChild(node);
+      }
       return hl;
     },
 
@@ -256,7 +401,7 @@
      * @param {number} offset
      * @return {!Text} Trailing Text Node.
      */
-    _splitText: function(node, offset) {
+    _splitTextNode: function(node, offset) {
       if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) {
         // DOM Api for splitText() is broken for Unicode:
         // https://mathiasbynens.be/notes/javascript-unicode
@@ -275,10 +420,41 @@
     },
 
     /**
+     * Split Node at offset.
+     * If Node is Element, it's cloned and the node at offset is split too.
+     *
+     * @param {!Node} node
+     * @param {number} offset
+     * @return {!Node} Trailing Node.
+     */
+    _splitNode: function(element, offset) {
+      if (element instanceof Text) {
+        return this._splitTextNode(element, offset);
+      }
+      var tail = element.cloneNode(false);
+      element.parentElement.insertBefore(tail, element.nextSibling);
+      // Skip nodes before offset.
+      var node = element.firstChild;
+      while (node &&
+          this._getLength(node) <= offset ||
+          this._getLength(node) === 0) {
+        offset -= this._getLength(node);
+        node = node.nextSibling;
+      }
+      if (this._getLength(node) > offset) {
+        tail.appendChild(this._splitNode(node, offset));
+      }
+      while (node.nextSibling) {
+        tail.appendChild(node.nextSibling);
+      }
+      return tail;
+    },
+
+    /**
      * Split Text Node and wrap it in hl with cssClass.
      * Wraps trailing part after split, tailing one if opt_firstPart is true.
      *
-     * @param {!Text} node
+     * @param {!Node} node
      * @param {number} offset
      * @param {string} cssClass
      * @param {boolean=} opt_firstPart
@@ -288,10 +464,10 @@
         return this._wrapInHighlight(node, cssClass);
       } else {
         if (opt_firstPart) {
-          this._splitText(node, offset);
+          this._splitNode(node, offset);
           // Node points to first part of the Text, second one is sibling.
         } else {
-          node = this._splitText(node, offset);
+          node = this._splitNode(node, offset);
         }
         return this._wrapInHighlight(node, cssClass);
       }
@@ -329,29 +505,21 @@
       if (startNode instanceof Text) {
         startNode =
             this._splitAndWrapInHighlight(startNode, startOffset, cssClass);
-        startContent.insertBefore(startNode, startNode.nextSibling);
         // Edge case: single line, text node wraps the highlight.
         if (isOneLine && this._getLength(startNode) > length) {
-          var extra = this._splitText(startNode.firstChild, length);
+          var extra = this._splitTextNode(startNode.firstChild, length);
           startContent.insertBefore(extra, startNode.nextSibling);
           startContent.normalize();
         }
       } else if (startNode.tagName == 'HL') {
         if (!startNode.classList.contains(cssClass)) {
-          var hl = startNode;
-          startNode = this._splitAndWrapInHighlight(
-              startNode.firstChild, startOffset, cssClass);
-          startContent.insertBefore(startNode, hl.nextSibling);
           // Edge case: single line, <hl> wraps the highlight.
-          if (isOneLine && this._getLength(startNode) > length) {
-            var trailingHl = hl.cloneNode(false);
-            trailingHl.appendChild(
-                this._splitText(startNode.firstChild, length));
-            startContent.insertBefore(trailingHl, startNode.nextSibling);
+          // Should leave wrapping HL's content after the highlight.
+          if (isOneLine && startOffset + length < this._getLength(startNode)) {
+            this._splitNode(startNode, startOffset + length);
           }
-          if (hl.textContent.length === 0) {
-            hl.remove();
-          }
+          startNode =
+              this._splitAndWrapInHighlight(startNode, startOffset, cssClass);
         }
       } else {
         startNode = null;
@@ -393,8 +561,7 @@
           // Split text inside HL.
           var hl = endNode;
           endNode = this._splitAndWrapInHighlight(
-              endNode.firstChild, endOffset, cssClass, true);
-          endContent.insertBefore(endNode, hl);
+              endNode, endOffset, cssClass, true);
           if (hl.textContent.length === 0) {
             hl.remove();
           }
@@ -424,19 +591,32 @@
 
       // Grow starting highlight until endNode or end of line.
       if (startNode && startNode != endNode) {
-        this._traverseContentSiblings(startNode.nextSibling, function(node) {
-          startNode.textContent += node.textContent;
-          node.remove();
+        var growStartHl = function(node) {
+          if (node instanceof Text || node.tagName === 'SPAN') {
+            startNode.appendChild(node);
+          } else if (node.tagName === 'HL') {
+            this._traverseContentSiblings(node.firstChild, growStartHl);
+            node.remove();
+          }
           return node == endNode;
-        });
+        }.bind(this);
+        this._traverseContentSiblings(startNode.nextSibling, growStartHl);
+        startNode.normalize();
       }
 
       if (!isOneLine && endNode) {
+        var growEndHl = function(node) {
+          if (node instanceof Text || node.tagName === 'SPAN') {
+            endNode.insertBefore(node, endNode.firstChild);
+          } else if (node.tagName === 'HL') {
+            this._traverseContentSiblings(node.firstChild, growEndHl);
+            node.remove();
+          }
+        }.bind(this);
         // Prepend text up to line start to the ending highlight.
-        this._traverseContentSiblings(endNode.previousSibling, function(node) {
-          endNode.textContent = node.textContent + endNode.textContent;
-          node.remove();
-        }, {left: true});
+        this._traverseContentSiblings(
+          endNode.previousSibling, growEndHl, {left: true});
+        endNode.normalize();
       }
     },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 60bfb96..0df6a20 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -31,38 +31,74 @@
 
         <tbody class="section both">
           <tr class="diff-row side-by-side" left-type="both" right-type="both">
-            <td class="left lineNum" data-value="138"></td>
+            <td class="left lineNum" data-value="138">138</td>
             <td class="content both darkHighlight">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</td>
-            <td class="right lineNum" data-value="119"></td>
+            <td class="right lineNum" data-value="119">119</td>
             <td class="content both darkHighlight">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</td>
           </tr>
         </tbody>
 
         <tbody class="section delta">
           <tr class="diff-row side-by-side" left-type="remove" right-type="add">
-            <td class="left lineNum" data-value="140"></td>
+            <td class="left lineNum" data-value="140">140</td>
             <!-- Next tag is formatted to eliminate zero-length text nodes. -->
-            <td class="content remove lightHighlight">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a udiam, <hl>quid</hl> sit, quod <hl>Epicurum</hl><gr-diff-comment-thread>
+            <td class="content remove lightHighlight">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl>udiam, <hl>quid</hl> sit, <span class="tab withIndicator" style="tab-size:8;"></span>quod <hl>Epicurum</hl><gr-diff-comment-thread>
                 [Yet another random diff thread content here]
               </gr-diff-comment-thread></td>
-            <td class="right lineNum" data-value="121"></td>
-            <td class="content add lightHighlight">
-              nacti ,
-              <hl>,</hl>
-              sumus  otiosum,  audiam,  sit, quod
-            </td>
+            <td class="right lineNum" data-value="120">120</td>
+            <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+            <td class="content add lightHighlight">nacti , <hl>,</hl> sumus <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl> otiosum,  <span class="tab withIndicator" style="tab-size:8;"></span> audiam,  sit, quod</td>
           </tr>
         </tbody>
 
         <tbody class="section both">
           <tr class="diff-row side-by-side" left-type="both" right-type="both">
-            <td class="left lineNum" data-value="149"></td>
-            <td class="content both darkHighlight">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</td>
+            <td class="left lineNum" data-value="141"></td>
+            <td class="content both darkHighlight">nam et<hl><span class="tab withIndicator" style="tab-size:8;">	</span></hl>complectitur<span class="tab withIndicator" style="tab-size:8;">	</span>verbis, quod vult, et dicit plane, quod intellegam;</td>
             <td class="right lineNum" data-value="130"></td>
             <td class="content both darkHighlight">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</td>
           </tr>
         </tbody>
 
+        <tbody class="section contextControl">
+          <tr class="diff-row side-by-side" left-type="contextControl" right-type="contextControl">
+            <td class="left contextLineNum" data-value="@@"></td>
+            <td>
+              <gr-button>+10↑</gr-button>
+              -
+              <gr-button>Show 21 common lines</gr-button>
+              -
+              <gr-button>+10↓</gr-button>
+            </td>
+            <td class="right contextLineNum" data-value="@@"></td>
+            <td>
+              <gr-button>+10↑</gr-button>
+              -
+              <gr-button>Show 21 common lines</gr-button>
+              -
+              <gr-button>+10↓</gr-button>
+            </td>
+          </tr>
+        </tbody>
+
+        <tbody class="section delta">
+          <tr class="diff-row side-by-side" left-type="blank" right-type="add">
+            <td class="left"></td>
+            <td class="blank darkHighlight"></td>
+            <td class="right lineNum" data-value="146"></td>
+            <td class="content add darkHighlight">[17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,</td>
+          </tr>
+        </tbody>
+
+        <tbody class="section both">
+          <tr class="diff-row side-by-side" left-type="both" right-type="both">
+            <td class="left lineNum" data-value="165"></td>
+            <td class="content both darkHighlight">in physicis, quibus maxime gloriatur, primum totus est alienus. Democritea dicit</td>
+            <td class="right lineNum" data-value="147"></td>
+            <td class="content both darkHighlight">in physicis, <hl><span class="tab withIndicator" style="tab-size:8;">	</span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</td>
+          </tr>
+        </tbody>
+
       </table>
     </gr-diff-highlight>
   </template>
@@ -116,6 +152,28 @@
       }
     });
 
+    test('does not listen to selectionchange when disabled', function() {
+      sandbox.stub(element, '_handleSelection');
+      sandbox.stub(element, '_removeActionBox');
+      element.enabled = false;
+      document.dispatchEvent(new CustomEvent('selectionchange'));
+      element.flushDebouncer('selectionChange');
+      assert.isFalse(element._handleSelection.called);
+      element.flushDebouncer('removeActionBox');
+      assert.isFalse(element._removeActionBox.called);
+    });
+
+    test('listens to selectionchange when enabled', function() {
+      sandbox.stub(element, '_handleSelection');
+      sandbox.stub(element, '_removeActionBox');
+      element.enabled = true;
+      document.dispatchEvent(new CustomEvent('selectionchange'));
+      element.flushDebouncer('selectionChange');
+      assert.isTrue(element._handleSelection.called);
+      element.flushDebouncer('removeActionBox');
+      assert.isTrue(element._removeActionBox.called);
+    });
+
     suite('comment events', function() {
       var builder;
 
@@ -162,7 +220,6 @@
         });
       });
 
-
       test('renders lines in comment range on comment discard', function(done) {
         element.fire('comment-discard', {
           comment: {
@@ -254,10 +311,10 @@
       var diff = element.querySelector('#diffTable');
       var startContent =
           diff.querySelector('.left.lineNum[data-value="138"] ~ .content');
-      var endContent =
-          diff.querySelector('.left.lineNum[data-value="149"] ~ .content');
       var betweenContent =
           diff.querySelector('.left.lineNum[data-value="140"] ~ .content');
+      var endContent =
+          diff.querySelector('.left.lineNum[data-value="141"] ~ .content');
       var commentThread =
           diff.querySelector('gr-diff-comment-thread');
       var builder = {
@@ -271,9 +328,9 @@
       element.enabled = true;
       builder.getContentByLine.withArgs(138, 'left').returns(
           startContent);
-      builder.getContentByLine.withArgs(149, 'left').returns(
+      builder.getContentByLine.withArgs(141, 'left').returns(
           endContent);
-      element._applyRangedHighlight('some', 138, 4, 149, 8, 'left');
+      element._applyRangedHighlight('some', 138, 4, 141, 28, 'left');
       assert.instanceOf(startContent.childNodes[0], Text);
       assert.equal(startContent.childNodes[0].textContent, '[14]');
       assert.instanceOf(startContent.childNodes[1], Element);
@@ -282,14 +339,6 @@
       assert.equal(startContent.childNodes[1].tagName, 'HL');
       assert.equal(startContent.childNodes[1].className, 'some');
 
-      assert.instanceOf(endContent.childNodes[0], Element);
-      assert.equal(endContent.childNodes[0].textContent, 'nam et c');
-      assert.equal(endContent.childNodes[0].tagName, 'HL');
-      assert.equal(endContent.childNodes[0].className, 'some');
-      assert.instanceOf(endContent.childNodes[1], Text);
-      assert.equal(endContent.childNodes[1].textContent,
-          'omplectitur verbis, quod vult, et dicit plane, quod intellegam;');
-
       assert.instanceOf(betweenContent.firstChild, Element);
       assert.equal(betweenContent.firstChild.tagName, 'HL');
       assert.equal(betweenContent.firstChild.className, 'some');
@@ -304,6 +353,22 @@
 
       assert.strictEqual(betweenContent.querySelector('gr-diff-comment-thread'),
           commentThread, 'Comment threads should be preserved.');
+
+      assert.instanceOf(endContent.childNodes[0], Element);
+      assert.equal(endContent.childNodes[0].textContent,
+          'nam et\tcomplectitur\tverbis, ');
+      assert.equal(endContent.childNodes[0].tagName, 'HL');
+      assert.equal(endContent.childNodes[0].className, 'some');
+      assert.instanceOf(endContent.childNodes[1], Text);
+      assert.equal(endContent.childNodes[1].textContent,
+          'quod vult, et dicit plane, quod intellegam;');
+      var endHl = endContent.querySelector('hl.some');
+      assert.equal(endHl.childNodes.length, 5);
+      var tabs = endHl.querySelectorAll('span.tab');
+      assert.equal(tabs.length, 2);
+      assert.equal(tabs[0].previousSibling.textContent, 'nam et');
+      assert.equal(tabs[1].previousSibling.textContent, 'complectitur');
+      assert.equal(tabs[1].nextSibling.textContent, 'verbis, ');
     });
 
     suite('single line ranges', function() {
@@ -334,10 +399,18 @@
         assert.equal(content.firstChild.tagName, 'HL');
         assert.equal(content.firstChild.className, 'some');
         assert.equal(content.childNodes.length, 2);
-        assert.equal(content.firstChild.childNodes.length, 1);
+        assert.equal(content.firstChild.childNodes.length, 5);
         assert.equal(content.firstChild.textContent,
             'na💢ti te, inquit, sumus aliquando otiosum, certe a udiam, ' +
             'quid sit, quod Epicurum');
+        var tabs = content.querySelectorAll('span.tab');
+        assert.equal(tabs.length, 2);
+        assert.strictEqual(tabs[1].previousSibling, tabs[0].nextSibling);
+        assert.equal(tabs[0].previousSibling.textContent,
+            'na💢ti te, inquit, sumus aliquando otiosum, certe a ');
+        assert.equal(tabs[1].previousSibling.textContent,
+            'udiam, quid sit, ');
+        assert.equal(tabs[1].nextSibling.textContent, 'quod Epicurum');
       });
 
       test('merging multiple other hls', function() {
@@ -346,7 +419,8 @@
         assert.equal(content.childNodes.length, 4);
         var hl = content.querySelector('hl.some');
         assert.strictEqual(content.firstChild, hl.previousSibling);
-        assert.equal(hl.childNodes.length, 1);
+        assert.equal(hl.childNodes.length, 5);
+        assert.equal(content.querySelectorAll('span.tab').length, 2);
         assert.equal(hl.textContent,
             'a💢ti te, inquit, sumus aliquando otiosum, certe a udiam, ' +
             'quid sit, quod Epicuru');
@@ -375,7 +449,7 @@
         //  After: na💢ti <hl class="foo">te, in</hl><hl class="some">quit, ...
         element._applyRangedHighlight('some', 140, 12, 140, 21, 'left');
         var hl = content.querySelector('hl.some');
-        assert.equal(hl.outerHTML, '<hl class="some">quit, sum</hl>');
+        assert.equal(hl.textContent, 'quit, sum');
         assert.equal(
             hl.previousSibling.outerHTML, '<hl class="foo">te, in</hl>');
       });
@@ -385,7 +459,7 @@
         //  After: <hl class="foo">t</hl><hl="some">e, i</hl><hl class="foo">n..
         element._applyRangedHighlight('some', 140, 7, 140, 12, 'left');
         var hl = content.querySelector('hl.some');
-        assert.equal(hl.outerHTML, '<hl class="some">e, in</hl>');
+        assert.equal(hl.textContent, 'e, in');
         assert.equal(hl.previousSibling.outerHTML, '<hl class="foo">t</hl>');
         assert.equal(hl.nextSibling.outerHTML, '<hl class="foo">quit</hl>');
       });
@@ -393,7 +467,7 @@
       test('hl starts and ends in different hls', function() {
         element._applyRangedHighlight('some', 140, 8, 140, 27, 'left');
         var hl = content.querySelector('hl.some');
-        assert.equal(hl.outerHTML, '<hl class="some">, inquit, sumus ali</hl>');
+        assert.equal(hl.textContent, ', inquit, sumus ali');
         assert.equal(hl.previousSibling.outerHTML, '<hl class="foo">te</hl>');
         assert.equal(hl.nextSibling.outerHTML, '<hl class="bar">quando</hl>');
       });
@@ -408,9 +482,7 @@
       test('hl starting and ending in boundaries', function() {
         element._applyRangedHighlight('some', 140, 6, 140, 33, 'left');
         var hl = content.querySelector('hl.some');
-        assert.equal(
-            hl.outerHTML, '<hl class="some">te, inquit, sumus aliquando</hl>');
-        assert.notOk(content.querySelector('.foo'));
+        assert.equal(hl.textContent, 'te, inquit, sumus aliquando');
         assert.notOk(content.querySelector('.bar'));
       });
 
@@ -422,7 +494,7 @@
         assert.equal(hl.outerHTML, '<hl class="some">a💢t</hl>');
       });
 
-      test('growing hl left including another hl', function() {
+      test('growing hl right including another hl', function() {
         element._applyRangedHighlight('some', 140, 1, 140, 4, 'left');
         element._applyRangedHighlight('some', 140, 3, 140, 10, 'left');
         assert.equal(content.querySelectorAll('hl.some').length, 1);
@@ -431,7 +503,7 @@
         assert.equal(hl.nextSibling.outerHTML, '<hl class="foo">inquit</hl>');
       });
 
-      test('growing hl right to start of line', function() {
+      test('growing hl left to start of line', function() {
         element._applyRangedHighlight('some', 140, 2, 140, 5, 'left');
         element._applyRangedHighlight('some', 140, 0, 140, 3, 'left');
         assert.equal(content.querySelectorAll('hl.some').length, 1);
@@ -439,6 +511,14 @@
         assert.equal(hl.outerHTML, '<hl class="some">na💢ti</hl>');
         assert.strictEqual(content.firstChild, hl);
       });
+
+      test('splitting hl containing a tab', function() {
+        element._applyRangedHighlight('some', 140, 63, 140, 72, 'left');
+        assert.equal(content.querySelector('hl.some').textContent, 'sit, quod');
+        element._applyRangedHighlight('another', 140, 66, 140, 81, 'left');
+        assert.equal(content.querySelector('hl.another').textContent,
+            ', quod Epicurum');
+      });
     });
 
     test('_applyAllHighlights', function() {
@@ -499,5 +579,281 @@
       element.fire('show-context');
       assert.isFalse(element._applyAllHighlights.called);
     });
+
+    suite('selection', function() {
+      var diff;
+      var builder;
+      var contentStubs;
+
+      var stubContent = function(line, side, opt_child) {
+        var content = diff.querySelector(
+            '.' + side + '.lineNum[data-value="' + line + '"] ~ .content');
+        var lineEl = diff.querySelector(
+            '.' + side + '.lineNum[data-value="' + line + '"]');
+        contentStubs.push({
+          lineEl: lineEl,
+          content: content,
+        });
+        builder.getContentByLineEl.withArgs(lineEl).returns(content);
+        builder.getLineNumberByChild.withArgs(lineEl).returns(line);
+        builder.getContentByLine.withArgs(line, side).returns(content);
+        builder.getSideByLineEl.withArgs(lineEl).returns(side);
+        return content;
+      };
+
+      var emulateSelection = function(
+          startNode, startOffset, endNode, endOffset) {
+        var selection = window.getSelection();
+        var range = document.createRange();
+        range.setStart(startNode, startOffset);
+        range.setEnd(endNode, endOffset);
+        selection.addRange(range);
+        element._handleSelection();
+      };
+
+      var getActionRange = function() {
+        return Polymer.dom(element.root).querySelector(
+            'gr-selection-action-box').range;
+      };
+
+      var getActionSide = function() {
+        return Polymer.dom(element.root).querySelector(
+            'gr-selection-action-box').side;
+      };
+
+      var getLineElByChild = function(node) {
+        var stubs = contentStubs.find(function(stub) {
+          return stub.content.contains(node);
+        });
+        return stubs && stubs.lineEl;
+      };
+
+      setup(function() {
+        contentStubs = [];
+        stub('gr-selection-action-box', {
+          placeAbove: sandbox.stub(),
+        });
+        diff = element.querySelector('#diffTable');
+        builder = {
+          getContentByLine: sandbox.stub(),
+          getContentByLineEl: sandbox.stub(),
+          getLineElByChild: getLineElByChild,
+          getLineNumberByChild: sandbox.stub(),
+          getSideByLineEl: sandbox.stub(),
+        };
+        element._cachedDiffBuilder = builder;
+        element.enabled = true;
+      });
+
+      teardown(function() {
+        contentStubs = null;
+        window.getSelection().removeAllRanges();
+      });
+
+      test('single line', function() {
+        var content = stubContent(138, 'left');
+        emulateSelection(content.firstChild, 5, content.firstChild, 12);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 138,
+          startChar: 5,
+          endLine: 138,
+          endChar: 12,
+        });
+        assert.equal(getActionSide(), 'left');
+      });
+
+      test('multiline', function() {
+        var startContent = stubContent(119, 'right');
+        var endContent = stubContent(120, 'right');
+        emulateSelection(
+            startContent.firstChild, 10, endContent.lastChild, 7);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 119,
+          startChar: 10,
+          endLine: 120,
+          endChar: 34,
+        });
+        assert.equal(getActionSide(), 'right');
+      });
+
+      test('multiline grow end highlight over tabs', function() {
+        var startContent = stubContent(119, 'right');
+        var endContent = stubContent(120, 'right');
+        emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 119,
+          startChar: 10,
+          endLine: 120,
+          endChar: 2,
+        });
+        assert.equal(getActionSide(), 'right');
+      });
+
+      test('collapsed', function() {
+        var content = stubContent(138, 'left');
+        emulateSelection(content.firstChild, 5, content.firstChild, 5);
+        assert.isOk(window.getSelection().getRangeAt(0).startContainer);
+        assert.isFalse(element.isRangeSelected());
+      });
+
+      test('starts inside hl', function() {
+        var content = stubContent(140, 'left');
+        var hl = content.querySelector('.foo');
+        emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 140,
+          startChar: 8,
+          endLine: 140,
+          endChar: 23,
+        });
+        assert.equal(getActionSide(), 'left');
+      });
+
+      test('ends inside hl', function() {
+        var content = stubContent(140, 'left');
+        var hl = content.querySelector('.bar');
+        emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 140,
+          startChar: 18,
+          endLine: 140,
+          endChar: 27,
+        });
+      });
+
+      test('multiple hl', function() {
+        var content = stubContent(140, 'left');
+        var hl = content.querySelectorAll('hl')[4];
+        emulateSelection(content.firstChild, 2, hl.firstChild, 2);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 140,
+          startChar: 2,
+          endLine: 140,
+          endChar: 60,
+        });
+        assert.equal(getActionSide(), 'left');
+      });
+
+      test('starts outside of diff', function() {
+        var content = stubContent(140, 'left');
+        emulateSelection(content.previousElementSibling.firstChild, 2,
+            content.firstChild, 2);
+        assert.isFalse(element.isRangeSelected());
+      });
+
+      test('ends outside of diff', function() {
+        var content = stubContent(140, 'left');
+        emulateSelection(content.nextElementSibling.firstChild, 2,
+            content.firstChild, 2);
+        assert.isFalse(element.isRangeSelected());
+      });
+
+      test('starts and ends on different sides', function() {
+        var startContent = stubContent(140, 'left');
+        var endContent = stubContent(130, 'right');
+        emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
+        assert.isFalse(element.isRangeSelected());
+      });
+
+      test('starts in comment thread element', function() {
+        var startContent = stubContent(140, 'left');
+        var comment = startContent.querySelector('gr-diff-comment-thread');
+        var endContent = stubContent(141, 'left');
+        emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 140,
+          startChar: 81,
+          endLine: 141,
+          endChar: 4,
+        });
+        assert.equal(getActionSide(), 'left');
+      });
+
+      test('ends in comment thread element', function() {
+        var content = stubContent(140, 'left');
+        var comment = content.querySelector('gr-diff-comment-thread');
+        emulateSelection(content.firstChild, 4, comment.firstChild, 1);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 140,
+          startChar: 4,
+          endLine: 140,
+          endChar: 81,
+        });
+        assert.equal(getActionSide(), 'left');
+      });
+
+      test('starts in context element', function() {
+        var contextControl = diff.querySelector('.contextControl');
+        var content = stubContent(146, 'right');
+        emulateSelection(contextControl, 0, content.firstChild, 7);
+        // TODO (viktard): Select nearest line.
+        assert.isFalse(element.isRangeSelected());
+      });
+
+      test('ends in context element', function() {
+        var contextControl = diff.querySelector('.contextControl');
+        var content = stubContent(141, 'left');
+        emulateSelection(content.firstChild, 2, contextControl, 0);
+        // TODO (viktard): Select nearest line.
+        assert.isFalse(element.isRangeSelected());
+      });
+
+      test('selection containing context element', function() {
+        var startContent = stubContent(130, 'right');
+        var endContent = stubContent(146, 'right');
+        emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 130,
+          startChar: 3,
+          endLine: 146,
+          endChar: 14,
+        });
+        assert.equal(getActionSide(), 'right');
+      });
+
+      test('ends at a tab', function() {
+        var content = stubContent(140, 'left');
+        emulateSelection(
+            content.firstChild, 1, content.querySelector('span'), 0);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 140,
+          startChar: 1,
+          endLine: 140,
+          endChar: 51,
+        });
+        assert.equal(getActionSide(), 'left');
+      });
+
+      test('starts at a tab', function() {
+        var content = stubContent(140, 'left');
+        emulateSelection(
+            content.querySelectorAll('hl')[3], 0,
+            content.querySelectorAll('span')[1], 0);
+        assert.isTrue(element.isRangeSelected());
+        assert.deepEqual(getActionRange(), {
+          startLine: 140,
+          startChar: 51,
+          endLine: 140,
+          endChar: 68,
+        });
+        assert.equal(getActionSide(), 'left');
+      });
+
+      // TODO (viktard): Selection starts in line number.
+      // TODO (viktard): Empty lines in selection start.
+      // TODO (viktard): Empty lines in selection end.
+      // TODO (viktard): Only empty lines selected.
+      // TODO (viktard): Unified mode.
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
index 6f95789..9a8ea37 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
@@ -29,6 +29,7 @@
         cursor: pointer;
         padding: .3em;
         position: absolute;
+        white-space: nowrap;
       }
       .arrow {
         background: #fff;
@@ -36,6 +37,7 @@
         border-width: 0 1px 1px 0;
         height: var(--gr-arrow-size);
         left: calc(50% - 1em);
+        margin-top: .05em;
         position: absolute;
         transform: rotate(45deg);
         width: var(--gr-arrow-size);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
new file mode 100644
index 0000000..f7c337b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -0,0 +1,62 @@
+// 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.
+(function(window) {
+  'use strict';
+
+  function GrChangeActionsInterface(el) {
+    this._el = el;
+    this.RevisionActions = el.RevisionActions;
+    this.ChangeActions = el.ChangeActions;
+    this.ActionType = el.ActionType;
+  }
+
+  GrChangeActionsInterface.prototype.addPrimaryActionKey = function(key) {
+    if (this._el.primaryActionKeys.indexOf(key) !== -1) { return; }
+
+    this._el.push('primaryActionKeys', key);
+  };
+
+  GrChangeActionsInterface.prototype.removePrimaryActionKey = function(key) {
+    this._el.primaryActionKeys = this._el.primaryActionKeys.filter(function(k) {
+      return k !== key;
+    });
+  };
+
+  GrChangeActionsInterface.prototype.add = function(type, label) {
+    return this._el.addActionButton(type, label);
+  };
+
+  GrChangeActionsInterface.prototype.remove = function(key) {
+    return this._el.removeActionButton(key);
+  };
+
+  GrChangeActionsInterface.prototype.addTapListener = function(key, handler) {
+    this._el.addEventListener(key + '-tap', handler);
+  };
+
+  GrChangeActionsInterface.prototype.removeTapListener = function(key,
+      handler) {
+    this._el.removeEventListener(key + '-tap', handler);
+  };
+
+  GrChangeActionsInterface.prototype.setLabel = function(key, text) {
+    this._el.setActionButtonProp(key, 'label', text);
+  };
+
+  GrChangeActionsInterface.prototype.setEnabled = function(key, enabled) {
+    this._el.setActionButtonProp(key, 'enabled', enabled);
+  };
+
+  window.GrChangeActionsInterface = GrChangeActionsInterface;
+})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
new file mode 100644
index 0000000..3030870
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-change-actions-js-api</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<!--
+This must refer to the element this interface is wrapping around. Otherwise
+breaking changes to gr-change-actions won’t be noticed.
+-->
+<link rel="import" href="../../change/gr-change-actions/gr-change-actions.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-change-actions></gr-change-actions>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-js-api-interface tests', function() {
+    var element;
+    var changeActions;
+
+    setup(function() {
+      element = fixture('basic');
+      var plugin;
+      Gerrit.install(function(p) { plugin = p; }, '0.1',
+          'http://test.com/plugins/testplugin/static/test.js');
+      changeActions = plugin.changeActions();
+    });
+
+    teardown(function() {
+      changeActions = null;
+    });
+
+    test('property existence', function() {
+      [
+        'ActionType',
+        'ChangeActions',
+        'RevisionActions',
+      ].forEach(function(p) {
+        assertArraysEqual(changeActions[p], element[p]);
+      });
+    });
+
+    // Because deepEqual doesn’t behave in Safari.
+    function assertArraysEqual(actual, expected) {
+      assert.equal(actual.length, expected.length);
+      for (var i = 0; i < actual.length; i++) {
+        assert.equal(actual[i], expected[i]);
+      }
+    }
+
+    test('add/remove primary action keys', function() {
+      element.primaryActionKeys = [];
+      changeActions.addPrimaryActionKey('foo');
+      assertArraysEqual(element.primaryActionKeys, ['foo']);
+      changeActions.addPrimaryActionKey('foo');
+      assertArraysEqual(element.primaryActionKeys, ['foo']);
+      changeActions.addPrimaryActionKey('bar');
+      assertArraysEqual(element.primaryActionKeys, ['foo', 'bar']);
+      changeActions.removePrimaryActionKey('foo');
+      assertArraysEqual(element.primaryActionKeys, ['bar']);
+      changeActions.removePrimaryActionKey('baz');
+      assertArraysEqual(element.primaryActionKeys, ['bar']);
+      changeActions.removePrimaryActionKey('bar');
+      assertArraysEqual(element.primaryActionKeys, []);
+    });
+
+    test('action buttons', function(done) {
+      var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+      var handler = sinon.spy();
+      changeActions.addTapListener(key, handler);
+      flush(function() {
+        MockInteractions.tap(element.$$('[data-action-key="' + key + '"]'));
+        assert(handler.calledOnce);
+        changeActions.removeTapListener(key, handler);
+        MockInteractions.tap(element.$$('[data-action-key="' + key + '"]'));
+        assert(handler.calledOnce);
+        changeActions.remove(key);
+        flush(function() {
+          assert.isNull(element.$$('[data-action-key="' + key + '"]'));
+          done();
+        });
+      });
+    });
+
+    test('action button properties', function(done) {
+      var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+      flush(function() {
+        var button = element.$$('[data-action-key="' + key + '"]');
+        assert.isOk(button);
+        assert.equal(button.getAttribute('data-label'), 'Bork!');
+        assert.isFalse(button.disabled);
+        changeActions.setLabel(key, 'Yo');
+        changeActions.setEnabled(key, false);
+        flush(function() {
+          assert.equal(button.getAttribute('data-label'), 'Yo');
+          assert.isTrue(button.disabled);
+          done();
+        });
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
new file mode 100644
index 0000000..9d6b83b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
@@ -0,0 +1,30 @@
+// 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.
+(function(window) {
+  'use strict';
+
+  function GrChangeReplyInterface(el) {
+    this._el = el;
+  }
+
+  GrChangeReplyInterface.prototype.setLabelValue = function(label, value) {
+    this._el.setLabelValue(label, value);
+  };
+
+  GrChangeReplyInterface.prototype.send = function() {
+    return this._el.send();
+  };
+
+  window.GrChangeReplyInterface = GrChangeReplyInterface;
+})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
new file mode 100644
index 0000000..2e5aa56
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-change-reply-js-api</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<!--
+This must refer to the element this interface is wrapping around. Otherwise
+breaking changes to gr-reply-dialog won’t be noticed.
+-->
+<link rel="import" href="../../change/gr-reply-dialog/gr-reply-dialog.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-reply-dialog></gr-reply-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-change-reply-js-api tests', function() {
+    var element;
+    var sandbox;
+    var changeReply;
+
+    setup(function() {
+      stub('gr-rest-api-interface', {
+        getAccount: function() { return Promise.resolve(null); },
+      });
+      element = fixture('basic');
+      sandbox = sinon.sandbox.create();
+      var plugin;
+      Gerrit.install(function(p) { plugin = p; }, '0.1',
+          'http://test.com/plugins/testplugin/static/test.js');
+      changeReply = plugin.changeReply();
+    });
+
+    teardown(function() {
+      changeReply = null;
+      sandbox.restore();
+    });
+
+    test('calls', function() {
+      var setLabelValueStub = sinon.stub(element, 'setLabelValue');
+      changeReply.setLabelValue('My-Label', '+1337');
+      assert(setLabelValueStub.calledWithExactly('My-Label', '+1337'));
+
+      var sendStub = sinon.stub(element, 'send');
+      changeReply.send();
+      assert(sendStub.calledWithExactly());
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index a3b489d..1967b80 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -14,9 +14,12 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-js-api-interface">
   <template></template>
+  <script src="gr-change-actions-js-api.js"></script>
+  <script src="gr-change-reply-js-api.js"></script>
   <script src="gr-js-api-interface.js"></script>
   <script src="gr-public-js-api.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index cf4edf3..bb37085 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -16,6 +16,7 @@
 
   var EventType = {
     HISTORY: 'history',
+    LABEL_CHANGE: 'labelchange',
     SHOW_CHANGE: 'showchange',
     SUBMIT_CHANGE: 'submitchange',
     COMMENT: 'comment',
@@ -23,6 +24,7 @@
 
   var Element = {
     CHANGE_ACTIONS: 'changeactions',
+    REPLY_DIALOG: 'replydialog',
   };
 
   Polymer({
@@ -53,6 +55,9 @@
         case EventType.COMMENT:
           this._handleComment(detail);
           break;
+        case EventType.LABEL_CHANGE:
+          this._handleLabelChange(detail);
+          break;
         default:
           console.warn('handleEvent called with unsupported event type:', type);
           break;
@@ -133,6 +138,16 @@
       });
     },
 
+    _handleLabelChange: function(detail) {
+      this._getEventCallbacks(EventType.LABEL_CHANGE).forEach(function(cb) {
+        try {
+          cb(detail.change);
+        } catch (err) {
+          console.error(err);
+        }
+      });
+    },
+
     _getEventCallbacks: function(type) {
       return this._eventCallbacks[type] || [];
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 3be232e..5936bee 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -38,9 +38,14 @@
     };
 
     setup(function() {
+      stub('gr-rest-api-interface', {
+        getAccount: function() {
+          return Promise.resolve({name: 'Judy Hopps'});
+        },
+      })
       element = fixture('basic');
       errorStub = sinon.stub(console, 'error');
-      Gerrit.install(function(p) { plugin = p; },
+      Gerrit.install(function(p) { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
     });
 
@@ -97,6 +102,17 @@
       element.handleEvent(element.EventType.COMMENT, {node: testCommentNode});
     });
 
+    test('labelchange event', function(done) {
+      var testChange = {_number: 42};
+      plugin.on(element.EventType.LABEL_CHANGE, throwErrFn);
+      plugin.on(element.EventType.LABEL_CHANGE, function(change) {
+        assert.deepEqual(change, testChange);
+        assert.isTrue(errorStub.calledOnce);
+        done();
+      });
+      element.handleEvent(element.EventType.LABEL_CHANGE, {change: testChange});
+    });
+
     test('submitchange', function() {
       plugin.on(element.EventType.SUBMIT_CHANGE, throwErrFn);
       plugin.on(element.EventType.SUBMIT_CHANGE, function() { return true; });
@@ -108,5 +124,18 @@
       assert.isTrue(errorStub.calledTwice);
     });
 
+    test('versioning', function() {
+      var callback = sinon.spy();
+      Gerrit.install(callback, '0.0pre-alpha');
+      assert(callback.notCalled);
+    });
+
+    test('getAccount', function(done) {
+      Gerrit.getLoggedIn().then(function(loggedIn) {
+        assert.isTrue(loggedIn);
+        done();
+      });
+    });
+
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 578f44d..21d76f1 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -14,11 +14,19 @@
 (function(window) {
   'use strict';
 
+  var API_VERSION = '0.1';
+
   // GWT JSNI uses $wnd to refer to window.
   // http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html
   window.$wnd = window;
 
   function Plugin(opt_url) {
+    if (!opt_url) {
+      console.warn('Plugin not being loaded from /plugins base path.',
+          'Unable to determine name.');
+      return;
+    }
+
     this._url = new URL(opt_url);
     if (this._url.pathname.indexOf('/plugins') !== 0) {
       console.warn('Plugin not being loaded from /plugins base path:',
@@ -44,9 +52,14 @@
     return this._url.origin + '/plugins/' + this._name + (opt_path || '/');
   };
 
-  Plugin.prototype.getChangeActionsElement = function() {
-    return Plugin._sharedAPIElement.getElement(
-        Plugin._sharedAPIElement.Element.CHANGE_ACTIONS);
+  Plugin.prototype.changeActions = function() {
+    return new GrChangeActionsInterface(Plugin._sharedAPIElement.getElement(
+        Plugin._sharedAPIElement.Element.CHANGE_ACTIONS));
+  };
+
+  Plugin.prototype.changeReply = function() {
+    return new GrChangeReplyInterface(Plugin._sharedAPIElement.getElement(
+        Plugin._sharedAPIElement.Element.REPLY_DIALOG));
   };
 
   var Gerrit = window.Gerrit || {};
@@ -68,12 +81,22 @@
     return name;
   };
 
-  Gerrit.install = function(callback, opt_src) {
+  Gerrit.install = function(callback, opt_version, opt_src) {
+    if (opt_version && opt_version !== API_VERSION) {
+      console.warn('Only version ' + API_VERSION +
+          ' is supported in PolyGerrit. ' + opt_version + ' was given.');
+      return;
+    }
+
     // TODO(andybons): Polyfill currentScript for IE10/11 (edge supports it).
     var src = opt_src || (document.currentScript && document.currentScript.src);
     callback(new Plugin(src));
   };
 
+  Gerrit.getLoggedIn = function() {
+    return document.createElement('gr-rest-api-interface').getLoggedIn();
+  };
+
   Gerrit.installGwt = function() {
     // NOOP since PolyGerrit doesn’t support GWT plugins.
   };
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 98f7eef..153db20 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -75,6 +75,8 @@
     'shared/gr-date-formatter/gr-date-formatter_test.html',
     'shared/gr-editable-content/gr-editable-content_test.html',
     'shared/gr-editable-label/gr-editable-label_test.html',
+    'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
+    'shared/gr-js-api-interface/gr-change-reply-js-api_test.html',
     'shared/gr-js-api-interface/gr-js-api-interface_test.html',
     'shared/gr-linked-text/gr-linked-text_test.html',
     'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',