Merge "Fix: Default values for Elastic index.* config values"
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 39fa333..232e402 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -30,7 +30,8 @@
   AutoAnnotation_Commands_named cannot be resolved to a type
 ----
 
-In Eclipse, choose 'Import existing project' and select the `gerrit` project
+First, generate the Eclipse project by running the `tools/eclipse/project.py` script.
+Then, in Eclipse, choose 'Import existing project' and select the `gerrit` project
 from the current working directory.
 
 Expand the `gerrit` project, right-click on the `eclipse-out` folder, select
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index b6d2c53..04adcbd 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -736,6 +736,68 @@
   }
 ----
 
+[[query_attributes]]
+=== Query Attributes ===
+
+Plugins can provide additional attributes to be returned in Gerrit queries by
+implementing the ChangeAttributeFactory interface and registering it to the
+ChangeQueryProcessor.ChangeAttributeFactory class in the plugin module's
+'configure()' method. The new attribute(s) will be output under a "plugin"
+attribute in the change query output.
+
+The example below shows a plugin that adds two attributes ('exampleName' and
+'changeValue'), to the change query output.
+
+[source, java]
+----
+public class Module extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(ChangeAttributeFactory.class)
+        .annotatedWith(Exports.named("example"))
+        .to(AttributeFactory.class);
+  }
+}
+
+public class AttributeFactory implements ChangeAttributeFactory {
+
+  public class PluginAttribute extends PluginDefinedInfo {
+    public String exampleName;
+    public String changeValue;
+
+    public PluginAttribute(ChangeData c) {
+      this.exampleName = "Attribute Example";
+      this.changeValue = Integer.toString(c.getId().get());
+    }
+  }
+
+  @Override
+  public PluginDefinedInfo create(ChangeData c, ChangeQueryProcessor qp, String plugin) {
+    return new PluginAttribute(c);
+  }
+}
+----
+
+Example
+----
+
+ssh -p 29418 localhost gerrit query "change:1" --format json
+
+Output:
+
+{
+   "url" : "http://localhost:8080/1",
+   "plugins" : [
+      {
+         "name" : "myplugin-name",
+         "exampleName" : "Attribute Example",
+         "changeValue" : "1"
+      }
+   ],
+    ...
+}
+----
+
 [[simple-configuration]]
 == Simple Configuration in `gerrit.config`
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index dc97f97..2d417c5 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3422,7 +3422,7 @@
   }
 ----
 
-As response a link:#review-info[ReviewInfo] entity is returned that
+As response a link:#review-result[ReviewResult] entity is returned that
 describes the applied labels.
 
 .Response
@@ -6275,6 +6275,24 @@
 representing reviewers that should be added to the change.
 |============================
 
+[[review-result]]
+=== ReviewResult
+The `ReviewResult` entity contains information regarding the updates
+that were made to a review.
+
+[options="header",cols="1,^1,5"]
+|============================
+|Field Name               ||Description
+|`labels`                 |optional|
+Map of labels to values after the review was posted. Null if any reviewer
+additions were rejected.
+|`reviewers`              |optional|
+Map of account or group identifier to
+link:rest-api-changes.html#add-reviewer-result[AddReviewerResult]
+representing the outcome of adding as a reviewer.
+Absent if no reviewer additions were requested.
+|============================
+
 [[reviewer-info]]
 === ReviewerInfo
 The `ReviewerInfo` entity contains information about a reviewer and its
@@ -6383,6 +6401,9 @@
 patch set as a link:#push-certificate-info[PushCertificateInfo] entity.
 This field is always set if the option is requested; if no push
 certificate was provided, it is set to an empty object.
+|`description` |optional|
+The description of this patchset, as displayed in the patchset
+selector menu. May be null if no description is set.
 |===========================
 
 [[robot-comment-info]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 62e3ee4..fe2025c 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -138,6 +138,51 @@
   }
 ----
 
+[[check-consistency]]
+=== Check Consistency
+--
+'POST /config/server/check'
+--
+
+Runs consistency checks and returns detected problems.
+
+Input for the consistency checks that should be run must be provided in
+the request body inside a
+link:#consistency-check-input[ConsistencyCheckInput] entity.
+
+.Request
+----
+  POST /config/server/check HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "check_account_external_ids": {}
+  }
+----
+
+As result a link:#consistency-check-info[ConsistencyCheckInfo] entity
+is returned that contains detected consistency problems.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "results": {
+      "account_external_id_result": {
+        "problems": [
+          {
+            "status": "ERROR",
+            "message": "External ID \u0027uuid:ccb8d323-1361-45aa-8874-41987a660c46\u0027 belongs to account that doesn\u0027t exist: 1000012"
+          }
+        ]
+      }
+    }
+  }
+----
+
 [[confirm-email]]
 === Confirm Email
 --
@@ -1365,6 +1410,66 @@
 the whole topic is submitted].
 |=============================
 
+[[check-account-external-ids-input]]
+=== CheckAccountExternalIdsInput
+The `CheckAccountExternalIdsInput` entity contains input for the
+account external IDs consistency check.
+
+Currently this entity contains no fields.
+
+[[check-account-external-ids-result-info]]
+=== CheckAccountExternalIdsResultInfo
+The `CheckAccountExternalIdsResultInfo` entity contains the result of
+running the account external IDs consistency check.
+
+[options="header",cols="1,6"]
+|======================
+|Field Name|Description
+|`problems`|A list of link:#consistency-problem-info[
+ConsistencyProblemInfo] entities.
+|======================
+
+[[consistency-check-info]]
+=== ConsistencyCheckInfo
+The `ConsistencyCheckInfo` entity contains the results of running
+consistency checks.
+
+[options="header",cols="1,^1,5"]
+|================================================
+|Field Name                         ||Description
+|`check_account_external_ids_result`|optional|
+The result of running the account external ID consistency check as a
+link:#check-account-external-ids-result-info[
+CheckAccountExternalIdsResultInfo] entity.
+|================================================
+
+[[consistency-check-input]]
+=== ConsistencyCheckInput
+The `ConsistencyCheckInput` entity contains information about which
+consistency checks should be run.
+
+[options="header",cols="1,^1,5"]
+|=========================================
+|Field Name                  ||Description
+|`check_account_external_ids`|optional|
+Input for the account external ID consistency check as
+link:#check-account-external-ids-input[CheckAccountExternalIdsInput]
+entity.
+|=========================================
+
+[[consistency-problem-info]]
+=== ConsistencyProblemInfo
+The `ConsistencyProblemInfo` entity contains information about a
+consistency problem.
+
+[options="header",cols="1,6"]
+|======================
+|Field Name|Description
+|`status`  |The status of the consistency problem. +
+Possible values are `ERROR` and `WARNING`.
+|`message` |Message describing the consistency problem.
+|======================
+
 [[download-info]]
 === DownloadInfo
 The `DownloadInfo` entity contains information about supported download
diff --git a/WORKSPACE b/WORKSPACE
index 0ccbe3d..2e95e9a 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -409,8 +409,8 @@
 
 maven_jar(
     name = "auto_value",
-    artifact = "com.google.auto.value:auto-value:1.4",
-    sha1 = "6d1448fcd13074bd3658ef915022410b7c48343b",
+    artifact = "com.google.auto.value:auto-value:1.4.1",
+    sha1 = "8172ebbd7970188aff304c8a420b9f17168f6f48",
 )
 
 maven_jar(
@@ -701,9 +701,10 @@
     sha1 = "2862787ce34cb6f385ada891e36ec7f9e7bd0902",
 )
 
+# When bumping the easymock version number, make sure to also move powermock to a compatible version
 maven_jar(
     name = "easymock",
-    artifact = "org.easymock:easymock:3.1",  # When bumping the version
+    artifact = "org.easymock:easymock:3.1",
     sha1 = "3e127311a86fc2e8f550ef8ee4abe094bbcf7e7e",
 )
 
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
index f62c767..44e7e0e 100755
--- a/contrib/abandon_stale.py
+++ b/contrib/abandon_stale.py
@@ -59,7 +59,11 @@
                       help='gerrit server URL')
     parser.add_option('-b', '--basic-auth', dest='basic_auth',
                       action='store_true',
-                      help='use HTTP basic authentication instead of digest')
+                      help='(deprecated) use HTTP basic authentication instead'
+                      ' of digest')
+    parser.add_option('-d', '--digest-auth', dest='digest_auth',
+                      action='store_true',
+                      help='use HTTP digest authentication instead of basic')
     parser.add_option('-n', '--dry-run', dest='dry_run',
                       action='store_true',
                       help='enable dry-run mode: show stale changes but do '
@@ -115,10 +119,10 @@
     message = "Abandoning after %s %s or more of inactivity." % \
         (match.group(1), match.group(2))
 
-    if options.basic_auth:
-        auth_type = HTTPBasicAuthFromNetrc
-    else:
+    if options.digest_auth:
         auth_type = HTTPDigestAuthFromNetrc
+    else:
+        auth_type = HTTPBasicAuthFromNetrc
 
     try:
         auth = auth_type(url=options.gerrit_url)
diff --git a/contrib/populate-fixture-data.py b/contrib/populate-fixture-data.py
index b77c41a..0e3dffe 100644
--- a/contrib/populate-fixture-data.py
+++ b/contrib/populate-fixture-data.py
@@ -46,7 +46,7 @@
 PLUGINS_URL = BASE_URL + "plugins/"
 PROJECTS_URL = BASE_URL + "projects/"
 
-ADMIN_DIGEST = requests.auth.HTTPDigestAuth("admin", "secret")
+ADMIN_BASIC_AUTH = requests.auth.HTTPBasicAuth("admin", "secret")
 
 # GROUP_ADMIN stores a GroupInfo for the admin group (see Gerrit rest docs)
 # In addition, GROUP_ADMIN["name"] stores the admin group"s name.
@@ -151,8 +151,8 @@
   return json_string
 
 
-def digest_auth(user):
-  return requests.auth.HTTPDigestAuth(user["username"], user["http_password"])
+def basic_auth(user):
+  return requests.auth.HTTPBasicAuth(user["username"], user["http_password"])
 
 
 def fetch_admin_group():
@@ -160,7 +160,7 @@
   # Get admin group
   r = json.loads(clean(requests.get(GROUPS_URL + "?suggest=ad&p=All-Projects",
                                     headers=HEADERS,
-                                    auth=ADMIN_DIGEST).text))
+                                    auth=ADMIN_BASIC_AUTH).text))
   admin_group_name = r.keys()[0]
   GROUP_ADMIN = r[admin_group_name]
   GROUP_ADMIN["name"] = admin_group_name
@@ -225,7 +225,7 @@
     requests.put(GROUPS_URL + g["name"],
                  json.dumps(g),
                  headers=HEADERS,
-                 auth=ADMIN_DIGEST)
+                 auth=ADMIN_BASIC_AUTH)
   return [g["name"] for g in groups]
 
 
@@ -247,7 +247,7 @@
     requests.put(PROJECTS_URL + p["name"],
                  json.dumps(p),
                  headers=HEADERS,
-                 auth=ADMIN_DIGEST)
+                 auth=ADMIN_BASIC_AUTH)
   return [p["name"] for p in projects]
 
 
@@ -256,7 +256,7 @@
     requests.put(ACCOUNTS_URL + user["username"],
                  json.dumps(user),
                  headers=HEADERS,
-                 auth=ADMIN_DIGEST)
+                 auth=ADMIN_BASIC_AUTH)
 
 
 def create_change(user, project_name):
@@ -270,7 +270,7 @@
   requests.post(CHANGES_URL,
                 json.dumps(change),
                 headers=HEADERS,
-                auth=digest_auth(user))
+                auth=basic_auth(user))
 
 
 def clean_up():
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
index 246b1f9..152a003 100644
--- a/gerrit-acceptance-framework/pom.xml
+++ b/gerrit-acceptance-framework/pom.xml
@@ -26,6 +26,9 @@
       <name>Andrew Bonventre</name>
     </developer>
     <developer>
+      <name>Becky Siegel</name>
+    </developer>
+    <developer>
       <name>Dave Borowitz</name>
     </developer>
     <developer>
@@ -41,6 +44,12 @@
       <name>Hugo Arès</name>
     </developer>
     <developer>
+      <name>Kasper Nilsson</name>
+    </developer>
+    <developer>
+      <name>Logan Hanks</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index cef6128..711b8cf 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -53,7 +53,7 @@
   private final Map<String, TestAccount> accounts;
 
   private final SchemaFactory<ReviewDb> reviewDbProvider;
-  private final AccountsUpdate accountsUpdate;
+  private final AccountsUpdate.Server accountsUpdate;
   private final VersionedAuthorizedKeys.Accessor authorizedKeys;
   private final GroupCache groupCache;
   private final SshKeyCache sshKeyCache;
@@ -65,7 +65,7 @@
   @Inject
   AccountCreator(
       SchemaFactory<ReviewDb> schema,
-      AccountsUpdate accountsUpdate,
+      AccountsUpdate.Server accountsUpdate,
       VersionedAuthorizedKeys.Accessor authorizedKeys,
       GroupCache groupCache,
       SshKeyCache sshKeyCache,
@@ -114,7 +114,7 @@
       Account a = new Account(id, TimeUtil.nowTs());
       a.setFullName(fullName);
       a.setPreferredEmail(email);
-      accountsUpdate.insert(db, a);
+      accountsUpdate.create().insert(db, a);
 
       if (groups != null) {
         for (String n : groups) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 89585c3..079c666 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -51,7 +51,6 @@
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
@@ -74,6 +73,7 @@
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.testutil.ConfigSuite;
@@ -100,6 +100,8 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PushCertificateIdent;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
@@ -184,6 +186,26 @@
   }
 
   @Test
+  public void create() throws Exception {
+    TestAccount foo = accounts.create("foo");
+    AccountInfo info = gApi.accounts().id(foo.id.get()).get();
+    assertThat(info.username).isEqualTo("foo");
+
+    // check user branch
+    try (Repository repo = repoManager.openRepository(allUsers);
+        RevWalk rw = new RevWalk(repo)) {
+      Ref ref = repo.exactRef(RefNames.refsUsers(foo.getId()));
+      assertThat(ref).isNotNull();
+      RevCommit c = rw.parseCommit(ref.getObjectId());
+      long timestampDiffMs =
+          Math.abs(
+              c.getCommitTime() * 1000L
+                  - accountCache.get(foo.getId()).getAccount().getRegisteredOn().getTime());
+      assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
+    }
+  }
+
+  @Test
   public void get() throws Exception {
     AccountInfo info = gApi.accounts().id("admin").get();
     assertThat(info.name).isEqualTo("Administrator");
@@ -511,7 +533,7 @@
 
     // user cannot delete email of admin
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to delete email address");
+    exception.expectMessage("modify account not permitted");
     gApi.accounts().id(admin.id.get()).deleteEmail(admin.email);
   }
 
@@ -553,7 +575,7 @@
   @Test
   @Sandboxed
   public void fetchUserBranch() throws Exception {
-    ensureUserBranchCreated(user);
+    setApiUser(user);
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
     String userRefName = RefNames.refsUsers(user.id);
@@ -603,8 +625,6 @@
 
   @Test
   public void pushToUserBranch() throws Exception {
-    ensureUserBranchCreated(admin);
-
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
     allUsersRepo.reset("userRef");
@@ -617,8 +637,6 @@
 
   @Test
   public void pushToUserBranchForReview() throws Exception {
-    ensureUserBranchCreated(admin);
-
     String userRefName = RefNames.refsUsers(admin.id);
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, userRefName + ":userRef");
@@ -640,8 +658,6 @@
 
   @Test
   public void pushWatchConfigToUserBranch() throws Exception {
-    ensureUserBranchCreated(admin);
-
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
     allUsersRepo.reset("userRef");
@@ -683,8 +699,6 @@
   @Test
   @Sandboxed
   public void cannotDeleteUserBranch() throws Exception {
-    ensureUserBranchCreated(admin);
-
     grant(
         Permission.DELETE,
         allUsers,
@@ -707,8 +721,6 @@
   @Test
   @Sandboxed
   public void deleteUserBranchWithAccessDatabaseCapability() throws Exception {
-    ensureUserBranchCreated(admin);
-
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     grant(
         Permission.DELETE,
@@ -896,7 +908,7 @@
 
     // user cannot reindex any account
     exception.expect(AuthException.class);
-    exception.expectMessage("not allowed to index account");
+    exception.expectMessage("modify account not permitted");
     gApi.accounts().id(admin.username).index();
   }
 
@@ -1019,12 +1031,4 @@
     assertThat(accounts).hasSize(1);
     assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId());
   }
-
-  private void ensureUserBranchCreated(TestAccount account) throws Exception {
-    // Change something in the user preferences to ensure that the user branch is created.
-    setApiUser(account);
-    GeneralPreferencesInfo input = new GeneralPreferencesInfo();
-    input.changesPerPage = GeneralPreferencesInfo.defaults().changesPerPage + 10;
-    gApi.accounts().self().setPreferences(input);
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index d5d4620..f6c70b0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -19,6 +19,8 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.junit.Assert.fail;
 
 import com.github.rholder.retry.BlockStrategy;
@@ -33,7 +35,12 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInput.CheckAccountExternalIdsInput;
 import com.google.gerrit.extensions.common.AccountExternalIdInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -54,6 +61,7 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -61,11 +69,13 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 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.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.MutableInteger;
 import org.junit.Test;
 
 @Sandboxed
@@ -199,6 +209,277 @@
   }
 
   @Test
+  @GerritConfig(name = "user.readExternalIdsFromGit", value = "true")
+  public void readExternalIdsWhenInvalidExternalIdsExist() throws Exception {
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    resetCurrentApiUser();
+
+    insertValidExternalIds();
+    insertInvalidButParsableExternalIds();
+
+    Set<ExternalId> parseableExtIds = externalIds.all(db);
+
+    insertNonParsableExternalIds();
+
+    Set<ExternalId> extIds = externalIds.all(db);
+    assertThat(extIds).containsExactlyElementsIn(parseableExtIds);
+
+    for (ExternalId parseableExtId : parseableExtIds) {
+      ExternalId extId = externalIds.get(db, parseableExtId.key());
+      assertThat(extId).isEqualTo(parseableExtId);
+    }
+  }
+
+  @Test
+  public void checkConsistency() throws Exception {
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    resetCurrentApiUser();
+
+    insertValidExternalIds();
+
+    ConsistencyCheckInput input = new ConsistencyCheckInput();
+    input.checkAccountExternalIds = new CheckAccountExternalIdsInput();
+    ConsistencyCheckInfo checkInfo = gApi.config().server().checkConsistency(input);
+    assertThat(checkInfo.checkAccountExternalIdsResult.problems).isEmpty();
+
+    Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>();
+    expectedProblems.addAll(insertInvalidButParsableExternalIds());
+    expectedProblems.addAll(insertNonParsableExternalIds());
+
+    checkInfo = gApi.config().server().checkConsistency(input);
+    assertThat(checkInfo.checkAccountExternalIdsResult.problems).hasSize(expectedProblems.size());
+    assertThat(checkInfo.checkAccountExternalIdsResult.problems)
+        .containsExactlyElementsIn(expectedProblems);
+  }
+
+  @Test
+  public void checkConsistencyNotAllowed() throws Exception {
+    exception.expect(AuthException.class);
+    exception.expectMessage("not allowed to run consistency checks");
+    gApi.config().server().checkConsistency(new ConsistencyCheckInput());
+  }
+
+  private ConsistencyProblemInfo consistencyError(String message) {
+    return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, message);
+  }
+
+  private void insertValidExternalIds() throws IOException, ConfigInvalidException, OrmException {
+    MutableInteger i = new MutableInteger();
+    String scheme = "valid";
+    ExternalIdsUpdate u = extIdsUpdate.create();
+
+    // create valid external IDs
+    u.insert(
+        db,
+        ExternalId.createWithPassword(
+            ExternalId.Key.parse(nextId(scheme, i)),
+            admin.id,
+            "admin.other@example.com",
+            "secret-password"));
+    u.insert(db, createExternalIdWithOtherCaseEmail(nextId(scheme, i)));
+  }
+
+  private Set<ConsistencyProblemInfo> insertInvalidButParsableExternalIds()
+      throws IOException, ConfigInvalidException, OrmException {
+    MutableInteger i = new MutableInteger();
+    String scheme = "invalid";
+    ExternalIdsUpdate u = extIdsUpdate.create();
+
+    Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>();
+    ExternalId extIdForNonExistingAccount =
+        createExternalIdForNonExistingAccount(nextId(scheme, i));
+    u.insert(db, extIdForNonExistingAccount);
+    expectedProblems.add(
+        consistencyError(
+            "External ID '"
+                + extIdForNonExistingAccount.key().get()
+                + "' belongs to account that doesn't exist: "
+                + extIdForNonExistingAccount.accountId().get()));
+
+    ExternalId extIdWithInvalidEmail = createExternalIdWithInvalidEmail(nextId(scheme, i));
+    u.insert(db, extIdWithInvalidEmail);
+    expectedProblems.add(
+        consistencyError(
+            "External ID '"
+                + extIdWithInvalidEmail.key().get()
+                + "' has an invalid email: "
+                + extIdWithInvalidEmail.email()));
+
+    ExternalId extIdWithDuplicateEmail = createExternalIdWithDuplicateEmail(nextId(scheme, i));
+    u.insert(db, extIdWithDuplicateEmail);
+    expectedProblems.add(
+        consistencyError(
+            "Email '"
+                + extIdWithDuplicateEmail.email()
+                + "' is not unique, it's used by the following external IDs: '"
+                + extIdWithDuplicateEmail.key().get()
+                + "', 'mailto:"
+                + extIdWithDuplicateEmail.email()
+                + "'"));
+
+    ExternalId extIdWithBadPassword = createExternalIdWithBadPassword("admin-username");
+    u.insert(db, extIdWithBadPassword);
+    expectedProblems.add(
+        consistencyError(
+            "External ID '"
+                + extIdWithBadPassword.key().get()
+                + "' has an invalid password: unrecognized algorithm"));
+
+    return expectedProblems;
+  }
+
+  private Set<ConsistencyProblemInfo> insertNonParsableExternalIds() throws IOException {
+    MutableInteger i = new MutableInteger();
+    String scheme = "corrupt";
+
+    Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>();
+    try (Repository repo = repoManager.openRepository(allUsers);
+        RevWalk rw = new RevWalk(repo)) {
+      String externalId = nextId(scheme, i);
+      String noteId = insertExternalIdWithoutAccountId(repo, rw, externalId);
+      expectedProblems.add(
+          consistencyError(
+              "Invalid external ID config for note '"
+                  + noteId
+                  + "': Value for 'externalId."
+                  + externalId
+                  + ".accountId' is missing, expected account ID"));
+
+      externalId = nextId(scheme, i);
+      noteId = insertExternalIdWithKeyThatDoesntMatchNoteId(repo, rw, externalId);
+      expectedProblems.add(
+          consistencyError(
+              "Invalid external ID config for note '"
+                  + noteId
+                  + "': SHA1 of external ID '"
+                  + externalId
+                  + "' does not match note ID '"
+                  + noteId
+                  + "'"));
+
+      noteId = insertExternalIdWithInvalidConfig(repo, rw, nextId(scheme, i));
+      expectedProblems.add(
+          consistencyError(
+              "Invalid external ID config for note '" + noteId + "': Invalid line in config file"));
+
+      noteId = insertExternalIdWithEmptyNote(repo, rw, nextId(scheme, i));
+      expectedProblems.add(
+          consistencyError(
+              "Invalid external ID config for note '"
+                  + noteId
+                  + "': Expected exactly 1 'externalId' section, found 0"));
+    }
+
+    return expectedProblems;
+  }
+
+  private ExternalId createExternalIdWithOtherCaseEmail(String externalId) {
+    return ExternalId.createWithPassword(
+        ExternalId.Key.parse(externalId), admin.id, admin.email.toUpperCase(Locale.US), "password");
+  }
+
+  private String insertExternalIdWithoutAccountId(Repository repo, RevWalk rw, String externalId)
+      throws IOException {
+    ObjectId rev = ExternalIdReader.readRevision(repo);
+    NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
+
+    ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id);
+
+    try (ObjectInserter ins = repo.newObjectInserter()) {
+      ObjectId noteId = extId.key().sha1();
+      Config c = new Config();
+      extId.writeToConfig(c);
+      c.unset("externalId", extId.key().get(), "accountId");
+      byte[] raw = c.toText().getBytes(UTF_8);
+      ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
+      noteMap.set(noteId, dataBlob);
+
+      ExternalIdsUpdate.commit(
+          repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent());
+      return noteId.getName();
+    }
+  }
+
+  private String insertExternalIdWithKeyThatDoesntMatchNoteId(
+      Repository repo, RevWalk rw, String externalId) throws IOException {
+    ObjectId rev = ExternalIdReader.readRevision(repo);
+    NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
+
+    ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id);
+
+    try (ObjectInserter ins = repo.newObjectInserter()) {
+      ObjectId noteId = ExternalId.Key.parse(externalId + "x").sha1();
+      Config c = new Config();
+      extId.writeToConfig(c);
+      byte[] raw = c.toText().getBytes(UTF_8);
+      ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
+      noteMap.set(noteId, dataBlob);
+
+      ExternalIdsUpdate.commit(
+          repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent());
+      return noteId.getName();
+    }
+  }
+
+  private String insertExternalIdWithInvalidConfig(Repository repo, RevWalk rw, String externalId)
+      throws IOException {
+    ObjectId rev = ExternalIdReader.readRevision(repo);
+    NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
+
+    try (ObjectInserter ins = repo.newObjectInserter()) {
+      ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
+      byte[] raw = "bad-config".getBytes(UTF_8);
+      ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
+      noteMap.set(noteId, dataBlob);
+
+      ExternalIdsUpdate.commit(
+          repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent());
+      return noteId.getName();
+    }
+  }
+
+  private String insertExternalIdWithEmptyNote(Repository repo, RevWalk rw, String externalId)
+      throws IOException {
+    ObjectId rev = ExternalIdReader.readRevision(repo);
+    NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
+
+    try (ObjectInserter ins = repo.newObjectInserter()) {
+      ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
+      byte[] raw = "".getBytes(UTF_8);
+      ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
+      noteMap.set(noteId, dataBlob);
+
+      ExternalIdsUpdate.commit(
+          repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent());
+      return noteId.getName();
+    }
+  }
+
+  private ExternalId createExternalIdForNonExistingAccount(String externalId) {
+    return ExternalId.create(ExternalId.Key.parse(externalId), new Account.Id(1));
+  }
+
+  private ExternalId createExternalIdWithInvalidEmail(String externalId) {
+    return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id, "invalid-email");
+  }
+
+  private ExternalId createExternalIdWithDuplicateEmail(String externalId) {
+    return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id, admin.email);
+  }
+
+  private ExternalId createExternalIdWithBadPassword(String username) {
+    return ExternalId.create(
+        ExternalId.Key.create(SCHEME_USERNAME, username),
+        admin.id,
+        null,
+        "non-hashed-password-is-not-allowed");
+  }
+
+  private static String nextId(String scheme, MutableInteger i) {
+    return scheme + ":foo" + ++i.value;
+  }
+
+  @Test
   public void retryOnLockFailure() throws Exception {
     Retryer<RefsMetaExternalIdsUpdate> retryer =
         ExternalIdsUpdate.retryerBuilder()
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 846c580..0809bf2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -23,12 +23,17 @@
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewResult;
 import com.google.gerrit.extensions.client.ReviewerState;
@@ -681,6 +686,40 @@
     assertThat(changeLabels.get(crLabel).all).isNull();
   }
 
+  @Test
+  public void notifyDetailsWorkOnPostReview() throws Exception {
+    PushOneCommit.Result r = createChange();
+    TestAccount userToNotify = createAccounts(1, "notify-details-post-review").get(0);
+
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.reviewer(user.email, ReviewerState.REVIEWER, true);
+    reviewInput.notify = NotifyHandling.NONE;
+    reviewInput.notifyDetails =
+        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+
+    sender.clear();
+    gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.emailAddress);
+  }
+
+  @Test
+  public void notifyDetailsWorkOnPostReviewers() throws Exception {
+    PushOneCommit.Result r = createChange();
+    TestAccount userToNotify = createAccounts(1, "notify-details-post-reviewers").get(0);
+
+    AddReviewerInput addReviewer = new AddReviewerInput();
+    addReviewer.reviewer = user.email;
+    addReviewer.notify = NotifyHandling.NONE;
+    addReviewer.notifyDetails =
+        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+
+    sender.clear();
+    gApi.changes().id(r.getChangeId()).addReviewer(addReviewer);
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.emailAddress);
+  }
+
   private AddReviewerResult addReviewer(String changeId, String reviewer) throws Exception {
     return addReviewer(changeId, reviewer, SC_OK);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index f7fe4f1..329716f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -41,6 +41,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ConsistencyChecker;
 import com.google.gerrit.server.change.PatchSetInserter;
@@ -90,6 +91,8 @@
 
   @Inject private Sequences sequences;
 
+  @Inject private AccountsUpdate.Server accountsUpdate;
+
   private RevCommit tip;
   private Account.Id adminId;
   private ConsistencyChecker checker;
@@ -119,7 +122,7 @@
   public void missingOwner() throws Exception {
     TestAccount owner = accounts.create("missing");
     ChangeControl ctl = insertChange(owner);
-    db.accounts().deleteKeys(singleton(owner.getId()));
+    accountsUpdate.create().deleteByKey(db, owner.getId());
 
     assertProblems(ctl, null, problem("Missing change owner: " + owner.getId()));
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index b1efb4a..19fcff9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -83,10 +83,7 @@
     assertThat(userSshSession.hasError()).isTrue();
     String error = userSshSession.getError();
     assertThat(error).isNotNull();
-    assertError(
-        "One of the following capabilities is required to access this"
-            + " resource: [runGC, maintainServer]",
-        error);
+    assertError("maintain server not permitted", error);
   }
 
   @Test
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
index 566e159..53e7c19 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -68,6 +68,6 @@
   @Provides
   @Singleton
   IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
-    return IndexConfig.fromConfig(cfg);
+    return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build();
   }
 }
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 45b6f8b..2c884be 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -26,6 +26,9 @@
       <name>Andrew Bonventre</name>
     </developer>
     <developer>
+      <name>Becky Siegel</name>
+    </developer>
+    <developer>
       <name>Dave Borowitz</name>
     </developer>
     <developer>
@@ -41,6 +44,12 @@
       <name>Hugo Arès</name>
     </developer>
     <developer>
+      <name>Kasper Nilsson</name>
+    </developer>
+    <developer>
+      <name>Logan Hanks</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java
new file mode 100644
index 0000000..e3b7dd3
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.config;
+
+import java.util.List;
+import java.util.Objects;
+
+public class ConsistencyCheckInfo {
+  public CheckAccountExternalIdsResultInfo checkAccountExternalIdsResult;
+
+  public static class CheckAccountExternalIdsResultInfo {
+    public List<ConsistencyProblemInfo> problems;
+
+    public CheckAccountExternalIdsResultInfo(List<ConsistencyProblemInfo> problems) {
+      this.problems = problems;
+    }
+  }
+
+  public static class ConsistencyProblemInfo {
+    public enum Status {
+      ERROR,
+      WARNING,
+    }
+
+    public final Status status;
+    public final String message;
+
+    public ConsistencyProblemInfo(Status status, String message) {
+      this.status = status;
+      this.message = message;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o instanceof ConsistencyProblemInfo) {
+        ConsistencyProblemInfo other = ((ConsistencyProblemInfo) o);
+        return Objects.equals(status, other.status) && Objects.equals(message, other.message);
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(status, message);
+    }
+
+    @Override
+    public String toString() {
+      return status.name() + ": " + message;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInput.java
new file mode 100644
index 0000000..170db0f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInput.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.config;
+
+public class ConsistencyCheckInput {
+  public CheckAccountExternalIdsInput checkAccountExternalIds;
+
+  public static class CheckAccountExternalIdsInput {}
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
index 97f4af0..ee0960c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
@@ -34,6 +34,8 @@
 
   DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in) throws RestApiException;
 
+  ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -68,5 +70,10 @@
     public DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in) {
       throw new NotImplementedException();
     }
+
+    @Override
+    public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index e13962d..2cb8384 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -63,4 +63,5 @@
   public Boolean _moreChanges;
 
   public List<ProblemInfo> problems;
+  public List<PluginDefinedInfo> plugins;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java
new file mode 100644
index 0000000..e6fef0f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class PluginDefinedInfo {
+  public String name;
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java
index 4ac3da7..b55cf6c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java
@@ -47,7 +47,7 @@
             .newRenderer("com.google.gerrit.httpd.raw.Index")
             .setContentKind(SanitizedContent.ContentKind.HTML)
             .setData(getTemplateData(canonicalURL, cdnPath));
-    indexSource = renderer.render().getBytes();
+    indexSource = renderer.render().getBytes(UTF_8);
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 6dec9a4..6960fae 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -36,6 +36,7 @@
 import com.google.gson.JsonPrimitive;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
+import com.google.inject.Injector;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.util.HashSet;
@@ -51,11 +52,16 @@
       ImmutableSet.of("pp", "prettyPrint", "strict", "callback", "alt", "fields");
 
   private final CmdLineParser.Factory parserFactory;
+  private final Injector injector;
   private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
 
   @Inject
-  ParameterParser(CmdLineParser.Factory pf, DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
+  ParameterParser(
+      CmdLineParser.Factory pf,
+      Injector injector,
+      DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
     this.parserFactory = pf;
+    this.injector = injector;
     this.dynamicBeans = dynamicBeans;
   }
 
@@ -63,7 +69,7 @@
       T param, ListMultimap<String, String> in, HttpServletRequest req, HttpServletResponse res)
       throws IOException {
     CmdLineParser clp = parserFactory.create(param);
-    DynamicOptions pluginOptions = new DynamicOptions(param, dynamicBeans);
+    DynamicOptions pluginOptions = new DynamicOptions(param, injector, dynamicBeans);
     pluginOptions.parseDynamicBeans(clp);
     pluginOptions.setDynamicBeans();
     pluginOptions.onBeanParseStart();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index abf5323..3385bf2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -95,8 +95,10 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OptionUtil;
 import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.account.CapabilityUtils;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.util.http.RequestUtil;
 import com.google.gson.ExclusionStrategy;
 import com.google.gson.FieldAttributes;
@@ -188,6 +190,7 @@
     final Provider<CurrentUser> currentUser;
     final DynamicItem<WebSession> webSession;
     final Provider<ParameterParser> paramParser;
+    final PermissionBackend permissionBackend;
     final AuditService auditService;
     final RestApiMetrics metrics;
     final Pattern allowOrigin;
@@ -197,12 +200,14 @@
         Provider<CurrentUser> currentUser,
         DynamicItem<WebSession> webSession,
         Provider<ParameterParser> paramParser,
+        PermissionBackend permissionBackend,
         AuditService auditService,
         RestApiMetrics metrics,
         @GerritServerConfig Config cfg) {
       this.currentUser = currentUser;
       this.webSession = webSession;
       this.paramParser = paramParser;
+      this.permissionBackend = permissionBackend;
       this.auditService = auditService;
       this.metrics = metrics;
       allowOrigin = makeAllowOrigin(cfg);
@@ -263,7 +268,10 @@
 
       List<IdString> path = splitPath(req);
       RestCollection<RestResource, RestResource> rc = members.get();
-      CapabilityUtils.checkRequiresCapability(globals.currentUser, null, rc.getClass());
+      globals
+          .permissionBackend
+          .user(globals.currentUser)
+          .checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
 
       viewData = new ViewData(null, null);
 
@@ -1106,9 +1114,12 @@
     return "GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod());
   }
 
-  private void checkRequiresCapability(ViewData viewData) throws AuthException {
-    CapabilityUtils.checkRequiresCapability(
-        globals.currentUser, viewData.pluginName, viewData.view.getClass());
+  private void checkRequiresCapability(ViewData d)
+      throws AuthException, PermissionBackendException {
+    globals
+        .permissionBackend
+        .user(globals.currentUser)
+        .checkAny(GlobalPermission.fromAnnotation(d.pluginName, d.view.getClass()));
   }
 
   private static long handleException(
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 699fd51..89fd819 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -84,7 +84,7 @@
   IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
     BooleanQuery.setMaxClauseCount(
         cfg.getInt("index", "maxTerms", BooleanQuery.getMaxClauseCount()));
-    return IndexConfig.fromConfig(cfg);
+    return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build();
   }
 
   private static class MultiVersionModule extends LifecycleModule {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
new file mode 100644
index 0000000..09626d7
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
+
+public class AccountsOnInit {
+  private final InitFlags flags;
+  private final SitePaths site;
+  private final String allUsers;
+
+  @Inject
+  public AccountsOnInit(InitFlags flags, SitePaths site, AllUsersNameOnInitProvider allUsers) {
+    this.flags = flags;
+    this.site = site;
+    this.allUsers = allUsers.get();
+  }
+
+  public void insert(ReviewDb db, Account account) throws OrmException, IOException {
+    db.accounts().insert(ImmutableSet.of(account));
+
+    File path = getPath();
+    if (path != null) {
+      try (Repository repo = new FileRepository(path);
+          ObjectInserter oi = repo.newObjectInserter()) {
+        PersonIdent serverIdent = new GerritPersonIdentProvider(flags.cfg).get();
+        AccountsUpdate.createUserBranch(repo, oi, serverIdent, serverIdent, account);
+      }
+    }
+  }
+
+  private File getPath() {
+    Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
+    checkArgument(basePath != null, "gerrit.basePath must be configured");
+    return FileKey.resolve(basePath.resolve(allUsers).toFile(), FS.DETECTED);
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 529b7e7..1e6bfa8 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
@@ -48,7 +47,7 @@
 public class InitAdminUser implements InitStep {
   private final ConsoleUI ui;
   private final InitFlags flags;
-  private final AccountsUpdate accountsUpdate;
+  private final AccountsOnInit accounts;
   private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
   private final ExternalIdsOnInit externalIds;
   private SchemaFactory<ReviewDb> dbFactory;
@@ -58,12 +57,12 @@
   InitAdminUser(
       InitFlags flags,
       ConsoleUI ui,
-      AccountsUpdate accountsUpdate,
+      AccountsOnInit accounts,
       VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory,
       ExternalIdsOnInit externalIds) {
     this.flags = flags;
     this.ui = ui;
-    this.accountsUpdate = accountsUpdate;
+    this.accounts = accounts;
     this.authorizedKeysFactory = authorizedKeysFactory;
     this.externalIds = externalIds;
   }
@@ -110,7 +109,7 @@
           Account a = new Account(id, TimeUtil.nowTs());
           a.setFullName(name);
           a.setPreferredEmail(email);
-          accountsUpdate.insert(db, a);
+          accounts.insert(db, a);
 
           AccountGroupName adminGroupName =
               db.accountGroupNames().get(new AccountGroup.NameKey("Administrators"));
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index 368cf7f..a52d8ba 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -14,9 +14,14 @@
 
 package com.google.gerrit.pgm.init;
 
+import static com.google.gerrit.extensions.client.GitBasicAuthPolicy.HTTP;
+import static com.google.gerrit.extensions.client.GitBasicAuthPolicy.HTTP_LDAP;
+import static com.google.gerrit.extensions.client.GitBasicAuthPolicy.LDAP;
+import static com.google.gerrit.extensions.client.GitBasicAuthPolicy.OAUTH;
 import static com.google.gerrit.pgm.init.api.InitUtil.dnOf;
 
 import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -24,6 +29,7 @@
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.EnumSet;
 
 /** Initialize the {@code auth} configuration section. */
 @Singleton
@@ -78,12 +84,32 @@
           break;
         }
 
+      case LDAP:
+        {
+          auth.select(
+              "Git/HTTP authentication",
+              "gitBasicAuthPolicy",
+              HTTP,
+              EnumSet.of(HTTP, HTTP_LDAP, LDAP));
+          break;
+        }
+      case OAUTH:
+        {
+          GitBasicAuthPolicy gitBasicAuth =
+              auth.select(
+                  "Git/HTTP authentication", "gitBasicAuthPolicy", HTTP, EnumSet.of(HTTP, OAUTH));
+
+          if (gitBasicAuth == OAUTH) {
+            ui.message(
+                "*WARNING* Please make sure that your chosen OAuth provider\n"
+                    + "supports Git token authentication.\n");
+          }
+          break;
+        }
       case CLIENT_SSL_CERT_LDAP:
       case CUSTOM_EXTENSION:
       case DEVELOPMENT_BECOME_ANY_ACCOUNT:
-      case LDAP:
       case LDAP_BIND:
-      case OAUTH:
       case OPENID:
       case OPENID_SSO:
         break;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index 33c9bfe..18ccb1a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.common.Die;
 import java.io.Console;
-import java.lang.reflect.InvocationTargetException;
+import java.util.EnumSet;
 import java.util.Set;
 
 /** Console based interaction with the invoking user. */
@@ -37,20 +37,6 @@
     return new Die("aborted by user");
   }
 
-  /** Obtain all values from an enumeration. */
-  @SuppressWarnings("unchecked")
-  protected static <T extends Enum<?>> T[] all(final T value) {
-    try {
-      return (T[]) value.getDeclaringClass().getMethod("values").invoke(null);
-    } catch (IllegalArgumentException
-        | NoSuchMethodException
-        | InvocationTargetException
-        | IllegalAccessException
-        | SecurityException e) {
-      throw new IllegalArgumentException("Cannot obtain enumeration values", e);
-    }
-  }
-
   /** @return true if this is a batch UI that has no user interaction. */
   public abstract boolean isBatch();
 
@@ -95,7 +81,8 @@
   }
 
   /** Prompt the user to make a choice from an enumeration's values. */
-  public abstract <T extends Enum<?>> T readEnum(T def, String fmt, Object... args);
+  public abstract <T extends Enum<?>, A extends EnumSet<? extends T>> T readEnum(
+      T def, A options, String fmt, Object... args);
 
   private static class Interactive extends ConsoleUI {
     private final Console console;
@@ -208,9 +195,9 @@
     }
 
     @Override
-    public <T extends Enum<?>> T readEnum(T def, String fmt, Object... args) {
+    public <T extends Enum<?>, A extends EnumSet<? extends T>> T readEnum(
+        T def, A options, String fmt, Object... args) {
       final String prompt = String.format(fmt, args);
-      final T[] options = all(def);
       for (; ; ) {
         String r = console.readLine("%-30s [%s/?]: ", prompt, def.toString());
         if (r == null) {
@@ -277,7 +264,8 @@
     }
 
     @Override
-    public <T extends Enum<?>> T readEnum(T def, String fmt, Object... args) {
+    public <T extends Enum<?>, A extends EnumSet<? extends T>> T readEnum(
+        T def, A options, String fmt, Object... args) {
       return def;
     }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java
index 7ec4604..d52005f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java
@@ -22,6 +22,7 @@
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.EnumSet;
 import java.util.Set;
 
 /** Helper to edit a section of the configuration files. */
@@ -110,15 +111,28 @@
     return site.resolve(string(title, name, defValue));
   }
 
-  public <T extends Enum<?>> T select(final String title, final String name, final T defValue) {
+  public <T extends Enum<?>, E extends EnumSet<? extends T>> T select(
+      String title, String name, T defValue) {
     return select(title, name, defValue, false);
   }
 
-  public <T extends Enum<?>> T select(
-      final String title, final String name, final T defValue, final boolean nullIfDefault) {
+  public <T extends Enum<?>, E extends EnumSet<? extends T>> T select(
+      String title, String name, T defValue, boolean nullIfDefault) {
+    @SuppressWarnings("unchecked")
+    E allowedValues = (E) EnumSet.allOf(defValue.getClass());
+    return select(title, name, defValue, allowedValues, nullIfDefault);
+  }
+
+  public <T extends Enum<?>, E extends EnumSet<? extends T>> T select(
+      String title, String name, T defValue, E allowedValues) {
+    return select(title, name, defValue, allowedValues, false);
+  }
+
+  public <T extends Enum<?>, A extends EnumSet<? extends T>> T select(
+      String title, String name, T defValue, A allowedValues, final boolean nullIfDefault) {
     final boolean set = get(name) != null;
     T oldValue = flags.cfg.getEnum(section, subsection, name, defValue);
-    T newValue = ui.readEnum(oldValue, "%s", title);
+    T newValue = ui.readEnum(oldValue, allowedValues, "%s", title);
     if (nullIfDefault && newValue == defValue) {
       newValue = null;
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index c86d5af..e625219 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -68,6 +68,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -112,6 +113,8 @@
     bind(new TypeLiteral<List<CommentLinkInfo>>() {})
         .toProvider(CommentLinkProvider.class)
         .in(SINGLETON);
+    bind(new TypeLiteral<DynamicMap<ChangeQueryProcessor.ChangeAttributeFactory>>() {})
+        .toInstance(DynamicMap.<ChangeQueryProcessor.ChangeAttributeFactory>emptyMap());
     bind(String.class)
         .annotatedWith(CanonicalWebUrl.class)
         .toProvider(CanonicalWebUrlProvider.class);
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 608bcb1..a03f991 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -26,6 +26,9 @@
       <name>Andrew Bonventre</name>
     </developer>
     <developer>
+      <name>Becky Siegel</name>
+    </developer>
+    <developer>
       <name>Dave Borowitz</name>
     </developer>
     <developer>
@@ -41,6 +44,12 @@
       <name>Hugo Arès</name>
     </developer>
     <developer>
+      <name>Kasper Nilsson</name>
+    </developer>
+    <developer>
+      <name>Logan Hanks</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 048da36..d047206 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -26,6 +26,9 @@
       <name>Andrew Bonventre</name>
     </developer>
     <developer>
+      <name>Becky Siegel</name>
+    </developer>
+    <developer>
       <name>Dave Borowitz</name>
     </developer>
     <developer>
@@ -41,6 +44,12 @@
       <name>Hugo Arès</name>
     </developer>
     <developer>
+      <name>Kasper Nilsson</name>
+    </developer>
+    <developer>
+      <name>Logan Hanks</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
index 82660cb..b8bc9f0 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
@@ -29,8 +29,4 @@
 
   @Query("ORDER BY name")
   ResultSet<AccountGroupName> all() throws OrmException;
-
-  @Query("WHERE name.name >= ? AND name.name <= ? ORDER BY name LIMIT ?")
-  ResultSet<AccountGroupName> suggestByName(String nameA, String nameB, int limit)
-      throws OrmException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
index 2f3a76f..2d80ceb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeTriplet;
+import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -29,13 +31,16 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 
 @Singleton
 public class ChangeFinder {
+  private final IndexConfig indexConfig;
   private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
-  ChangeFinder(Provider<InternalChangeQuery> queryProvider) {
+  ChangeFinder(IndexConfig indexConfig, Provider<InternalChangeQuery> queryProvider) {
+    this.indexConfig = indexConfig;
     this.queryProvider = queryProvider;
   }
 
@@ -93,8 +98,24 @@
   private List<ChangeControl> asChangeControls(List<ChangeData> cds, CurrentUser user)
       throws OrmException {
     List<ChangeControl> ctls = new ArrayList<>(cds.size());
+    if (!indexConfig.separateChangeSubIndexes()) {
+      for (ChangeData cd : cds) {
+        ctls.add(cd.changeControl(user));
+      }
+      return ctls;
+    }
+
+    // If an index implementation uses separate non-atomic subindexes, it's possible to temporarily
+    // observe a change as present in both subindexes, if this search is concurrent with a write.
+    // Dedup to avoid confusing the caller. We can choose an arbitrary ChangeData instance because
+    // the index results have no stored fields, so the data is already reloaded. (It's also possible
+    // that a change might appear in zero subindexes, but there's nothing we can do here to help
+    // this case.)
+    Set<Change.Id> seen = Sets.newHashSetWithExpectedSize(cds.size());
     for (ChangeData cd : cds) {
-      ctls.add(cd.changeControl(user));
+      if (seen.add(cd.getId())) {
+        ctls.add(cd.changeControl(user));
+      }
     }
     return ctls;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java
index c66174d..6267dca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.plugins.DelegatingClassLoader;
 import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.inject.Injector;
 import com.google.inject.Provider;
 import java.util.HashMap;
 import java.util.Map;
@@ -80,15 +82,16 @@
     void setDynamicBean(String plugin, DynamicBean dynamicBean);
   }
 
-  Object bean;
-  Map<String, DynamicBean> beansByPlugin;
+  protected Object bean;
+  protected Map<String, DynamicBean> beansByPlugin;
+  protected Injector injector;
 
   /**
    * Internal: For Gerrit to include options from DynamicBeans, setup a DynamicMap and instantiate
    * this class so the following methods can be called if desired:
    *
    * <pre>
-   *    DynamicOptions pluginOptions = new DynamicOptions(bean, dynamicBeans);
+   *    DynamicOptions pluginOptions = new DynamicOptions(bean, injector, dynamicBeans);
    *    pluginOptions.parseDynamicBeans(clp);
    *    pluginOptions.setDynamicBeans();
    *    pluginOptions.onBeanParseStart();
@@ -98,18 +101,41 @@
    *    pluginOptions.onBeanParseEnd();
    * </pre>
    */
-  public DynamicOptions(Object bean, DynamicMap<DynamicBean> dynamicBeans) {
+  public DynamicOptions(Object bean, Injector injector, DynamicMap<DynamicBean> dynamicBeans) {
     this.bean = bean;
+    this.injector = injector;
     beansByPlugin = new HashMap<>();
     for (String plugin : dynamicBeans.plugins()) {
       Provider<DynamicBean> provider =
           dynamicBeans.byPlugin(plugin).get(bean.getClass().getCanonicalName());
       if (provider != null) {
-        beansByPlugin.put(plugin, provider.get());
+        beansByPlugin.put(plugin, getDynamicBean(bean, provider.get()));
       }
     }
   }
 
+  @SuppressWarnings("unchecked")
+  public DynamicBean getDynamicBean(Object bean, DynamicBean dynamicBean) {
+    ClassLoader coreCl = getClass().getClassLoader();
+    ClassLoader beanCl = bean.getClass().getClassLoader();
+    if (beanCl != coreCl) { // bean from a plugin?
+      ClassLoader dynamicBeanCl = dynamicBean.getClass().getClassLoader();
+      if (beanCl != dynamicBeanCl) { // in a different plugin?
+        ClassLoader mergedCL = new DelegatingClassLoader(beanCl, dynamicBeanCl);
+        try {
+          return injector
+              .createChildInjector()
+              .getInstance(
+                  (Class<DynamicOptions.DynamicBean>)
+                      mergedCL.loadClass(dynamicBean.getClass().getCanonicalName()));
+        } catch (ClassNotFoundException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    }
+    return dynamicBean;
+  }
+
   public void parseDynamicBeans(CmdLineParser clp) {
     for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
       clp.parseWithPrefix(e.getKey(), e.getValue());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 944d008..012ed5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -52,6 +52,7 @@
   private static final Logger log = LoggerFactory.getLogger(AccountManager.class);
 
   private final SchemaFactory<ReviewDb> schema;
+  private final AccountsUpdate.Server accountsUpdateFactory;
   private final AccountCache byIdCache;
   private final AccountByEmailCache byEmailCache;
   private final Realm realm;
@@ -67,6 +68,7 @@
   @Inject
   AccountManager(
       SchemaFactory<ReviewDb> schema,
+      AccountsUpdate.Server accountsUpdateFactory,
       AccountCache byIdCache,
       AccountByEmailCache byEmailCache,
       Realm accountMapper,
@@ -78,6 +80,7 @@
       ExternalIds externalIds,
       ExternalIdsUpdate.Server externalIdsUpdateFactory) {
     this.schema = schema;
+    this.accountsUpdateFactory = accountsUpdateFactory;
     this.byIdCache = byIdCache;
     this.byEmailCache = byEmailCache;
     this.realm = accountMapper;
@@ -229,12 +232,13 @@
         awaitsFirstAccountCheck.getAndSet(false) && db.accounts().anyAccounts().toList().isEmpty();
 
     try {
-      db.accounts().upsert(Collections.singleton(account));
+      AccountsUpdate accountsUpdate = accountsUpdateFactory.create();
+      accountsUpdate.upsert(db, account);
 
       ExternalId existingExtId = externalIds.get(db, extId.key());
       if (existingExtId != null && !existingExtId.accountId().equals(extId.accountId())) {
         // external ID is assigned to another account, do not overwrite
-        db.accounts().delete(Collections.singleton(account));
+        accountsUpdate.delete(db, account);
         throw new AccountException(
             "Cannot assign external ID \""
                 + extId.key().get()
@@ -342,7 +346,7 @@
       // such an account cannot be used for uploading changes,
       // this is why the best we can do here is to fail early and cleanup
       // the database
-      db.accounts().delete(Collections.singleton(account));
+      accountsUpdateFactory.create().delete(db, account);
       externalIdsUpdateFactory.create().delete(db, extId);
       throw new AccountUserNameException(errorMessage, e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
index f45ee7b..de87fc1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -14,22 +14,238 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
+import java.sql.Timestamp;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
 
 /** Updates accounts. */
 @Singleton
 public class AccountsUpdate {
   /**
+   * Factory to create an AccountsUpdate instance for updating accounts by the Gerrit server.
+   *
+   * <p>The Gerrit server identity will be used as author and committer for all commits that update
+   * the accounts.
+   */
+  @Singleton
+  public static class Server {
+    private final GitRepositoryManager repoManager;
+    private final AllUsersName allUsersName;
+    private final Provider<PersonIdent> serverIdent;
+
+    @Inject
+    public Server(
+        GitRepositoryManager repoManager,
+        AllUsersName allUsersName,
+        @GerritPersonIdent Provider<PersonIdent> serverIdent) {
+      this.repoManager = repoManager;
+      this.allUsersName = allUsersName;
+      this.serverIdent = serverIdent;
+    }
+
+    public AccountsUpdate create() {
+      PersonIdent i = serverIdent.get();
+      return new AccountsUpdate(repoManager, allUsersName, i, i);
+    }
+  }
+
+  /**
+   * Factory to create an AccountsUpdate instance for updating accounts by the current user.
+   *
+   * <p>The identity of the current user will be used as author for all commits that update the
+   * accounts. The Gerrit server identity will be used as committer.
+   */
+  @Singleton
+  public static class User {
+    private final GitRepositoryManager repoManager;
+    private final AllUsersName allUsersName;
+    private final Provider<PersonIdent> serverIdent;
+    private final Provider<IdentifiedUser> identifiedUser;
+
+    @Inject
+    public User(
+        GitRepositoryManager repoManager,
+        AllUsersName allUsersName,
+        @GerritPersonIdent Provider<PersonIdent> serverIdent,
+        Provider<IdentifiedUser> identifiedUser) {
+      this.repoManager = repoManager;
+      this.allUsersName = allUsersName;
+      this.serverIdent = serverIdent;
+      this.identifiedUser = identifiedUser;
+    }
+
+    public AccountsUpdate create() {
+      PersonIdent i = serverIdent.get();
+      return new AccountsUpdate(
+          repoManager, allUsersName, createPersonIdent(i, identifiedUser.get()), i);
+    }
+
+    private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
+      return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone());
+    }
+  }
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+  private final PersonIdent committerIdent;
+  private final PersonIdent authorIdent;
+
+  private AccountsUpdate(
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      PersonIdent committerIdent,
+      PersonIdent authorIdent) {
+    this.repoManager = checkNotNull(repoManager, "repoManager");
+    this.allUsersName = checkNotNull(allUsersName, "allUsersName");
+    this.committerIdent = checkNotNull(committerIdent, "committerIdent");
+    this.authorIdent = checkNotNull(authorIdent, "authorIdent");
+  }
+
+  /**
    * Inserts a new account.
    *
    * @throws OrmDuplicateKeyException if the account already exists
+   * @throws IOException if updating the user branch fails
    */
-  public void insert(ReviewDb db, Account account) throws OrmException {
+  public void insert(ReviewDb db, Account account) throws OrmException, IOException {
     db.accounts().insert(ImmutableSet.of(account));
+    createUserBranch(account);
+  }
+
+  /**
+   * Inserts or updates an account.
+   *
+   * <p>If the account already exists, it is overwritten, otherwise it is inserted.
+   */
+  public void upsert(ReviewDb db, Account account) throws OrmException, IOException {
+    db.accounts().upsert(ImmutableSet.of(account));
+    createUserBranchIfNeeded(account);
+  }
+
+  /** Deletes the account. */
+  public void delete(ReviewDb db, Account account) throws OrmException, IOException {
+    db.accounts().delete(ImmutableSet.of(account));
+    deleteUserBranch(account.getId());
+  }
+
+  /** Deletes the account. */
+  public void deleteByKey(ReviewDb db, Account.Id accountId) throws OrmException, IOException {
+    db.accounts().deleteKeys(ImmutableSet.of(accountId));
+    deleteUserBranch(accountId);
+  }
+
+  private void createUserBranch(Account account) throws IOException {
+    try (Repository repo = repoManager.openRepository(allUsersName);
+        ObjectInserter oi = repo.newObjectInserter()) {
+      String refName = RefNames.refsUsers(account.getId());
+      if (repo.exactRef(refName) != null) {
+        throw new IOException(
+            String.format(
+                "User branch %s for newly created account %s already exists.",
+                refName, account.getId().get()));
+      }
+      createUserBranch(repo, oi, committerIdent, authorIdent, account);
+    }
+  }
+
+  private void createUserBranchIfNeeded(Account account) throws IOException {
+    try (Repository repo = repoManager.openRepository(allUsersName);
+        ObjectInserter oi = repo.newObjectInserter()) {
+      if (repo.exactRef(RefNames.refsUsers(account.getId())) == null) {
+        createUserBranch(repo, oi, committerIdent, authorIdent, account);
+      }
+    }
+  }
+
+  public static void createUserBranch(
+      Repository repo,
+      ObjectInserter oi,
+      PersonIdent committerIdent,
+      PersonIdent authorIdent,
+      Account account)
+      throws IOException {
+    ObjectId id =
+        createInitialEmptyCommit(oi, committerIdent, authorIdent, account.getRegisteredOn());
+
+    String refName = RefNames.refsUsers(account.getId());
+    RefUpdate ru = repo.updateRef(refName);
+    ru.setExpectedOldObjectId(ObjectId.zeroId());
+    ru.setNewObjectId(id);
+    ru.setForceUpdate(true);
+    ru.setRefLogIdent(committerIdent);
+    ru.setRefLogMessage("Create Account", true);
+    Result result = ru.update();
+    if (result != Result.NEW) {
+      throw new IOException(String.format("Failed to update ref %s: %s", refName, result.name()));
+    }
+  }
+
+  private static ObjectId createInitialEmptyCommit(
+      ObjectInserter oi,
+      PersonIdent committerIdent,
+      PersonIdent authorIdent,
+      Timestamp registrationDate)
+      throws IOException {
+    CommitBuilder cb = new CommitBuilder();
+    cb.setTreeId(emptyTree(oi));
+    cb.setCommitter(new PersonIdent(committerIdent, registrationDate));
+    cb.setAuthor(new PersonIdent(authorIdent, registrationDate));
+    cb.setMessage("Create Account");
+    ObjectId id = oi.insert(cb);
+    oi.flush();
+    return id;
+  }
+
+  private static ObjectId emptyTree(ObjectInserter oi) throws IOException {
+    return oi.insert(Constants.OBJ_TREE, new byte[] {});
+  }
+
+  private void deleteUserBranch(Account.Id accountId) throws IOException {
+    try (Repository repo = repoManager.openRepository(allUsersName)) {
+      deleteUserBranch(repo, committerIdent, accountId);
+    }
+  }
+
+  public static void deleteUserBranch(
+      Repository repo, PersonIdent refLogIdent, Account.Id accountId) throws IOException {
+    String refName = RefNames.refsUsers(accountId);
+    Ref ref = repo.exactRef(refName);
+    if (ref == null) {
+      return;
+    }
+
+    RefUpdate ru = repo.updateRef(refName);
+    ru.setExpectedOldObjectId(ref.getObjectId());
+    ru.setNewObjectId(ObjectId.zeroId());
+    ru.setForceUpdate(true);
+    ru.setRefLogIdent(refLogIdent);
+    ru.setRefLogMessage("Delete Account", true);
+    Result result = ru.delete();
+    if (result != Result.FORCED) {
+      throw new IOException(String.format("Failed to delete ref %s: %s", refName, result.name()));
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java
index d35656c..545c57f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
@@ -22,7 +21,13 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource.Capability;
+import com.google.gerrit.server.permissions.GlobalOrPluginPermission;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.PluginPermission;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -30,15 +35,18 @@
 @Singleton
 class Capabilities implements ChildCollection<AccountResource, AccountResource.Capability> {
   private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
   private final DynamicMap<RestView<AccountResource.Capability>> views;
   private final Provider<GetCapabilities> get;
 
   @Inject
   Capabilities(
       Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
       DynamicMap<RestView<AccountResource.Capability>> views,
       Provider<GetCapabilities> get) {
     this.self = self;
+    this.permissionBackend = permissionBackend;
     this.views = views;
     this.get = get;
   }
@@ -50,20 +58,39 @@
 
   @Override
   public Capability parse(AccountResource parent, IdString id)
-      throws ResourceNotFoundException, AuthException {
-    if (self.get() != parent.getUser() && !self.get().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("restricted to administrator");
+      throws ResourceNotFoundException, AuthException, PermissionBackendException {
+    IdentifiedUser target = parent.getUser();
+    if (self.get() != target) {
+      permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER);
     }
 
-    String name = id.get();
-    CapabilityControl cap = parent.getUser().getCapabilities();
-    if (cap.canPerform(name)
-        || (cap.canAdministrateServer() && GlobalCapability.isCapability(name))) {
-      return new AccountResource.Capability(parent.getUser(), name);
+    GlobalOrPluginPermission perm = parse(id);
+    if (permissionBackend.user(target).test(perm)) {
+      return new AccountResource.Capability(target, perm.permissionName());
     }
     throw new ResourceNotFoundException(id);
   }
 
+  private GlobalOrPluginPermission parse(IdString id) throws ResourceNotFoundException {
+    String name = id.get();
+    GlobalOrPluginPermission perm = GlobalPermission.byName(name);
+    if (perm != null) {
+      return perm;
+    }
+
+    int dash = name.lastIndexOf('-');
+    if (dash < 0) {
+      throw new ResourceNotFoundException(id);
+    }
+
+    String pluginName = name.substring(0, dash);
+    String capability = name.substring(dash + 1);
+    if (pluginName.isEmpty() || capability.isEmpty()) {
+      throw new ResourceNotFoundException(id);
+    }
+    return new PluginPermission(pluginName, capability);
+  }
+
   @Override
   public DynamicMap<RestView<Capability>> views() {
     return views;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index f678379..f38d019 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -26,8 +26,10 @@
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.permissions.GlobalOrPluginPermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.PluginPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -87,26 +89,11 @@
     return canEmailReviewers;
   }
 
-  /** @return true if the user can modify an account for another user. */
-  public boolean canModifyAccount() {
-    return canPerform(GlobalCapability.MODIFY_ACCOUNT) || canAdministrateServer();
-  }
-
   /** @return true if the user can view all accounts. */
   public boolean canViewAllAccounts() {
     return canPerform(GlobalCapability.VIEW_ALL_ACCOUNTS) || canAdministrateServer();
   }
 
-  /** @return true if the user can perform basic server maintenance. */
-  public boolean canMaintainServer() {
-    return canPerform(GlobalCapability.MAINTAIN_SERVER) || canAdministrateServer();
-  }
-
-  /** @return true if the user can view the entire queue. */
-  public boolean canViewQueue() {
-    return canPerform(GlobalCapability.VIEW_QUEUE) || canMaintainServer();
-  }
-
   /** @return true if the user can access the database (with gsql). */
   public boolean canAccessDatabase() {
     try {
@@ -155,11 +142,8 @@
     return QueueProvider.QueueType.INTERACTIVE;
   }
 
-  /** True if the user has this permission. Works only for non labels. */
-  public boolean canPerform(String permissionName) {
-    if (GlobalCapability.ADMINISTRATE_SERVER.equals(permissionName)) {
-      return canAdministrateServer();
-    }
+  /** @return true if the user has this permission. */
+  private boolean canPerform(String permissionName) {
     return !access(permissionName).isEmpty();
   }
 
@@ -231,31 +215,39 @@
   }
 
   /** Do not use unless inside DefaultPermissionBackend. */
-  public boolean doCanForDefaultPermissionBackend(GlobalPermission perm)
+  public boolean doCanForDefaultPermissionBackend(GlobalOrPluginPermission perm)
       throws PermissionBackendException {
+    if (perm instanceof GlobalPermission) {
+      return can((GlobalPermission) perm);
+    } else if (perm instanceof PluginPermission) {
+      return canPerform(perm.permissionName()) || canAdministrateServer();
+    }
+    throw new PermissionBackendException(perm + " unsupported");
+  }
+
+  private boolean can(GlobalPermission perm) throws PermissionBackendException {
     switch (perm) {
       case ADMINISTRATE_SERVER:
         return canAdministrateServer();
       case EMAIL_REVIEWERS:
         return canEmailReviewers();
-      case MAINTAIN_SERVER:
-        return canMaintainServer();
-      case MODIFY_ACCOUNT:
-        return canModifyAccount();
       case VIEW_ALL_ACCOUNTS:
         return canViewAllAccounts();
-      case VIEW_QUEUE:
-        return canViewQueue();
 
       case FLUSH_CACHES:
       case KILL_TASK:
       case RUN_GC:
       case VIEW_CACHES:
-        return canPerform(perm.permissionName()) || canMaintainServer();
+      case VIEW_QUEUE:
+        return canPerform(perm.permissionName())
+            || canPerform(GlobalCapability.MAINTAIN_SERVER)
+            || canAdministrateServer();
 
       case CREATE_ACCOUNT:
       case CREATE_GROUP:
       case CREATE_PROJECT:
+      case MAINTAIN_SERVER:
+      case MODIFY_ACCOUNT:
       case STREAM_EVENTS:
       case VIEW_CONNECTIONS:
       case VIEW_PLUGINS:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
deleted file mode 100644
index 21399f4..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
+++ /dev/null
@@ -1,132 +0,0 @@
-// Copyright (C) 2013 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.gerrit.extensions.annotations.CapabilityScope;
-import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.inject.Provider;
-import java.lang.annotation.Annotation;
-import java.util.Arrays;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class CapabilityUtils {
-  private static final Logger log = LoggerFactory.getLogger(CapabilityUtils.class);
-
-  public static void checkRequiresCapability(
-      Provider<CurrentUser> userProvider, String pluginName, Class<?> clazz) throws AuthException {
-    checkRequiresCapability(userProvider.get(), pluginName, clazz);
-  }
-
-  public static void checkRequiresCapability(CurrentUser user, String pluginName, Class<?> clazz)
-      throws AuthException {
-    RequiresCapability rc = getClassAnnotation(clazz, RequiresCapability.class);
-    RequiresAnyCapability rac = getClassAnnotation(clazz, RequiresAnyCapability.class);
-    if (rc != null && rac != null) {
-      log.error(
-          String.format(
-              "Class %s uses both @%s and @%s",
-              clazz.getName(),
-              RequiresCapability.class.getSimpleName(),
-              RequiresAnyCapability.class.getSimpleName()));
-      throw new AuthException("cannot check capability");
-    }
-    CapabilityControl ctl = user.getCapabilities();
-    if (ctl.canAdministrateServer()) {
-      return;
-    }
-    checkRequiresCapability(ctl, pluginName, clazz, rc);
-    checkRequiresAnyCapability(ctl, pluginName, clazz, rac);
-  }
-
-  private static void checkRequiresCapability(
-      CapabilityControl ctl, String pluginName, Class<?> clazz, RequiresCapability rc)
-      throws AuthException {
-    if (rc == null) {
-      return;
-    }
-    String capability = resolveCapability(pluginName, rc.value(), rc.scope(), clazz);
-    if (!ctl.canPerform(capability)) {
-      throw new AuthException(
-          String.format("Capability %s is required to access this resource", capability));
-    }
-  }
-
-  private static void checkRequiresAnyCapability(
-      CapabilityControl ctl, String pluginName, Class<?> clazz, RequiresAnyCapability rac)
-      throws AuthException {
-    if (rac == null) {
-      return;
-    }
-    if (rac.value().length == 0) {
-      log.error(
-          String.format(
-              "Class %s uses @%s with no capabilities listed",
-              clazz.getName(), RequiresAnyCapability.class.getSimpleName()));
-      throw new AuthException("cannot check capability");
-    }
-    for (String capability : rac.value()) {
-      capability = resolveCapability(pluginName, capability, rac.scope(), clazz);
-      if (ctl.canPerform(capability)) {
-        return;
-      }
-    }
-    throw new AuthException(
-        "One of the following capabilities is required to access this"
-            + " resource: "
-            + Arrays.asList(rac.value()));
-  }
-
-  private static String resolveCapability(
-      String pluginName, String capability, CapabilityScope scope, Class<?> clazz)
-      throws AuthException {
-    if (pluginName != null
-        && !"gerrit".equals(pluginName)
-        && (scope == CapabilityScope.PLUGIN || scope == CapabilityScope.CONTEXT)) {
-      capability = String.format("%s-%s", pluginName, capability);
-    } else if (scope == CapabilityScope.PLUGIN) {
-      log.error(
-          String.format(
-              "Class %s uses @%s(scope=%s), but is not within a plugin",
-              clazz.getName(),
-              RequiresCapability.class.getSimpleName(),
-              CapabilityScope.PLUGIN.name()));
-      throw new AuthException("cannot check capability");
-    }
-    return capability;
-  }
-
-  /**
-   * Find an instance of the specified annotation, walking up the inheritance tree if necessary.
-   *
-   * @param <T> Annotation type to search for
-   * @param clazz root class to search, may be null
-   * @param annotationClass class object of Annotation subclass to search for
-   * @return the requested annotation or null if none
-   */
-  private static <T extends Annotation> T getClassAnnotation(
-      Class<?> clazz, Class<T> annotationClass) {
-    for (; clazz != null; clazz = clazz.getSuperclass()) {
-      T t = clazz.getAnnotation(annotationClass);
-      if (t != null) {
-        return t;
-      }
-    }
-    return null;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index b16733c..2f75390 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -69,7 +69,7 @@
   private final VersionedAuthorizedKeys.Accessor authorizedKeys;
   private final SshKeyCache sshKeyCache;
   private final AccountCache accountCache;
-  private final AccountsUpdate accountsUpdate;
+  private final AccountsUpdate.User accountsUpdate;
   private final AccountIndexer indexer;
   private final AccountByEmailCache byEmailCache;
   private final AccountLoader.Factory infoLoader;
@@ -87,7 +87,7 @@
       VersionedAuthorizedKeys.Accessor authorizedKeys,
       SshKeyCache sshKeyCache,
       AccountCache accountCache,
-      AccountsUpdate accountsUpdate,
+      AccountsUpdate.User accountsUpdate,
       AccountIndexer indexer,
       AccountByEmailCache byEmailCache,
       AccountLoader.Factory infoLoader,
@@ -175,7 +175,7 @@
     Account a = new Account(id, TimeUtil.nowTs());
     a.setFullName(input.name);
     a.setPreferredEmail(input.email);
-    accountsUpdate.insert(db, a);
+    accountsUpdate.create().insert(db, a);
 
     for (AccountGroup.Id groupId : groups) {
       AccountGroupMember m = new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
index b1a5d3b..4af7162 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -32,6 +32,9 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -50,6 +53,7 @@
 
   private final Provider<CurrentUser> self;
   private final Realm realm;
+  private final PermissionBackend permissionBackend;
   private final AccountManager accountManager;
   private final RegisterNewEmailSender.Factory registerNewEmailFactory;
   private final PutPreferred putPreferred;
@@ -60,6 +64,7 @@
   CreateEmail(
       Provider<CurrentUser> self,
       Realm realm,
+      PermissionBackend permissionBackend,
       AuthConfig authConfig,
       AccountManager accountManager,
       RegisterNewEmailSender.Factory registerNewEmailFactory,
@@ -67,6 +72,7 @@
       @Assisted String email) {
     this.self = self;
     this.realm = realm;
+    this.permissionBackend = permissionBackend;
     this.accountManager = accountManager;
     this.registerNewEmailFactory = registerNewEmailFactory;
     this.putPreferred = putPreferred;
@@ -78,9 +84,9 @@
   public Response<EmailInfo> apply(AccountResource rsrc, EmailInput input)
       throws AuthException, BadRequestException, ResourceConflictException,
           ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
-          IOException, ConfigInvalidException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("not allowed to add email address");
+          IOException, ConfigInvalidException, PermissionBackendException {
+    if (self.get() != rsrc.getUser() || input.noConfirmation) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     if (input == null) {
@@ -91,10 +97,6 @@
       throw new BadRequestException("invalid email address");
     }
 
-    if (input.noConfirmation && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("not allowed to use no_confirmation");
-    }
-
     if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow adding emails");
     }
@@ -105,7 +107,7 @@
   public Response<EmailInfo> apply(IdentifiedUser user, EmailInput input)
       throws AuthException, BadRequestException, ResourceConflictException,
           ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
-          IOException, ConfigInvalidException {
+          IOException, ConfigInvalidException, PermissionBackendException {
     if (input.email != null && !email.equals(input.email)) {
       throw new BadRequestException("email address must match URL");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
index bfdf06c..b4e2bdb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -29,6 +29,9 @@
 import com.google.gerrit.server.account.DeleteEmail.Input;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -43,6 +46,7 @@
 
   private final Provider<CurrentUser> self;
   private final Realm realm;
+  private final PermissionBackend permissionBackend;
   private final Provider<ReviewDb> dbProvider;
   private final AccountManager accountManager;
   private final ExternalIds externalIds;
@@ -51,11 +55,13 @@
   DeleteEmail(
       Provider<CurrentUser> self,
       Realm realm,
+      PermissionBackend permissionBackend,
       Provider<ReviewDb> dbProvider,
       AccountManager accountManager,
       ExternalIds externalIds) {
     this.self = self;
     this.realm = realm;
+    this.permissionBackend = permissionBackend;
     this.dbProvider = dbProvider;
     this.accountManager = accountManager;
     this.externalIds = externalIds;
@@ -64,9 +70,10 @@
   @Override
   public Response<?> apply(AccountResource.Email rsrc, Input input)
       throws AuthException, ResourceNotFoundException, ResourceConflictException,
-          MethodNotAllowedException, OrmException, IOException, ConfigInvalidException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("not allowed to delete email address");
+          MethodNotAllowedException, OrmException, IOException, ConfigInvalidException,
+          PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), rsrc.getEmail());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 1268ef2..5276e8d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -29,14 +29,15 @@
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.account.AccountResource.Capability;
 import com.google.gerrit.server.git.QueueProvider;
+import com.google.gerrit.server.permissions.GlobalOrPluginPermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.PluginPermission;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -77,11 +78,10 @@
     }
 
     Map<String, Object> have = new LinkedHashMap<>();
-    for (GlobalPermission p : testGlobalPermissions(perm)) {
+    for (GlobalOrPluginPermission p : perm.test(permissionsToTest())) {
       have.put(p.permissionName(), true);
     }
     addRanges(have, rsrc);
-    addPluginCapabilities(have, rsrc);
     addPriority(have, rsrc);
 
     return OutputFormat.JSON
@@ -89,20 +89,23 @@
         .toJsonTree(have, new TypeToken<Map<String, Object>>() {}.getType());
   }
 
-  private Set<GlobalPermission> testGlobalPermissions(PermissionBackend.WithUser perm)
-      throws PermissionBackendException {
-    EnumSet<GlobalPermission> toTest;
-    if (query != null) {
-      toTest = EnumSet.noneOf(GlobalPermission.class);
-      for (GlobalPermission p : GlobalPermission.values()) {
+  private Set<GlobalOrPluginPermission> permissionsToTest() {
+    Set<GlobalOrPluginPermission> toTest = new HashSet<>();
+    for (GlobalPermission p : GlobalPermission.values()) {
+      if (want(p.permissionName())) {
+        toTest.add(p);
+      }
+    }
+
+    for (String pluginName : pluginCapabilities.plugins()) {
+      for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) {
+        PluginPermission p = new PluginPermission(pluginName, capability);
         if (want(p.permissionName())) {
           toTest.add(p);
         }
       }
-    } else {
-      toTest = EnumSet.allOf(GlobalPermission.class);
     }
-    return perm.test(toTest);
+    return toTest;
   }
 
   private boolean want(String name) {
@@ -118,18 +121,6 @@
     }
   }
 
-  private void addPluginCapabilities(Map<String, Object> have, AccountResource rsrc) {
-    CapabilityControl cc = rsrc.getUser().getCapabilities();
-    for (String pluginName : pluginCapabilities.plugins()) {
-      for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) {
-        String name = String.format("%s-%s", pluginName, capability);
-        if (want(name) && cc.canPerform(name)) {
-          have.put(name, true);
-        }
-      }
-    }
-  }
-
   private void addPriority(Map<String, Object> have, AccountResource rsrc) {
     QueueProvider.QueueType queue = rsrc.getUser().getCapabilities().getQueueType();
     if (queue != QueueProvider.QueueType.INTERACTIVE
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
index e385020..bb207f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
@@ -24,6 +24,9 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.UserConfigSections;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -35,22 +38,27 @@
 @Singleton
 public class GetEditPreferences implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
   private final AllUsersName allUsersName;
   private final GitRepositoryManager gitMgr;
 
   @Inject
   GetEditPreferences(
-      Provider<CurrentUser> self, AllUsersName allUsersName, GitRepositoryManager gitMgr) {
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
+      AllUsersName allUsersName,
+      GitRepositoryManager gitMgr) {
     this.self = self;
+    this.permissionBackend = permissionBackend;
     this.allUsersName = allUsersName;
     this.gitMgr = gitMgr;
   }
 
   @Override
   public EditPreferencesInfo apply(AccountResource rsrc)
-      throws AuthException, IOException, ConfigInvalidException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("requires Modify Account capability");
+      throws AuthException, IOException, ConfigInvalidException, PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     return readFromGit(rsrc.getUser().getAccountId(), gitMgr, allUsersName, null);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
index 77cdbd4..3ebf864 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -19,6 +19,9 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -26,18 +29,22 @@
 @Singleton
 public class GetPreferences implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
   private final AccountCache accountCache;
 
   @Inject
-  GetPreferences(Provider<CurrentUser> self, AccountCache accountCache) {
+  GetPreferences(
+      Provider<CurrentUser> self, PermissionBackend permissionBackend, AccountCache accountCache) {
     this.self = self;
+    this.permissionBackend = permissionBackend;
     this.accountCache = accountCache;
   }
 
   @Override
-  public GeneralPreferencesInfo apply(AccountResource rsrc) throws AuthException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("requires Modify Account capability");
+  public GeneralPreferencesInfo apply(AccountResource rsrc)
+      throws AuthException, PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     Account.Id id = rsrc.getUser().getAccountId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
index 980d880..9f5b9d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
@@ -22,6 +22,9 @@
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -35,20 +38,25 @@
 public class GetSshKeys implements RestReadView<AccountResource> {
 
   private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
   private final VersionedAuthorizedKeys.Accessor authorizedKeys;
 
   @Inject
-  GetSshKeys(Provider<CurrentUser> self, VersionedAuthorizedKeys.Accessor authorizedKeys) {
+  GetSshKeys(
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
+      VersionedAuthorizedKeys.Accessor authorizedKeys) {
     this.self = self;
+    this.permissionBackend = permissionBackend;
     this.authorizedKeys = authorizedKeys;
   }
 
   @Override
   public List<SshKeyInfo> apply(AccountResource rsrc)
       throws AuthException, OrmException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("not allowed to get SSH keys");
+          ConfigInvalidException, PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java
index 6943dca..ecc6b8c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java
@@ -19,6 +19,9 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.Index.Input;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -29,18 +32,22 @@
   public static class Input {}
 
   private final AccountCache accountCache;
+  private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> self;
 
   @Inject
-  Index(AccountCache accountCache, Provider<CurrentUser> self) {
+  Index(
+      AccountCache accountCache, PermissionBackend permissionBackend, Provider<CurrentUser> self) {
     this.accountCache = accountCache;
+    this.permissionBackend = permissionBackend;
     this.self = self;
   }
 
   @Override
-  public Response<?> apply(AccountResource rsrc, Input input) throws IOException, AuthException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("not allowed to index account");
+  public Response<?> apply(AccountResource rsrc, Input input)
+      throws IOException, AuthException, PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     // evicting the account from the cache, reindexes the account
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
index 443a549..7a2868e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -27,6 +27,9 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.PutName.Input;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -42,6 +45,7 @@
 
   private final Provider<CurrentUser> self;
   private final Realm realm;
+  private final PermissionBackend permissionBackend;
   private final Provider<ReviewDb> dbProvider;
   private final AccountCache byIdCache;
 
@@ -49,10 +53,12 @@
   PutName(
       Provider<CurrentUser> self,
       Realm realm,
+      PermissionBackend permissionBackend,
       Provider<ReviewDb> dbProvider,
       AccountCache byIdCache) {
     this.self = self;
     this.realm = realm;
+    this.permissionBackend = permissionBackend;
     this.dbProvider = dbProvider;
     this.byIdCache = byIdCache;
   }
@@ -60,9 +66,9 @@
   @Override
   public Response<String> apply(AccountResource rsrc, Input input)
       throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
-          IOException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("not allowed to change name");
+          IOException, PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), input);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
index fb87e1e..4941cc8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -23,6 +23,9 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.PutPreferred.Input;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -37,20 +40,27 @@
 
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> dbProvider;
+  private final PermissionBackend permissionBackend;
   private final AccountCache byIdCache;
 
   @Inject
-  PutPreferred(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider, AccountCache byIdCache) {
+  PutPreferred(
+      Provider<CurrentUser> self,
+      Provider<ReviewDb> dbProvider,
+      PermissionBackend permissionBackend,
+      AccountCache byIdCache) {
     this.self = self;
     this.dbProvider = dbProvider;
+    this.permissionBackend = permissionBackend;
     this.byIdCache = byIdCache;
   }
 
   @Override
   public Response<String> apply(AccountResource.Email rsrc, Input input)
-      throws AuthException, ResourceNotFoundException, OrmException, IOException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("not allowed to set preferred email address");
+      throws AuthException, ResourceNotFoundException, OrmException, IOException,
+          PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), rsrc.getEmail());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
index ff541fd..73a720b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
@@ -25,6 +25,9 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.PutStatus.Input;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -46,20 +49,27 @@
 
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> dbProvider;
+  private final PermissionBackend permissionBackend;
   private final AccountCache byIdCache;
 
   @Inject
-  PutStatus(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider, AccountCache byIdCache) {
+  PutStatus(
+      Provider<CurrentUser> self,
+      Provider<ReviewDb> dbProvider,
+      PermissionBackend permissionBackend,
+      AccountCache byIdCache) {
     this.self = self;
     this.dbProvider = dbProvider;
+    this.permissionBackend = permissionBackend;
     this.byIdCache = byIdCache;
   }
 
   @Override
   public Response<String> apply(AccountResource rsrc, Input input)
-      throws AuthException, ResourceNotFoundException, OrmException, IOException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("not allowed to set status");
+      throws AuthException, ResourceNotFoundException, OrmException, IOException,
+          PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
     return apply(rsrc.getUser(), input);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
index ac0cc96..88e9e20 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -29,6 +29,9 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.UserConfigSections;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -41,6 +44,7 @@
   private final Provider<CurrentUser> self;
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final AllUsersName allUsersName;
+  private final PermissionBackend permissionBackend;
   private final GitRepositoryManager gitMgr;
 
   @Inject
@@ -48,19 +52,21 @@
       Provider<CurrentUser> self,
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       AllUsersName allUsersName,
+      PermissionBackend permissionBackend,
       GitRepositoryManager gitMgr) {
     this.self = self;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allUsersName = allUsersName;
+    this.permissionBackend = permissionBackend;
     this.gitMgr = gitMgr;
   }
 
   @Override
   public DiffPreferencesInfo apply(AccountResource rsrc, DiffPreferencesInfo in)
       throws AuthException, BadRequestException, ConfigInvalidException,
-          RepositoryNotFoundException, IOException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("requires Modify Account capability");
+          RepositoryNotFoundException, IOException, PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     if (in == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
index ca981b8..53285db 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
@@ -28,6 +28,9 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.UserConfigSections;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -40,6 +43,7 @@
 
   private final Provider<CurrentUser> self;
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+  private final PermissionBackend permissionBackend;
   private final GitRepositoryManager gitMgr;
   private final AllUsersName allUsersName;
 
@@ -47,10 +51,12 @@
   SetEditPreferences(
       Provider<CurrentUser> self,
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      PermissionBackend permissionBackend,
       GitRepositoryManager gitMgr,
       AllUsersName allUsersName) {
     this.self = self;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.permissionBackend = permissionBackend;
     this.gitMgr = gitMgr;
     this.allUsersName = allUsersName;
   }
@@ -58,9 +64,9 @@
   @Override
   public EditPreferencesInfo apply(AccountResource rsrc, EditPreferencesInfo in)
       throws AuthException, BadRequestException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("requires Modify Account capability");
+          ConfigInvalidException, PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     if (in == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index 91672f7..c033d9d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -36,6 +36,9 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.UserConfigSections;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -51,6 +54,7 @@
 public class SetPreferences implements RestModifyView<AccountResource, GeneralPreferencesInfo> {
   private final Provider<CurrentUser> self;
   private final AccountCache cache;
+  private final PermissionBackend permissionBackend;
   private final GeneralPreferencesLoader loader;
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final AllUsersName allUsersName;
@@ -60,6 +64,7 @@
   SetPreferences(
       Provider<CurrentUser> self,
       AccountCache cache,
+      PermissionBackend permissionBackend,
       GeneralPreferencesLoader loader,
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       AllUsersName allUsersName,
@@ -67,6 +72,7 @@
     this.self = self;
     this.loader = loader;
     this.cache = cache;
+    this.permissionBackend = permissionBackend;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.allUsersName = allUsersName;
     this.downloadSchemes = downloadSchemes;
@@ -74,9 +80,10 @@
 
   @Override
   public GeneralPreferencesInfo apply(AccountResource rsrc, GeneralPreferencesInfo i)
-      throws AuthException, BadRequestException, IOException, ConfigInvalidException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new AuthException("requires Modify Account capability");
+      throws AuthException, BadRequestException, IOException, ConfigInvalidException,
+          PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
     checkDownloadScheme(i.downloadScheme);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
index 6336e08..70c02a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -22,6 +23,9 @@
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -34,6 +38,7 @@
   private final DynamicMap<RestView<AccountResource.SshKey>> views;
   private final GetSshKeys list;
   private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
   private final VersionedAuthorizedKeys.Accessor authorizedKeys;
 
   @Inject
@@ -41,10 +46,12 @@
       DynamicMap<RestView<AccountResource.SshKey>> views,
       GetSshKeys list,
       Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
       VersionedAuthorizedKeys.Accessor authorizedKeys) {
     this.views = views;
     this.list = list;
     this.self = self;
+    this.permissionBackend = permissionBackend;
     this.authorizedKeys = authorizedKeys;
   }
 
@@ -55,9 +62,15 @@
 
   @Override
   public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
-      throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
-    if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
-      throw new ResourceNotFoundException();
+      throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException,
+          PermissionBackendException {
+    if (self.get() != rsrc.getUser()) {
+      try {
+        permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
+      } catch (AuthException e) {
+        // If lacking MODIFY_ACCOUNT claim the resource does not exist.
+        throw new ResourceNotFoundException();
+      }
     }
     return parse(rsrc.getUser(), id);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 74e1fda..b4fef67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -215,21 +215,21 @@
       throw invalidConfig(
           noteId,
           String.format(
-              "Expected exactly 1 %s section, found %d",
+              "Expected exactly 1 '%s' section, found %d",
               EXTERNAL_ID_SECTION, externalIdKeys.size()));
     }
 
     String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
     Key externalIdKey = Key.parse(externalIdKeyStr);
     if (externalIdKey == null) {
-      throw invalidConfig(noteId, String.format("Invalid external id: %s", externalIdKeyStr));
+      throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr));
     }
 
     if (!externalIdKey.sha1().getName().equals(noteId)) {
       throw invalidConfig(
           noteId,
           String.format(
-              "SHA1 of external ID %s does not match note ID %s", externalIdKeyStr, noteId));
+              "SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId));
     }
 
     String email = externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, EMAIL_KEY);
@@ -252,7 +252,7 @@
       throw invalidConfig(
           noteId,
           String.format(
-              "Value for %s.%s.%s is missing, expected account ID",
+              "Value for '%s.%s.%s' is missing, expected account ID",
               EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
     }
 
@@ -263,7 +263,7 @@
         throw invalidConfig(
             noteId,
             String.format(
-                "Value %s for %s.%s.%s is invalid, expected account ID",
+                "Value %s for '%s.%s.%s' is invalid, expected account ID",
                 accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
       }
       return accountId;
@@ -271,14 +271,14 @@
       throw invalidConfig(
           noteId,
           String.format(
-              "Value %s for %s.%s.%s is invalid, expected account ID",
+              "Value %s for '%s.%s.%s' is invalid, expected account ID",
               accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
     }
   }
 
   private static ConfigInvalidException invalidConfig(String noteId, String message) {
     return new ConfigInvalidException(
-        String.format("Invalid external id config for note %s: %s", noteId, message));
+        String.format("Invalid external ID config for note '%s': %s", noteId, message));
   }
 
   public static ExternalId from(AccountExternalId externalId) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
index b12e7ed..e25c36f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
@@ -156,7 +156,7 @@
             rw.getObjectReader().open(note.getData(), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
         try {
           extIds.add(ExternalId.parse(note.getName(), raw));
-        } catch (ConfigInvalidException e) {
+        } catch (Exception e) {
           log.error(String.format("Ignoring invalid external ID note %s", note.getName()), e);
         }
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
new file mode 100644
index 0000000..bc681a2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.externalids;
+
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static java.util.stream.Collectors.joining;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.HashedPassword;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.codec.DecoderException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+@Singleton
+public class ExternalIdsConsistencyChecker {
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsers;
+  private final AccountCache accountCache;
+
+  @Inject
+  ExternalIdsConsistencyChecker(
+      GitRepositoryManager repoManager, AllUsersName allUsers, AccountCache accountCache) {
+    this.repoManager = repoManager;
+    this.allUsers = allUsers;
+    this.accountCache = accountCache;
+  }
+
+  public List<ConsistencyProblemInfo> check() throws IOException {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      return check(repo, ExternalIdReader.readRevision(repo));
+    }
+  }
+
+  public List<ConsistencyProblemInfo> check(ObjectId rev) throws IOException {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      return check(repo, rev);
+    }
+  }
+
+  private List<ConsistencyProblemInfo> check(Repository repo, ObjectId commit) throws IOException {
+    List<ConsistencyProblemInfo> problems = new ArrayList<>();
+
+    ListMultimap<String, ExternalId.Key> emails =
+        MultimapBuilder.hashKeys().arrayListValues().build();
+
+    try (RevWalk rw = new RevWalk(repo)) {
+      NoteMap noteMap = ExternalIdReader.readNoteMap(rw, commit);
+      for (Note note : noteMap) {
+        byte[] raw =
+            rw.getObjectReader()
+                .open(note.getData(), OBJ_BLOB)
+                .getCachedBytes(ExternalIdReader.MAX_NOTE_SZ);
+        try {
+          ExternalId extId = ExternalId.parse(note.getName(), raw);
+          problems.addAll(validateExternalId(extId));
+
+          if (extId.email() != null) {
+            emails.put(extId.email(), extId.key());
+          }
+        } catch (ConfigInvalidException e) {
+          addError(String.format(e.getMessage()), problems);
+        }
+      }
+    }
+
+    emails
+        .asMap()
+        .entrySet()
+        .stream()
+        .filter(e -> e.getValue().size() > 1)
+        .forEach(
+            e ->
+                addError(
+                    String.format(
+                        "Email '%s' is not unique, it's used by the following external IDs: %s",
+                        e.getKey(),
+                        e.getValue()
+                            .stream()
+                            .map(k -> "'" + k.get() + "'")
+                            .sorted()
+                            .collect(joining(", "))),
+                    problems));
+
+    return problems;
+  }
+
+  private List<ConsistencyProblemInfo> validateExternalId(ExternalId extId) {
+    List<ConsistencyProblemInfo> problems = new ArrayList<>();
+
+    if (accountCache.getIfPresent(extId.accountId()) == null) {
+      addError(
+          String.format(
+              "External ID '%s' belongs to account that doesn't exist: %s",
+              extId.key().get(), extId.accountId().get()),
+          problems);
+    }
+
+    if (extId.email() != null && !OutgoingEmailValidator.isValid(extId.email())) {
+      addError(
+          String.format(
+              "External ID '%s' has an invalid email: %s", extId.key().get(), extId.email()),
+          problems);
+    }
+
+    if (extId.password() != null && extId.isScheme(SCHEME_USERNAME)) {
+      try {
+        HashedPassword.decode(extId.password());
+      } catch (DecoderException e) {
+        addError(
+            String.format(
+                "External ID '%s' has an invalid password: %s", extId.key().get(), e.getMessage()),
+            problems);
+      }
+    }
+
+    return problems;
+  }
+
+  private static void addError(String error, List<ConsistencyProblemInfo> problems) {
+    problems.add(new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, error));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 23c6537..dc5ea4c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -71,6 +71,7 @@
 import com.google.gerrit.server.account.Stars;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -234,14 +235,18 @@
 
   @Override
   public GeneralPreferencesInfo getPreferences() throws RestApiException {
-    return getPreferences.apply(account);
+    try {
+      return getPreferences.apply(account);
+    } catch (PermissionBackendException e) {
+      throw new RestApiException("Cannot get preferences", e);
+    }
   }
 
   @Override
   public GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in) throws RestApiException {
     try {
       return setPreferences.apply(account, in);
-    } catch (IOException | ConfigInvalidException e) {
+    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
       throw new RestApiException("Cannot set preferences", e);
     }
   }
@@ -259,7 +264,7 @@
   public DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) throws RestApiException {
     try {
       return setDiffPreferences.apply(account, in);
-    } catch (IOException | ConfigInvalidException e) {
+    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
       throw new RestApiException("Cannot set diff preferences", e);
     }
   }
@@ -268,7 +273,7 @@
   public EditPreferencesInfo getEditPreferences() throws RestApiException {
     try {
       return getEditPreferences.apply(account);
-    } catch (IOException | ConfigInvalidException e) {
+    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
       throw new RestApiException("Cannot query edit preferences", e);
     }
   }
@@ -277,7 +282,7 @@
   public EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) throws RestApiException {
     try {
       return setEditPreferences.apply(account, in);
-    } catch (IOException | ConfigInvalidException e) {
+    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
       throw new RestApiException("Cannot set edit preferences", e);
     }
   }
@@ -372,7 +377,11 @@
     AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), input.email);
     try {
       createEmailFactory.create(input.email).apply(rsrc, input);
-    } catch (EmailException | OrmException | IOException | ConfigInvalidException e) {
+    } catch (EmailException
+        | OrmException
+        | IOException
+        | ConfigInvalidException
+        | PermissionBackendException e) {
       throw new RestApiException("Cannot add email", e);
     }
   }
@@ -382,7 +391,7 @@
     AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), email);
     try {
       deleteEmail.apply(rsrc, null);
-    } catch (OrmException | IOException | ConfigInvalidException e) {
+    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
       throw new RestApiException("Cannot delete email", e);
     }
   }
@@ -392,7 +401,7 @@
     PutStatus.Input in = new PutStatus.Input(status);
     try {
       putStatus.apply(account, in);
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | PermissionBackendException e) {
       throw new RestApiException("Cannot set status", e);
     }
   }
@@ -401,7 +410,7 @@
   public List<SshKeyInfo> listSshKeys() throws RestApiException {
     try {
       return getSshKeys.apply(account);
-    } catch (OrmException | IOException | ConfigInvalidException e) {
+    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
       throw new RestApiException("Cannot list SSH keys", e);
     }
   }
@@ -423,7 +432,7 @@
       AccountResource.SshKey sshKeyRes =
           sshKeys.parse(account, IdString.fromDecoded(Integer.toString(seq)));
       deleteSshKey.apply(sshKeyRes, null);
-    } catch (OrmException | IOException | ConfigInvalidException e) {
+    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
       throw new RestApiException("Cannot delete SSH key", e);
     }
   }
@@ -476,7 +485,7 @@
   public void index() throws RestApiException {
     try {
       index.apply(account, new Index.Input());
-    } catch (IOException e) {
+    } catch (IOException | PermissionBackendException e) {
       throw new RestApiException("Cannot index account", e);
     }
   }
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 498b720..bade8ce 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
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.api.accounts;
 
 import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
 
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
@@ -32,6 +31,9 @@
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.CreateAccount;
 import com.google.gerrit.server.account.QueryAccounts;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -44,6 +46,7 @@
 public class AccountsImpl implements Accounts {
   private final AccountsCollection accounts;
   private final AccountApiImpl.Factory api;
+  private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> self;
   private final CreateAccount.Factory createAccount;
   private final Provider<QueryAccounts> queryAccountsProvider;
@@ -52,11 +55,13 @@
   AccountsImpl(
       AccountsCollection accounts,
       AccountApiImpl.Factory api,
+      PermissionBackend permissionBackend,
       Provider<CurrentUser> self,
       CreateAccount.Factory createAccount,
       Provider<QueryAccounts> queryAccountsProvider) {
     this.accounts = accounts;
     this.api = api;
+    this.permissionBackend = permissionBackend;
     this.self = self;
     this.createAccount = createAccount;
     this.queryAccountsProvider = queryAccountsProvider;
@@ -96,12 +101,12 @@
     if (checkNotNull(in, "AccountInput").username == null) {
       throw new BadRequestException("AccountInput must specify username");
     }
-    checkRequiresCapability(self, null, CreateAccount.class);
     try {
-      AccountInfo info =
-          createAccount.create(in.username).apply(TopLevelResource.INSTANCE, in).value();
+      CreateAccount impl = createAccount.create(in.username);
+      permissionBackend.user(self).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
+      AccountInfo info = impl.apply(TopLevelResource.INSTANCE, in).value();
       return id(info._accountId);
-    } catch (OrmException | IOException | ConfigInvalidException e) {
+    } catch (OrmException | IOException | ConfigInvalidException | PermissionBackendException e) {
       throw new RestApiException("Cannot create account " + in.username, e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index d534c5a..cee2403 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -572,7 +572,7 @@
   public ChangeInfo check(FixInput fix) throws RestApiException {
     try {
       return check.apply(change, fix).value();
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       throw new RestApiException("Cannot check change", e);
     }
   }
@@ -581,7 +581,7 @@
   public void index() throws RestApiException {
     try {
       index.apply(change, new Index.Input());
-    } catch (IOException | OrmException e) {
+    } catch (IOException | OrmException | PermissionBackendException e) {
       throw new RestApiException("Cannot index change", e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
index 9b6ead0..d3c5135 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -15,11 +15,14 @@
 package com.google.gerrit.server.api.config;
 
 import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
 import com.google.gerrit.extensions.api.config.Server;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.config.CheckConsistency;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.GetDiffPreferences;
 import com.google.gerrit.server.config.GetPreferences;
@@ -27,6 +30,7 @@
 import com.google.gerrit.server.config.SetDiffPreferences;
 import com.google.gerrit.server.config.SetPreferences;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -38,6 +42,7 @@
   private final GetDiffPreferences getDiffPreferences;
   private final SetDiffPreferences setDiffPreferences;
   private final GetServerInfo getServerInfo;
+  private final Provider<CheckConsistency> checkConsistency;
 
   @Inject
   ServerImpl(
@@ -45,12 +50,14 @@
       SetPreferences setPreferences,
       GetDiffPreferences getDiffPreferences,
       SetDiffPreferences setDiffPreferences,
-      GetServerInfo getServerInfo) {
+      GetServerInfo getServerInfo,
+      Provider<CheckConsistency> checkConsistency) {
     this.getPreferences = getPreferences;
     this.setPreferences = setPreferences;
     this.getDiffPreferences = getDiffPreferences;
     this.setDiffPreferences = setDiffPreferences;
     this.getServerInfo = getServerInfo;
+    this.checkConsistency = checkConsistency;
   }
 
   @Override
@@ -104,4 +111,13 @@
       throw new RestApiException("Cannot set default diff preferences", e);
     }
   }
+
+  @Override
+  public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException {
+    try {
+      return checkConsistency.get().apply(new ConfigResource(), in);
+    } catch (IOException e) {
+      throw new RestApiException("Cannot check consistency", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
index 1d725a8..6eef5e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.api.groups;
 
 import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
 
 import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -32,6 +31,9 @@
 import com.google.gerrit.server.group.GroupsCollection;
 import com.google.gerrit.server.group.ListGroups;
 import com.google.gerrit.server.group.QueryGroups;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -49,6 +51,7 @@
   private final Provider<ListGroups> listGroups;
   private final Provider<QueryGroups> queryGroups;
   private final Provider<CurrentUser> user;
+  private final PermissionBackend permissionBackend;
   private final CreateGroup.Factory createGroup;
   private final GroupApiImpl.Factory api;
 
@@ -60,6 +63,7 @@
       Provider<ListGroups> listGroups,
       Provider<QueryGroups> queryGroups,
       Provider<CurrentUser> user,
+      PermissionBackend permissionBackend,
       CreateGroup.Factory createGroup,
       GroupApiImpl.Factory api) {
     this.accounts = accounts;
@@ -68,6 +72,7 @@
     this.listGroups = listGroups;
     this.queryGroups = queryGroups;
     this.user = user;
+    this.permissionBackend = permissionBackend;
     this.createGroup = createGroup;
     this.api = api;
   }
@@ -89,11 +94,12 @@
     if (checkNotNull(in, "GroupInput").name == null) {
       throw new BadRequestException("GroupInput must specify name");
     }
-    checkRequiresCapability(user, null, CreateGroup.class);
     try {
-      GroupInfo info = createGroup.create(in.name).apply(TopLevelResource.INSTANCE, in);
+      CreateGroup impl = createGroup.create(in.name);
+      permissionBackend.user(user).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
+      GroupInfo info = impl.apply(TopLevelResource.INSTANCE, in);
       return id(info.id);
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | PermissionBackendException e) {
       throw new RestApiException("Cannot create group " + in.name, e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 025b62a..65673de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.api.projects;
 
-import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability;
-
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.api.projects.BranchApi;
@@ -39,6 +37,9 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChildProjectsCollection;
 import com.google.gerrit.server.project.CommitsCollection;
 import com.google.gerrit.server.project.CreateProject;
@@ -71,6 +72,7 @@
   }
 
   private final CurrentUser user;
+  private final PermissionBackend permissionBackend;
   private final CreateProject.Factory createProjectFactory;
   private final ProjectApiImpl.Factory projectApi;
   private final ProjectsCollection projects;
@@ -97,6 +99,7 @@
   @AssistedInject
   ProjectApiImpl(
       CurrentUser user,
+      PermissionBackend permissionBackend,
       CreateProject.Factory createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
@@ -120,6 +123,7 @@
       @Assisted ProjectResource project) {
     this(
         user,
+        permissionBackend,
         createProjectFactory,
         projectApi,
         projects,
@@ -147,6 +151,7 @@
   @AssistedInject
   ProjectApiImpl(
       CurrentUser user,
+      PermissionBackend permissionBackend,
       CreateProject.Factory createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
@@ -170,6 +175,7 @@
       @Assisted String name) {
     this(
         user,
+        permissionBackend,
         createProjectFactory,
         projectApi,
         projects,
@@ -196,6 +202,7 @@
 
   private ProjectApiImpl(
       CurrentUser user,
+      PermissionBackend permissionBackend,
       CreateProject.Factory createProjectFactory,
       ProjectApiImpl.Factory projectApi,
       ProjectsCollection projects,
@@ -219,6 +226,7 @@
       CommitApiImpl.Factory commitApi,
       String name) {
     this.user = user;
+    this.permissionBackend = permissionBackend;
     this.createProjectFactory = createProjectFactory;
     this.projectApi = projectApi;
     this.projects = projects;
@@ -257,10 +265,11 @@
       if (in.name != null && !name.equals(in.name)) {
         throw new BadRequestException("name must match input.name");
       }
-      checkRequiresCapability(user, null, CreateProject.class);
-      createProjectFactory.create(name).apply(TopLevelResource.INSTANCE, in);
+      CreateProject impl = createProjectFactory.create(name);
+      permissionBackend.user(user).checkAny(GlobalPermission.fromAnnotation(impl.getClass()));
+      impl.apply(TopLevelResource.INSTANCE, in);
       return projectApi.create(projects.parse(name));
-    } catch (IOException | ConfigInvalidException e) {
+    } catch (IOException | ConfigInvalidException | PermissionBackendException e) {
       throw new RestApiException("Cannot create project: " + e.getMessage(), e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
index af619d7..19fdcfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -30,14 +30,11 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
 import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import com.google.inject.util.Providers;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -48,6 +45,7 @@
   private final Revisions revisions;
   private final ChangeJson.Factory changeJsonFactory;
   private final ChangeResource.Factory changeResourceFactory;
+  private final UiActions uiActions;
   private final DynamicMap<RestView<ChangeResource>> changeViews;
   private final DynamicSet<ActionVisitor> visitorSet;
 
@@ -56,11 +54,13 @@
       Revisions revisions,
       ChangeJson.Factory changeJsonFactory,
       ChangeResource.Factory changeResourceFactory,
+      UiActions uiActions,
       DynamicMap<RestView<ChangeResource>> changeViews,
       DynamicSet<ActionVisitor> visitorSet) {
     this.revisions = revisions;
     this.changeJsonFactory = changeJsonFactory;
     this.changeResourceFactory = changeResourceFactory;
+    this.uiActions = uiActions;
     this.changeViews = changeViews;
     this.visitorSet = visitorSet;
   }
@@ -162,9 +162,9 @@
       return out;
     }
 
-    Provider<CurrentUser> userProvider = Providers.of(ctl.getUser());
     FluentIterable<UiAction.Description> descs =
-        UiActions.from(changeViews, changeResourceFactory.create(ctl), userProvider);
+        uiActions.from(changeViews, changeResourceFactory.create(ctl));
+
     // The followup action is a client-side only operation that does not
     // have a server side handler. It must be manually registered into the
     // resulting action map.
@@ -198,10 +198,10 @@
     if (!rsrc.getControl().getUser().isIdentifiedUser()) {
       return ImmutableMap.of();
     }
+
     Map<String, ActionInfo> out = new LinkedHashMap<>();
-    Provider<CurrentUser> userProvider = Providers.of(rsrc.getControl().getUser());
     ACTION:
-    for (UiAction.Description d : UiActions.from(revisions, rsrc, userProvider)) {
+    for (UiAction.Description d : uiActions.from(revisions, rsrc)) {
       ActionInfo actionInfo = new ActionInfo(d);
       for (ActionVisitor visitor : visitors) {
         if (!visitor.visit(d.getId(), actionInfo, changeInfo, revisionInfo)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 8ac89ae..6cb1926 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -363,9 +363,7 @@
     update.setBranch(change.getDest().get());
     update.setTopic(change.getTopic());
     update.setPsDescription(patchSetDescription);
-    if (isPrivate) {
-      update.setPrivate(isPrivate);
-    }
+    update.setPrivate(isPrivate);
 
     boolean draft = status == Change.Status.DRAFT;
     List<String> newGroups = groups;
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 c86714a..4724ea1 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
@@ -118,6 +118,7 @@
 import com.google.gerrit.server.query.QueryResult;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
+import com.google.gerrit.server.query.change.PluginDefinedAttributesFactory;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -213,6 +214,7 @@
   private boolean lazyLoad = true;
   private AccountLoader accountLoader;
   private FixInput fix;
+  private PluginDefinedAttributesFactory pluginDefinedAttributesFactory;
 
   @Inject
   ChangeJson(
@@ -276,6 +278,10 @@
     return this;
   }
 
+  public void setPluginDefinedAttributesFactory(PluginDefinedAttributesFactory pluginsFactory) {
+    this.pluginDefinedAttributesFactory = pluginsFactory;
+  }
+
   public ChangeInfo format(ChangeResource rsrc) throws OrmException {
     return format(changeDataFactory.create(db.get(), rsrc.getControl()));
   }
@@ -520,6 +526,8 @@
 
     out.labels = labelsFor(perm, ctl, cd, has(LABELS), has(DETAILED_LABELS));
     out.submitted = getSubmittedOn(cd);
+    out.plugins =
+        pluginDefinedAttributesFactory != null ? pluginDefinedAttributesFactory.create(cd) : null;
 
     if (out.labels != null && has(DETAILED_LABELS)) {
       // If limited to specific patch sets but not the current patch set, don't
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
index 3b67930..5f6923e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -17,21 +17,29 @@
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 public class Check
     implements RestReadView<ChangeResource>, RestModifyView<ChangeResource, FixInput> {
+  private final PermissionBackend permissionBackend;
+  private final Provider<CurrentUser> user;
   private final ChangeJson.Factory jsonFactory;
 
   @Inject
-  Check(ChangeJson.Factory json) {
+  Check(PermissionBackend permissionBackend, Provider<CurrentUser> user, ChangeJson.Factory json) {
+    this.permissionBackend = permissionBackend;
+    this.user = user;
     this.jsonFactory = json;
   }
 
@@ -42,12 +50,10 @@
 
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
-      throws RestApiException, OrmException {
+      throws RestApiException, OrmException, PermissionBackendException {
     ChangeControl ctl = rsrc.getControl();
-    if (!ctl.isOwner()
-        && !ctl.getProjectControl().isOwner()
-        && !ctl.getUser().getCapabilities().canMaintainServer()) {
-      throw new AuthException("Cannot fix change");
+    if (!ctl.isOwner() && !ctl.getProjectControl().isOwner()) {
+      permissionBackend.user(user).check(GlobalPermission.MAINTAIN_SERVER);
     }
     return Response.withMustRevalidate(newChangeJson().fix(input).format(rsrc));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
index 9257445..ab92281 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
@@ -18,8 +18,12 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.Index.Input;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -32,20 +36,28 @@
   public static class Input {}
 
   private final Provider<ReviewDb> db;
+  private final PermissionBackend permissionBackend;
+  private final Provider<CurrentUser> user;
   private final ChangeIndexer indexer;
 
   @Inject
-  Index(Provider<ReviewDb> db, ChangeIndexer indexer) {
+  Index(
+      Provider<ReviewDb> db,
+      PermissionBackend permissionBackend,
+      Provider<CurrentUser> user,
+      ChangeIndexer indexer) {
     this.db = db;
+    this.permissionBackend = permissionBackend;
+    this.user = user;
     this.indexer = indexer;
   }
 
   @Override
   public Response<?> apply(ChangeResource rsrc, Input input)
-      throws IOException, AuthException, OrmException {
+      throws IOException, AuthException, OrmException, PermissionBackendException {
     ChangeControl ctl = rsrc.getControl();
-    if (!ctl.isOwner() && !ctl.getUser().getCapabilities().canMaintainServer()) {
-      throw new AuthException("Only change owner or server maintainer can reindex");
+    if (!ctl.isOwner()) {
+      permissionBackend.user(user).check(GlobalPermission.MAINTAIN_SERVER);
     }
     indexer.index(db.get(), rsrc.getChange());
     return Response.none();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckConsistency.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckConsistency.java
new file mode 100644
index 0000000..f424995
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckConsistency.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.CheckAccountExternalIdsResultInfo;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+
+@Singleton
+public class CheckConsistency implements RestModifyView<ConfigResource, ConsistencyCheckInput> {
+  private final Provider<IdentifiedUser> userProvider;
+  private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
+
+  @Inject
+  CheckConsistency(
+      Provider<IdentifiedUser> currentUser,
+      ExternalIdsConsistencyChecker externalIdsConsistencyChecker) {
+    this.userProvider = currentUser;
+    this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
+  }
+
+  @Override
+  public ConsistencyCheckInfo apply(ConfigResource resource, ConsistencyCheckInput input)
+      throws RestApiException, IOException {
+    IdentifiedUser user = userProvider.get();
+    if (!user.isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    if (!user.getCapabilities().canAccessDatabase()) {
+      throw new AuthException("not allowed to run consistency checks");
+    }
+
+    if (input == null || input.checkAccountExternalIds == null) {
+      throw new BadRequestException("input required");
+    }
+
+    ConsistencyCheckInfo consistencyCheckInfo = new ConsistencyCheckInfo();
+    if (input.checkAccountExternalIds != null) {
+      consistencyCheckInfo.checkAccountExternalIdsResult =
+          new CheckAccountExternalIdsResultInfo(externalIdsConsistencyChecker.check());
+    }
+
+    return consistencyCheckInfo;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
index 5e19091..366dae1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
@@ -23,6 +23,9 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.FlushCache.Input;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -34,17 +37,20 @@
 
   public static final String WEB_SESSIONS = "web_sessions";
 
+  private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> self;
 
   @Inject
-  public FlushCache(Provider<CurrentUser> self) {
+  public FlushCache(PermissionBackend permissionBackend, Provider<CurrentUser> self) {
+    this.permissionBackend = permissionBackend;
     this.self = self;
   }
 
   @Override
-  public Response<String> apply(CacheResource rsrc, Input input) throws AuthException {
-    if (WEB_SESSIONS.equals(rsrc.getName()) && !self.get().getCapabilities().canMaintainServer()) {
-      throw new AuthException(String.format("only site maintainers can flush %s", WEB_SESSIONS));
+  public Response<String> apply(CacheResource rsrc, Input input)
+      throws AuthException, PermissionBackendException {
+    if (WEB_SESSIONS.equals(rsrc.getName())) {
+      permissionBackend.user(self).check(GlobalPermission.MAINTAIN_SERVER);
     }
 
     rsrc.getCache().invalidateAll();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index c9f3e10..9754a3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -107,6 +107,7 @@
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.events.EventsMetrics;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.AbandonOp;
 import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.git.EmailMerge;
@@ -166,6 +167,7 @@
 import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.query.change.ConflictsCacheImpl;
 import com.google.gerrit.server.ssh.SshAddressesModule;
 import com.google.gerrit.server.tools.ToolsCatalog;
@@ -292,6 +294,7 @@
     bind(AccountControl.Factory.class);
 
     install(new AuditModule());
+    bind(UiActions.class);
     install(new com.google.gerrit.server.access.Module());
     install(new com.google.gerrit.server.account.Module());
     install(new com.google.gerrit.server.api.Module());
@@ -378,6 +381,8 @@
 
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class);
+    DynamicMap.mapOf(binder(), ChangeQueryProcessor.ChangeAttributeFactory.class);
+
     install(new GitwebConfig.LegacyModule(cfg));
 
     bind(AnonymousUser.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java
index 7e9bd71..be2edfd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java
@@ -19,11 +19,13 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.TaskInfoFactory;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.ProjectTask;
 import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.IdGenerator;
@@ -41,30 +43,40 @@
 
 @Singleton
 public class ListTasks implements RestReadView<ConfigResource> {
+  private final PermissionBackend permissionBackend;
   private final WorkQueue workQueue;
   private final ProjectCache projectCache;
-  private final Provider<IdentifiedUser> self;
+  private final Provider<CurrentUser> self;
 
   @Inject
-  public ListTasks(WorkQueue workQueue, ProjectCache projectCache, Provider<IdentifiedUser> self) {
+  public ListTasks(
+      PermissionBackend permissionBackend,
+      WorkQueue workQueue,
+      ProjectCache projectCache,
+      Provider<CurrentUser> self) {
+    this.permissionBackend = permissionBackend;
     this.workQueue = workQueue;
     this.projectCache = projectCache;
     this.self = self;
   }
 
   @Override
-  public List<TaskInfo> apply(ConfigResource resource) throws AuthException {
+  public List<TaskInfo> apply(ConfigResource resource)
+      throws AuthException, PermissionBackendException {
     CurrentUser user = self.get();
     if (!user.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
 
     List<TaskInfo> allTasks = getTasks();
-    if (user.getCapabilities().canViewQueue()) {
+    try {
+      permissionBackend.user(user).check(GlobalPermission.VIEW_QUEUE);
       return allTasks;
+    } catch (AuthException e) {
+      // Fall through to filter tasks.
     }
-    Map<String, Boolean> visibilityCache = new HashMap<>();
 
+    Map<String, Boolean> visibilityCache = new HashMap<>();
     List<TaskInfo> visibleTasks = new ArrayList<>();
     for (TaskInfo task : allTasks) {
       if (task.projectName != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
index a05058e..612fea2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -36,6 +36,7 @@
     child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
     get(CONFIG_KIND, "version").to(GetVersion.class);
     get(CONFIG_KIND, "info").to(GetServerInfo.class);
+    post(CONFIG_KIND, "check").to(CheckConsistency.class);
     get(CONFIG_KIND, "preferences").to(GetPreferences.class);
     put(CONFIG_KIND, "preferences").to(SetPreferences.class);
     get(CONFIG_KIND, "preferences.diff").to(GetDiffPreferences.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
index 3cfa2b9..d08f0a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.config.PostCaches.Input;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -66,7 +67,8 @@
 
   @Override
   public Response<String> apply(ConfigResource rsrc, Input input)
-      throws AuthException, BadRequestException, UnprocessableEntityException {
+      throws AuthException, BadRequestException, UnprocessableEntityException,
+          PermissionBackendException {
     if (input == null || input.operation == null) {
       throw new BadRequestException("operation must be specified");
     }
@@ -90,7 +92,7 @@
     }
   }
 
-  private void flushAll() throws AuthException {
+  private void flushAll() throws AuthException, PermissionBackendException {
     for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
       CacheResource cacheResource =
           new CacheResource(e.getPluginName(), e.getExportName(), e.getProvider());
@@ -101,7 +103,8 @@
     }
   }
 
-  private void flush(List<String> cacheNames) throws UnprocessableEntityException, AuthException {
+  private void flush(List<String> cacheNames)
+      throws UnprocessableEntityException, AuthException, PermissionBackendException {
     List<CacheResource> cacheResources = new ArrayList<>(cacheNames.size());
 
     for (String n : cacheNames) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java
index b239856..b33b1c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java
@@ -21,10 +21,12 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.ProjectTask;
 import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
@@ -36,7 +38,8 @@
   private final DynamicMap<RestView<TaskResource>> views;
   private final ListTasks list;
   private final WorkQueue workQueue;
-  private final Provider<IdentifiedUser> self;
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
 
   @Inject
@@ -44,12 +47,14 @@
       DynamicMap<RestView<TaskResource>> views,
       ListTasks list,
       WorkQueue workQueue,
-      Provider<IdentifiedUser> self,
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
       ProjectCache projectCache) {
     this.views = views;
     this.list = list;
     this.workQueue = workQueue;
     this.self = self;
+    this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
   }
 
@@ -60,30 +65,37 @@
 
   @Override
   public TaskResource parse(ConfigResource parent, IdString id)
-      throws ResourceNotFoundException, AuthException {
+      throws ResourceNotFoundException, AuthException, PermissionBackendException {
     CurrentUser user = self.get();
     if (!user.isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
 
+    int taskId;
     try {
-      int taskId = (int) Long.parseLong(id.get(), 16);
-      Task<?> task = workQueue.getTask(taskId);
-      if (task != null) {
-        if (self.get().getCapabilities().canViewQueue()) {
-          return new TaskResource(task);
-        } else if (task instanceof ProjectTask) {
-          ProjectTask<?> projectTask = ((ProjectTask<?>) task);
-          ProjectState e = projectCache.get(projectTask.getProjectNameKey());
-          if (e != null && e.controlFor(user).isVisible()) {
-            return new TaskResource(task);
-          }
-        }
-      }
-      throw new ResourceNotFoundException(id);
+      taskId = (int) Long.parseLong(id.get(), 16);
     } catch (NumberFormatException e) {
       throw new ResourceNotFoundException(id);
     }
+
+    Task<?> task = workQueue.getTask(taskId);
+    if (task != null) {
+      try {
+        permissionBackend.user(user).check(GlobalPermission.VIEW_QUEUE);
+        return new TaskResource(task);
+      } catch (AuthException e) {
+        // Fall through and try filtering.
+      }
+
+      if (task instanceof ProjectTask) {
+        ProjectTask<?> projectTask = ((ProjectTask<?>) task);
+        ProjectState e = projectCache.get(projectTask.getProjectNameKey());
+        if (e != null && e.controlFor(user).isVisible()) {
+          return new TaskResource(task);
+        }
+      }
+    }
+    throw new ResourceNotFoundException(id);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
index 1a8a788..0467c92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.data;
 
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import java.util.List;
 
@@ -43,4 +44,5 @@
   public List<DependencyAttribute> neededBy;
   public List<SubmitRecordAttribute> submitRecords;
   public List<AccountAttribute> allReviewers;
+  public List<PluginDefinedInfo> plugins;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
index 85ee4f9..bd5d6a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -16,20 +16,27 @@
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.FluentIterable;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestCollection;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.CapabilityUtils;
+import com.google.gerrit.server.permissions.GlobalOrPluginPermission;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import java.util.Objects;
+import java.util.Set;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+@Singleton
 public class UiActions {
   private static final Logger log = LoggerFactory.getLogger(UiActions.class);
 
@@ -37,57 +44,70 @@
     return UiAction.Description::isEnabled;
   }
 
-  public static <R extends RestResource> FluentIterable<UiAction.Description> from(
-      RestCollection<?, R> collection, R resource, Provider<CurrentUser> userProvider) {
-    return from(collection.views(), resource, userProvider);
+  private final PermissionBackend permissionBackend;
+  private final Provider<CurrentUser> userProvider;
+
+  @Inject
+  UiActions(PermissionBackend permissionBackend, Provider<CurrentUser> userProvider) {
+    this.permissionBackend = permissionBackend;
+    this.userProvider = userProvider;
   }
 
-  public static <R extends RestResource> FluentIterable<UiAction.Description> from(
-      DynamicMap<RestView<R>> views, R resource, Provider<CurrentUser> userProvider) {
+  public <R extends RestResource> FluentIterable<UiAction.Description> from(
+      RestCollection<?, R> collection, R resource) {
+    return from(collection.views(), resource);
+  }
+
+  public <R extends RestResource> FluentIterable<UiAction.Description> from(
+      DynamicMap<RestView<R>> views, R resource) {
     return FluentIterable.from(views)
-        .transform(
-            (DynamicMap.Entry<RestView<R>> e) -> {
-              int d = e.getExportName().indexOf('.');
-              if (d < 0) {
-                return null;
-              }
-
-              RestView<R> view;
-              try {
-                view = e.getProvider().get();
-              } catch (RuntimeException err) {
-                log.error(
-                    String.format(
-                        "error creating view %s.%s", e.getPluginName(), e.getExportName()),
-                    err);
-                return null;
-              }
-
-              if (!(view instanceof UiAction)) {
-                return null;
-              }
-
-              try {
-                CapabilityUtils.checkRequiresCapability(
-                    userProvider, e.getPluginName(), view.getClass());
-              } catch (AuthException exc) {
-                return null;
-              }
-
-              UiAction.Description dsc = ((UiAction<R>) view).getDescription(resource);
-              if (dsc == null || !dsc.isVisible()) {
-                return null;
-              }
-
-              String name = e.getExportName().substring(d + 1);
-              PrivateInternals_UiActionDescription.setMethod(
-                  dsc, e.getExportName().substring(0, d));
-              PrivateInternals_UiActionDescription.setId(
-                  dsc, "gerrit".equals(e.getPluginName()) ? name : e.getPluginName() + '~' + name);
-              return dsc;
-            })
+        .transform((e) -> describe(e, resource))
         .filter(Objects::nonNull);
   }
 
-  private UiActions() {}
+  @Nullable
+  private <R extends RestResource> UiAction.Description describe(
+      DynamicMap.Entry<RestView<R>> e, R resource) {
+    int d = e.getExportName().indexOf('.');
+    if (d < 0) {
+      return null;
+    }
+
+    RestView<R> view;
+    try {
+      view = e.getProvider().get();
+    } catch (RuntimeException err) {
+      log.error(
+          String.format("error creating view %s.%s", e.getPluginName(), e.getExportName()), err);
+      return null;
+    }
+
+    if (!(view instanceof UiAction)) {
+      return null;
+    }
+
+    try {
+      Set<GlobalOrPluginPermission> need =
+          GlobalPermission.fromAnnotation(e.getPluginName(), view.getClass());
+      if (!need.isEmpty() && permissionBackend.user(userProvider).test(need).isEmpty()) {
+        // A permission is required, but test returned no candidates.
+        return null;
+      }
+    } catch (PermissionBackendException err) {
+      log.error(
+          String.format("exception testing view %s.%s", e.getPluginName(), e.getExportName()), err);
+      return null;
+    }
+
+    UiAction.Description dsc = ((UiAction<R>) view).getDescription(resource);
+    if (dsc == null || !dsc.isVisible()) {
+      return null;
+    }
+
+    String name = e.getExportName().substring(d + 1);
+    PrivateInternals_UiActionDescription.setMethod(dsc, e.getExportName().substring(0, d));
+    PrivateInternals_UiActionDescription.setId(
+        dsc, "gerrit".equals(e.getPluginName()) ? name : e.getPluginName() + '~' + name);
+    return dsc;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
index a368190..5c3cdf2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.auto.value.AutoValue;
+import java.util.function.IntConsumer;
 import org.eclipse.jgit.lib.Config;
 
 /**
@@ -30,29 +31,61 @@
   private static final int DEFAULT_MAX_TERMS = 1024;
 
   public static IndexConfig createDefault() {
-    return create(0, 0, DEFAULT_MAX_TERMS);
+    return builder().build();
   }
 
-  public static IndexConfig fromConfig(Config cfg) {
-    return create(
-        cfg.getInt("index", null, "maxLimit", 0),
-        cfg.getInt("index", null, "maxPages", 0),
-        cfg.getInt("index", null, "maxTerms", 0));
+  public static Builder fromConfig(Config cfg) {
+    Builder b = builder();
+    setIfPresent(cfg, "maxLimit", b::maxLimit);
+    setIfPresent(cfg, "maxPages", b::maxPages);
+    setIfPresent(cfg, "maxTerms", b::maxTerms);
+    return b;
   }
 
-  public static IndexConfig create(int maxLimit, int maxPages, int maxTerms) {
-    return new AutoValue_IndexConfig(
-        checkLimit(maxLimit, "maxLimit", Integer.MAX_VALUE),
-        checkLimit(maxPages, "maxPages", Integer.MAX_VALUE),
-        checkLimit(maxTerms, "maxTerms", DEFAULT_MAX_TERMS));
-  }
-
-  private static int checkLimit(int limit, String name, int defaultValue) {
-    if (limit == 0) {
-      return defaultValue;
+  private static void setIfPresent(Config cfg, String name, IntConsumer setter) {
+    int n = cfg.getInt("index", null, name, 0);
+    if (n != 0) {
+      setter.accept(n);
     }
+  }
+
+  public static Builder builder() {
+    return new AutoValue_IndexConfig.Builder()
+        .maxLimit(Integer.MAX_VALUE)
+        .maxPages(Integer.MAX_VALUE)
+        .maxTerms(DEFAULT_MAX_TERMS)
+        .separateChangeSubIndexes(false);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder maxLimit(int maxLimit);
+
+    abstract int maxLimit();
+
+    public abstract Builder maxPages(int maxPages);
+
+    abstract int maxPages();
+
+    public abstract Builder maxTerms(int maxTerms);
+
+    abstract int maxTerms();
+
+    public abstract Builder separateChangeSubIndexes(boolean separate);
+
+    abstract IndexConfig autoBuild();
+
+    public IndexConfig build() {
+      IndexConfig cfg = autoBuild();
+      checkLimit(cfg.maxLimit(), "maxLimit");
+      checkLimit(cfg.maxPages(), "maxPages");
+      checkLimit(cfg.maxTerms(), "maxTerms");
+      return cfg;
+    }
+  }
+
+  private static void checkLimit(int limit, String name) {
     checkArgument(limit > 0, "%s must be positive: %s", name, limit);
-    return limit;
   }
 
   /**
@@ -71,4 +104,9 @@
    *     for performance reasons.
    */
   public abstract int maxTerms();
+
+  /**
+   * @return whether different subsets of changes may be stored in different physical sub-indexes.
+   */
+  public abstract boolean separateChangeSubIndexes();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index afa0617..4ecd684 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -455,6 +455,15 @@
     return draftIds;
   }
 
+  public void flush() throws IOException {
+    if (changeRepo != null) {
+      changeRepo.flush();
+    }
+    if (allUsersRepo != null) {
+      allUsersRepo.flush();
+    }
+  }
+
   @Nullable
   public BatchRefUpdate execute() throws OrmException, IOException {
     return execute(false);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalOrPluginPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalOrPluginPermission.java
new file mode 100644
index 0000000..d2198d3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalOrPluginPermission.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+/** A {@link GlobalPermission} or a {@link PluginPermission}. */
+public interface GlobalOrPluginPermission {
+  /** @return name used in {@code project.config} permissions. */
+  public String permissionName();
+
+  /** @return readable identifier of this permission for exception message. */
+  public String describeForException();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java
index 575a08b..4111253 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java
@@ -14,10 +14,22 @@
 
 package com.google.gerrit.server.permissions;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.CapabilityScope;
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import java.lang.annotation.Annotation;
+import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.Locale;
+import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-public enum GlobalPermission {
+/** Global server permissions built into Gerrit. */
+public enum GlobalPermission implements GlobalOrPluginPermission {
   ACCESS_DATABASE(GlobalCapability.ACCESS_DATABASE),
   ADMINISTRATE_SERVER(GlobalCapability.ADMINISTRATE_SERVER),
   CREATE_ACCOUNT(GlobalCapability.CREATE_ACCOUNT),
@@ -37,6 +49,63 @@
   VIEW_PLUGINS(GlobalCapability.VIEW_PLUGINS),
   VIEW_QUEUE(GlobalCapability.VIEW_QUEUE);
 
+  private static final Logger log = LoggerFactory.getLogger(GlobalPermission.class);
+  private static final ImmutableMap<String, GlobalPermission> BY_NAME;
+
+  static {
+    ImmutableMap.Builder<String, GlobalPermission> m = ImmutableMap.builder();
+    for (GlobalPermission p : values()) {
+      m.put(p.permissionName(), p);
+    }
+    BY_NAME = m.build();
+  }
+
+  @Nullable
+  public static GlobalPermission byName(String name) {
+    return BY_NAME.get(name);
+  }
+
+  /**
+   * Extracts the {@code @RequiresCapability} or {@code @RequiresAnyCapability} annotation.
+   *
+   * @param pluginName name of the declaring plugin. May be {@code null} or {@code "gerrit"} for
+   *     classes originating from the core server.
+   * @param clazz target class to extract annotation from.
+   * @return empty set if no annotations were found, or a collection of permissions, any of which
+   *     are suitable to enable access.
+   * @throws PermissionBackendException the annotation could not be parsed.
+   */
+  public static Set<GlobalOrPluginPermission> fromAnnotation(
+      @Nullable String pluginName, Class<?> clazz) throws PermissionBackendException {
+    RequiresCapability rc = findAnnotation(clazz, RequiresCapability.class);
+    RequiresAnyCapability rac = findAnnotation(clazz, RequiresAnyCapability.class);
+    if (rc != null && rac != null) {
+      log.error(
+          String.format(
+              "Class %s uses both @%s and @%s",
+              clazz.getName(),
+              RequiresCapability.class.getSimpleName(),
+              RequiresAnyCapability.class.getSimpleName()));
+      throw new PermissionBackendException("cannot extract permission");
+    } else if (rc != null) {
+      return Collections.singleton(
+          resolve(pluginName, rc.value(), rc.scope(), clazz, RequiresCapability.class));
+    } else if (rac != null) {
+      Set<GlobalOrPluginPermission> r = new LinkedHashSet<>();
+      for (String capability : rac.value()) {
+        r.add(resolve(pluginName, capability, rac.scope(), clazz, RequiresAnyCapability.class));
+      }
+      return Collections.unmodifiableSet(r);
+    } else {
+      return Collections.emptySet();
+    }
+  }
+
+  public static Set<GlobalOrPluginPermission> fromAnnotation(Class<?> clazz)
+      throws PermissionBackendException {
+    return fromAnnotation(null, clazz);
+  }
+
   private final String name;
 
   GlobalPermission(String name) {
@@ -44,11 +113,54 @@
   }
 
   /** @return name used in {@code project.config} permissions. */
+  @Override
   public String permissionName() {
     return name;
   }
 
+  @Override
   public String describeForException() {
     return toString().toLowerCase(Locale.US).replace('_', ' ');
   }
+
+  private static GlobalOrPluginPermission resolve(
+      @Nullable String pluginName,
+      String capability,
+      CapabilityScope scope,
+      Class<?> clazz,
+      Class<?> annotationClass)
+      throws PermissionBackendException {
+    if (pluginName != null
+        && !"gerrit".equals(pluginName)
+        && (scope == CapabilityScope.PLUGIN || scope == CapabilityScope.CONTEXT)) {
+      return new PluginPermission(pluginName, capability);
+    }
+
+    if (scope == CapabilityScope.PLUGIN) {
+      log.error(
+          String.format(
+              "Class %s uses @%s(scope=%s), but is not within a plugin",
+              clazz.getName(), annotationClass.getSimpleName(), scope.name()));
+      throw new PermissionBackendException("cannot extract permission");
+    }
+
+    GlobalPermission perm = byName(capability);
+    if (perm == null) {
+      log.error(
+          String.format("Class %s requires unknown capability %s", clazz.getName(), capability));
+      throw new PermissionBackendException("cannot extract permission");
+    }
+    return perm;
+  }
+
+  @Nullable
+  private static <T extends Annotation> T findAnnotation(Class<?> clazz, Class<T> annotation) {
+    for (; clazz != null; clazz = clazz.getSuperclass()) {
+      T t = clazz.getAnnotation(annotation);
+      if (t != null) {
+        return t;
+      }
+    }
+    return null;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
index b3a858c..83b9182 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -31,6 +31,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.Iterator;
 import java.util.Set;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -134,18 +135,47 @@
     }
 
     /** Verify scoped user can {@code perm}, throwing if denied. */
-    public abstract void check(GlobalPermission perm)
+    public abstract void check(GlobalOrPluginPermission perm)
         throws AuthException, PermissionBackendException;
 
-    /** Filter {@code permSet} to permissions scoped user might be able to perform. */
-    public abstract Set<GlobalPermission> test(Collection<GlobalPermission> permSet)
-        throws PermissionBackendException;
-
-    public boolean test(GlobalPermission perm) throws PermissionBackendException {
-      return test(EnumSet.of(perm)).contains(perm);
+    /**
+     * Verify scoped user can perform at least one listed permission.
+     *
+     * <p>If {@code any} is empty, the method completes normally and allows the caller to continue.
+     * Since no permissions were supplied to check, its assumed no permissions are necessary to
+     * continue with the caller's operation.
+     *
+     * <p>If the user has at least one of the permissions in {@code any}, the method completes
+     * normally, possibly without checking all listed permissions.
+     *
+     * <p>If {@code any} is non-empty and the user has none, {@link AuthException} is thrown for one
+     * of the failed permissions.
+     *
+     * @param any set of permissions to check.
+     */
+    public void checkAny(Set<GlobalOrPluginPermission> any)
+        throws PermissionBackendException, AuthException {
+      for (Iterator<GlobalOrPluginPermission> itr = any.iterator(); itr.hasNext(); ) {
+        try {
+          check(itr.next());
+          return;
+        } catch (AuthException err) {
+          if (!itr.hasNext()) {
+            throw err;
+          }
+        }
+      }
     }
 
-    public boolean testOrFalse(GlobalPermission perm) {
+    /** Filter {@code permSet} to permissions scoped user might be able to perform. */
+    public abstract <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet)
+        throws PermissionBackendException;
+
+    public boolean test(GlobalOrPluginPermission perm) throws PermissionBackendException {
+      return test(Collections.singleton(perm)).contains(perm);
+    }
+
+    public boolean testOrFalse(GlobalOrPluginPermission perm) {
       try {
         return test(perm);
       } catch (PermissionBackendException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PluginPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PluginPermission.java
new file mode 100644
index 0000000..6d503df
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PluginPermission.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+
+/** A global capability type permission used by a plugin. */
+public class PluginPermission implements GlobalOrPluginPermission {
+  private final String pluginName;
+  private final String capability;
+
+  public PluginPermission(String pluginName, String capability) {
+    this.pluginName = checkNotNull(pluginName, "pluginName");
+    this.capability = checkNotNull(capability, "capability");
+  }
+
+  public String pluginName() {
+    return pluginName;
+  }
+
+  public String capability() {
+    return capability;
+  }
+
+  @Override
+  public String permissionName() {
+    return pluginName + '-' + capability;
+  }
+
+  @Override
+  public String describeForException() {
+    return capability + " for plugin " + pluginName;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(pluginName, capability);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other instanceof PluginPermission) {
+      PluginPermission b = (PluginPermission) other;
+      return pluginName.equals(b.pluginName) && capability.equals(b.capability);
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return "PluginPermission[plugin=" + pluginName + ", capability=" + capability + ']';
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DelegatingClassLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DelegatingClassLoader.java
new file mode 100644
index 0000000..3908b72
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DelegatingClassLoader.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.plugins;
+
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+
+public class DelegatingClassLoader extends ClassLoader {
+  public ClassLoader target;
+
+  public DelegatingClassLoader(ClassLoader parent, ClassLoader target) {
+    super(parent);
+    this.target = target;
+  }
+
+  @Override
+  public Class<?> findClass(String name) throws ClassNotFoundException {
+    String path = name.replace('.', '/') + ".class";
+    InputStream resource = target.getResourceAsStream(path);
+    if (resource != null) {
+      try {
+        byte[] bytes = ByteStreams.toByteArray(resource);
+        return defineClass(name, bytes, 0, bytes.length);
+      } catch (IOException e) {
+      }
+    }
+    throw new ClassNotFoundException(name);
+  }
+
+  @Override
+  public URL getResource(String name) {
+    URL rtn = getParent().getResource(name);
+    if (rtn == null) {
+      rtn = target.getResource(name);
+    }
+    return rtn;
+  }
+
+  @Override
+  public Enumeration<URL> getResources(String name) throws IOException {
+    Enumeration<URL> rtn = getParent().getResources(name);
+    if (rtn == null) {
+      rtn = target.getResources(name);
+    }
+    return rtn;
+  }
+
+  @Override
+  public InputStream getResourceAsStream(String name) {
+    InputStream rtn = getParent().getResourceAsStream(name);
+    if (rtn == null) {
+      rtn = target.getResourceAsStream(name);
+    }
+    return rtn;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
index 0dcb5f8..4f83d5f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.TransferConfig;
-import com.google.inject.util.Providers;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -45,6 +44,7 @@
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsName allProjects,
+      UiActions uiActions,
       DynamicMap<RestView<ProjectResource>> views) {
     ProjectState projectState = control.getProjectState();
     Project p = control.getProject();
@@ -126,8 +126,7 @@
         getPluginConfig(control.getProjectState(), pluginConfigEntries, cfgFactory, allProjects);
 
     actions = new TreeMap<>();
-    for (UiAction.Description d :
-        UiActions.from(views, new ProjectResource(control), Providers.of(control.getUser()))) {
+    for (UiAction.Description d : uiActions.from(views, new ProjectResource(control))) {
       actions.put(d.getId(), new ActionInfo(d));
     }
     this.theme = projectState.getTheme();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
index a8f9efa..feaaccc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
@@ -16,11 +16,12 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.FailedPermissionBackend;
-import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.GlobalOrPluginPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
@@ -65,17 +66,18 @@
     }
 
     @Override
-    public void check(GlobalPermission perm) throws AuthException, PermissionBackendException {
+    public void check(GlobalOrPluginPermission perm)
+        throws AuthException, PermissionBackendException {
       if (!can(perm)) {
         throw new AuthException(perm.describeForException() + " not permitted");
       }
     }
 
     @Override
-    public Set<GlobalPermission> test(Collection<GlobalPermission> permSet)
+    public <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet)
         throws PermissionBackendException {
-      EnumSet<GlobalPermission> ok = EnumSet.noneOf(GlobalPermission.class);
-      for (GlobalPermission perm : permSet) {
+      Set<T> ok = newSet(permSet);
+      for (T perm : permSet) {
         if (can(perm)) {
           ok.add(perm);
         }
@@ -83,8 +85,18 @@
       return ok;
     }
 
-    private boolean can(GlobalPermission perm) throws PermissionBackendException {
+    private boolean can(GlobalOrPluginPermission perm) throws PermissionBackendException {
       return user.getCapabilities().doCanForDefaultPermissionBackend(perm);
     }
   }
+
+  private static <T extends GlobalOrPluginPermission> Set<T> newSet(Collection<T> permSet) {
+    if (permSet instanceof EnumSet) {
+      @SuppressWarnings({"unchecked", "rawtypes"})
+      Set<T> s = ((EnumSet) permSet).clone();
+      s.clear();
+      return s;
+    }
+    return Sets.newHashSetWithExpectedSize(permSet.size());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
index 8192e29..b1ba281 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -33,6 +34,7 @@
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final PluginConfigFactory cfgFactory;
   private final AllProjectsName allProjects;
+  private final UiActions uiActions;
   private final DynamicMap<RestView<ProjectResource>> views;
 
   @Inject
@@ -42,12 +44,14 @@
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsName allProjects,
+      UiActions uiActions,
       DynamicMap<RestView<ProjectResource>> views) {
     this.serverEnableSignedPush = serverEnableSignedPush;
     this.config = config;
     this.pluginConfigEntries = pluginConfigEntries;
     this.allProjects = allProjects;
     this.cfgFactory = cfgFactory;
+    this.uiActions = uiActions;
     this.views = views;
   }
 
@@ -60,6 +64,7 @@
         pluginConfigEntries,
         cfgFactory,
         allProjects,
+        uiActions,
         views);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index a5b6458..09a6b86 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
-import com.google.inject.util.Providers;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -48,6 +47,7 @@
 public class ListBranches implements RestReadView<ProjectResource> {
   private final GitRepositoryManager repoManager;
   private final DynamicMap<RestView<BranchResource>> branchViews;
+  private final UiActions uiActions;
   private final WebLinks webLinks;
 
   @Option(
@@ -99,9 +99,11 @@
   public ListBranches(
       GitRepositoryManager repoManager,
       DynamicMap<RestView<BranchResource>> branchViews,
+      UiActions uiActions,
       WebLinks webLinks) {
     this.repoManager = repoManager;
     this.branchViews = branchViews;
+    this.uiActions = uiActions;
     this.webLinks = webLinks;
   }
 
@@ -197,16 +199,15 @@
     info.ref = ref.getName();
     info.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null;
     info.canDelete = !targets.contains(ref.getName()) && refControl.canDelete() ? true : null;
-    for (UiAction.Description d :
-        UiActions.from(
-            branchViews,
-            new BranchResource(refControl.getProjectControl(), info),
-            Providers.of(refControl.getUser()))) {
+
+    BranchResource rsrc = new BranchResource(refControl.getProjectControl(), info);
+    for (UiAction.Description d : uiActions.from(branchViews, rsrc)) {
       if (info.actions == null) {
         info.actions = new TreeMap<>();
       }
       info.actions.put(d.getId(), new ActionInfo(d));
     }
+
     List<WebLinkInfo> links =
         webLinks.getBranchLinks(
             refControl.getProjectControl().getProject().getName(), ref.getName());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index 8705f3b..806c01a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.TransferConfig;
@@ -64,6 +65,7 @@
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final PluginConfigFactory cfgFactory;
   private final AllProjectsName allProjects;
+  private final UiActions uiActions;
   private final DynamicMap<RestView<ProjectResource>> views;
   private final Provider<CurrentUser> user;
 
@@ -77,6 +79,7 @@
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsName allProjects,
+      UiActions uiActions,
       DynamicMap<RestView<ProjectResource>> views,
       Provider<CurrentUser> user) {
     this.serverEnableSignedPush = serverEnableSignedPush;
@@ -87,6 +90,7 @@
     this.pluginConfigEntries = pluginConfigEntries;
     this.cfgFactory = cfgFactory;
     this.allProjects = allProjects;
+    this.uiActions = uiActions;
     this.views = views;
     this.user = user;
   }
@@ -185,6 +189,7 @@
           pluginConfigEntries,
           cfgFactory,
           allProjects,
+          uiActions,
           views);
     } catch (RepositoryNotFoundException notFound) {
       throw new ResourceNotFoundException(projectName.get());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java
index 6627687..2abcd58 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java
@@ -16,25 +16,25 @@
 
 /** Predicate to filter a field by matching integer value. */
 public abstract class IntPredicate<T> extends OperatorPredicate<T> {
-  private final int value;
+  private final int intValue;
 
   public IntPredicate(final String name, final String value) {
     super(name, value);
-    this.value = Integer.parseInt(value);
+    this.intValue = Integer.parseInt(value);
   }
 
-  public IntPredicate(final String name, final int value) {
-    super(name, String.valueOf(value));
-    this.value = value;
+  public IntPredicate(final String name, final int intValue) {
+    super(name, String.valueOf(intValue));
+    this.intValue = intValue;
   }
 
   public int intValue() {
-    return value;
+    return intValue;
   }
 
   @Override
   public int hashCode() {
-    return getOperator().hashCode() * 31 + value;
+    return getOperator().hashCode() * 31 + intValue;
   }
 
   @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 96a30ee..9413c5d 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
@@ -18,10 +18,10 @@
 
 /** Predicate to filter a field by matching value. */
 public abstract class OperatorPredicate<T> extends Predicate<T> {
-  private final String name;
-  private final String value;
+  protected final String name;
+  protected final String value;
 
-  protected OperatorPredicate(final String name, final String value) {
+  public 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/account/AccountIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
index 0a74647..e5ed44d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
@@ -20,9 +20,9 @@
 import com.google.gwtorm.server.OrmException;
 
 public class AccountIsVisibleToPredicate extends IsVisibleToPredicate<AccountState> {
-  private final AccountControl accountControl;
+  protected final AccountControl accountControl;
 
-  AccountIsVisibleToPredicate(AccountControl accountControl) {
+  public 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/change/AddedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java
index b3cdd6a..05bf24bd2 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
@@ -19,7 +19,7 @@
 import com.google.gwtorm.server.OrmException;
 
 public class AddedPredicate extends IntegerRangeChangePredicate {
-  AddedPredicate(String value) throws QueryParseException {
+  public AddedPredicate(String value) throws QueryParseException {
     super(ChangeField.ADDED, value);
   }
 
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 7d51217..b9c4694 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
@@ -20,9 +20,9 @@
 import java.util.Date;
 
 public class AfterPredicate extends TimestampRangeChangePredicate {
-  private final Date cut;
+  protected final Date cut;
 
-  AfterPredicate(String value) throws QueryParseException {
+  public AfterPredicate(String value) throws QueryParseException {
     super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_BEFORE, value);
     cut = parse(value);
   }
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 0cd76bb..a5f4965 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
@@ -25,9 +25,9 @@
 import java.sql.Timestamp;
 
 public class AgePredicate extends TimestampRangeChangePredicate {
-  private final long cut;
+  protected final long cut;
 
-  AgePredicate(String value) {
+  public AgePredicate(String value) {
     super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_AGE, value);
 
     long s = ConfigUtil.getTimeUnit(getValue(), 0, SECONDS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java
index 38622ed..848fd09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java
@@ -18,10 +18,10 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class AssigneePredicate extends ChangeIndexPredicate {
-  private final Account.Id id;
+public class AssigneePredicate extends ChangeIndexPredicate {
+  protected final Account.Id id;
 
-  AssigneePredicate(Account.Id id) {
+  public AssigneePredicate(Account.Id id) {
     super(ChangeField.ASSIGNEE, id.toString());
     this.id = id;
   }
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 dccd17e..3ee3352 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
@@ -22,7 +22,7 @@
 import java.io.IOException;
 
 public class AuthorPredicate extends ChangeIndexPredicate {
-  AuthorPredicate(String value) {
+  public 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 9e443c9..bc57f15 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
@@ -20,9 +20,9 @@
 import java.util.Date;
 
 public class BeforePredicate extends TimestampRangeChangePredicate {
-  private final Date cut;
+  protected final Date cut;
 
-  BeforePredicate(String value) throws QueryParseException {
+  public BeforePredicate(String value) throws QueryParseException {
     super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_BEFORE, value);
     cut = parse(value);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BooleanPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BooleanPredicate.java
index ad43c5f..31e3ee1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BooleanPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BooleanPredicate.java
@@ -18,10 +18,10 @@
 import com.google.gerrit.server.index.FieldDef.FillArgs;
 import com.google.gwtorm.server.OrmException;
 
-class BooleanPredicate extends ChangeIndexPredicate {
-  private final FillArgs args;
+public class BooleanPredicate extends ChangeIndexPredicate {
+  protected final FillArgs args;
 
-  BooleanPredicate(FieldDef<ChangeData, String> field, FillArgs args) {
+  public BooleanPredicate(FieldDef<ChangeData, String> field, FillArgs args) {
     super(field, "1");
     this.args = args;
   }
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 85d433a..d541d18 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
@@ -19,8 +19,8 @@
 import com.google.gwtorm.server.OrmException;
 
 /** Predicate over Change-Id strings (aka Change.Key). */
-class ChangeIdPredicate extends ChangeIndexPredicate {
-  ChangeIdPredicate(String id) {
+public class ChangeIdPredicate extends ChangeIndexPredicate {
+  public ChangeIdPredicate(String id) {
     super(ChangeField.ID, ChangeQueryBuilder.FIELD_CHANGE, id);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 8db62a7..632ec04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -24,13 +24,13 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> {
-  private final Provider<ReviewDb> db;
-  private final ChangeNotes.Factory notesFactory;
-  private final ChangeControl.GenericFactory changeControl;
-  private final CurrentUser user;
+public class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> {
+  protected final Provider<ReviewDb> db;
+  protected final ChangeNotes.Factory notesFactory;
+  protected final ChangeControl.GenericFactory changeControl;
+  protected final CurrentUser user;
 
-  ChangeIsVisibleToPredicate(
+  public ChangeIsVisibleToPredicate(
       Provider<ReviewDb> db,
       ChangeNotes.Factory notesFactory,
       ChangeControl.GenericFactory changeControlFactory,
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 91a37d5..efe44fa 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,6 +17,8 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_LIMIT;
 
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.index.IndexConfig;
@@ -32,12 +34,26 @@
 import com.google.gerrit.server.query.QueryProcessor;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
-public class ChangeQueryProcessor extends QueryProcessor<ChangeData> {
+public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
+    implements PluginDefinedAttributesFactory {
+  /**
+   * Register a ChangeAttributeFactory in a config Module like this:
+   *
+   * <p>bind(ChangeAttributeFactory.class) .annotatedWith(Exports.named("export-name"))
+   * .to(YourClass.class);
+   */
+  public interface ChangeAttributeFactory {
+    PluginDefinedInfo create(ChangeData a, ChangeQueryProcessor qp, String plugin);
+  }
+
   private final Provider<ReviewDb> db;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangeNotes.Factory notesFactory;
+  private final DynamicMap<ChangeAttributeFactory> attributeFactories;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -55,7 +71,8 @@
       ChangeIndexRewriter rewriter,
       Provider<ReviewDb> db,
       ChangeControl.GenericFactory changeControlFactory,
-      ChangeNotes.Factory notesFactory) {
+      ChangeNotes.Factory notesFactory,
+      DynamicMap<ChangeAttributeFactory> attributeFactories) {
     super(
         userProvider,
         metrics,
@@ -67,6 +84,7 @@
     this.db = db;
     this.changeControlFactory = changeControlFactory;
     this.notesFactory = notesFactory;
+    this.attributeFactories = attributeFactories;
   }
 
   @Override
@@ -82,6 +100,30 @@
   }
 
   @Override
+  public List<PluginDefinedInfo> create(ChangeData cd) {
+    List<PluginDefinedInfo> plugins = new ArrayList<>(attributeFactories.plugins().size());
+    for (String plugin : attributeFactories.plugins()) {
+      for (Provider<ChangeAttributeFactory> provider :
+          attributeFactories.byPlugin(plugin).values()) {
+        PluginDefinedInfo pda = null;
+        try {
+          pda = provider.get().create(cd, this, plugin);
+        } catch (RuntimeException e) {
+          /* Eat runtime exceptions so that queries don't fail. */
+        }
+        if (pda != null) {
+          pda.name = plugin;
+          plugins.add(pda);
+        }
+      }
+    }
+    if (plugins.isEmpty()) {
+      plugins = null;
+    }
+    return plugins;
+  }
+
+  @Override
   protected Predicate<ChangeData> enforceVisibility(Predicate<ChangeData> pred) {
     return new AndChangeSource(
         pred,
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 9c16777..562608e 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
@@ -36,9 +36,9 @@
  * <p>Status names are looked up by prefix case-insensitively.
  */
 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;
+  protected static final TreeMap<String, Predicate<ChangeData>> PREDICATES;
+  protected static final Predicate<ChangeData> CLOSED;
+  protected static final Predicate<ChangeData> OPEN;
 
   static {
     PREDICATES = new TreeMap<>();
@@ -84,9 +84,9 @@
     return CLOSED;
   }
 
-  private final Change.Status status;
+  protected final Change.Status status;
 
-  ChangeStatusPredicate(Change.Status status) {
+  public ChangeStatusPredicate(Change.Status status) {
     super(ChangeField.STATUS, canonicalize(status));
     this.status = status;
   }
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 668c6f2..7ad7afe 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
@@ -21,10 +21,10 @@
 import com.google.gwtorm.server.OrmException;
 import java.util.Objects;
 
-class CommentByPredicate extends ChangeIndexPredicate {
-  private final Account.Id id;
+public class CommentByPredicate extends ChangeIndexPredicate {
+  protected final Account.Id id;
 
-  CommentByPredicate(Account.Id id) {
+  public CommentByPredicate(Account.Id id) {
     super(ChangeField.COMMENTBY, id.toString());
     this.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 4779a16..85efe90 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
@@ -21,10 +21,10 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 
-class CommentPredicate extends ChangeIndexPredicate {
-  private final ChangeIndex index;
+public class CommentPredicate extends ChangeIndexPredicate {
+  protected final ChangeIndex index;
 
-  CommentPredicate(ChangeIndex index, String value) {
+  public CommentPredicate(ChangeIndex index, String value) {
     super(ChangeField.COMMENT, value);
     this.index = index;
   }
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 1188d5d..3fac217 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
@@ -22,7 +22,7 @@
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gwtorm.server.OrmException;
 
-class CommitPredicate extends ChangeIndexPredicate {
+public class CommitPredicate extends ChangeIndexPredicate {
   static FieldDef<ChangeData, ?> commitField(String id) {
     if (id.length() == OBJECT_ID_STRING_LENGTH) {
       return EXACT_COMMIT;
@@ -30,7 +30,7 @@
     return COMMIT;
   }
 
-  CommitPredicate(String id) {
+  public CommitPredicate(String id) {
     super(commitField(id), id);
   }
 
@@ -45,7 +45,7 @@
     return false;
   }
 
-  private boolean equals(PatchSet p, String id) {
+  protected boolean equals(PatchSet p, String id) {
     boolean exact = getField() == EXACT_COMMIT;
     String rev = p.getRevision() != null ? p.getRevision().get() : null;
     return (exact && id.equals(rev)) || (!exact && rev != null && rev.startsWith(id));
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 cd1f3b2..797cb9d 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
@@ -22,7 +22,7 @@
 import java.io.IOException;
 
 public class CommitterPredicate extends ChangeIndexPredicate {
-  CommitterPredicate(String value) {
+  public 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 9b45890..4d8c6a1 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
@@ -45,19 +45,19 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 
-class ConflictsPredicate extends OrPredicate<ChangeData> {
+public class ConflictsPredicate extends OrPredicate<ChangeData> {
   // UI code may depend on this string, so use caution when changing.
-  private static final String TOO_MANY_FILES = "too many files to find conflicts";
+  protected static final String TOO_MANY_FILES = "too many files to find conflicts";
 
-  private final String value;
+  protected final String value;
 
-  ConflictsPredicate(Arguments args, String value, List<Change> changes)
+  public ConflictsPredicate(Arguments args, String value, List<Change> changes)
       throws QueryParseException, OrmException {
     super(predicates(args, value, changes));
     this.value = value;
   }
 
-  private static List<Predicate<ChangeData>> predicates(
+  public static List<Predicate<ChangeData>> predicates(
       final Arguments args, String value, List<Change> changes)
       throws QueryParseException, OrmException {
     int indexTerms = 0;
@@ -160,7 +160,7 @@
     return changePredicates;
   }
 
-  private static List<String> listFiles(Change c, Arguments args, ChangeDataCache changeDataCache)
+  public static List<String> listFiles(Change c, Arguments args, ChangeDataCache changeDataCache)
       throws OrmException {
     try (Repository repo = args.repoManager.openRepository(c.getProject());
         RevWalk rw = new RevWalk(repo)) {
@@ -200,17 +200,17 @@
     return ChangeQueryBuilder.FIELD_CONFLICTS + ":" + value;
   }
 
-  private static class ChangeDataCache {
-    private final Change change;
-    private final Provider<ReviewDb> db;
-    private final ChangeData.Factory changeDataFactory;
-    private final ProjectCache projectCache;
+  public static class ChangeDataCache {
+    protected final Change change;
+    protected final Provider<ReviewDb> db;
+    protected final ChangeData.Factory changeDataFactory;
+    protected final ProjectCache projectCache;
 
-    private ObjectId testAgainst;
-    private ProjectState projectState;
-    private Iterable<ObjectId> alreadyAccepted;
+    protected ObjectId testAgainst;
+    protected ProjectState projectState;
+    protected Iterable<ObjectId> alreadyAccepted;
 
-    ChangeDataCache(
+    public ChangeDataCache(
         Change change,
         Provider<ReviewDb> db,
         ChangeData.Factory changeDataFactory,
@@ -221,7 +221,7 @@
       this.projectCache = projectCache;
     }
 
-    ObjectId getTestAgainst() throws OrmException {
+    protected ObjectId getTestAgainst() throws OrmException {
       if (testAgainst == null) {
         testAgainst =
             ObjectId.fromString(
@@ -230,7 +230,7 @@
       return testAgainst;
     }
 
-    ProjectState getProjectState() {
+    protected ProjectState getProjectState() {
       if (projectState == null) {
         projectState = projectCache.get(change.getProject());
         if (projectState == null) {
@@ -240,7 +240,7 @@
       return projectState;
     }
 
-    Iterable<ObjectId> getAlreadyAccepted(Repository repo) throws IOException {
+    protected Iterable<ObjectId> getAlreadyAccepted(Repository repo) throws IOException {
       if (alreadyAccepted == null) {
         alreadyAccepted = SubmitDryRun.getAlreadyAccepted(repo);
       }
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 9e49269..9c46da8 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
@@ -19,7 +19,7 @@
 import com.google.gwtorm.server.OrmException;
 
 public class DeletedPredicate extends IntegerRangeChangePredicate {
-  DeletedPredicate(String value) throws QueryParseException {
+  public DeletedPredicate(String value) throws QueryParseException {
     super(ChangeField.DELETED, value);
   }
 
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 ce33225..68a4b84 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
@@ -19,7 +19,7 @@
 import com.google.gwtorm.server.OrmException;
 
 public class DeltaPredicate extends IntegerRangeChangePredicate {
-  DeltaPredicate(String value) throws QueryParseException {
+  public DeltaPredicate(String value) throws QueryParseException {
     super(ChangeField.DELTA, value);
   }
 
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 809e7a1..4e8d30d 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
@@ -19,10 +19,10 @@
 import com.google.gwtorm.server.OrmException;
 import java.util.Set;
 
-class DestinationPredicate extends ChangeOperatorPredicate {
-  Set<Branch.NameKey> destinations;
+public class DestinationPredicate extends ChangeOperatorPredicate {
+  protected Set<Branch.NameKey> destinations;
 
-  DestinationPredicate(Set<Branch.NameKey> destinations, String value) {
+  public DestinationPredicate(Set<Branch.NameKey> destinations, String value) {
     super(ChangeQueryBuilder.FIELD_DESTINATION, value);
     this.destinations = destinations;
   }
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 8be5235..3238dc9 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
@@ -18,10 +18,10 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class EditByPredicate extends ChangeIndexPredicate {
-  private final Account.Id id;
+public class EditByPredicate extends ChangeIndexPredicate {
+  protected final Account.Id id;
 
-  EditByPredicate(Account.Id id) {
+  public EditByPredicate(Account.Id id) {
     super(ChangeField.EDITBY, id.toString());
     this.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 fb6c56b..66958695 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
@@ -19,8 +19,8 @@
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.OrmException;
 
-class EqualsFilePredicate extends ChangeIndexPredicate {
-  static Predicate<ChangeData> create(Arguments args, String value) {
+public class EqualsFilePredicate extends ChangeIndexPredicate {
+  public static Predicate<ChangeData> create(Arguments args, String value) {
     Predicate<ChangeData> eqPath = new EqualsPathPredicate(ChangeQueryBuilder.FIELD_FILE, value);
     if (!args.getSchema().hasField(ChangeField.FILE_PART)) {
       return eqPath;
@@ -28,11 +28,8 @@
     return Predicate.or(eqPath, new EqualsFilePredicate(value));
   }
 
-  private final String value;
-
   private EqualsFilePredicate(String value) {
     super(ChangeField.FILE_PART, ChangeQueryBuilder.FIELD_FILE, value);
-    this.value = value;
   }
 
   @Override
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 a5814fe..1917d6f 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
@@ -31,17 +31,18 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
-class EqualsLabelPredicate extends ChangeIndexPredicate {
-  private final ProjectCache projectCache;
-  private final PermissionBackend permissionBackend;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final Provider<ReviewDb> dbProvider;
-  private final String label;
-  private final int expVal;
-  private final Account.Id account;
-  private final AccountGroup.UUID group;
+public class EqualsLabelPredicate extends ChangeIndexPredicate {
+  protected final ProjectCache projectCache;
+  protected final PermissionBackend permissionBackend;
+  protected final IdentifiedUser.GenericFactory userFactory;
+  protected final Provider<ReviewDb> dbProvider;
+  protected final String label;
+  protected final int expVal;
+  protected final Account.Id account;
+  protected final AccountGroup.UUID group;
 
-  EqualsLabelPredicate(LabelPredicate.Args args, String label, int expVal, Account.Id account) {
+  public EqualsLabelPredicate(
+      LabelPredicate.Args args, String label, int expVal, Account.Id account) {
     super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account));
     this.permissionBackend = args.permissionBackend;
     this.projectCache = args.projectCache;
@@ -91,7 +92,7 @@
     return false;
   }
 
-  private static LabelType type(LabelTypes types, String toFind) {
+  protected static LabelType type(LabelTypes types, String toFind) {
     if (types.byLabel(toFind) != null) {
       return types.byLabel(toFind);
     }
@@ -104,7 +105,7 @@
     return null;
   }
 
-  private boolean match(ChangeData cd, short value, Account.Id approver, LabelType type) {
+  protected boolean match(ChangeData cd, short value, Account.Id approver, LabelType type) {
     if (value != expVal) {
       return false;
     }
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 9d841f3..56ed797 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
@@ -19,12 +19,9 @@
 import java.util.Collections;
 import java.util.List;
 
-class EqualsPathPredicate extends ChangeIndexPredicate {
-  private final String value;
-
-  EqualsPathPredicate(String fieldName, String value) {
+public class EqualsPathPredicate extends ChangeIndexPredicate {
+  public EqualsPathPredicate(String fieldName, String value) {
     super(ChangeField.PATH, fieldName, value);
-    this.value = value;
   }
 
   @Override
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 510910e..dc85ece 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
@@ -19,8 +19,8 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwtorm.server.OrmException;
 
-class ExactTopicPredicate extends ChangeIndexPredicate {
-  ExactTopicPredicate(String topic) {
+public class ExactTopicPredicate extends ChangeIndexPredicate {
+  public 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 5651544..5f3b621 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
@@ -24,10 +24,10 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 
-class FuzzyTopicPredicate extends ChangeIndexPredicate {
-  private final ChangeIndex index;
+public class FuzzyTopicPredicate extends ChangeIndexPredicate {
+  protected final ChangeIndex index;
 
-  FuzzyTopicPredicate(String topic, ChangeIndex index) {
+  public FuzzyTopicPredicate(String topic, ChangeIndex index) {
     super(FUZZY_TOPIC, topic);
     this.index = 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 54e1c97..d2645dc 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
@@ -19,8 +19,8 @@
 import com.google.gwtorm.server.OrmException;
 import java.util.List;
 
-class GroupPredicate extends ChangeIndexPredicate {
-  GroupPredicate(String group) {
+public class GroupPredicate extends ChangeIndexPredicate {
+  public GroupPredicate(String group) {
     super(ChangeField.GROUP, group);
   }
 
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 244589c..e422b74 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
@@ -18,10 +18,10 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class HasDraftByPredicate extends ChangeIndexPredicate {
-  private final Account.Id accountId;
+public class HasDraftByPredicate extends ChangeIndexPredicate {
+  protected final Account.Id accountId;
 
-  HasDraftByPredicate(Account.Id accountId) {
+  public HasDraftByPredicate(Account.Id accountId) {
     super(ChangeField.DRAFTBY, accountId.toString());
     this.accountId = 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 eb3a137..b17fffd 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
@@ -19,9 +19,9 @@
 import com.google.gwtorm.server.OrmException;
 
 public class HasStarsPredicate extends ChangeIndexPredicate {
-  private final Account.Id accountId;
+  protected final Account.Id accountId;
 
-  HasStarsPredicate(Account.Id accountId) {
+  public HasStarsPredicate(Account.Id accountId) {
     super(ChangeField.STARBY, accountId.toString());
     this.accountId = 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 4fd4156..a348d48 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
@@ -18,8 +18,8 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class HashtagPredicate extends ChangeIndexPredicate {
-  HashtagPredicate(String hashtag) {
+public class HashtagPredicate extends ChangeIndexPredicate {
+  public HashtagPredicate(String hashtag) {
     super(ChangeField.HASHTAG, HashtagsUtil.cleanupHashtag(hashtag));
   }
 
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 50e5bd9..28fb7cf 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
@@ -24,7 +24,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 public class IsMergePredicate extends ChangeOperatorPredicate {
-  private final Arguments args;
+  protected final Arguments args;
 
   public IsMergePredicate(Arguments args, String value) {
     super(ChangeQueryBuilder.FIELD_MERGE, value);
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 92de09a..8b6c8e6 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
@@ -25,14 +25,14 @@
 import java.util.List;
 import java.util.Set;
 
-class IsReviewedPredicate extends ChangeIndexPredicate {
-  private static final Account.Id NOT_REVIEWED = new Account.Id(ChangeField.NOT_REVIEWED);
+public class IsReviewedPredicate extends ChangeIndexPredicate {
+  protected static final Account.Id NOT_REVIEWED = new Account.Id(ChangeField.NOT_REVIEWED);
 
-  static Predicate<ChangeData> create() {
+  public static Predicate<ChangeData> create() {
     return Predicate.not(new IsReviewedPredicate(NOT_REVIEWED));
   }
 
-  static Predicate<ChangeData> create(Collection<Account.Id> ids) {
+  public static Predicate<ChangeData> create(Collection<Account.Id> ids) {
     List<Predicate<ChangeData>> predicates = new ArrayList<>(ids.size());
     for (Account.Id id : ids) {
       predicates.add(new IsReviewedPredicate(id));
@@ -40,7 +40,7 @@
     return Predicate.or(predicates);
   }
 
-  private final Account.Id id;
+  protected final Account.Id id;
 
   private IsReviewedPredicate(Account.Id id) {
     super(REVIEWEDBY, Integer.toString(id.get()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
index 17a6347..e9b2899 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
@@ -19,11 +19,11 @@
 import com.google.gwtorm.server.OrmException;
 
 public class IsUnresolvedPredicate extends IntegerRangeChangePredicate {
-  IsUnresolvedPredicate() throws QueryParseException {
+  public IsUnresolvedPredicate() throws QueryParseException {
     this(">0");
   }
 
-  IsUnresolvedPredicate(String value) throws QueryParseException {
+  public IsUnresolvedPredicate(String value) throws QueryParseException {
     super(ChangeField.UNRESOLVED_COMMENT_COUNT, value);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index dda834b..a1a5070 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -26,23 +26,23 @@
 import java.util.Collections;
 import java.util.List;
 
-class IsWatchedByPredicate extends AndPredicate<ChangeData> {
-  private static String describe(CurrentUser user) {
+public class IsWatchedByPredicate extends AndPredicate<ChangeData> {
+  protected static String describe(CurrentUser user) {
     if (user.isIdentifiedUser()) {
       return user.getAccountId().toString();
     }
     return user.toString();
   }
 
-  private final CurrentUser user;
+  protected final CurrentUser user;
 
-  IsWatchedByPredicate(ChangeQueryBuilder.Arguments args, boolean checkIsVisible)
+  public IsWatchedByPredicate(ChangeQueryBuilder.Arguments args, boolean checkIsVisible)
       throws QueryParseException {
     super(filters(args, checkIsVisible));
     this.user = args.getUser();
   }
 
-  private static List<Predicate<ChangeData>> filters(
+  protected static List<Predicate<ChangeData>> filters(
       ChangeQueryBuilder.Arguments args, boolean checkIsVisible) throws QueryParseException {
     List<Predicate<ChangeData>> r = new ArrayList<>();
     ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
@@ -89,7 +89,7 @@
     }
   }
 
-  private static Collection<ProjectWatchKey> getWatches(ChangeQueryBuilder.Arguments args)
+  protected static Collection<ProjectWatchKey> getWatches(ChangeQueryBuilder.Arguments args)
       throws QueryParseException {
     CurrentUser user = args.getUser();
     if (user.isIdentifiedUser()) {
@@ -98,7 +98,7 @@
     return Collections.<ProjectWatchKey>emptySet();
   }
 
-  private static List<Predicate<ChangeData>> none() {
+  protected static List<Predicate<ChangeData>> none() {
     Predicate<ChangeData> any = any();
     return ImmutableList.of(not(any));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 3fe6a6f..bd342d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -33,19 +33,19 @@
 import java.util.Set;
 
 public class LabelPredicate extends OrPredicate<ChangeData> {
-  private static final int MAX_LABEL_VALUE = 4;
+  protected static final int MAX_LABEL_VALUE = 4;
 
-  static class Args {
-    final ProjectCache projectCache;
-    final PermissionBackend permissionBackend;
-    final ChangeControl.GenericFactory ccFactory;
-    final IdentifiedUser.GenericFactory userFactory;
-    final Provider<ReviewDb> dbProvider;
-    final String value;
-    final Set<Account.Id> accounts;
-    final AccountGroup.UUID group;
+  protected static class Args {
+    protected final ProjectCache projectCache;
+    protected final PermissionBackend permissionBackend;
+    protected final ChangeControl.GenericFactory ccFactory;
+    protected final IdentifiedUser.GenericFactory userFactory;
+    protected final Provider<ReviewDb> dbProvider;
+    protected final String value;
+    protected final Set<Account.Id> accounts;
+    protected final AccountGroup.UUID group;
 
-    private Args(
+    protected Args(
         ProjectCache projectCache,
         PermissionBackend permissionBackend,
         ChangeControl.GenericFactory ccFactory,
@@ -65,21 +65,21 @@
     }
   }
 
-  private static class Parsed {
-    private final String label;
-    private final String test;
-    private final int expVal;
+  protected static class Parsed {
+    protected final String label;
+    protected final String test;
+    protected final int expVal;
 
-    private Parsed(String label, String test, int expVal) {
+    protected Parsed(String label, String test, int expVal) {
       this.label = label;
       this.test = test;
       this.expVal = expVal;
     }
   }
 
-  private final String value;
+  protected final String value;
 
-  LabelPredicate(
+  public LabelPredicate(
       ChangeQueryBuilder.Arguments a,
       String value,
       Set<Account.Id> accounts,
@@ -98,7 +98,7 @@
     this.value = value;
   }
 
-  private static List<Predicate<ChangeData>> predicates(Args args) {
+  protected static List<Predicate<ChangeData>> predicates(Args args) {
     String v = args.value;
     Parsed parsed = null;
 
@@ -138,14 +138,14 @@
     return r;
   }
 
-  private static Predicate<ChangeData> onePredicate(Args args, String label, int expVal) {
+  protected static Predicate<ChangeData> onePredicate(Args args, String label, int expVal) {
     if (expVal != 0) {
       return equalsLabelPredicate(args, label, expVal);
     }
     return noLabelQuery(args, label);
   }
 
-  private static Predicate<ChangeData> noLabelQuery(Args args, String label) {
+  protected static Predicate<ChangeData> noLabelQuery(Args args, String label) {
     List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
     for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
       r.add(equalsLabelPredicate(args, label, i));
@@ -154,7 +154,7 @@
     return not(or(r));
   }
 
-  private static Predicate<ChangeData> equalsLabelPredicate(Args args, String label, int expVal) {
+  protected static Predicate<ChangeData> equalsLabelPredicate(Args args, String label, int expVal) {
     if (args.accounts == null || args.accounts.isEmpty()) {
       return new EqualsLabelPredicate(args, label, expVal, null);
     }
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 f7f98d5..7cc8b31 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
@@ -20,7 +20,7 @@
 
 /** Predicate over change number (aka legacy ID or Change.Id). */
 public class LegacyChangeIdPredicate extends ChangeIndexPredicate {
-  private final Change.Id id;
+  protected final Change.Id id;
 
   public LegacyChangeIdPredicate(Change.Id id) {
     super(LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
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 9e525c2..92d1ed3 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
@@ -22,10 +22,10 @@
 import com.google.gwtorm.server.OrmException;
 
 /** Predicate to match changes that contains specified text in commit messages body. */
-class MessagePredicate extends ChangeIndexPredicate {
-  private final ChangeIndex index;
+public class MessagePredicate extends ChangeIndexPredicate {
+  protected final ChangeIndex index;
 
-  MessagePredicate(ChangeIndex index, String value) {
+  public MessagePredicate(ChangeIndex index, String value) {
     super(ChangeField.COMMIT_MESSAGE, value);
     this.index = index;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index cd98087..0d12132 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -313,6 +313,7 @@
       eventFactory.addDependencies(rw, c, d.change(), d.currentPatchSet());
     }
 
+    c.plugins = queryProcessor.create(d);
     return c;
   }
 
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 dfaac08..5fd1ca0 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
@@ -19,15 +19,15 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class OwnerPredicate extends ChangeIndexPredicate {
-  private final Account.Id id;
+public class OwnerPredicate extends ChangeIndexPredicate {
+  protected final Account.Id id;
 
-  OwnerPredicate(Account.Id id) {
+  public OwnerPredicate(Account.Id id) {
     super(ChangeField.OWNER, id.toString());
     this.id = id;
   }
 
-  Account.Id getAccountId() {
+  protected Account.Id getAccountId() {
     return 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 f3239af..f828970 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
@@ -19,17 +19,17 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.server.OrmException;
 
-class OwnerinPredicate extends ChangeOperatorPredicate {
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final AccountGroup.UUID uuid;
+public class OwnerinPredicate extends ChangeOperatorPredicate {
+  protected final IdentifiedUser.GenericFactory userFactory;
+  protected final AccountGroup.UUID uuid;
 
-  OwnerinPredicate(IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) {
+  public OwnerinPredicate(IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) {
     super(ChangeQueryBuilder.FIELD_OWNERIN, uuid.toString());
     this.userFactory = userFactory;
     this.uuid = uuid;
   }
 
-  AccountGroup.UUID getAccountGroupUUID() {
+  protected AccountGroup.UUID getAccountGroupUUID() {
     return uuid;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index d3a3f20..64a2fa6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -28,10 +28,10 @@
 import java.util.Collections;
 import java.util.List;
 
-class ParentProjectPredicate extends OrPredicate<ChangeData> {
-  private final String value;
+public class ParentProjectPredicate extends OrPredicate<ChangeData> {
+  protected final String value;
 
-  ParentProjectPredicate(
+  public ParentProjectPredicate(
       ProjectCache projectCache,
       Provider<ListChildProjects> listChildProjects,
       Provider<CurrentUser> self,
@@ -40,7 +40,7 @@
     this.value = value;
   }
 
-  private static List<Predicate<ChangeData>> predicates(
+  protected static List<Predicate<ChangeData>> predicates(
       ProjectCache projectCache,
       Provider<ListChildProjects> listChildProjects,
       Provider<CurrentUser> self,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
new file mode 100644
index 0000000..a795025
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.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.change;
+
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import java.util.List;
+
+public interface PluginDefinedAttributesFactory {
+  List<PluginDefinedInfo> create(ChangeData cd);
+}
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 644870d..ef25ddb 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
@@ -19,12 +19,12 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class ProjectPredicate extends ChangeIndexPredicate {
-  ProjectPredicate(String id) {
+public class ProjectPredicate extends ChangeIndexPredicate {
+  public ProjectPredicate(String id) {
     super(ChangeField.PROJECT, id);
   }
 
-  Project.NameKey getValueKey() {
+  protected Project.NameKey getValueKey() {
     return new Project.NameKey(getValue());
   }
 
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 4c06d1b..28b1302 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
@@ -18,8 +18,8 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class ProjectPrefixPredicate extends ChangeIndexPredicate {
-  ProjectPrefixPredicate(String prefix) {
+public class ProjectPrefixPredicate extends ChangeIndexPredicate {
+  public ProjectPrefixPredicate(String prefix) {
     super(ChangeField.PROJECTS, prefix);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index 7eccf45..f0ef40d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -137,13 +137,18 @@
 
     int cnt = queries.size();
     List<QueryResult<ChangeData>> results = imp.query(qb.parse(queries));
+
     boolean requireLazyLoad =
         containsAnyOf(options, ImmutableSet.of(DETAILED_LABELS, LABELS))
             && !qb.getArgs().getSchema().hasField(ChangeField.STORED_SUBMIT_RECORD_LENIENT);
+
+    ChangeJson cjson = json.create(options);
+    cjson.setPluginDefinedAttributesFactory(this.imp);
     List<List<ChangeInfo>> res =
-        json.create(options)
+        cjson
             .lazyLoad(requireLazyLoad || containsAnyOf(options, ChangeJson.REQUIRE_LAZY_LOAD))
             .formatQueryResults(results);
+
     for (int n = 0; n < cnt; n++) {
       List<ChangeInfo> info = res.get(n);
       if (results.get(n).more()) {
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 491aed9..b8bece9 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
@@ -18,8 +18,8 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class RefPredicate extends ChangeIndexPredicate {
-  RefPredicate(String ref) {
+public class RefPredicate extends ChangeIndexPredicate {
+  public 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 5b9774c..ca21247 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
@@ -19,8 +19,8 @@
 import com.google.gwtorm.server.OrmException;
 import java.util.List;
 
-class RegexPathPredicate extends ChangeRegexPredicate {
-  RegexPathPredicate(String re) {
+public class RegexPathPredicate extends ChangeRegexPredicate {
+  public 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 1284e88..cf78c57 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
@@ -21,10 +21,10 @@
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
-class RegexProjectPredicate extends ChangeRegexPredicate {
-  private final RunAutomaton pattern;
+public class RegexProjectPredicate extends ChangeRegexPredicate {
+  protected final RunAutomaton pattern;
 
-  RegexProjectPredicate(String re) {
+  public RegexProjectPredicate(String re) {
     super(ChangeField.PROJECT, re);
 
     if (re.startsWith("^")) {
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 671d4cc..ac7af9b 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
@@ -20,10 +20,10 @@
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
-class RegexRefPredicate extends ChangeRegexPredicate {
-  private final RunAutomaton pattern;
+public class RegexRefPredicate extends ChangeRegexPredicate {
+  protected final RunAutomaton pattern;
 
-  RegexRefPredicate(String re) {
+  public RegexRefPredicate(String re) {
     super(ChangeField.REF, re);
 
     if (re.startsWith("^")) {
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 a4ba059..8a9f8cd 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
@@ -21,10 +21,10 @@
 import dk.brics.automaton.RegExp;
 import dk.brics.automaton.RunAutomaton;
 
-class RegexTopicPredicate extends ChangeRegexPredicate {
-  private final RunAutomaton pattern;
+public class RegexTopicPredicate extends ChangeRegexPredicate {
+  protected final RunAutomaton pattern;
 
-  RegexTopicPredicate(String re) {
+  public RegexTopicPredicate(String re) {
     super(EXACT_TOPIC, re);
 
     if (re.startsWith("^")) {
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 5b86494..f3a8619 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
@@ -25,14 +25,14 @@
 import com.google.gwtorm.server.OrmException;
 import java.util.stream.Stream;
 
-class ReviewerPredicate extends ChangeIndexPredicate {
-  static Predicate<ChangeData> forState(
+public class ReviewerPredicate extends ChangeIndexPredicate {
+  protected static Predicate<ChangeData> forState(
       Arguments args, Account.Id id, ReviewerStateInternal state) {
     checkArgument(state != ReviewerStateInternal.REMOVED, "can't query by removed reviewer");
     return create(args, new ReviewerPredicate(state, id));
   }
 
-  static Predicate<ChangeData> reviewer(Arguments args, Account.Id id) {
+  protected static Predicate<ChangeData> reviewer(Arguments args, Account.Id id) {
     Predicate<ChangeData> p;
     if (args.notesMigration.readChanges()) {
       // With NoteDb, Reviewer/CC are clearly distinct states, so only choose reviewer.
@@ -45,14 +45,14 @@
     return create(args, p);
   }
 
-  static Predicate<ChangeData> cc(Arguments args, Account.Id id) {
+  protected static Predicate<ChangeData> cc(Arguments args, Account.Id id) {
     // As noted above, CC is nebulous without NoteDb, but it certainly doesn't make sense to return
     // Reviewers for cc:foo. Most likely this will just not match anything, but let the index sort
     // it out.
     return create(args, new ReviewerPredicate(ReviewerStateInternal.CC, id));
   }
 
-  private static Predicate<ChangeData> anyReviewerState(Account.Id id) {
+  protected static Predicate<ChangeData> anyReviewerState(Account.Id id) {
     return Predicate.or(
         Stream.of(ReviewerStateInternal.values())
             .filter(s -> s != ReviewerStateInternal.REMOVED)
@@ -60,8 +60,8 @@
             .collect(toList()));
   }
 
-  private final ReviewerStateInternal state;
-  private final Account.Id id;
+  protected final ReviewerStateInternal state;
+  protected final Account.Id id;
 
   private ReviewerPredicate(ReviewerStateInternal state, Account.Id id) {
     super(ChangeField.REVIEWER, ChangeField.getReviewerFieldValue(state, id));
@@ -69,7 +69,7 @@
     this.id = id;
   }
 
-  Account.Id getAccountId() {
+  protected Account.Id getAccountId() {
     return id;
   }
 
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 63e7859..df28de3 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
@@ -19,17 +19,17 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.server.OrmException;
 
-class ReviewerinPredicate extends ChangeOperatorPredicate {
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final AccountGroup.UUID uuid;
+public class ReviewerinPredicate extends ChangeOperatorPredicate {
+  protected final IdentifiedUser.GenericFactory userFactory;
+  protected final AccountGroup.UUID uuid;
 
-  ReviewerinPredicate(IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) {
+  public ReviewerinPredicate(IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) {
     super(ChangeQueryBuilder.FIELD_REVIEWERIN, uuid.toString());
     this.userFactory = userFactory;
     this.uuid = uuid;
   }
 
-  AccountGroup.UUID getAccountGroupUUID() {
+  protected AccountGroup.UUID getAccountGroupUUID() {
     return 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 98965bf..12d4753 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
@@ -20,10 +20,10 @@
 import com.google.gwtorm.server.OrmException;
 
 public class StarPredicate extends ChangeIndexPredicate {
-  private final Account.Id accountId;
-  private final String label;
+  protected final Account.Id accountId;
+  protected final String label;
 
-  StarPredicate(Account.Id accountId, String label) {
+  public StarPredicate(Account.Id accountId, String label) {
     super(ChangeField.STAR, StarredChangesUtil.StarField.create(accountId, label).toString());
     this.accountId = accountId;
     this.label = 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 d8d5258..5fdeb68 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
@@ -18,9 +18,8 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class SubmissionIdPredicate extends ChangeIndexPredicate {
-
-  SubmissionIdPredicate(String changeSet) {
+public class SubmissionIdPredicate extends ChangeIndexPredicate {
+  public SubmissionIdPredicate(String changeSet) {
     super(ChangeField.SUBMISSIONID, changeSet);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
index 5b01ea2..81d64e0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
@@ -23,8 +23,8 @@
 import com.google.gwtorm.server.OrmException;
 import java.util.Set;
 
-class SubmitRecordPredicate extends ChangeIndexPredicate {
-  static Predicate<ChangeData> create(
+public class SubmitRecordPredicate extends ChangeIndexPredicate {
+  public static Predicate<ChangeData> create(
       String label, SubmitRecord.Label.Status status, Set<Account.Id> accounts) {
     String lowerLabel = label.toLowerCase();
     if (accounts == null || accounts.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
index 0812c6a..df78315 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
@@ -18,10 +18,10 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gwtorm.server.OrmException;
 
-class SubmittablePredicate extends ChangeIndexPredicate {
-  private final SubmitRecord.Status status;
+public class SubmittablePredicate extends ChangeIndexPredicate {
+  protected final SubmitRecord.Status status;
 
-  SubmittablePredicate(SubmitRecord.Status status) {
+  public SubmittablePredicate(SubmitRecord.Status status) {
     super(ChangeField.SUBMIT_RECORD, status.name());
     this.status = status;
   }
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 afaea5c..6a5f260 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
@@ -24,12 +24,12 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-class TrackingIdPredicate extends ChangeIndexPredicate {
+public class TrackingIdPredicate extends ChangeIndexPredicate {
   private static final Logger log = LoggerFactory.getLogger(TrackingIdPredicate.class);
 
-  private final TrackingFooters trackingFooters;
+  protected final TrackingFooters trackingFooters;
 
-  TrackingIdPredicate(TrackingFooters trackingFooters, String trackingId) {
+  public TrackingIdPredicate(TrackingFooters trackingFooters, String trackingId) {
     super(ChangeField.TR, trackingId);
     this.trackingFooters = trackingFooters;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
index 8f72945..3ac9c39 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
@@ -23,10 +23,11 @@
 import com.google.gwtorm.server.OrmException;
 
 public class GroupIsVisibleToPredicate extends IsVisibleToPredicate<AccountGroup> {
-  private final GroupControl.GenericFactory groupControlFactory;
-  private final CurrentUser user;
+  protected final GroupControl.GenericFactory groupControlFactory;
+  protected final CurrentUser user;
 
-  GroupIsVisibleToPredicate(GroupControl.GenericFactory groupControlFactory, CurrentUser user) {
+  public GroupIsVisibleToPredicate(
+      GroupControl.GenericFactory groupControlFactory, CurrentUser user) {
     super(AccountQueryBuilder.FIELD_VISIBLETO, describe(user));
     this.groupControlFactory = groupControlFactory;
     this.user = user;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index caeba3a..b88a4b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_145> C = Schema_145.class;
+  public static final Class<Schema_148> C = Schema_148.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java
new file mode 100644
index 0000000..dd11396
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java
@@ -0,0 +1,156 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * Make sure that for every account a user branch exists that has an initial empty commit with the
+ * registration date as commit time.
+ *
+ * <p>For accounts that don't have a user branch yet the user branch is created with an initial
+ * empty commit that has the registration date as commit time.
+ *
+ * <p>For accounts that already have a user branch the user branch is rewritten and an initial empty
+ * commit with the registration date as commit time is inserted (if such a commit doesn't exist
+ * yet).
+ */
+public class Schema_146 extends SchemaVersion {
+  private static final String CREATE_ACCOUNT_MSG = "Create Account";
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+  private final PersonIdent serverIdent;
+
+  @Inject
+  Schema_146(
+      Provider<Schema_145> prior,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      @GerritPersonIdent PersonIdent serverIdent) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+    this.serverIdent = serverIdent;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+    try (Repository repo = repoManager.openRepository(allUsersName);
+        RevWalk rw = new RevWalk(repo);
+        ObjectInserter oi = repo.newObjectInserter()) {
+      ObjectId emptyTree = emptyTree(oi);
+
+      for (Account account : db.accounts().all()) {
+        String refName = RefNames.refsUsers(account.getId());
+        Ref ref = repo.exactRef(refName);
+        if (ref != null) {
+          rewriteUserBranch(repo, rw, oi, emptyTree, ref, account);
+        } else {
+          AccountsUpdate.createUserBranch(repo, oi, serverIdent, serverIdent, account);
+        }
+      }
+    } catch (IOException e) {
+      throw new OrmException("Failed to rewrite user branches.", e);
+    }
+  }
+
+  private void rewriteUserBranch(
+      Repository repo, RevWalk rw, ObjectInserter oi, ObjectId emptyTree, Ref ref, Account account)
+      throws IOException {
+    ObjectId current = createInitialEmptyCommit(oi, emptyTree, account.getRegisteredOn());
+
+    rw.reset();
+    rw.sort(RevSort.TOPO);
+    rw.sort(RevSort.REVERSE, true);
+    rw.markStart(rw.parseCommit(ref.getObjectId()));
+
+    RevCommit c;
+    while ((c = rw.next()) != null) {
+      if (isInitialEmptyCommit(emptyTree, c)) {
+        return;
+      }
+
+      CommitBuilder cb = new CommitBuilder();
+      cb.setParentId(current);
+      cb.setTreeId(c.getTree());
+      cb.setAuthor(c.getAuthorIdent());
+      cb.setCommitter(c.getCommitterIdent());
+      cb.setMessage(c.getFullMessage());
+      cb.setEncoding(c.getEncoding());
+      current = oi.insert(cb);
+    }
+
+    oi.flush();
+
+    RefUpdate ru = repo.updateRef(ref.getName());
+    ru.setExpectedOldObjectId(ref.getObjectId());
+    ru.setNewObjectId(current);
+    ru.setForceUpdate(true);
+    ru.setRefLogIdent(serverIdent);
+    ru.setRefLogMessage(getClass().getSimpleName(), true);
+    Result result = ru.update();
+    if (result != Result.FORCED) {
+      throw new IOException(
+          String.format("Failed to update ref %s: %s", ref.getName(), result.name()));
+    }
+  }
+
+  private ObjectId createInitialEmptyCommit(
+      ObjectInserter oi, ObjectId emptyTree, Timestamp registrationDate) throws IOException {
+    PersonIdent ident = new PersonIdent(serverIdent, registrationDate);
+
+    CommitBuilder cb = new CommitBuilder();
+    cb.setTreeId(emptyTree);
+    cb.setCommitter(ident);
+    cb.setAuthor(ident);
+    cb.setMessage(CREATE_ACCOUNT_MSG);
+    return oi.insert(cb);
+  }
+
+  private boolean isInitialEmptyCommit(ObjectId emptyTree, RevCommit c) {
+    return c.getParentCount() == 0
+        && c.getTree().equals(emptyTree)
+        && c.getShortMessage().equals(CREATE_ACCOUNT_MSG);
+  }
+
+  private static ObjectId emptyTree(ObjectInserter oi) throws IOException {
+    return oi.insert(Constants.OBJ_TREE, new byte[] {});
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java
new file mode 100644
index 0000000..8585988
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static java.util.stream.Collectors.toSet;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Objects;
+import java.util.Set;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/** Delete user branches for which no account exists. */
+public class Schema_147 extends SchemaVersion {
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+  private final PersonIdent serverIdent;
+
+  @Inject
+  Schema_147(
+      Provider<Schema_146> prior,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      @GerritPersonIdent PersonIdent serverIdent) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+    this.serverIdent = serverIdent;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+    try (Repository repo = repoManager.openRepository(allUsersName)) {
+      Set<Account.Id> accountIdsFromReviewDb =
+          db.accounts().all().toList().stream().map(a -> a.getId()).collect(toSet());
+      Set<Account.Id> accountIdsFromUserBranches =
+          repo.getRefDatabase()
+              .getRefs(RefNames.REFS_USERS)
+              .values()
+              .stream()
+              .map(r -> Account.Id.fromRef(r.getName()))
+              .filter(Objects::nonNull)
+              .collect(toSet());
+      accountIdsFromUserBranches.removeAll(accountIdsFromReviewDb);
+      for (Account.Id accountId : accountIdsFromUserBranches) {
+        AccountsUpdate.deleteUserBranch(repo, serverIdent, accountId);
+      }
+    } catch (IOException e) {
+      throw new OrmException("Failed to delete user branches for non-existing accounts.", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_148.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_148.java
new file mode 100644
index 0000000..abb3bb2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_148.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.primitives.Ints;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdReader;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.sql.SQLException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class Schema_148 extends SchemaVersion {
+  private static final String COMMIT_MSG = "Make account IDs of external IDs human-readable";
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_148(
+      Provider<Schema_147> prior,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+    try (Repository repo = repoManager.openRepository(allUsersName);
+        RevWalk rw = new RevWalk(repo);
+        ObjectInserter ins = repo.newObjectInserter()) {
+      ObjectId rev = ExternalIdReader.readRevision(repo);
+      NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
+      boolean dirty = false;
+      for (Note note : noteMap) {
+        byte[] raw =
+            rw.getObjectReader()
+                .open(note.getData(), OBJ_BLOB)
+                .getCachedBytes(ExternalIdReader.MAX_NOTE_SZ);
+        try {
+          ExternalId extId = ExternalId.parse(note.getName(), raw);
+
+          if (needsUpdate(extId)) {
+            ExternalIdsUpdate.upsert(rw, ins, noteMap, extId);
+            dirty = true;
+          }
+        } catch (ConfigInvalidException e) {
+          ui.message(
+              String.format("Warning: Ignoring invalid external ID note %s", note.getName()));
+        }
+      }
+      if (dirty) {
+        ExternalIdsUpdate.commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, serverUser, serverUser);
+      }
+    } catch (IOException e) {
+      throw new OrmException("Failed to update external IDs", e);
+    }
+  }
+
+  private static boolean needsUpdate(ExternalId extId) {
+    Config cfg = new Config();
+    cfg.setInt("externalId", extId.key().get(), "accountId", extId.accountId().get());
+    return Ints.tryParse(cfg.getString("externalId", extId.key().get(), "accountId")) == null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java
index e635da9..9faf628 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java
@@ -14,12 +14,11 @@
 
 package com.google.gerrit.server.update;
 
+import java.io.IOException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
-import java.io.IOException;
-
 /** Context for performing the {@link BatchUpdateOp#updateRepo} phase. */
 public interface RepoContext extends Context {
   /**
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index 1724c51..cb1d97b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -58,7 +58,7 @@
     indexes = new ChangeIndexCollection();
     indexes.setSearchIndex(index);
     queryBuilder = new FakeQueryBuilder(indexes);
-    rewrite = new ChangeIndexRewriter(indexes, IndexConfig.create(0, 0, 3));
+    rewrite = new ChangeIndexRewriter(indexes, IndexConfig.builder().maxTerms(3).build());
   }
 
   @Test
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
index 45835d9..7b7893a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -16,12 +16,16 @@
 
 import com.google.common.base.Throwables;
 import com.google.common.util.concurrent.Atomics;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.permissions.GlobalOrPluginPermission;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import java.io.IOException;
 import java.util.LinkedList;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.Environment;
@@ -30,14 +34,17 @@
 public class AliasCommand extends BaseCommand {
   private final DispatchCommandProvider root;
   private final CurrentUser currentUser;
+  private final PermissionBackend permissionBackend;
   private final CommandName command;
   private final AtomicReference<Command> atomicCmd;
 
   AliasCommand(
       @CommandName(Commands.ROOT) DispatchCommandProvider root,
+      PermissionBackend permissionBackend,
       CurrentUser currentUser,
       CommandName command) {
     this.root = root;
+    this.permissionBackend = permissionBackend;
     this.currentUser = currentUser;
     this.command = command;
     this.atomicCmd = Atomics.newReference();
@@ -47,7 +54,7 @@
   public void start(Environment env) throws IOException {
     try {
       begin(env);
-    } catch (UnloggedFailure e) {
+    } catch (Failure e) {
       String msg = e.getMessage();
       if (!msg.endsWith("\n")) {
         msg += "\n";
@@ -58,7 +65,7 @@
     }
   }
 
-  private void begin(Environment env) throws UnloggedFailure, IOException {
+  private void begin(Environment env) throws IOException, Failure {
     Map<String, CommandProvider> map = root.getMap();
     for (String name : chain(command)) {
       CommandProvider p = map.get(name);
@@ -103,17 +110,16 @@
     }
   }
 
-  private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
-    RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
-    if (rc != null) {
-      CapabilityControl ctl = currentUser.getCapabilities();
-      if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
-        String msg =
-            String.format(
-                "fatal: %s does not have \"%s\" capability.",
-                currentUser.getUserName(), rc.value());
-        throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+  private void checkRequiresCapability(Command cmd) throws Failure {
+    try {
+      Set<GlobalOrPluginPermission> check = GlobalPermission.fromAnnotation(cmd.getClass());
+      try {
+        permissionBackend.user(currentUser).checkAny(check);
+      } catch (AuthException err) {
+        throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, "fatal: " + err.getMessage());
       }
+    } catch (PermissionBackendException err) {
+      throw new Failure(1, "fatal: permissions unavailable", err);
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
index 10beb40..0ef0473 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import org.apache.sshd.server.Command;
@@ -27,6 +28,7 @@
   @CommandName(Commands.ROOT)
   private DispatchCommandProvider root;
 
+  @Inject private PermissionBackend permissionBackend;
   @Inject private CurrentUser currentUser;
 
   public AliasCommandProvider(CommandName command) {
@@ -35,6 +37,6 @@
 
   @Override
   public Command get() {
-    return new AliasCommand(root, currentUser, command);
+    return new AliasCommand(root, permissionBackend, currentUser, command);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index 8f4cfad..9971b0c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.EndOfOptionsHandler;
 import com.google.inject.Inject;
+import com.google.inject.Injector;
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.InputStream;
@@ -86,13 +87,15 @@
 
   @Inject private SshScope.Context context;
 
-  @Inject private DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
-
   /** Commands declared by a plugin can be scoped by the plugin name. */
   @Inject(optional = true)
   @PluginName
   private String pluginName;
 
+  @Inject private Injector injector;
+
+  @Inject private DynamicMap<DynamicOptions.DynamicBean> dynamicBeans = null;
+
   /** The task, as scheduled on a worker thread. */
   private final AtomicReference<Future<?>> task;
 
@@ -197,7 +200,7 @@
    */
   protected void parseCommandLine(Object options) throws UnloggedFailure {
     final CmdLineParser clp = newCmdLineParser(options);
-    DynamicOptions pluginOptions = new DynamicOptions(options, dynamicBeans);
+    DynamicOptions pluginOptions = new DynamicOptions(options, injector, dynamicBeans);
     pluginOptions.parseDynamicBeans(clp);
     pluginOptions.setDynamicBeans();
     pluginOptions.onBeanParseStart();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index 4488c71..e64ab0e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toList;
 
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -24,6 +25,9 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -43,6 +47,7 @@
   private final ReviewDb db;
   private final ChangeNotes.Factory changeNotesFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
+  private final PermissionBackend permissionBackend;
 
   @Inject
   ChangeArgumentParser(
@@ -51,13 +56,15 @@
       ChangeFinder changeFinder,
       ReviewDb db,
       ChangeNotes.Factory changeNotesFactory,
-      ChangeControl.GenericFactory changeControlFactory) {
+      ChangeControl.GenericFactory changeControlFactory,
+      PermissionBackend permissionBackend) {
     this.currentUser = currentUser;
     this.changesCollection = changesCollection;
     this.changeFinder = changeFinder;
     this.db = db;
     this.changeNotesFactory = changeNotesFactory;
     this.changeControlFactory = changeControlFactory;
+    this.permissionBackend = permissionBackend;
   }
 
   public void addChange(String id, Map<Change.Id, ChangeResource> changes)
@@ -80,9 +87,13 @@
     List<ChangeControl> matched =
         useIndex ? changeFinder.find(id, currentUser) : changeFromNotesFactory(id, currentUser);
     List<ChangeControl> toAdd = new ArrayList<>(changes.size());
-    boolean canMaintainServer =
-        currentUser.isIdentifiedUser()
-            && currentUser.asIdentifiedUser().getCapabilities().canMaintainServer();
+    boolean canMaintainServer;
+    try {
+      permissionBackend.user(currentUser).check(GlobalPermission.MAINTAIN_SERVER);
+      canMaintainServer = true;
+    } catch (AuthException | PermissionBackendException e) {
+      canMaintainServer = false;
+    }
     for (ChangeControl ctl : matched) {
       if (!changes.containsKey(ctl.getId())
           && inProject(projectControl, ctl.getProject())
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index 2f3d10f6..87e90f4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -20,8 +20,10 @@
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.CapabilityUtils;
 import com.google.gerrit.server.args4j.SubcommandHandler;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -41,6 +43,7 @@
   }
 
   private final CurrentUser currentUser;
+  private final PermissionBackend permissionBackend;
   private final Map<String, CommandProvider> commands;
   private final AtomicReference<Command> atomicCmd;
 
@@ -51,8 +54,12 @@
   private List<String> args = new ArrayList<>();
 
   @Inject
-  DispatchCommand(CurrentUser cu, @Assisted final Map<String, CommandProvider> all) {
-    currentUser = cu;
+  DispatchCommand(
+      CurrentUser user,
+      PermissionBackend permissionBackend,
+      @Assisted Map<String, CommandProvider> all) {
+    this.currentUser = user;
+    this.permissionBackend = permissionBackend;
     commands = all;
     atomicCmd = Atomics.newReference();
   }
@@ -117,9 +124,13 @@
       pluginName = ((BaseCommand) cmd).getPluginName();
     }
     try {
-      CapabilityUtils.checkRequiresCapability(currentUser, pluginName, cmd.getClass());
+      permissionBackend
+          .user(currentUser)
+          .checkAny(GlobalPermission.fromAnnotation(pluginName, cmd.getClass()));
     } catch (AuthException e) {
       throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, e.getMessage());
+    } catch (PermissionBackendException e) {
+      throw new UnloggedFailure(1, "fatal: permission check unavailable", e);
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 21bfe9b..392fd29 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.config.ListCaches;
 import com.google.gerrit.server.config.ListCaches.OutputFormat;
 import com.google.gerrit.server.config.PostCaches;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -81,6 +82,8 @@
       }
     } catch (RestApiException e) {
       throw die(e.getMessage());
+    } catch (PermissionBackendException e) {
+      throw new Failure(1, "unavailable", e);
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index fc65cf3..0ee1c28 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.Index;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -57,7 +58,7 @@
     for (ChangeResource rsrc : changes.values()) {
       try {
         index.apply(rsrc, new Index.Input());
-      } catch (IOException | RestApiException | OrmException e) {
+      } catch (IOException | RestApiException | OrmException | PermissionBackendException e) {
         ok = false;
         writeError(
             "error", String.format("failed to index change %s: %s", rsrc.getId(), e.getMessage()));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
index 4ebc568..3465a9c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.config.DeleteTask;
 import com.google.gerrit.server.config.TaskResource;
 import com.google.gerrit.server.config.TasksCollection;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -50,7 +51,7 @@
       try {
         TaskResource taskRsrc = tasksCollection.parse(cfgRsrc, IdString.fromDecoded(id));
         deleteTask.apply(taskRsrc, null);
-      } catch (AuthException | ResourceNotFoundException e) {
+      } catch (AuthException | ResourceNotFoundException | PermissionBackendException e) {
         stderr.print("kill: " + id + ": No such task\n");
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index 1192eb5..2e5bf71 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -24,7 +24,7 @@
 import org.kohsuke.args4j.Option;
 
 @CommandMetaData(name = "query", description = "Query the change database")
-class Query extends SshCommand {
+public class Query extends SshCommand {
   @Inject private OutputStreamQuery processor;
 
   @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 21591dd..bfa1a81 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.account.PutHttpPassword;
 import com.google.gerrit.server.account.PutName;
 import com.google.gerrit.server.account.PutPreferred;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
@@ -174,7 +175,8 @@
   }
 
   private void setAccount()
-      throws OrmException, IOException, UnloggedFailure, ConfigInvalidException {
+      throws OrmException, IOException, UnloggedFailure, ConfigInvalidException,
+          PermissionBackendException {
     user = genericUserFactory.create(id);
     rsrc = new AccountResource(user);
     try {
@@ -237,7 +239,7 @@
 
   private void deleteSshKeys(List<String> sshKeys)
       throws RestApiException, OrmException, RepositoryNotFoundException, IOException,
-          ConfigInvalidException {
+          ConfigInvalidException, PermissionBackendException {
     List<SshKeyInfo> infos = getSshKeys.apply(rsrc);
     if (sshKeys.contains("ALL")) {
       for (SshKeyInfo i : infos) {
@@ -263,7 +265,8 @@
   }
 
   private void addEmail(String email)
-      throws UnloggedFailure, RestApiException, OrmException, IOException, ConfigInvalidException {
+      throws UnloggedFailure, RestApiException, OrmException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     EmailInput in = new EmailInput();
     in.email = email;
     in.noConfirmation = true;
@@ -275,7 +278,8 @@
   }
 
   private void deleteEmail(String email)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, OrmException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     if (email.equals("ALL")) {
       List<EmailInfo> emails = getEmails.apply(rsrc);
       for (EmailInfo e : emails) {
@@ -286,7 +290,8 @@
     }
   }
 
-  private void putPreferred(String email) throws RestApiException, OrmException, IOException {
+  private void putPreferred(String email)
+      throws RestApiException, OrmException, IOException, PermissionBackendException {
     for (EmailInfo e : getEmails.apply(rsrc)) {
       if (e.email.equals(email)) {
         putPreferred.apply(new AccountResource.Email(user, email), null);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index e16f270..1ed7db3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.Version;
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.GetSummary;
@@ -34,6 +35,9 @@
 import com.google.gerrit.server.config.ListCaches;
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
 import com.google.gerrit.server.config.ListCaches.CacheType;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
@@ -80,12 +84,10 @@
   private boolean showThreads;
 
   @Inject private SshDaemon daemon;
-
   @Inject private ListCaches listCaches;
-
   @Inject private GetSummary getSummary;
-
   @Inject private CurrentUser self;
+  @Inject private PermissionBackend permissionBackend;
 
   @Option(
     name = "--width",
@@ -168,7 +170,15 @@
     printDiskCaches(caches);
     stdout.print('\n');
 
-    if (self.getCapabilities().canMaintainServer()) {
+    boolean showJvm;
+    try {
+      permissionBackend.user(self).check(GlobalPermission.MAINTAIN_SERVER);
+      showJvm = true;
+    } catch (AuthException | PermissionBackendException e) {
+      // Silently ignore and do not display detailed JVM information.
+      showJvm = false;
+    }
+    if (showJvm) {
       sshSummary();
 
       SummaryInfo summary = getSummary.setGc(gc).setJvm(showJVM).apply(new ConfigResource());
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 13db697..dfb9c9c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -27,6 +27,9 @@
 import com.google.gerrit.server.config.ListTasks.TaskInfo;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -60,10 +63,9 @@
   )
   private boolean groupByQueue;
 
+  @Inject private PermissionBackend permissionBackend;
   @Inject private ListTasks listTasks;
-
   @Inject private IdentifiedUser currentUser;
-
   @Inject private WorkQueue workQueue;
 
   private int columns = 80;
@@ -83,7 +85,7 @@
   }
 
   @Override
-  protected void run() throws UnloggedFailure {
+  protected void run() throws Failure {
     maxCommandWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 12 - 4 - 4;
     stdout.print(
         String.format(
@@ -97,10 +99,12 @@
       tasks = listTasks.apply(new ConfigResource());
     } catch (AuthException e) {
       throw die(e);
+    } catch (PermissionBackendException e) {
+      throw new Failure(1, "permission backend unavailable", e);
     }
-    boolean viewAll = currentUser.getCapabilities().canViewQueue();
-    long now = TimeUtil.nowMs();
 
+    boolean viewAll = permissionBackend.user(currentUser).testOrFalse(GlobalPermission.VIEW_QUEUE);
+    long now = TimeUtil.nowMs();
     if (groupByQueue) {
       ListMultimap<String, TaskInfo> byQueue = byQueue(tasks);
       for (String queueName : byQueue.keySet()) {
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index c96c87e..0357e6c4 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -26,6 +26,9 @@
       <name>Andrew Bonventre</name>
     </developer>
     <developer>
+      <name>Becky Siegel</name>
+    </developer>
+    <developer>
       <name>Dave Borowitz</name>
     </developer>
     <developer>
@@ -41,6 +44,12 @@
       <name>Hugo Arès</name>
     </developer>
     <developer>
+      <name>Kasper Nilsson</name>
+    </developer>
+    <developer>
+      <name>Logan Hanks</name>
+    </developer>
+    <developer>
       <name>Martin Fick</name>
     </developer>
     <developer>
diff --git a/plugins/download-commands b/plugins/download-commands
index 8357e94..6ee2462 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 8357e942dd9da82884a4e1b6e4697479153d0496
+Subproject commit 6ee246245b9200062e753d1c6943d5782cb7fee0
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
index 6678944..580bb4b 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
@@ -23,6 +23,8 @@
 <link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-tooltip-behavior.html">
 
+<script>void(0);</script>
+
 <test-fixture id="basic">
   <template>
     <tooltip-behavior-element></tooltip-behavior-element>
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 71ccb04..2cfa5ba 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
@@ -81,8 +81,7 @@
           down-arrow
           vertical-offset="32"
           horizontal-align="right"
-          on-tap-item-cherrypick="_handleCherrypickTap"
-          on-tap-item-delete="_handleDeleteTap"
+          on-tap-item="_handleOveflowItemTap"
           hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]"
           disabled-ids="[[_disabledMenuActions]]"
           items="[[_menuActions]]">More</gr-dropdown>
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 2b0916d..ea519d7 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
@@ -87,14 +87,13 @@
     method: 'POST',
   };
 
-  /**
-   * Keys for actions to appear in the overflow menu rather than the top-level
-   * set of action buttons.
-   */
-  var MENU_ACTION_KEYS = [
-    'cherrypick',
-    '/', // '/' is the key for the delete action.
-  ];
+  var ActionPriority = {
+    CHANGE: 2,
+    DEFAULT: 0,
+    PRIMARY: 3,
+    REVIEW: -3,
+    REVISION: 1,
+  };
 
   Polymer({
     is: 'gr-change-actions',
@@ -158,16 +157,42 @@
       _allActionValues: {
         type: Array,
         computed: '_computeAllActions(actions.*, revisionActions.*,' +
-            'primaryActionKeys.*, _additionalActions.*, change)',
+            'primaryActionKeys.*, _additionalActions.*, change, ' +
+            '_actionPriorityOverrides.*)',
       },
       _topLevelActions: {
         type: Array,
         computed: '_computeTopLevelActions(_allActionValues.*, ' +
-            '_hiddenActions.*)',
+            '_hiddenActions.*, _overflowActions.*)',
       },
       _menuActions: {
         type: Array,
-        computed: '_computeMenuActions(_allActionValues.*, _hiddenActions.*)',
+        computed: '_computeMenuActions(_allActionValues.*, _hiddenActions.*, ' +
+            '_overflowActions.*)',
+      },
+      _overflowActions: {
+        type: Array,
+        value: function() {
+          var value = [
+            {
+              type: ActionType.CHANGE,
+              key: ChangeActions.DELETE,
+            },
+            {
+              type: ActionType.REVISION,
+              key: RevisionActions.DELETE,
+            },
+            {
+              type: ActionType.REVISION,
+              key: RevisionActions.CHERRYPICK,
+            }
+          ];
+          return value;
+        },
+      },
+      _actionPriorityOverrides: {
+        type: Array,
+        value: function() { return []; },
       },
       _additionalActions: {
         type: Array,
@@ -227,7 +252,8 @@
         enabled: true,
         label: label,
         __type: type,
-        __key: ADDITIONAL_ACTION_KEY_PREFIX + Math.random().toString(36),
+        __key: ADDITIONAL_ACTION_KEY_PREFIX +
+            Math.random().toString(36).substr(2),
       };
       this.push('_additionalActions', action);
       return action.__key;
@@ -249,6 +275,42 @@
       ], value);
     },
 
+    setActionOverflow: function(type, key, overflow) {
+      if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
+        throw Error('Invalid action type given: ' + type);
+      }
+      var index = this._getActionOverflowIndex(type, key);
+      var action = {
+        type: type,
+        key: key,
+        overflow: overflow,
+      };
+      if (!overflow && index !== -1) {
+        this.splice('_overflowActions', index, 1);
+      } else if (overflow) {
+        this.push('_overflowActions', action);
+      }
+    },
+
+    setActionPriority: function(type, key, priority) {
+      if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
+        throw Error('Invalid action type given: ' + type);
+      }
+      var index = this._actionPriorityOverrides.findIndex(function(action) {
+        return action.type === type && action.key === key;
+      });
+      var action = {
+        type: type,
+        key: key,
+        priority: priority,
+      };
+      if (index !== -1) {
+        this.set('_actionPriorityOverrides', index, action);
+      } else {
+        this.push('_actionPriorityOverrides', action);
+      }
+    },
+
     setActionHidden: function(type, key, hidden) {
       if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
         throw Error('Invalid action type given: ' + type);
@@ -459,20 +521,46 @@
         return;
       }
       var type = el.getAttribute('data-action-type');
-      if (type === ActionType.REVISION) {
-        this._handleRevisionAction(key);
-      } else if (key === ChangeActions.REVERT) {
-        this.showRevertDialog();
-      } else if (key === ChangeActions.ABANDON) {
-        this._showActionDialog(this.$.confirmAbandonDialog);
-      } else if (key === QUICK_APPROVE_ACTION.key) {
-        var action = this._allActionValues.find(function(o) {
-          return o.key === key;
-        });
-        this._fireAction(
-            this._prependSlash(key), action, true, action.payload);
-      } else {
-        this._fireAction(this._prependSlash(key), this.actions[key], false);
+      this._handleAction(type, key);
+    },
+
+    _handleOveflowItemTap: function(e) {
+      this._handleAction(e.detail.action.__type, e.detail.action.__key);
+    },
+
+    _handleAction: function(type, key) {
+      switch (type) {
+        case ActionType.REVISION:
+          this._handleRevisionAction(key);
+          break;
+        case ActionType.CHANGE:
+          this._handleChangeAction(key);
+          break;
+        default:
+          this._fireAction(this._prependSlash(key), this.actions[key], false);
+      }
+    },
+
+    _handleChangeAction: function(key) {
+      switch (key) {
+        case ChangeActions.REVERT:
+          this.showRevertDialog();
+          break;
+        case ChangeActions.ABANDON:
+          this._showActionDialog(this.$.confirmAbandonDialog);
+          break;
+        case QUICK_APPROVE_ACTION.key:
+          var action = this._allActionValues.find(function(o) {
+            return o.key === key;
+          });
+          this._fireAction(
+              this._prependSlash(key), action, true, action.payload);
+          break;
+        case ChangeActions.DELETE:
+          this._handleDeleteTap();
+          break;
+        default:
+          this._fireAction(this._prependSlash(key), this.actions[key], false);
       }
     },
 
@@ -481,6 +569,12 @@
         case RevisionActions.REBASE:
           this._showActionDialog(this.$.confirmRebase);
           break;
+        case RevisionActions.DELETE:
+          this._handleDeleteConfirm();
+          break;
+        case RevisionActions.CHERRYPICK:
+          this._handleCherrypickTap();
+          break;
         case RevisionActions.SUBMIT:
           if (!this._canSubmitChange()) {
             return;
@@ -577,11 +671,17 @@
       this._fireAction('/', this.actions[ChangeActions.DELETE], false);
     },
 
-    _setLoadingOnButtonWithKey: function(key) {
+    _getActionOverflowIndex: function(type, key) {
+      return this._overflowActions.findIndex(function(action) {
+        return action.type === type && action.key === key;
+      });
+    },
+
+    _setLoadingOnButtonWithKey: function(type, key) {
       this._actionLoadingMessage = this._computeLoadingLabel(key);
 
       // If the action appears in the overflow menu.
-      if (MENU_ACTION_KEYS.indexOf(key) !== -1) {
+      if (this._getActionOverflowIndex(type, key) !== -1) {
         this.push('_disabledMenuActions', key === '/' ? 'delete' : key);
         return function() {
           this._actionLoadingMessage = null;
@@ -601,8 +701,8 @@
     },
 
     _fireAction: function(endpoint, action, revAction, opt_payload) {
-      var cleanupFn = this._setLoadingOnButtonWithKey(action.__key);
-
+      var cleanupFn =
+          this._setLoadingOnButtonWithKey(action.__type, action.__key);
       this._send(action.method, opt_payload, endpoint, revAction, cleanupFn)
           .then(this._handleResponse.bind(this, action));
     },
@@ -696,10 +796,12 @@
      * @param {splices} primariesRecord
      * @param {splices} additionalActionsRecord
      * @param {Object} change The change object.
+     * @param {Object} priorityOverridesRecord
      * @return {Array}
      */
     _computeAllActions: function(changeActionsRecord, revisionActionsRecord,
-        primariesRecord, additionalActionsRecord, change) {
+        primariesRecord, additionalActionsRecord, change,
+        priorityOverridesRecord) {
       var revisionActionValues = this._getActionValues(revisionActionsRecord,
           primariesRecord, additionalActionsRecord, ActionType.REVISION);
       var changeActionValues = this._getActionValues(changeActionsRecord,
@@ -710,58 +812,69 @@
       }
       return revisionActionValues
           .concat(changeActionValues)
-          .sort(this._actionComparator);
+          .sort(this._actionComparator.bind(this));
+    },
+
+    _getActionPriority: function(action) {
+      if (action.__type && action.__key) {
+        var overrideAction = this._actionPriorityOverrides.find(function(i) {
+          return i.type === action.__type && i.key === action.__key;
+        });
+
+        if (overrideAction !== undefined) {
+          return overrideAction.priority;
+        }
+      }
+      if (action.__key === 'review') {
+        return ActionPriority.REVIEW;
+      } else if (action.__primary) {
+        return ActionPriority.PRIMARY;
+      } else if (action.__type === ActionType.CHANGE) {
+        return ActionPriority.CHANGE;
+      } else if (action.__type === ActionType.REVISION) {
+        return ActionPriority.REVISION;
+      }
+      return ActionPriority.DEFAULT;
     },
 
     /**
      * Sort comparator to define the order of change actions.
      */
     _actionComparator: function(actionA, actionB) {
-      // The code review action always appears first.
-      if (actionA.__key === 'review') {
-        return -1;
-      } else if (actionB.__key === 'review') {
-        return 1;
+      var priorityDelta = this._getActionPriority(actionA) -
+          this._getActionPriority(actionB);
+      // Sort by the button label if same priority.
+      if (priorityDelta === 0) {
+        return actionA.label > actionB.label ? 1 : -1;
+      } else {
+        return priorityDelta;
       }
-
-      // Primary actions always appear last.
-      if (actionA.__primary) {
-        return 1;
-      } else if (actionB.__primary) {
-        return -1;
-      }
-
-      // Change actions appear before revision actions.
-     if (actionA.__type === 'change' && actionB.__type === 'revision') {
-        return 1;
-      } else if (actionA.__type === 'revision' && actionB.__type === 'change') {
-        return -1;
-      }
-
-      // Otherwise, sort by the button label.
-      return actionA.label > actionB.label ? 1 : -1;
     },
 
-    _computeTopLevelActions: function(actionRecord, hiddenActionsRecord) {
+    _computeTopLevelActions: function(actionRecord, hiddenActionsRecord,
+        overflowActionsRecord) {
       var hiddenActions = hiddenActionsRecord.base || [];
       return actionRecord.base.filter(function(a) {
-        return MENU_ACTION_KEYS.indexOf(a.__key) === -1 &&
-                hiddenActions.indexOf(a.__key) === -1;
-      });
+        var overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1;
+        return !overflow && hiddenActions.indexOf(a.__key) === -1;
+      }.bind(this));
     },
 
-    _computeMenuActions: function(actionRecord, hiddenActionsRecord) {
+    _computeMenuActions: function(actionRecord, hiddenActionsRecord,
+        overflowActionsRecord) {
       var hiddenActions = hiddenActionsRecord.base || [];
-      return actionRecord.base
-          .filter(function(a) {
-            return MENU_ACTION_KEYS.indexOf(a.__key) !== -1 &&
-                hiddenActions.indexOf(a.__key) === -1;
-          })
-          .map(function(action) {
-            var key = action.__key;
-            if (key === '/') { key = 'delete'; }
-            return {name: action.label, id: key, };
-          });
+      return actionRecord.base.filter(function(a) {
+        var overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1;
+        return overflow && hiddenActions.indexOf(a.__key) === -1;
+      }.bind(this)).map(function(action) {
+        var key = action.__key;
+        if (key === '/') { key = 'delete'; }
+        return {
+          name: action.label,
+          id: key + '-' + action.__type,
+          action: action,
+        };
+      });
     },
   });
 })();
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 83a1c7e..b7159ea 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
@@ -135,7 +135,8 @@
 
     test('hide menu action', function(done) {
       flush(function() {
-        var buttonEl = element.$.moreActions.$$('span[data-id="delete"]');
+        var buttonEl =
+            element.$.moreActions.$$('span[data-id="delete-revision"]');
         assert.isOk(buttonEl);
         assert.throws(element.setActionHidden.bind(element, 'invalid type'));
         element.setActionHidden(element.ActionType.CHANGE,
@@ -145,13 +146,15 @@
             element.ChangeActions.DELETE, true);
         assert.lengthOf(element._hiddenActions, 1);
         flush(function() {
-          var buttonEl = element.$.moreActions.$$('span[data-id="delete"]');
+          var buttonEl =
+              element.$.moreActions.$$('span[data-id="delete-revision"]');
           assert.isNotOk(buttonEl);
 
           element.setActionHidden(element.ActionType.CHANGE,
             element.RevisionActions.DELETE, false);
           flush(function() {
-            var buttonEl = element.$.moreActions.$$('span[data-id="delete"]');
+            var buttonEl =
+                element.$.moreActions.$$('span[data-id="delete-revision"]');
             assert.isOk(buttonEl);
             done();
           });
@@ -174,7 +177,7 @@
     test('delete buttons have explicit labels', function(done) {
       flush(function() {
         var deleteItems = element.$.moreActions.items.filter(function(item) {
-          return item.id === 'delete';
+          return item.id.indexOf('delete') === 0;
         });
         assert.equal(deleteItems.length, 2);
         assert.notEqual(deleteItems[0].name, deleteItems[1].name);
@@ -204,16 +207,15 @@
     test('_actionComparator sort order', function() {
       var actions = [
         {label: '123', __type: 'change', __key: 'review'},
-        {label: 'abc', __type: 'revision'},
+        {label: 'abc-ro', __type: 'revision'},
         {label: 'abc', __type: 'change'},
         {label: 'def', __type: 'change'},
-        {label: 'def', __type: 'change', __primary: true},
+        {label: 'def-p', __type: 'change', __primary: true},
       ];
 
       var result = actions.slice();
       result.reverse();
-      result.sort(element._actionComparator);
-
+      result.sort(element._actionComparator.bind(element));
       assert.deepEqual(result, actions);
     });
 
@@ -414,7 +416,8 @@
 
     test('_setLoadingOnButtonWithKey top-level', function() {
       var key = 'rebase';
-      var cleanup = element._setLoadingOnButtonWithKey(key);
+      var type = 'revision';
+      var cleanup = element._setLoadingOnButtonWithKey(type, key);
       assert.equal(element._actionLoadingMessage, 'Rebasing...');
 
       var button = element.$$('[data-action-key="' + key + '"]');
@@ -432,7 +435,8 @@
 
     test('_setLoadingOnButtonWithKey overflow menu', function() {
       var key = 'cherrypick';
-      var cleanup = element._setLoadingOnButtonWithKey(key);
+      var type = 'revision';
+      var cleanup = element._setLoadingOnButtonWithKey(type, key);
       assert.equal(element._actionLoadingMessage, 'Cherry-Picking...');
       assert.include(element._disabledMenuActions, 'cherrypick');
       assert.isFunction(cleanup);
@@ -728,5 +732,27 @@
         assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
       });
     });
+
+    suite('setActionOverflow', function() {
+      test('move action from overflow', function() {
+        assert.isNotOk(element.$$('[data-action-key="cherrypick"]'));
+        assert.strictEqual(
+            element.$.moreActions.items[0].id, 'cherrypick-revision');
+        element.setActionOverflow('revision', 'cherrypick', false);
+        flushAsynchronousOperations();
+        assert.isOk(element.$$('[data-action-key="cherrypick"]'));
+        assert.notEqual(
+            element.$.moreActions.items[0].id, 'cherrypick-revision');
+      });
+
+      test('move action to overflow', function() {
+        assert.isOk(element.$$('[data-action-key="submit"]'));
+        element.setActionOverflow('revision', 'submit', true);
+        flushAsynchronousOperations();
+        assert.isNotOk(element.$$('[data-action-key="submit"]'));
+        assert.strictEqual(
+            element.$.moreActions.items[3].id, 'submit-revision');
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 834069e..8435692 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -235,7 +235,7 @@
     },
 
     _computeProjectURL: function(project) {
-      return this.getBaseUrl() + '/q/status:open+project:' +
+      return this.getBaseUrl() + '/q/project:' +
         this.encodeURL(project, false);
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index cbe5dad..09877a0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -435,7 +435,7 @@
                 commit-info="[[_commitInfo]]"></gr-commit-info>
             <span class="latestPatchContainer">
               /
-              <a href$="/c/[[_change._number]]">Go to latest patch set</a>
+              <a href$="[[getBaseUrl()]]/c/[[_change._number]]">Go to latest patch set</a>
             </span>
             <span class="downloadContainer desktop">
               /
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
index 14361f4..4449131 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -77,11 +77,14 @@
       <gr-button id="oldMessagesBtn" link on-tap="_handleShowAllTap">
           [[_computeNumMessagesText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
       </gr-button>
-      /
-      <gr-button id="incrementMessagesBtn" link
-          on-tap="_handleIncrementShownMessages">
-        [[_computeIncrementText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
-      </gr-button>
+      <span
+          hidden$="[[_computeIncrementHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
+        /
+        <gr-button id="incrementMessagesBtn" link
+            on-tap="_handleIncrementShownMessages">
+          [[_computeIncrementText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
+        </gr-button>
+      </span>
     </span>
     <template
         is="dom-repeat"
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index 0d58d96..8750e9d 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -327,5 +327,11 @@
       var total = this._numRemaining(visibleMessages, messages, hideAutomated);
       return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages';
     },
+
+    _computeIncrementHidden: function(visibleMessages, messages,
+        hideAutomated) {
+      var total = this._numRemaining(visibleMessages, messages, hideAutomated);
+      return total <= this._getDelta(visibleMessages, messages, hideAutomated);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index cdca365..23fc5aa 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -87,13 +87,13 @@
 
       assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
       assert.equal(getMessages().length, 20);
-      assert.equal(element.$.incrementMessagesBtn.innerText,
+      assert.equal(element.$.incrementMessagesBtn.innerText.trim(),
           'Show 5 more');
       MockInteractions.tap(element.$.incrementMessagesBtn);
       flushAsynchronousOperations();
 
       assert.equal(getMessages().length, 25);
-      assert.equal(element.$.incrementMessagesBtn.innerText,
+      assert.equal(element.$.incrementMessagesBtn.innerText.trim(),
           'Show 1 more');
       MockInteractions.tap(element.$.incrementMessagesBtn);
       flushAsynchronousOperations();
@@ -400,6 +400,18 @@
       assert.equal(messageEls.length, 1);
       assert.equal(messageEls[0].message.message, messages[0].message);
     });
+
+    test('hide increment text if increment >= total remaining', function() {
+      // Test with stubbed return values, as _numRemaining and _getDelta have
+      // their own tests.
+      sandbox.stub(element, '_getDelta').returns(5);
+      var remainingStub = sandbox.stub(element, '_numRemaining').returns(6);
+      assert.isFalse(element._computeIncrementHidden(null, null, null));
+      remainingStub.restore();
+
+      sandbox.stub(element, '_numRemaining').returns(4);
+      assert.isTrue(element._computeIncrementHidden(null, null, null));
+    });
   });
 
   suite('gr-messages-list automate tests', function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index 368c613..952dc67 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -17,6 +17,8 @@
   // Prevent redefinition.
   if (window.GrDiffBuilderImage) { return; }
 
+  var IMAGE_MIME_PATTERN = /^image\/(bmp|gif|jpeg|jpg|png|tiff|webp)$/;
+
   function GrDiffBuilderImage(diff, comments, prefs, outputEl, baseImage,
       revisionImage) {
     GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl, []);
@@ -53,7 +55,7 @@
   GrDiffBuilderImage.prototype._createImageCell =
       function(image, className, section) {
     var td = this._createElement('td', className);
-    if (image) {
+    if (image && IMAGE_MIME_PATTERN.test(image.type)) {
       var imageEl = this._createElement('img');
       imageEl.onload = function() {
         image._height = imageEl.naturalHeight;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index fe57c43..5a49daa 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -17,6 +17,8 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-storage/gr-storage.html">
 
 <dom-module id="gr-diff-preferences">
@@ -70,71 +72,79 @@
         color: #888;
       }
     </style>
-    <div class="header">
-      Diff View Preferences
-    </div>
-    <div class="mainContainer">
-      <div class="pref">
-        <label for="contextSelect">Context</label>
-        <select id="contextSelect" on-change="_handleContextSelectChange">
-          <option value="3">3 lines</option>
-          <option value="10">10 lines</option>
-          <option value="25">25 lines</option>
-          <option value="50">50 lines</option>
-          <option value="75">75 lines</option>
-          <option value="100">100 lines</option>
-          <option value="-1">Whole file</option>
-        </select>
+
+    <gr-overlay id="prefsOverlay" with-backdrop>
+      <div class="header">
+        Diff View Preferences
       </div>
-      <div class="pref">
-        <label for="lineWrappingInput">Fit to screen</label>
-        <input
-            is="iron-input"
-            type="checkbox"
-            id="lineWrappingInput"
-            on-tap="_handlelineWrappingTap">
+      <div class="mainContainer">
+        <div class="pref">
+          <label for="contextSelect">Context</label>
+          <select id="contextSelect" on-change="_handleContextSelectChange">
+            <option value="3">3 lines</option>
+            <option value="10">10 lines</option>
+            <option value="25">25 lines</option>
+            <option value="50">50 lines</option>
+            <option value="75">75 lines</option>
+            <option value="100">100 lines</option>
+            <option value="-1">Whole file</option>
+          </select>
+        </div>
+        <div class="pref">
+          <label for="lineWrappingInput">Fit to screen</label>
+          <input
+              is="iron-input"
+              type="checkbox"
+              id="lineWrappingInput"
+              on-tap="_handlelineWrappingTap">
+        </div>
+        <div class="pref" id="columnsPref"
+            hidden$="[[_newPrefs.line_wrapping]]">
+          <label for="columnsInput">Diff width</label>
+          <input is="iron-input" type="number" id="columnsInput"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{_newPrefs.line_length}}">
+        </div>
+        <div class="pref">
+          <label for="tabSizeInput">Tab width</label>
+          <input is="iron-input" type="number" id="tabSizeInput"
+              prevent-invalid-input
+              allowed-pattern="[0-9]"
+              bind-value="{{_newPrefs.tab_size}}">
+        </div>
+        <div class="pref" hidden$="[[!_newPrefs.font_size]]">
+          <label for="fontSizeInput">Font size</label>
+          <input is="iron-input" type="number" id="fontSizeInput"
+                prevent-invalid-input
+                allowed-pattern="[0-9]"
+                bind-value="{{_newPrefs.font_size}}">
+        </div>
+        <div class="pref">
+          <label for="showTabsInput">Show tabs</label>
+          <input is="iron-input" type="checkbox" id="showTabsInput"
+              on-tap="_handleShowTabsTap">
+        </div>
+        <div class="pref">
+          <label for="showTrailingWhitespaceInput">
+            Show trailing whitespace</label>
+          <input is="iron-input" type="checkbox"
+              id="showTrailingWhitespaceInput"
+              on-tap="_handleShowTrailingWhitespaceTap">
+        </div>
+        <div class="pref">
+          <label for="syntaxHighlightInput">Syntax highlighting</label>
+          <input is="iron-input" type="checkbox" id="syntaxHighlightInput"
+              on-tap="_handleSyntaxHighlightTap">
+        </div>
       </div>
-      <div class="pref" id="columnsPref" hidden$="[[_newPrefs.line_wrapping]]">
-        <label for="columnsInput">Diff width</label>
-        <input is="iron-input" type="number" id="columnsInput"
-            prevent-invalid-input
-            allowed-pattern="[0-9]"
-            bind-value="{{_newPrefs.line_length}}">
+      <div class="actions">
+        <gr-button id="saveButton" primary on-tap="_handleSave">Save</gr-button>
+        <gr-button id="cancelButton" on-tap="_handleCancel">Cancel</gr-button>
       </div>
-      <div class="pref">
-        <label for="tabSizeInput">Tab width</label>
-        <input is="iron-input" type="number" id="tabSizeInput"
-            prevent-invalid-input
-            allowed-pattern="[0-9]"
-            bind-value="{{_newPrefs.tab_size}}">
-      </div>
-      <div class="pref" hidden$="[[!_newPrefs.font_size]]">
-        <label for="fontSizeInput">Font size</label>
-        <input is="iron-input" type="number" id="fontSizeInput"
-               prevent-invalid-input
-               allowed-pattern="[0-9]"
-               bind-value="{{_newPrefs.font_size}}">
-      </div>
-      <div class="pref">
-        <label for="showTabsInput">Show tabs</label>
-        <input is="iron-input" type="checkbox" id="showTabsInput"
-            on-tap="_handleShowTabsTap">
-      </div>
-      <div class="pref">
-        <label for="showTrailingWhitespaceInput">Show trailing whitespace</label>
-        <input is="iron-input" type="checkbox" id="showTrailingWhitespaceInput"
-            on-tap="_handleShowTrailingWhitespaceTap">
-      </div>
-      <div class="pref">
-        <label for="syntaxHighlightInput">Syntax highlighting</label>
-        <input is="iron-input" type="checkbox" id="syntaxHighlightInput"
-            on-tap="_handleSyntaxHighlightTap">
-      </div>
-    </div>
-    <div class="actions">
-      <gr-button id="saveButton" primary on-tap="_handleSave">Save</gr-button>
-      <gr-button id="cancelButton" on-tap="_handleCancel">Cancel</gr-button>
-    </div>
+    </overlay>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-storage id="storage"></gr-storage>
   </template>
   <script src="gr-diff-preferences.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
index fd2a6f5..e29f8ef 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -17,18 +17,6 @@
   Polymer({
     is: 'gr-diff-preferences',
 
-    /**
-     * Fired when the user presses the save button.
-     *
-     * @event save
-     */
-
-    /**
-     * Fired when the user presses the cancel button.
-     *
-     * @event cancel
-     */
-
     properties: {
       prefs: {
         type: Object,
@@ -106,14 +94,43 @@
       this.set('_newPrefs.line_wrapping', Polymer.dom(e).rootTarget.checked);
     },
 
-    _handleSave: function() {
+    _handleSave: function(e) {
+      e.stopPropagation();
       this.prefs = this._newPrefs;
       this.localPrefs = this._newLocalPrefs;
-      this.fire('save', null, {bubbles: false});
+      var el = Polymer.dom(e).rootTarget;
+      el.disabled = true;
+      this.$.storage.savePreferences(this._localPrefs);
+      this._saveDiffPreferences().then(function(response) {
+        el.disabled = false;
+        if (!response.ok) { return response; }
+
+        this.$.prefsOverlay.close();
+      }.bind(this)).catch(function(err) {
+        el.disabled = false;
+      }.bind(this));
     },
 
-    _handleCancel: function() {
-      this.fire('cancel', null, {bubbles: false});
+    _handleCancel: function(e) {
+      e.stopPropagation();
+      this.$.prefsOverlay.close();
+    },
+
+    _handlePrefsTap: function(e) {
+      e.preventDefault();
+      this._openPrefs();
+    },
+
+    open: function() {
+      this.$.prefsOverlay.open().then(function() {
+        var focusStops = this.getFocusStops();
+        this.$.prefsOverlay.setFocusStops(focusStops);
+        this.resetFocus();
+      }.bind(this));
+    },
+
+    _saveDiffPreferences: function() {
+      return this.$.restAPI.saveDiffPreferences(this.prefs);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
index 06f617a..b163c71 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -35,11 +35,17 @@
 <script>
   suite('gr-diff-preferences tests', function() {
     var element;
+    var sandbox;
 
     setup(function() {
+      sandbox = sinon.sandbox.create();
       element = fixture('basic');
     });
 
+    teardown(function() {
+      sandbox.restore();
+    });
+
     test('model changes', function() {
       element.prefs = {
         context: 10,
@@ -92,18 +98,25 @@
       savePrefs.restore();
     });
 
-    test('events', function(done) {
-      var savePromise = new Promise(function(resolve) {
-        element.addEventListener('save', function() { resolve(); });
-      });
-      var cancelPromise = new Promise(function(resolve) {
-        element.addEventListener('cancel', function() { resolve(); });
-      });
-      Promise.all([savePromise, cancelPromise]).then(function() {
-        done();
-      });
+    test('save button', function() {
+      element.prefs = {
+        font_size: '11',
+      };
+      element._newPrefs = {
+        font_size: '12',
+      };
+      var saveStub = sandbox.stub(element.$.restAPI, 'saveDiffPreferences',
+          function() { return Promise.resolve(); });
+
       MockInteractions.tap(element.$$('gr-button[primary]'));
+      assert.deepEqual(element.prefs, element._newPrefs);
+      assert.deepEqual(saveStub.lastCall.args[0], element._newPrefs);
+    });
+
+    test('cancel button', function() {
+      var closeStub = sandbox.stub(element.$.prefsOverlay, 'close');
       MockInteractions.tap(element.$$('gr-button:not([primary])'));
+      assert.isTrue(closeStub.called);
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 1204170..a3ff3d1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -21,7 +21,6 @@
 <link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-select/gr-select.html">
 <link rel="import" href="../gr-diff/gr-diff.html">
@@ -276,21 +275,17 @@
           </span>
         </div>
       </div>
-      <gr-overlay id="prefsOverlay" with-backdrop>
-        <gr-diff-preferences
-            id="diffPreferences"
-            prefs="{{_prefs}}"
-            local-prefs="{{_localPrefs}}"
-            on-save="_handlePrefsSave"
-            on-cancel="_handlePrefsCancel"></gr-diff-preferences>
-      </gr-overlay>
+      <gr-diff-preferences
+          id="diffPreferences"
+          prefs="{{_prefs}}"
+          local-prefs="{{_localPrefs}}"></gr-diff-preferences>
       <div class="fileNav mobile">
         <a class="mobileNavLink"
-           href$="[[_computeNavLinkURL(_path, _fileList, -1, 1)]]"><</a>
+           href$="[[_computeNavLinkURL(_path, _fileList, -1, 1)]]">&lt;</a>
         <div class="fullFileName mobile">[[_computeFileDisplayName(_path)]]
         </div>
         <a class="mobileNavLink"
-            href$="[[_computeNavLinkURL(_path, _fileList, 1, 1)]]">></a>
+            href$="[[_computeNavLinkURL(_path, _fileList, 1, 1)]]">&gt;</a>
       </div>
       <gr-diff
           id="diff"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index fcbbe23..3e73d52 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -363,7 +363,7 @@
           this.modifierPressed(e)) { return; }
 
       e.preventDefault();
-      this._openPrefs();
+      this.$.diffPreferences.open();
     },
 
     _navToChangeView: function() {
@@ -389,15 +389,6 @@
       page.show(this._computeNavLinkURL(path, fileList, direction));
     },
 
-    _openPrefs: function() {
-      this.$.prefsOverlay.open().then(function() {
-        var diffPreferences = this.$.diffPreferences;
-        var focusStops = diffPreferences.getFocusStops();
-        this.$.prefsOverlay.setFocusStops(focusStops);
-        this.$.diffPreferences.resetFocus();
-      }.bind(this));
-    },
-
     /**
      * @param {?string} path The path of the current file being shown.
      * @param {Array.<string>} fileList The list of files in this change and
@@ -599,7 +590,7 @@
 
     _handlePrefsTap: function(e) {
       e.preventDefault();
-      this._openPrefs();
+      this.$.diffPreferences.open();
     },
 
     _handlePrefsSave: function(e) {
@@ -617,15 +608,6 @@
       }.bind(this));
     },
 
-    _saveDiffPreferences: function() {
-      return this.$.restAPI.saveDiffPreferences(this._prefs);
-    },
-
-    _handlePrefsCancel: function(e) {
-      e.stopPropagation();
-      this.$.prefsOverlay.close();
-    },
-
     /**
      * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
      * the current state.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 4759086..d54f715 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -111,8 +111,8 @@
           'Should navigate to /c/42/');
       assert.equal(element.changeViewState.selectedFileIndex, 0);
 
-      var showPrefsStub = sandbox.stub(element.$.prefsOverlay, 'open',
-          function() { return Promise.resolve({}); });
+      var showPrefsStub = sandbox.stub(element.$.diffPreferences.$.prefsOverlay,
+          'open', function() { return Promise.resolve({}); });
 
       MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
       assert(showPrefsStub.calledOnce);
@@ -145,22 +145,6 @@
           false, 'SIDE_BY_SIDE', false));
     });
 
-    test('saving diff preferences', function() {
-      var savePrefs = sandbox.stub(element, '_handlePrefsSave');
-      var cancelPrefs = sandbox.stub(element, '_handlePrefsCancel');
-      element.$.diffPreferences._handleSave();
-      assert(savePrefs.calledOnce);
-      assert(cancelPrefs.notCalled);
-    });
-
-    test('cancelling diff preferences', function() {
-      var savePrefs = sandbox.stub(element, '_handlePrefsSave');
-      var cancelPrefs = sandbox.stub(element, '_handlePrefsCancel');
-      element.$.diffPreferences._handleCancel();
-      assert(cancelPrefs.calledOnce);
-      assert(savePrefs.notCalled);
-    });
-
     test('keyboard shortcuts with patch range', function() {
       element._changeNum = '42';
       element._patchRange = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index f010add..c267eb0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -577,6 +577,43 @@
             element.reload();
           });
         });
+
+        test('does not render disallowed image type', function(done) {
+          var mockDiff = {
+            meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
+                lines: 560},
+            intraline_status: 'OK',
+            change_type: 'DELETED',
+            diff_header: [
+              'diff --git a/carrot.jpg b/carrot.jpg',
+              'index f9c2f2c..0000000 100644',
+              '--- a/carrot.jpg',
+              '+++ /dev/null',
+              'Binary files differ',
+            ],
+            content: [{skip: 66}],
+            binary: true,
+          };
+          mockFile1.type = 'image/jpeg-evil';
+
+          stubs.push(sandbox.stub(element, '_getDiff',
+              function() { return Promise.resolve(mockDiff); }));
+
+          element.addEventListener('render', function() {
+            // Recognizes that it should be an image diff.
+            assert.isTrue(element.isImageDiff);
+            assert.instanceOf(
+                element.$.diffBuilder._builder, GrDiffBuilderImage);
+            var leftImage = element.$.diffTable.querySelector('td.left img');
+            assert.isNotOk(leftImage);
+            done();
+          });
+
+          element.$.restAPI.getDiffPreferences().then(function(prefs) {
+            element.prefs = prefs;
+            element.reload();
+          });
+        });
       });
 
       test('_handleTap lineNum', function(done) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index e84357f..6e1637c 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -23,6 +23,12 @@
      * @event tap-item-<id>
      */
 
+    /**
+     * Fired when a non-link dropdown item is tapped.
+     *
+     * @event tap-item
+     */
+
     properties: {
       items: Array,
       topContent: Object,
@@ -93,7 +99,13 @@
 
     _handleItemTap: function(e) {
       var id = e.target.getAttribute('data-id');
+      var item = this.items.find(function(item) {
+        return item.id === id;
+      });
       if (id && this.disabledIds.indexOf(id) === -1) {
+        if (item) {
+          this.dispatchEvent(new CustomEvent('tap-item', {detail: item}));
+        }
         this.dispatchEvent(new CustomEvent('tap-item-' + id));
       }
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index 2db38b9..f8b11ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -74,13 +74,17 @@
     });
 
     test('non link items', function() {
-      element.items = [
-          {name: 'item one', id: 'foo'}, {name: 'item two', id: 'bar'}];
-      var stub = sinon.stub();
-      element.addEventListener('tap-item-foo', stub);
+      var item0 = {name: 'item one', id: 'foo'};
+      element.items = [item0, {name: 'item two', id: 'bar'}];
+      var fooTapped = sinon.stub();
+      var tapped = sinon.stub();
+      element.addEventListener('tap-item-foo', fooTapped);
+      element.addEventListener('tap-item', tapped);
       flushAsynchronousOperations();
       MockInteractions.tap(element.$$('.itemAction'));
-      assert.isTrue(stub.called);
+      assert.isTrue(fooTapped.called);
+      assert.isTrue(tapped.called);
+      assert.deepEqual(tapped.lastCall.args[0].detail, item0);
     });
 
     test('disabled non link item', function() {
@@ -88,10 +92,13 @@
       element.disabledIds = ['foo'];
 
       var stub = sinon.stub();
+      var tapped = sinon.stub();
       element.addEventListener('tap-item-foo', stub);
+      element.addEventListener('tap-item', tapped);
       flushAsynchronousOperations();
       MockInteractions.tap(element.$$('.itemAction'));
       assert.isFalse(stub.called);
+      assert.isFalse(tapped.called);
     });
   });
 </script>
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
index 72c7f6e..9bcde07 100644
--- 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
@@ -33,6 +33,16 @@
     });
   };
 
+  GrChangeActionsInterface.prototype.setActionOverflow = function(type, key,
+      overflow) {
+    return this._el.setActionOverflow(type, key, overflow);
+  };
+
+  GrChangeActionsInterface.prototype.setActionPriority = function(type, key,
+      priority) {
+    return this._el.setActionPriority(type, key, priority);
+  };
+
   GrChangeActionsInterface.prototype.setActionHidden = function(type, key,
       hidden) {
     return this._el.setActionHidden(type, key, hidden);
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
index 20b5dcb..d964cde 100644
--- 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
@@ -130,8 +130,8 @@
         var button = element.$$('[data-action-key="' + key + '"]');
         assert.isOk(button);
         assert.isFalse(button.hasAttribute('hidden'));
-        changeActions.setActionHidden(changeActions.ActionType.REVISION, key,
-            true);
+        changeActions.setActionHidden(
+            changeActions.ActionType.REVISION, key, true);
         flush(function() {
           var button = element.$$('[data-action-key="' + key + '"]');
           assert.isNotOk(button);
@@ -139,5 +139,41 @@
         });
       });
     });
+
+    test('move action button to overflow', function(done) {
+      var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+      flush(function() {
+        assert.isTrue(element.$.moreActions.hidden);
+        assert.isOk(element.$$('[data-action-key="' + key + '"]'));
+        changeActions.setActionOverflow(
+            changeActions.ActionType.REVISION, key, true);
+        flush(function() {
+          assert.isNotOk(element.$$('[data-action-key="' + key + '"]'));
+          assert.isFalse(element.$.moreActions.hidden);
+          assert.strictEqual(element.$.moreActions.items[0].name, 'Bork!');
+          done();
+        });
+      });
+    });
+
+    test('change actions priority', function(done) {
+      var key1 = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+      var key2 = changeActions.add(changeActions.ActionType.CHANGE, 'Squanch?');
+      flush(function() {
+        var buttons =
+            Polymer.dom(element.root).querySelectorAll('[data-action-key]');
+        assert.equal(buttons[0].getAttribute('data-action-key'), key1);
+        assert.equal(buttons[1].getAttribute('data-action-key'), key2);
+        changeActions.setActionPriority(
+            changeActions.ActionType.REVISION, key1, 10);
+        flush(function() {
+          buttons =
+              Polymer.dom(element.root).querySelectorAll('[data-action-key]');
+          assert.equal(buttons[0].getAttribute('data-action-key'), key2);
+          assert.equal(buttons[1].getAttribute('data-action-key'), key1);
+          done();
+        });
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index e2f63c6..95fa2a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -666,6 +666,58 @@
       return this.send('POST', url, review, opt_errFn, opt_ctx);
     },
 
+    getFileInChangeEdit: function(changeNum, path) {
+      return this.send('GET',
+          this.getChangeActionURL(changeNum, null,
+              '/edit/' + encodeURIComponent(path)
+          ));
+    },
+
+    rebaseChangeEdit: function(changeNum) {
+      return this.send('POST',
+          this.getChangeActionURL(changeNum, null,
+              '/edit:rebase'
+          ));
+    },
+
+    deleteChangeEdit: function(changeNum) {
+      return this.send('DELETE',
+          this.getChangeActionURL(changeNum, null,
+              '/edit'
+          ));
+    },
+
+    restoreFileInChangeEdit: function(changeNum, restore_path) {
+      return this.send('POST',
+          this.getChangeActionURL(changeNum, null, '/edit'),
+          {restore_path: restore_path}
+      );
+    },
+
+    renameFileInChangeEdit: function(changeNum, old_path, new_path) {
+      return this.send('POST',
+          this.getChangeActionURL(changeNum, null, '/edit'),
+          {old_path: old_path},
+          {new_path: new_path}
+      );
+    },
+
+    deleteFileInChangeEdit: function(changeNum, path) {
+      return this.send('DELETE',
+          this.getChangeActionURL(changeNum, null,
+              '/edit/' + encodeURIComponent(path)
+          ));
+    },
+
+    saveChangeEdit: function(changeNum, path, contents) {
+      return this.send('PUT',
+          this.getChangeActionURL(changeNum, null,
+              '/edit/' + encodeURIComponent(path)
+          ),
+          contents
+      );
+    },
+
     saveChangeCommitMessageEdit: function(changeNum, message) {
       var url = this.getChangeActionURL(changeNum, null, '/edit:message');
       return this.send('PUT', url, {message: message});
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 31a98d9..0ff162d 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -595,5 +595,25 @@
         });
       });
     });
+
+    test('saveChangeEdit', function(done) {
+      var change_num = '1';
+      var file_name = 'index.php';
+      var file_contents = '<?php';
+      sandbox.stub(element, 'send').returns(
+          Promise.resolve([change_num, file_name, file_contents])
+      );
+      sandbox.stub(element, 'getResponseObject')
+          .returns(Promise.resolve([change_num, file_name, file_contents]));
+      element._cache['/changes/' + change_num + '/edit/' + file_name] = {};
+      element.saveChangeEdit(change_num, file_name, file_contents).then(
+          function() {
+            assert.isTrue(element.send.calledWith('PUT',
+                '/changes/' + change_num + '/edit/' + file_name,
+                file_contents));
+            done();
+          }
+      );
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/wct_test.sh b/polygerrit-ui/app/wct_test.sh
index e6f3e0e..e81adfb 100755
--- a/polygerrit-ui/app/wct_test.sh
+++ b/polygerrit-ui/app/wct_test.sh
@@ -36,10 +36,10 @@
         'sauce': {
           'disabled': true,
           'browsers': [
-            'OS X 10.11/chrome',
+            'OS X 10.12/chrome',
             'Windows 10/chrome',
             'Linux/firefox',
-            'OS X 10.11/safari',
+            'OS X 10.12/safari',
             'Windows 10/microsoftedge'
           ]
         }