Merge "PolyGerrit: Add support for "Included In""
diff --git a/.mailmap b/.mailmap
index bd4d222..f5f8f3e 100644
--- a/.mailmap
+++ b/.mailmap
@@ -11,6 +11,7 @@
 Bruce Zu <bruce.zu.run10@gmail.com>                                                         <bruce.zu@sonymobile.com>
 Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com>                           carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
 Dariusz Luksza <dluksza@collab.net>                                                         <dariusz@luksza.org>
+Dave Borowitz <dborowitz@google.com>                                                        <dborowitz@google.com>
 David Ostrovsky <david@ostrovsky.org>                                                       <d.ostrovsky@gmx.de>
 David Ostrovsky <david@ostrovsky.org>                                                       <david.ostrovsky@gmail.com>
 David Pursehouse <dpursehouse@collab.net>                                                   <david.pursehouse@sonymobile.com>
@@ -43,6 +44,7 @@
 Mark Derricutt <mark.derricutt@smxemail.com>                                                <mark@talios.com>
 Martin Fick <mfick@codeaurora.org>                                                          <mogulguy10@gmail.com>
 Martin Fick <mfick@codeaurora.org>                                                          <mogulguy@yahoo.com>
+Maxime Guerreiro <maximeg@google.com>                                                       <maximeg@google.com>
 Michael Zhou <moz@google.com>                                                               <zhoumotongxue008@gmail.com>
 Mônica Dionísio <monica.dionisio@sonyericsson.com>                                          monica.dionisio <monica.dionisio@sonyericsson.com>
 Nasser Grainawi <nasser@grainawi.org>                                                       <nasser@codeaurora.org>
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 189c6c7..e6a6227 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2731,10 +2731,114 @@
   @Override
   public String intercept(String in) {
     return pluginName + " mycommand";
+----
+
+
+[[pre-submit-evaluator]]
+== Pre-submit Validation Plugins
+
+Gerrit provides an extension point that enables plugins to prevent a change
+from being submitted.
+
+[IMPORTANT]
+This extension point **must NOT** be used for long or slow operations, like
+calling external programs or content, running unit tests...
+Slow operations will hurt the whole Gerrit instance.
+
+This can be used to implement custom rules that changes have to match to become
+submittable. A more concrete example: the Prolog rules engine can be
+implemented using this.
+
+Gerrit calls the plugins once per change and caches the results. Although it is
+possible to predict when this interface will be triggered, this should not be
+considered as a feature. Plugins should only rely on the internal state of the
+ChangeData, not on external values like date and time, remote content or
+randomness.
+
+Plugins are expected to support rules inheritance themselves, providing ways
+to configure it and handling the logic behind it.
+Please note that no inheritance is sometimes better than badly handled
+inheritance: mis-communication and strange behaviors caused by inheritance
+may and will confuse the users. Each plugins is responsible for handling the
+project hierarchy and taking wise actions. Gerrit does not enforce it.
+
+Once Gerrit has gathered every plugins' SubmitRecords, it stores them.
+
+Plugins accept or reject a given change using `SubmitRecord.Status`.
+If a change is ready to be submitted, `OK`. If it is not ready and requires
+modifications, `NOT_READY`. Other statuses are available for particular cases.
+A change can be submitted if all the plugins accept the change.
+
+Plugins may also decide not to vote on a given change by returning an empty
+Collection (ie: the plugin is not enabled for this repository), or to vote
+several times (ie: one SubmitRecord per project in the hierarchy).
+The results are handled as if multiple plugins voted for the change.
+
+If a plugin decides not to vote, it's name will not be displayed in the UI and
+it will not be recoded in the database.
+
+.Gerrit's Pre-submit handling with three plugins
+[width="50%",cols="^m,^m,^m,^m",frame="topbot",options="header"]
+|=======================================================
+|  Plugin A  |  Plugin B  |  Plugin C  | Final decision
+|     OK     |     OK     |     OK     | OK
+|     OK     |     OK     |     /      | OK
+|     OK     |     OK     | RULE_ERROR | NOT_READY
+|     OK     | NOT_READY  |     OK     | NOT_READY
+|  NOT_READY |     OK     |     OK     | NOT_READY
+|=======================================================
+
+
+This makes composing plugins really easy.
+
+- If a plugin places a veto on a change, it can't be submitted.
+- If a plugin isn't enabled for a project (or isn't needed for this change),
+  it returns an empty collection.
+- If all the plugins answer `OK`, the change can be submitted.
+
+
+A more rare case, but worth documenting: if there are no installed plugins,
+the labels will be compared to the rules defined in the project's config,
+and the permission system will be used to allow or deny a submit request.
+
+Some rules are defined internally to provide a common base ground (and sanity):
+changes that are marked as WIP or that are closed (abandoned, merged) can't be merged.
+
+
+[source, java]
+----
+import java.util.Collection;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRecord.Status;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.SubmitRule;
+
+public class MyPluginRules implements SubmitRule {
+  public Collection<SubmitRecord> evaluate(ChangeData changeData) {
+    // Implement your submitability logic here
+
+    // Assuming we want to prevent this change from being submitted:
+    SubmitRecord record;
+    record.status = Status.NOT_READY;
+    return record;
   }
 }
 ----
 
+Don't forget to register your class!
+
+[source, java]
+----
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.inject.AbstractModule;
+
+public class MyPluginModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(SubmitRule.class).annotatedWith(Exports.named("myPlugin")).to(MyPluginRules.class);
+  }
+}
+----
 
 == SEE ALSO
 
diff --git a/Documentation/json.txt b/Documentation/json.txt
index f1dbe73..2b34185 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -178,6 +178,21 @@
 labels:: This describes the state of each code review
 <<label,label attribute>>, unless the status is RULE_ERROR.
 
+requirements:: Each <<requirement>> describes what needs to be changed
+in order for the change to be submittable.
+
+
+[[requirement]]
+== requirement
+Information about a requirement (not met) in order to submit a change.
+
+shortReason:: A short description of the requirement (a hint).
+
+fullReason:: A longer and descriptive message explaining what needs to
+be changed to meet the requirement.
+
+label:: (Optional) The name of the linked label, if set by a pre-submit rule.
+
 [[label]]
 == label
 Information about a code review label for a change.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index c1ca595..70c00ff 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6653,12 +6653,20 @@
 The `RevertInput` entity contains information for reverting a change.
 
 [options="header",cols="1,^1,5"]
-|===========================
-|Field Name    ||Description
-|`message`     |optional|
+|=============================
+|Field Name      ||Description
+|`message`       |optional|
 Message to be added as review comment to the change when reverting the
 change.
-|===========================
+|`notify`        |optional|
+Notify handling that defines to whom email notifications should be sent
+for reverting the change. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
+|`notify_details`|optional|
+Additional information about whom to notify about the revert as a map
+of recipient type to link:#notify-info[NotifyInfo] entity.
+|=============================
 
 [[review-info]]
 === ReviewInfo
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index a3bcab1..49d5ee5 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -1518,7 +1518,9 @@
 `REMOVE_USER` the member is returned as detailed
 link:rest-api-accounts.html#account-info[AccountInfo] entity, if `type`
 is `ADD_GROUP` or `REMOVE_GROUP` the member is returned as
-link:#group-info[GroupInfo] entity.
+link:#group-info[GroupInfo] entity. Note that the `name` in
+link:#group-info[GroupInfo] will not be set if the member group is not
+available.
 |`type`    |
 The event type, can be: `ADD_USER`, `REMOVE_USER`, `ADD_GROUP` or
 `REMOVE_GROUP`.
diff --git a/WORKSPACE b/WORKSPACE
index b915ed10..d782c23 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -417,8 +417,8 @@
 
 maven_jar(
     name = "auto_value",
-    artifact = "com.google.auto.value:auto-value:1.5.3",
-    sha1 = "514df6a7c7938de35c7f68dc8b8f22df86037f38",
+    artifact = "com.google.auto.value:auto-value:1.5.4",
+    sha1 = "65183ddd1e9542d69d8f613fdae91540d04e1476",
 )
 
 # Transitive dependency of commons-compress
@@ -701,18 +701,18 @@
     sha1 = "4785a3c21320980282f9f33d0d1264a69040538f",
 )
 
-TRUTH_VERS = "0.36"
+TRUTH_VERS = "0.39"
 
 maven_jar(
     name = "truth",
     artifact = "com.google.truth:truth:" + TRUTH_VERS,
-    sha1 = "7485219d2c1d341097a19382c02bde07e69ff5d2",
+    sha1 = "bd1bf5706ff34eb7ff80fef8b0c4320f112ef899",
 )
 
 maven_jar(
     name = "truth-java8-extension",
     artifact = "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERS,
-    sha1 = "dcc60988c8f9a051840766ef192a2ef41e7992f1",
+    sha1 = "1499bc88cda9d674afb30da9813b44bcd4512d0d",
 )
 
 # When bumping the easymock version number, make sure to also move powermock to a compatible version
diff --git a/contrib/populate-fixture-data.py b/contrib/populate-fixture-data.py
old mode 100644
new mode 100755
index 0e3dffe..99d9a49
--- a/contrib/populate-fixture-data.py
+++ b/contrib/populate-fixture-data.py
@@ -17,34 +17,24 @@
 This script will populate an empty standard Gerrit instance with some
 data for local testing.
 
-This script requires 'requests'. If you do not have this module, run
-'pip3 install requests' to install it.
-
 TODO(hiesel): Make real git commits instead of empty changes
 TODO(hiesel): Add comments
 """
 
 import atexit
 import json
+import optparse
 import os
 import random
 import shutil
 import subprocess
 import tempfile
-
 import requests
 import requests.auth
 
 DEFAULT_TMP_PATH = "/tmp"
 TMP_PATH = ""
-BASE_URL = "http://localhost:8080/a/"
-ACCESS_URL = BASE_URL + "access/"
-ACCOUNTS_URL = BASE_URL + "accounts/"
-CHANGES_URL = BASE_URL + "changes/"
-CONFIG_URL = BASE_URL + "config/"
-GROUPS_URL = BASE_URL + "groups/"
-PLUGINS_URL = BASE_URL + "plugins/"
-PROJECTS_URL = BASE_URL + "projects/"
+BASE_URL = "http://localhost:%d/a/"
 
 ADMIN_BASIC_AUTH = requests.auth.HTTPBasicAuth("admin", "secret")
 
@@ -158,7 +148,7 @@
 def fetch_admin_group():
   global GROUP_ADMIN
   # Get admin group
-  r = json.loads(clean(requests.get(GROUPS_URL + "?suggest=ad&p=All-Projects",
+  r = json.loads(clean(requests.get(BASE_URL + "groups/" + "?suggest=ad&p=All-Projects",
                                     headers=HEADERS,
                                     auth=ADMIN_BASIC_AUTH).text))
   admin_group_name = r.keys()[0]
@@ -222,7 +212,7 @@
      "visible_to_all": False, "owner": GROUP_ADMIN["name"],
      "owner_id": GROUP_ADMIN["id"]}]
   for g in groups:
-    requests.put(GROUPS_URL + g["name"],
+    requests.put(BASE_URL + "groups/" + g["name"],
                  json.dumps(g),
                  headers=HEADERS,
                  auth=ADMIN_BASIC_AUTH)
@@ -244,7 +234,7 @@
      "branches": ["master"], "description": "some small scripts.",
      "owners": [owner_groups[3]], "create_empty_commit": True}]
   for p in projects:
-    requests.put(PROJECTS_URL + p["name"],
+    requests.put(BASE_URL + "projects/" + p["name"],
                  json.dumps(p),
                  headers=HEADERS,
                  auth=ADMIN_BASIC_AUTH)
@@ -253,7 +243,7 @@
 
 def create_gerrit_users(gerrit_users):
   for user in gerrit_users:
-    requests.put(ACCOUNTS_URL + user["username"],
+    requests.put(BASE_URL + "accounts/" + user["username"],
                  json.dumps(user),
                  headers=HEADERS,
                  auth=ADMIN_BASIC_AUTH)
@@ -267,7 +257,7 @@
     "branch": "master",
     "status": "NEW",
   }
-  requests.post(CHANGES_URL,
+  requests.post(BASE_URL + "changes/",
                 json.dumps(change),
                 headers=HEADERS,
                 auth=basic_auth(user))
@@ -278,8 +268,22 @@
 
 
 def main():
+  p = optparse.OptionParser()
+  p.add_option("-u", "--user_count", action="store",
+               default=100,
+               type='int',
+               help="number of users to generate")
+  p.add_option("-p", "--port", action="store",
+               default=8080,
+               type='int',
+               help="port of server")
+  (options, _) = p.parse_args()
+  global BASE_URL
+  BASE_URL = BASE_URL % options.port
+  print BASE_URL
+
   set_up()
-  gerrit_users = get_random_users(100)
+  gerrit_users = get_random_users(options.user_count)
 
   group_names = create_gerrit_groups()
   for idx, u in enumerate(gerrit_users):
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 06efbfd..54ba802 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -103,6 +103,7 @@
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexer;
@@ -201,7 +202,12 @@
                 firstTest = description;
               }
               beforeTest(description);
-              try (ProjectResetter resetter = resetProjects(projectResetter.builder())) {
+              ProjectResetter.Config input = resetProjects();
+              if (input == null) {
+                input = defaultResetProjects();
+              }
+
+              try (ProjectResetter resetter = projectResetter.builder().build(input)) {
                 AbstractDaemonTest.this.resetter = resetter;
                 base.evaluate();
               } finally {
@@ -268,6 +274,7 @@
   @Inject private InProcessProtocol inProcessProtocol;
   @Inject private Provider<AnonymousUser> anonymousUser;
   @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
+  @Inject private AccountIndexer accountIndexer;
   @Inject private Groups groups;
   @Inject private GroupIndexer groupIndexer;
 
@@ -317,8 +324,12 @@
   }
 
   /** Controls which project and branches should be reset after each test case. */
-  protected ProjectResetter resetProjects(ProjectResetter.Builder resetter) throws IOException {
-    return resetter
+  protected ProjectResetter.Config resetProjects() {
+    return null;
+  }
+
+  private ProjectResetter.Config defaultResetProjects() {
+    return new ProjectResetter.Config()
         // Don't reset all refs so that refs/sequences/changes is not touched and change IDs are
         // not reused.
         .reset(allProjects, RefNames.REFS_CONFIG)
@@ -331,8 +342,7 @@
             RefNames.REFS_USERS + "*",
             RefNames.REFS_EXTERNAL_IDS,
             RefNames.REFS_STARRED_CHANGES + "*",
-            RefNames.REFS_DRAFT_COMMENTS + "*")
-        .build();
+            RefNames.REFS_DRAFT_COMMENTS + "*");
   }
 
   protected void restartAsSlave() throws Exception {
@@ -345,6 +355,11 @@
     initSsh();
   }
 
+  protected void evictAndReindexAccount(Account.Id accountId) throws IOException {
+    accountCache.evict(accountId);
+    accountIndexer.index(accountId);
+  }
+
   private void reindexAllGroups() throws OrmException, IOException, ConfigInvalidException {
     Iterable<GroupReference> allGroups = groups.getAllGroupReferences(db)::iterator;
     for (GroupReference group : allGroups) {
@@ -408,9 +423,9 @@
     admin = accountCreator.admin();
     user = accountCreator.user();
 
-    // Evict cached user state in case tests modify it.
-    accountCache.evict(admin.getId());
-    accountCache.evict(user.getId());
+    // Evict and reindex accounts in case tests modify them.
+    evictAndReindexAccount(admin.getId());
+    evictAndReindexAccount(user.getId());
 
     adminRestSession = new RestSession(server, admin);
     userRestSession = new RestSession(server, user);
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index 086cacb..303a16a 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -25,7 +25,6 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
 import com.google.common.truth.Truth;
-import com.google.gerrit.acceptance.ProjectResetter.Builder;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -63,10 +62,10 @@
   }
 
   @Override
-  protected ProjectResetter resetProjects(Builder resetter) throws IOException {
+  protected ProjectResetter.Config resetProjects() {
     // Don't reset anything so that stagedUsers can be cached across all tests.
     // Without this caching these tests become much too slow.
-    return resetter.build();
+    return new ProjectResetter.Config();
   }
 
   protected static FakeEmailSenderSubject assertThat(FakeEmailSender sender) {
@@ -323,7 +322,7 @@
     }
 
     private TestAccount evictAndCopy(TestAccount account) throws IOException {
-      accountCache.evict(account.id);
+      evictAndReindexAccount(account.id);
       return account;
     }
 
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index 569a0c1..86718be 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.RefState;
+import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.RefPatternMatcher;
 import com.google.inject.Inject;
@@ -86,26 +87,45 @@
     private final AllUsersName allUsersName;
     @Nullable private final AccountCreator accountCreator;
     @Nullable private final AccountCache accountCache;
+    @Nullable private final AccountIndexer accountIndexer;
     @Nullable private final ProjectCache projectCache;
 
-    private final Multimap<Project.NameKey, String> refsByProject;
-
     @Inject
     public Builder(
         GitRepositoryManager repoManager,
         AllUsersName allUsersName,
         @Nullable AccountCreator accountCreator,
         @Nullable AccountCache accountCache,
+        @Nullable AccountIndexer accountIndexer,
         @Nullable ProjectCache projectCache) {
       this.repoManager = repoManager;
       this.allUsersName = allUsersName;
       this.accountCreator = accountCreator;
       this.accountCache = accountCache;
+      this.accountIndexer = accountIndexer;
       this.projectCache = projectCache;
+    }
+
+    public ProjectResetter build(ProjectResetter.Config input) throws IOException {
+      return new ProjectResetter(
+          repoManager,
+          allUsersName,
+          accountCreator,
+          accountCache,
+          accountIndexer,
+          projectCache,
+          input.refsByProject);
+    }
+  }
+
+  public static class Config {
+    private final Multimap<Project.NameKey, String> refsByProject;
+
+    public Config() {
       this.refsByProject = MultimapBuilder.hashKeys().arrayListValues().build();
     }
 
-    public Builder reset(Project.NameKey project, String... refPatterns) {
+    public Config reset(Project.NameKey project, String... refPatterns) {
       List<String> refPatternList = Arrays.asList(refPatterns);
       if (refPatternList.isEmpty()) {
         refPatternList = ImmutableList.of(RefNames.REFS + "*");
@@ -113,17 +133,13 @@
       refsByProject.putAll(project, refPatternList);
       return this;
     }
-
-    public ProjectResetter build() throws IOException {
-      return new ProjectResetter(
-          repoManager, allUsersName, accountCreator, accountCache, projectCache, refsByProject);
-    }
   }
 
   @Inject private GitRepositoryManager repoManager;
   @Inject private AllUsersName allUsersName;
   @Inject @Nullable private AccountCreator accountCreator;
   @Inject @Nullable private AccountCache accountCache;
+  @Inject @Nullable private AccountIndexer accountIndexer;
   @Inject @Nullable private ProjectCache projectCache;
 
   private final Multimap<Project.NameKey, String> refsPatternByProject;
@@ -138,6 +154,7 @@
       AllUsersName allUsersName,
       @Nullable AccountCreator accountCreator,
       @Nullable AccountCache accountCache,
+      @Nullable AccountIndexer accountIndexer,
       @Nullable ProjectCache projectCache,
       Multimap<Project.NameKey, String> refPatternByProject)
       throws IOException {
@@ -145,6 +162,7 @@
     this.allUsersName = allUsersName;
     this.accountCreator = accountCreator;
     this.accountCache = accountCache;
+    this.accountIndexer = accountIndexer;
     this.projectCache = projectCache;
     this.refsPatternByProject = refPatternByProject;
     this.savedRefStatesByProject = readRefStates();
@@ -282,7 +300,7 @@
     if (accountCreator != null) {
       accountCreator.evict(deletedAccounts);
     }
-    if (accountCache != null) {
+    if (accountCache != null || accountIndexer != null) {
       Set<Account.Id> modifiedAccounts =
           new HashSet<>(accountIds(restoredRefsByProject.get(allUsersName)));
 
@@ -294,23 +312,33 @@
           for (Account.Id id :
               accountIds(
                   repo.getAllRefs().values().stream().map(r -> r.getName()).collect(toSet()))) {
-            accountCache.evict(id);
+            evictAndReindexAccount(id);
           }
         }
 
         // Remove deleted accounts from the cache and index.
         for (Account.Id id : deletedAccounts) {
-          accountCache.evict(id);
+          evictAndReindexAccount(id);
         }
       } else {
         // Evict and reindex all modified and deleted accounts.
         for (Account.Id id : Sets.union(modifiedAccounts, deletedAccounts)) {
-          accountCache.evict(id);
+          evictAndReindexAccount(id);
         }
       }
     }
   }
 
+  private void evictAndReindexAccount(Account.Id accountId) throws IOException {
+    if (accountCache != null) {
+      accountCache.evict(accountId);
+    }
+
+    if (accountIndexer != null) {
+      accountIndexer.index(accountId);
+    }
+  }
+
   private Set<Account.Id> accountIds(Collection<String> refs) {
     return refs.stream()
         .filter(r -> r.startsWith(REFS_USERS))
diff --git a/java/com/google/gerrit/common/data/SubmitRecord.java b/java/com/google/gerrit/common/data/SubmitRecord.java
index 9151222..3cd9c43 100644
--- a/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -18,15 +18,20 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
-import java.util.Optional;
 
-/** Describes the state required to submit a change. */
+/** Describes the state and edits required to submit a change. */
 public class SubmitRecord {
-  public static Optional<SubmitRecord> findOkRecord(Collection<SubmitRecord> in) {
-    if (in == null) {
-      return Optional.empty();
+  public static boolean allRecordsOK(Collection<SubmitRecord> in) {
+    if (in == null || in.isEmpty()) {
+      // If the list is null or empty, it means that this Gerrit installation does not
+      // have any form of validation rules.
+      // Hence, the permission system should be used to determine if the change can be merged
+      // or not.
+      return true;
     }
-    return in.stream().filter(r -> r.status == Status.OK).findFirst();
+
+    // The change can be submitted, unless at least one plugin prevents it.
+    return in.stream().noneMatch(r -> r.status != Status.OK);
   }
 
   public enum Status {
@@ -36,7 +41,7 @@
     /** The change is ready for submission. */
     OK,
 
-    /** The change is missing a required label. */
+    /** Something is preventing this change from being submitted. */
     NOT_READY,
 
     /** The change has been closed. */
@@ -55,6 +60,7 @@
 
   public Status status;
   public List<Label> labels;
+  public List<SubmitRequirement> requirements;
   public String errorMessage;
 
   public static class Label {
@@ -140,6 +146,14 @@
         delimiter = ", ";
       }
     }
+    sb.append("],[");
+    if (requirements != null) {
+      String delimiter = "";
+      for (SubmitRequirement requirement : requirements) {
+        sb.append(delimiter).append(requirement);
+        delimiter = ", ";
+      }
+    }
     sb.append(']');
     return sb.toString();
   }
@@ -150,13 +164,14 @@
       SubmitRecord r = (SubmitRecord) o;
       return Objects.equals(status, r.status)
           && Objects.equals(labels, r.labels)
-          && Objects.equals(errorMessage, r.errorMessage);
+          && Objects.equals(errorMessage, r.errorMessage)
+          && Objects.equals(requirements, r.requirements);
     }
     return false;
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(status, labels, errorMessage);
+    return Objects.hash(status, labels, errorMessage, requirements);
   }
 }
diff --git a/java/com/google/gerrit/common/data/SubmitRequirement.java b/java/com/google/gerrit/common/data/SubmitRequirement.java
new file mode 100644
index 0000000..a75f3c0
--- /dev/null
+++ b/java/com/google/gerrit/common/data/SubmitRequirement.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.common.Nullable;
+import java.util.Objects;
+import java.util.Optional;
+
+/** Describes a requirement to submit a change. */
+public final class SubmitRequirement {
+  private final String shortReason;
+  private final String fullReason;
+  @Nullable private final String label;
+
+  public SubmitRequirement(String shortReason, String fullReason, @Nullable String label) {
+    this.shortReason = requireNonNull(shortReason);
+    this.fullReason = requireNonNull(fullReason);
+    this.label = label;
+  }
+
+  public String shortReason() {
+    return shortReason;
+  }
+
+  public String fullReason() {
+    return fullReason;
+  }
+
+  public Optional<String> label() {
+    return Optional.ofNullable(label);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o instanceof SubmitRequirement) {
+      SubmitRequirement that = (SubmitRequirement) o;
+      return Objects.equals(shortReason, that.shortReason)
+          && Objects.equals(fullReason, that.fullReason)
+          && Objects.equals(label, that.label);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(shortReason, fullReason, label);
+  }
+
+  @Override
+  public String toString() {
+    return "SubmitRequirement{"
+        + "shortReason='"
+        + shortReason
+        + '\''
+        + ", fullReason='"
+        + fullReason
+        + '\''
+        + ", label='"
+        + label
+        + '\''
+        + '}';
+  }
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/RevertInput.java b/java/com/google/gerrit/extensions/api/changes/RevertInput.java
index 893472e..c1be9b0 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevertInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevertInput.java
@@ -15,7 +15,13 @@
 package com.google.gerrit.extensions.api.changes;
 
 import com.google.gerrit.extensions.restapi.DefaultInput;
+import java.util.Map;
 
 public class RevertInput {
   @DefaultInput public String message;
+
+  /** Who to send email notifications to after change is created. */
+  public NotifyHandling notify = NotifyHandling.ALL;
+
+  public Map<RecipientType, NotifyInfo> notifyDetails;
 }
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index d070966..131dbf9 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -46,6 +46,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
+import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
@@ -102,9 +103,11 @@
 import com.google.gerrit.server.audit.AuditService;
 import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.LockFailureException;
 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.update.UpdateException;
 import com.google.gerrit.util.http.RequestUtil;
 import com.google.gson.ExclusionStrategy;
 import com.google.gson.FieldAttributes;
@@ -480,6 +483,15 @@
     } catch (NotImplementedException e) {
       responseBytes =
           replyError(req, res, status = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
+    } catch (UpdateException e) {
+      Throwable t = e.getCause();
+      if (t instanceof LockFailureException) {
+        responseBytes =
+            replyError(req, res, status = SC_SERVICE_UNAVAILABLE, messageOr(t, "Lock failure"), e);
+      } else {
+        status = SC_INTERNAL_SERVER_ERROR;
+        responseBytes = handleException(e, req, res);
+      }
     } catch (Exception e) {
       status = SC_INTERNAL_SERVER_ERROR;
       responseBytes = handleException(e, req, res);
diff --git a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
index c3eb39f..4487ae5 100644
--- a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
+++ b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
-import org.eclipse.jgit.internal.storage.file.WindowCacheStatAccessor;
+import org.eclipse.jgit.storage.file.WindowCacheStats;
 
 public class JGitMetricModule extends MetricModule {
   @Override
@@ -32,7 +32,7 @@
         new Supplier<Long>() {
           @Override
           public Long get() {
-            return WindowCacheStatAccessor.getOpenBytes();
+            return WindowCacheStats.getOpenBytes();
           }
         });
 
@@ -43,7 +43,7 @@
         new Supplier<Integer>() {
           @Override
           public Integer get() {
-            return WindowCacheStatAccessor.getOpenFiles();
+            return WindowCacheStats.getOpenFiles();
           }
         });
   }
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index a7803a9..357674d 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -157,7 +157,6 @@
     install(new ExternalIdModule());
     install(new GroupModule());
     install(new NoteDbModule(cfg));
-    install(new PrologModule());
     install(AccountCacheImpl.module());
     install(GroupCacheImpl.module());
     install(GroupIncludeCacheImpl.module());
@@ -169,7 +168,10 @@
     factory(CapabilityCollection.Factory.class);
     factory(ChangeData.AssistedFactory.class);
     factory(ProjectState.Factory.class);
+
+    // Submit rule evaluator
     factory(SubmitRuleEvaluator.Factory.class);
+    install(new PrologModule());
 
     bind(ChangeJson.Factory.class).toProvider(Providers.<ChangeJson.Factory>of(null));
     bind(EventUtil.class).toProvider(Providers.<EventUtil>of(null));
diff --git a/java/com/google/gerrit/server/account/AccountCache.java b/java/com/google/gerrit/server/account/AccountCache.java
index 875bf5f..b6ca1cb 100644
--- a/java/com/google/gerrit/server/account/AccountCache.java
+++ b/java/com/google/gerrit/server/account/AccountCache.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
-import java.io.IOException;
 import java.util.Optional;
 
 /** Caches important (but small) account state to avoid database hits. */
@@ -60,13 +59,12 @@
   Optional<AccountState> getByUsername(String username);
 
   /**
-   * Evicts the account from the cache and triggers a reindex for it.
+   * Evicts the account from the cache.
    *
    * @param accountId account ID of the account that should be evicted
-   * @throws IOException thrown if reindexing fails
    */
-  void evict(@Nullable Account.Id accountId) throws IOException;
+  void evict(@Nullable Account.Id accountId);
 
-  /** Evict all accounts from the cache, but doesn't trigger reindex of all accounts. */
-  void evictAllNoReindex();
+  /** Evict all accounts from the cache. */
+  void evictAll();
 }
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index a8c3436..105a457 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -25,10 +25,8 @@
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.inject.Inject;
 import com.google.inject.Module;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
@@ -62,18 +60,15 @@
   private final AllUsersName allUsersName;
   private final ExternalIds externalIds;
   private final LoadingCache<Account.Id, Optional<AccountState>> byId;
-  private final Provider<AccountIndexer> indexer;
 
   @Inject
   AccountCacheImpl(
       AllUsersName allUsersName,
       ExternalIds externalIds,
-      @Named(BYID_NAME) LoadingCache<Account.Id, Optional<AccountState>> byId,
-      Provider<AccountIndexer> indexer) {
+      @Named(BYID_NAME) LoadingCache<Account.Id, Optional<AccountState>> byId) {
     this.allUsersName = allUsersName;
     this.externalIds = externalIds;
     this.byId = byId;
-    this.indexer = indexer;
   }
 
   @Override
@@ -111,15 +106,14 @@
   }
 
   @Override
-  public void evict(@Nullable Account.Id accountId) throws IOException {
+  public void evict(@Nullable Account.Id accountId) {
     if (accountId != null) {
       byId.invalidate(accountId);
-      indexer.get().index(accountId);
     }
   }
 
   @Override
-  public void evictAllNoReindex() {
+  public void evictAll() {
     byId.invalidateAll();
   }
 
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index d86be16..79c57484 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -143,7 +143,21 @@
       try (ReviewDb db = schema.open()) {
         ExternalId id = externalIds.get(who.getExternalIdKey());
         if (id == null) {
+          if (who.getUserName().isPresent()) {
+            ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, who.getUserName().get());
+            ExternalId existingId = externalIds.get(key);
+            if (existingId != null) {
+              // An inconsistency is detected in the database, having a record for scheme "username:"
+              // but no record for scheme "gerrit:". Try to recover by linking
+              // "gerrit:" identity to the existing account.
+              log.warn(
+                  "User {} already has an account; link new identity to the existing account.",
+                  who.getUserName());
+              return link(existingId.accountId(), who);
+            }
+          }
           // New account, automatically create and return.
+          log.info("External ID not found. Attempting to create new account.");
           return create(db, who);
         }
 
@@ -383,6 +397,7 @@
   public AuthResult link(Account.Id to, AuthRequest who)
       throws AccountException, OrmException, IOException, ConfigInvalidException {
     ExternalId extId = externalIds.get(who.getExternalIdKey());
+    log.info("Link another authentication identity to an existing account");
     if (extId != null) {
       if (!extId.accountId().equals(to)) {
         throw new AccountException(
@@ -390,6 +405,7 @@
       }
       update(who, extId);
     } else {
+      log.info("Linking new external ID to the existing account");
       accountsUpdateProvider
           .get()
           .update(
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index c8244b8..d6e3ce5 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -90,13 +90,23 @@
  * are stored separately in the {@code refs/meta/external-ids} notes branch (see {@link
  * ExternalIdNotes}).
  *
- * <p>On updating an account the account is evicted from the account cache and thus reindexed. The
- * eviction from the account cache is done by the {@link ReindexAfterRefUpdate} class which receives
- * the event about updating the user branch that is triggered by this class. By passing an {@link
- * com.google.gerrit.server.account.externalids.ExternalIdNotes.FactoryNoReindex} factory as
- * parameter of {@link AccountsUpdate.Factory#create(IdentifiedUser, ExternalIdNotesLoader)},
- * reindexing and flushing the account from the account cache can be disabled. If external IDs are
- * updated, the ExternalIdCache is automatically updated.
+ * <p>On updating an account the account is evicted from the account cache and reindexed. The
+ * eviction from the account cache and the reindexing is done by the {@link ReindexAfterRefUpdate}
+ * class which receives the event about updating the user branch that is triggered by this class.
+ *
+ * <p>If external IDs are updated, the ExternalIdCache is automatically updated by {@link
+ * ExternalIdNotes}. In addition {@link ExternalIdNotes} takes care about evicting and reindexing
+ * corresponding accounts. This is needed because external ID updates don't touch the user branches.
+ * Hence in this case the accounts are not evicted and reindexed via {@link ReindexAfterRefUpdate}.
+ *
+ * <p>Reindexing and flushing accounts from the account cache can be disabled by
+ *
+ * <ul>
+ *   <li>binding {@link GitReferenceUpdated#DISABLED} and
+ *   <li>passing an {@link
+ *       com.google.gerrit.server.account.externalids.ExternalIdNotes.FactoryNoReindex} factory as
+ *       parameter of {@link AccountsUpdate.Factory#create(IdentifiedUser, ExternalIdNotesLoader)}
+ * </ul>
  *
  * <p>If there are concurrent account updates updating the user branch in NoteDb may fail with
  * {@link LockFailureException}. In this case the account update is automatically retried and the
diff --git a/java/com/google/gerrit/server/account/GroupControl.java b/java/com/google/gerrit/server/account/GroupControl.java
index 4b223bf..5649629 100644
--- a/java/com/google/gerrit/server/account/GroupControl.java
+++ b/java/com/google/gerrit/server/account/GroupControl.java
@@ -177,6 +177,6 @@
     if (group instanceof GroupDescription.Internal) {
       return ((GroupDescription.Internal) group).isVisibleToAll() || isOwner();
     }
-    return false;
+    return canAdministrateServer();
   }
 }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 29f69ba..972dfbd 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -31,7 +31,9 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -109,23 +111,30 @@
   public static class Factory implements ExternalIdNotesLoader {
     private final ExternalIdCache externalIdCache;
     private final AccountCache accountCache;
+    private final Provider<AccountIndexer> accountIndexer;
 
     @Inject
-    Factory(ExternalIdCache externalIdCache, AccountCache accountCache) {
+    Factory(
+        ExternalIdCache externalIdCache,
+        AccountCache accountCache,
+        Provider<AccountIndexer> accountIndexer) {
       this.externalIdCache = externalIdCache;
       this.accountCache = accountCache;
+      this.accountIndexer = accountIndexer;
     }
 
     @Override
     public ExternalIdNotes load(Repository allUsersRepo)
         throws IOException, ConfigInvalidException {
-      return new ExternalIdNotes(externalIdCache, accountCache, allUsersRepo).load();
+      return new ExternalIdNotes(externalIdCache, accountCache, accountIndexer, allUsersRepo)
+          .load();
     }
 
     @Override
     public ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
         throws IOException, ConfigInvalidException {
-      return new ExternalIdNotes(externalIdCache, accountCache, allUsersRepo).load(rev);
+      return new ExternalIdNotes(externalIdCache, accountCache, accountIndexer, allUsersRepo)
+          .load(rev);
     }
   }
 
@@ -141,13 +150,13 @@
     @Override
     public ExternalIdNotes load(Repository allUsersRepo)
         throws IOException, ConfigInvalidException {
-      return new ExternalIdNotes(externalIdCache, null, allUsersRepo).load();
+      return new ExternalIdNotes(externalIdCache, null, null, allUsersRepo).load();
     }
 
     @Override
     public ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
         throws IOException, ConfigInvalidException {
-      return new ExternalIdNotes(externalIdCache, null, allUsersRepo).load(rev);
+      return new ExternalIdNotes(externalIdCache, null, null, allUsersRepo).load(rev);
     }
   }
 
@@ -159,7 +168,7 @@
    */
   public static ExternalIdNotes loadReadOnly(Repository allUsersRepo)
       throws IOException, ConfigInvalidException {
-    return new ExternalIdNotes(new DisabledExternalIdCache(), null, allUsersRepo)
+    return new ExternalIdNotes(new DisabledExternalIdCache(), null, null, allUsersRepo)
         .setReadOnly()
         .load();
   }
@@ -176,7 +185,7 @@
    */
   public static ExternalIdNotes loadReadOnly(Repository allUsersRepo, @Nullable ObjectId rev)
       throws IOException, ConfigInvalidException {
-    return new ExternalIdNotes(new DisabledExternalIdCache(), null, allUsersRepo)
+    return new ExternalIdNotes(new DisabledExternalIdCache(), null, null, allUsersRepo)
         .setReadOnly()
         .load(rev);
   }
@@ -189,11 +198,12 @@
    */
   public static ExternalIdNotes loadNoCacheUpdate(Repository allUsersRepo)
       throws IOException, ConfigInvalidException {
-    return new ExternalIdNotes(new DisabledExternalIdCache(), null, allUsersRepo).load();
+    return new ExternalIdNotes(new DisabledExternalIdCache(), null, null, allUsersRepo).load();
   }
 
   private final ExternalIdCache externalIdCache;
   @Nullable private final AccountCache accountCache;
+  @Nullable private final Provider<AccountIndexer> accountIndexer;
   private final Repository repo;
 
   private NoteMap noteMap;
@@ -211,9 +221,11 @@
   private ExternalIdNotes(
       ExternalIdCache externalIdCache,
       @Nullable AccountCache accountCache,
+      @Nullable Provider<AccountIndexer> accountIndexer,
       Repository allUsersRepo) {
     this.externalIdCache = checkNotNull(externalIdCache, "externalIdCache");
     this.accountCache = accountCache;
+    this.accountIndexer = accountIndexer;
     this.repo = checkNotNull(allUsersRepo, "allUsersRepo");
   }
 
@@ -598,7 +610,8 @@
    *
    * <p>No-op if this instance was created by {@link #loadNoCacheUpdate(Repository)}.
    *
-   * <p>No eviction from account cache if this instance was created by {@link FactoryNoReindex}.
+   * <p>No eviction from account cache and no reindex if this instance was created by {@link
+   * FactoryNoReindex}.
    */
   public void updateCaches() throws IOException {
     checkState(oldRev != null, "no changes committed yet");
@@ -614,14 +627,19 @@
         externalIdCacheUpdates.getRemoved(),
         externalIdCacheUpdates.getAdded());
 
-    if (accountCache != null) {
+    if (accountCache != null || accountIndexer != null) {
       for (Account.Id id :
           Streams.concat(
                   externalIdCacheUpdates.getAdded().stream(),
                   externalIdCacheUpdates.getRemoved().stream())
               .map(ExternalId::accountId)
               .collect(toSet())) {
-        accountCache.evict(id);
+        if (accountCache != null) {
+          accountCache.evict(id);
+        }
+        if (accountIndexer != null) {
+          accountIndexer.get().index(id);
+        }
       }
     }
 
diff --git a/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java b/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
index 5958ed6..4d589a0 100644
--- a/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.inject.Inject;
@@ -35,20 +34,17 @@
 
 public class AccountGroupUUIDHandler extends OptionHandler<AccountGroup.UUID> {
   private final GroupBackend groupBackend;
-  private final GroupControl.Factory groupControlFactory;
   private final GroupCache groupCache;
 
   @Inject
   public AccountGroupUUIDHandler(
       final GroupBackend groupBackend,
-      final GroupControl.Factory groupControlFactory,
       @Assisted final CmdLineParser parser,
       @Assisted final OptionDef option,
       @Assisted final Setter<AccountGroup.UUID> setter,
       GroupCache groupCache) {
     super(parser, option, setter);
     this.groupBackend = groupBackend;
-    this.groupControlFactory = groupControlFactory;
     this.groupCache = groupCache;
   }
 
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 113a6bf..19c9666 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -657,7 +657,7 @@
   }
 
   private boolean submittable(ChangeData cd) {
-    return SubmitRecord.findOkRecord(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)).isPresent();
+    return SubmitRecord.allRecordsOK(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT));
   }
 
   private List<SubmitRecord> submitRecords(ChangeData cd) {
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index 7a6c209..fc9ae4b 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.readWithoutMarker;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.writeWithoutMarker;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.Cache;
@@ -172,14 +172,14 @@
     }
 
     private void writeObject(ObjectOutputStream out) throws IOException {
-      writeNotNull(out, prior);
-      writeNotNull(out, next);
+      writeWithoutMarker(out, prior);
+      writeWithoutMarker(out, next);
       out.writeUTF(strategyName);
     }
 
     private void readObject(ObjectInputStream in) throws IOException {
-      prior = readNotNull(in);
-      next = readNotNull(in);
+      prior = readWithoutMarker(in);
+      next = readWithoutMarker(in);
       strategyName = in.readUTF();
     }
   }
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index 7ba18e8..4c80eab 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -19,8 +19,8 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.readWithoutMarker;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.writeWithoutMarker;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.cache.Cache;
@@ -155,8 +155,8 @@
     }
 
     private void writeObject(ObjectOutputStream out) throws IOException {
-      writeNotNull(out, commit);
-      writeNotNull(out, into);
+      writeWithoutMarker(out, commit);
+      writeWithoutMarker(out, into);
       Character c = SUBMIT_TYPES.get(submitType);
       if (c == null) {
         throw new IOException("Invalid submit type: " + submitType);
@@ -166,8 +166,8 @@
     }
 
     private void readObject(ObjectInputStream in) throws IOException {
-      commit = readNotNull(in);
-      into = readNotNull(in);
+      commit = readWithoutMarker(in);
+      into = readWithoutMarker(in);
       char t = in.readChar();
       submitType = SUBMIT_TYPES.inverse().get(t);
       if (submitType == null) {
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 77312be..9536d55 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -177,6 +177,7 @@
 import com.google.gerrit.server.restapi.group.GroupModule;
 import com.google.gerrit.server.rules.PrologModule;
 import com.google.gerrit.server.rules.RulesCache;
+import com.google.gerrit.server.rules.SubmitRule;
 import com.google.gerrit.server.ssh.SshAddressesModule;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -391,6 +392,7 @@
     DynamicSet.setOf(binder(), ActionVisitor.class);
     DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
     DynamicItem.itemOf(binder(), ProjectNameLockManager.class);
+    DynamicSet.setOf(binder(), SubmitRule.class);
 
     DynamicMap.mapOf(binder(), MailFilter.class);
     bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
diff --git a/java/com/google/gerrit/server/config/PluginConfigFactory.java b/java/com/google/gerrit/server/config/PluginConfigFactory.java
index 6310c08..41230b2 100644
--- a/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -288,7 +288,7 @@
    */
   public Config getProjectPluginConfigWithInheritance(
       Project.NameKey projectName, String pluginName) throws NoSuchProjectException {
-    return getPluginConfig(projectName, pluginName).getWithInheritance();
+    return getPluginConfig(projectName, pluginName).getWithInheritance(false);
   }
 
   /**
@@ -310,7 +310,55 @@
    */
   public Config getProjectPluginConfigWithInheritance(
       ProjectState projectState, String pluginName) {
-    return projectState.getConfig(pluginName + EXTENSION).getWithInheritance();
+    return projectState.getConfig(pluginName + EXTENSION).getWithInheritance(false);
+  }
+
+  /**
+   * Returns the configuration for the specified plugin that is stored in the '{@code
+   * <plugin-name>.config}' file in the 'refs/meta/config' branch of the specified project.
+   * Parameters from the '{@code <plugin-name>.config}' of the parent project are appended to this
+   * project's '{@code <plugin-name>.config}' files.
+   *
+   * <p>E.g.: child project: [mySection "mySubsection"] myKey = childValue
+   *
+   * <p>parent project: [mySection "mySubsection"] myKey = parentValue anotherKey = someValue
+   *
+   * <p>return: [mySection "mySubsection"] myKey = childValue myKey = parentValue anotherKey =
+   * someValue
+   *
+   * @param projectName the name of the project for which the plugin configuration should be
+   *     returned
+   * @param pluginName the name of the plugin for which the configuration should be returned
+   * @return the plugin configuration from the '{@code <plugin-name>.config}' file of the specified
+   *     project with parameters from the parent projects appended to the project values
+   * @throws NoSuchProjectException thrown if the specified project does not exist
+   */
+  public Config getProjectPluginConfigWithMergedInheritance(
+      Project.NameKey projectName, String pluginName) throws NoSuchProjectException {
+    return getPluginConfig(projectName, pluginName).getWithInheritance(true);
+  }
+
+  /**
+   * Returns the configuration for the specified plugin that is stored in the '{@code
+   * <plugin-name>.config}' file in the 'refs/meta/config' branch of the specified project.
+   * Parameters from the '{@code <plugin-name>.config}' of the parent project are appended to this
+   * project's '{@code <plugin-name>.config}' files.
+   *
+   * <p>E.g.: child project: [mySection "mySubsection"] myKey = childValue
+   *
+   * <p>parent project: [mySection "mySubsection"] myKey = parentValue anotherKey = someValue
+   *
+   * <p>return: [mySection "mySubsection"] myKey = childValue myKey = parentValue anotherKey =
+   * someValue
+   *
+   * @param projectState the project for which the plugin configuration should be returned
+   * @param pluginName the name of the plugin for which the configuration should be returned
+   * @return the plugin configuration from the '{@code <plugin-name>.config}' file of the specified
+   *     project with inheriting non-set parameters from the parent projects
+   */
+  public Config getProjectPluginConfigWithMergedInheritance(
+      ProjectState projectState, String pluginName) {
+    return projectState.getConfig(pluginName + EXTENSION).getWithInheritance(true);
   }
 
   private ProjectLevelConfig getPluginConfig(Project.NameKey projectName, String pluginName)
diff --git a/java/com/google/gerrit/server/data/SubmitLabelAttribute.java b/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
index 1b3c6a48..fec8f7f 100644
--- a/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
+++ b/java/com/google/gerrit/server/data/SubmitLabelAttribute.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.data;
 
+/**
+ * Represents a {@link com.google.gerrit.common.data.SubmitRecord.Label} that does not depend on
+ * Gerrit internal classes, to be serialized.
+ */
 public class SubmitLabelAttribute {
   public String label;
   public String status;
diff --git a/java/com/google/gerrit/server/data/SubmitRecordAttribute.java b/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
index fec870f..2c3d401 100644
--- a/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
+++ b/java/com/google/gerrit/server/data/SubmitRecordAttribute.java
@@ -16,7 +16,12 @@
 
 import java.util.List;
 
+/**
+ * Represents a {@link com.google.gerrit.common.data.SubmitRecord} that does not depend on Gerrit
+ * internal classes, to be serialized.
+ */
 public class SubmitRecordAttribute {
   public String status;
   public List<SubmitLabelAttribute> labels;
+  public List<SubmitRequirementAttribute> requirements;
 }
diff --git a/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java b/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java
new file mode 100644
index 0000000..8a0ea51
--- /dev/null
+++ b/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.data;
+
+/**
+ * Represents a {@link com.google.gerrit.common.data.SubmitRequirement} that does not depend on
+ * Gerrit internal classes, to be serialized
+ */
+public class SubmitRequirementAttribute {
+  public String shortReason;
+  public String fullReason;
+  public String label;
+}
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index d356c2c..d3d52e4 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -52,6 +53,7 @@
 import com.google.gerrit.server.data.RefUpdateAttribute;
 import com.google.gerrit.server.data.SubmitLabelAttribute;
 import com.google.gerrit.server.data.SubmitRecordAttribute;
+import com.google.gerrit.server.data.SubmitRequirementAttribute;
 import com.google.gerrit.server.data.TrackingIdAttribute;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchList;
@@ -229,6 +231,7 @@
       sa.status = submitRecord.status.name();
       if (submitRecord.status != SubmitRecord.Status.RULE_ERROR) {
         addSubmitRecordLabels(submitRecord, sa);
+        addSubmitRecordRequirements(submitRecord, sa);
       }
       ca.submitRecords.add(sa);
     }
@@ -253,6 +256,19 @@
     }
   }
 
+  private void addSubmitRecordRequirements(SubmitRecord submitRecord, SubmitRecordAttribute sa) {
+    if (submitRecord.requirements != null && !submitRecord.requirements.isEmpty()) {
+      sa.requirements = new ArrayList<>();
+      for (SubmitRequirement req : submitRecord.requirements) {
+        SubmitRequirementAttribute re = new SubmitRequirementAttribute();
+        re.shortReason = req.shortReason();
+        re.fullReason = req.fullReason();
+        re.label = req.label().orElse(null);
+        sa.requirements.add(re);
+      }
+    }
+  }
+
   public void addDependencies(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs) {
     if (change == null || currentPs == null) {
       return;
diff --git a/java/com/google/gerrit/server/git/MergeOp.java b/java/com/google/gerrit/server/git/MergeOp.java
index b4a66f4..436faa7 100644
--- a/java/com/google/gerrit/server/git/MergeOp.java
+++ b/java/com/google/gerrit/server/git/MergeOp.java
@@ -291,7 +291,7 @@
       throw new ResourceConflictException("missing current patch set for change " + cd.getId());
     }
     List<SubmitRecord> results = getSubmitRecords(cd, allowClosed);
-    if (SubmitRecord.findOkRecord(results).isPresent()) {
+    if (SubmitRecord.allRecordsOK(results)) {
       // Rules supplied a valid solution.
       return;
     } else if (results.isEmpty()) {
@@ -303,6 +303,9 @@
 
     for (SubmitRecord record : results) {
       switch (record.status) {
+        case OK:
+          break;
+
         case CLOSED:
           throw new ResourceConflictException("change is closed");
 
@@ -313,7 +316,6 @@
           throw new ResourceConflictException(describeLabels(cd, record.labels));
 
         case FORCED:
-        case OK:
         default:
           throw new IllegalStateException(
               String.format(
diff --git a/java/com/google/gerrit/server/git/ProjectLevelConfig.java b/java/com/google/gerrit/server/git/ProjectLevelConfig.java
index 18dcef8..2044db0 100644
--- a/java/com/google/gerrit/server/git/ProjectLevelConfig.java
+++ b/java/com/google/gerrit/server/git/ProjectLevelConfig.java
@@ -14,12 +14,16 @@
 
 package com.google.gerrit.server.git;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.project.ProjectState;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Set;
+import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
@@ -53,6 +57,22 @@
   }
 
   public Config getWithInheritance() {
+    return getWithInheritance(false);
+  }
+
+  /**
+   * Get a Config that includes the values from all parent projects.
+   *
+   * <p>Merging means that matching sections/subsection will be merged to include the values from
+   * both parent and child config.
+   *
+   * <p>No merging means that matching sections/subsections in the child project will replace the
+   * corresponding value from the parent.
+   *
+   * @param merge whether to merge parent values with child values or not.
+   * @return a combined config.
+   */
+  public Config getWithInheritance(boolean merge) {
     Config cfgWithInheritance = new Config();
     try {
       cfgWithInheritance.fromText(get().toText());
@@ -65,21 +85,41 @@
       for (String section : parentCfg.getSections()) {
         Set<String> allNames = get().getNames(section);
         for (String name : parentCfg.getNames(section)) {
+          String[] parentValues = parentCfg.getStringList(section, null, name);
           if (!allNames.contains(name)) {
+            cfgWithInheritance.setStringList(section, null, name, Arrays.asList(parentValues));
+          } else if (merge) {
             cfgWithInheritance.setStringList(
-                section, null, name, Arrays.asList(parentCfg.getStringList(section, null, name)));
+                section,
+                null,
+                name,
+                Stream.concat(
+                        Arrays.stream(cfg.getStringList(section, null, name)),
+                        Arrays.stream(parentValues))
+                    .sorted()
+                    .distinct()
+                    .collect(toList()));
           }
         }
 
         for (String subsection : parentCfg.getSubsections(section)) {
           allNames = get().getNames(section, subsection);
           for (String name : parentCfg.getNames(section, subsection)) {
+            String[] parentValues = parentCfg.getStringList(section, subsection, name);
             if (!allNames.contains(name)) {
               cfgWithInheritance.setStringList(
+                  section, subsection, name, Arrays.asList(parentValues));
+            } else if (merge) {
+              cfgWithInheritance.setStringList(
                   section,
                   subsection,
                   name,
-                  Arrays.asList(parentCfg.getStringList(section, subsection, name)));
+                  Streams.concat(
+                          Arrays.stream(cfg.getStringList(section, subsection, name)),
+                          Arrays.stream(parentValues))
+                      .sorted()
+                      .distinct()
+                      .collect(toList()));
             }
           }
         }
diff --git a/java/com/google/gerrit/server/git/SubmoduleOp.java b/java/com/google/gerrit/server/git/SubmoduleOp.java
index 56ed432..0e3e146 100644
--- a/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import static java.util.Comparator.comparing;
+
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
@@ -47,6 +49,7 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -416,7 +419,10 @@
     DirCache dc = readTree(or.rw, currentCommit);
     DirCacheEditor ed = dc.editor();
     int count = 0;
-    for (SubmoduleSubscription s : targets.get(subscriber)) {
+
+    List<SubmoduleSubscription> subscriptions = new ArrayList<>(targets.get(subscriber));
+    Collections.sort(subscriptions, comparing((SubmoduleSubscription s) -> s.getPath()));
+    for (SubmoduleSubscription s : subscriptions) {
       if (count > 0) {
         msgbuf.append("\n\n");
       }
diff --git a/java/com/google/gerrit/server/git/TagSet.java b/java/com/google/gerrit/server/git/TagSet.java
index 1f54cd0..118223b 100644
--- a/java/com/google/gerrit/server/git/TagSet.java
+++ b/java/com/google/gerrit/server/git/TagSet.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.git;
 
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.readWithoutMarker;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.writeWithoutMarker;
 
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -194,13 +194,13 @@
     for (int i = 0; i < refCnt; i++) {
       String name = in.readUTF();
       int flag = in.readInt();
-      ObjectId id = readNotNull(in);
+      ObjectId id = readWithoutMarker(in);
       refs.put(name, new CachedRef(flag, id));
     }
 
     int tagCnt = in.readInt();
     for (int i = 0; i < tagCnt; i++) {
-      ObjectId id = readNotNull(in);
+      ObjectId id = readWithoutMarker(in);
       BitSet flags = (BitSet) in.readObject();
       tags.add(new Tag(id, flags));
     }
@@ -211,12 +211,12 @@
     for (Map.Entry<String, CachedRef> e : refs.entrySet()) {
       out.writeUTF(e.getKey());
       out.writeInt(e.getValue().flag);
-      writeNotNull(out, e.getValue().get());
+      writeWithoutMarker(out, e.getValue().get());
     }
 
     out.writeInt(tags.size());
     for (Tag tag : tags) {
-      writeNotNull(out, tag);
+      writeWithoutMarker(out, tag);
       out.writeObject(tag.refFlags);
     }
   }
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index d055a46..b7bb0dd 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -14,11 +14,7 @@
 
 package com.google.gerrit.server.index.account;
 
-import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
-
 import com.google.common.collect.ImmutableSet;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -26,17 +22,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.index.IndexExecutor;
-import com.google.gerrit.server.index.IndexUtils;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Optional;
-import java.util.concurrent.Future;
-import org.eclipse.jgit.lib.Config;
 
 public class AccountIndexerImpl implements AccountIndexer {
   public interface Factory {
@@ -48,8 +39,6 @@
   private final AccountCache byIdCache;
   private final DynamicSet<AccountIndexedListener> indexedListener;
   private final StalenessChecker stalenessChecker;
-  private final ListeningExecutorService batchExecutor;
-  private final boolean autoReindexIfStale;
   @Nullable private final AccountIndexCollection indexes;
   @Nullable private final AccountIndex index;
 
@@ -58,14 +47,10 @@
       AccountCache byIdCache,
       DynamicSet<AccountIndexedListener> indexedListener,
       StalenessChecker stalenessChecker,
-      @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
-      @GerritServerConfig Config config,
       @Assisted AccountIndexCollection indexes) {
     this.byIdCache = byIdCache;
     this.indexedListener = indexedListener;
     this.stalenessChecker = stalenessChecker;
-    this.batchExecutor = batchExecutor;
-    this.autoReindexIfStale = autoReindexIfStale(config);
     this.indexes = indexes;
     this.index = null;
   }
@@ -75,14 +60,10 @@
       AccountCache byIdCache,
       DynamicSet<AccountIndexedListener> indexedListener,
       StalenessChecker stalenessChecker,
-      @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
-      @GerritServerConfig Config config,
       @Assisted @Nullable AccountIndex index) {
     this.byIdCache = byIdCache;
     this.indexedListener = indexedListener;
     this.stalenessChecker = stalenessChecker;
-    this.batchExecutor = batchExecutor;
-    this.autoReindexIfStale = autoReindexIfStale(config);
     this.indexes = null;
     this.index = index;
   }
@@ -90,6 +71,8 @@
   @Override
   public void index(Account.Id id) throws IOException {
     for (Index<Account.Id, AccountState> i : getWriteIndexes()) {
+      // Evict the cache to get an up-to-date value for sure.
+      byIdCache.evict(id);
       Optional<AccountState> accountState = byIdCache.get(id);
       if (accountState.isPresent()) {
         i.replace(accountState.get());
@@ -98,7 +81,6 @@
       }
     }
     fireAccountIndexedEvent(id.get());
-    autoReindexIfStale(id);
   }
 
   @Override
@@ -110,35 +92,6 @@
     return false;
   }
 
-  private static boolean autoReindexIfStale(Config cfg) {
-    return cfg.getBoolean("index", null, "autoReindexIfStale", true);
-  }
-
-  private void autoReindexIfStale(Account.Id id) {
-    if (autoReindexIfStale) {
-      // Don't retry indefinitely; if this fails the account will be stale.
-      @SuppressWarnings("unused")
-      Future<?> possiblyIgnoredError = reindexIfStaleAsync(id);
-    }
-  }
-
-  /**
-   * Asynchronously check if a account is stale, and reindex if it is.
-   *
-   * <p>Always run on the batch executor, even if this indexer instance is configured to use a
-   * different executor.
-   *
-   * @param id the ID of the account.
-   * @return future for reindexing the account; returns true if the account was stale.
-   */
-  @SuppressWarnings("deprecation")
-  private com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStaleAsync(
-      Account.Id id) {
-    return Futures.makeChecked(
-        Futures.nonCancellationPropagating(batchExecutor.submit(() -> reindexIfStale(id))),
-        IndexUtils.MAPPER);
-  }
-
   private void fireAccountIndexedEvent(int id) {
     for (AccountIndexedListener listener : indexedListener) {
       listener.onAccountIndexed(id);
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 344048f..262c732 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -36,7 +36,9 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Table;
 import com.google.common.primitives.Longs;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.SchemaUtil;
 import com.google.gerrit.reviewdb.client.Account;
@@ -657,8 +659,15 @@
       Integer appliedBy;
     }
 
+    static class StoredRequirement {
+      String shortReason;
+      String fullReason;
+      @Nullable String label;
+    }
+
     SubmitRecord.Status status;
     List<StoredLabel> labels;
+    List<StoredRequirement> requirements;
     String errorMessage;
 
     StoredSubmitRecord(SubmitRecord rec) {
@@ -674,6 +683,16 @@
           this.labels.add(sl);
         }
       }
+      if (rec.requirements != null) {
+        this.requirements = new ArrayList<>(rec.requirements.size());
+        for (SubmitRequirement requirement : rec.requirements) {
+          StoredRequirement sr = new StoredRequirement();
+          sr.shortReason = requirement.shortReason();
+          sr.fullReason = requirement.fullReason();
+          sr.label = requirement.label().orElse(null);
+          this.requirements.add(sr);
+        }
+      }
     }
 
     private SubmitRecord toSubmitRecord() {
@@ -690,6 +709,15 @@
           rec.labels.add(srl);
         }
       }
+      if (requirements != null) {
+        rec.requirements = new ArrayList<>(requirements.size());
+        for (StoredRequirement requirement : requirements) {
+          SubmitRequirement sr =
+              new SubmitRequirement(
+                  requirement.shortReason, requirement.fullReason, requirement.label);
+          rec.requirements.add(sr);
+        }
+      }
       return rec;
     }
   }
diff --git a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index 2e1da65..0b76b1e 100644
--- a/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.QueueProvider.QueueType;
 import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -59,6 +60,7 @@
   private final ChangeNotes.Factory notesFactory;
   private final AllUsersName allUsersName;
   private final AccountCache accountCache;
+  private final Provider<AccountIndexer> indexer;
   private final ListeningExecutorService executor;
   private final boolean enabled;
 
@@ -72,6 +74,7 @@
       ChangeNotes.Factory notesFactory,
       AllUsersName allUsersName,
       AccountCache accountCache,
+      Provider<AccountIndexer> indexer,
       @IndexExecutor(QueueType.BATCH) ListeningExecutorService executor) {
     this.requestContext = requestContext;
     this.queryProvider = queryProvider;
@@ -80,6 +83,7 @@
     this.notesFactory = notesFactory;
     this.allUsersName = allUsersName;
     this.accountCache = accountCache;
+    this.indexer = indexer;
     this.executor = executor;
     this.enabled = cfg.getBoolean("index", null, "reindexAfterRefUpdate", true);
   }
@@ -91,6 +95,7 @@
       if (accountId != null && !event.getRefName().startsWith(RefNames.REFS_STARRED_CHANGES)) {
         try {
           accountCache.evict(accountId);
+          indexer.get().index(accountId);
         } catch (IOException e) {
           log.error(String.format("Reindex account %s failed.", accountId), e);
         }
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index a1da4ea..fcbdc57 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -14,29 +14,20 @@
 
 package com.google.gerrit.server.index.group;
 
-import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
-
 import com.google.common.collect.ImmutableSet;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.events.GroupIndexedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.index.IndexExecutor;
-import com.google.gerrit.server.index.IndexUtils;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Optional;
-import java.util.concurrent.Future;
-import org.eclipse.jgit.lib.Config;
 
 public class GroupIndexerImpl implements GroupIndexer {
   public interface Factory {
@@ -48,8 +39,6 @@
   private final GroupCache groupCache;
   private final DynamicSet<GroupIndexedListener> indexedListener;
   private final StalenessChecker stalenessChecker;
-  private final ListeningExecutorService batchExecutor;
-  private final boolean autoReindexIfStale;
   @Nullable private final GroupIndexCollection indexes;
   @Nullable private final GroupIndex index;
 
@@ -58,14 +47,10 @@
       GroupCache groupCache,
       DynamicSet<GroupIndexedListener> indexedListener,
       StalenessChecker stalenessChecker,
-      @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
-      @GerritServerConfig Config config,
       @Assisted GroupIndexCollection indexes) {
     this.groupCache = groupCache;
     this.indexedListener = indexedListener;
     this.stalenessChecker = stalenessChecker;
-    this.batchExecutor = batchExecutor;
-    this.autoReindexIfStale = autoReindexIfStale(config);
     this.indexes = indexes;
     this.index = null;
   }
@@ -75,14 +60,10 @@
       GroupCache groupCache,
       DynamicSet<GroupIndexedListener> indexedListener,
       StalenessChecker stalenessChecker,
-      @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
-      @GerritServerConfig Config config,
       @Assisted @Nullable GroupIndex index) {
     this.groupCache = groupCache;
     this.indexedListener = indexedListener;
     this.stalenessChecker = stalenessChecker;
-    this.batchExecutor = batchExecutor;
-    this.autoReindexIfStale = autoReindexIfStale(config);
     this.indexes = null;
     this.index = index;
   }
@@ -100,7 +81,6 @@
       }
     }
     fireGroupIndexedEvent(uuid.get());
-    autoReindexIfStale(uuid);
   }
 
   @Override
@@ -112,35 +92,6 @@
     return false;
   }
 
-  private static boolean autoReindexIfStale(Config cfg) {
-    return cfg.getBoolean("index", null, "autoReindexIfStale", true);
-  }
-
-  private void autoReindexIfStale(AccountGroup.UUID uuid) {
-    if (autoReindexIfStale) {
-      // Don't retry indefinitely; if this fails the group will be stale.
-      @SuppressWarnings("unused")
-      Future<?> possiblyIgnoredError = reindexIfStaleAsync(uuid);
-    }
-  }
-
-  /**
-   * Asynchronously check if a group is stale, and reindex if it is.
-   *
-   * <p>Always run on the batch executor, even if this indexer instance is configured to use a
-   * different executor.
-   *
-   * @param uuid the unique identifier of the group.
-   * @return future for reindexing the group; returns true if the group was stale.
-   */
-  @SuppressWarnings("deprecation")
-  private com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStaleAsync(
-      AccountGroup.UUID uuid) {
-    return Futures.makeChecked(
-        Futures.nonCancellationPropagating(batchExecutor.submit(() -> reindexIfStale(uuid))),
-        IndexUtils.MAPPER);
-  }
-
   private void fireGroupIndexedEvent(String uuid) {
     for (GroupIndexedListener listener : indexedListener) {
       listener.onGroupIndexed(uuid);
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 7010be7..ccc5859 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -728,6 +728,7 @@
             msg.append('\n');
           }
         }
+        // TODO(maximeg) We might want to list plugins that validated this submission.
       }
     }
 
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index b9f5fe6..c599c8e 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -454,7 +454,11 @@
         update.getProjectName(),
         projectName);
     checkState(staged == null, "cannot add new update after staging");
-    changeUpdates.put(update.getRefName(), update);
+    checkArgument(
+        !rewriters.containsKey(update.getRefName()),
+        "cannot update & rewrite ref %s in one BatchUpdate",
+        update.getRefName());
+
     ChangeDraftUpdate du = update.getDraftUpdate();
     if (du != null) {
       draftUpdates.put(du.getRefName(), du);
@@ -463,10 +467,17 @@
     if (rcu != null) {
       robotCommentUpdates.put(rcu.getRefName(), rcu);
     }
-    DeleteCommentRewriter deleteCommentRewriter = update.getDeleteCommentRewriter();
-    if (deleteCommentRewriter != null) {
-      rewriters.put(deleteCommentRewriter.getRefName(), deleteCommentRewriter);
+    DeleteCommentRewriter rwt = update.getDeleteCommentRewriter();
+    if (rwt != null) {
+      // Checks whether there is any ChangeUpdate added earlier trying to update the same ref.
+      checkArgument(
+          !changeUpdates.containsKey(rwt.getRefName()),
+          "cannot update & rewrite ref %s in one BatchUpdate",
+          rwt.getRefName());
+      rewriters.put(rwt.getRefName(), rwt);
     }
+
+    changeUpdates.put(update.getRefName(), update);
   }
 
   public void add(ChangeDraftUpdate draftUpdate) {
@@ -695,17 +706,6 @@
       addUpdates(robotCommentUpdates, changeRepo);
     }
     if (!rewriters.isEmpty()) {
-      Optional<String> conflictKey =
-          rewriters
-              .keySet()
-              .stream()
-              .filter(k -> (draftUpdates.containsKey(k) || robotCommentUpdates.containsKey(k)))
-              .findAny();
-      if (conflictKey.isPresent()) {
-        throw new IllegalArgumentException(
-            String.format(
-                "cannot update and rewrite ref %s in one BatchUpdate", conflictKey.get()));
-      }
       addRewrites(rewriters, changeRepo);
     }
 
diff --git a/java/com/google/gerrit/server/patch/DiffSummaryKey.java b/java/com/google/gerrit/server/patch/DiffSummaryKey.java
index 0a02e36..0f4757a 100644
--- a/java/com/google/gerrit/server/patch/DiffSummaryKey.java
+++ b/java/com/google/gerrit/server/patch/DiffSummaryKey.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.patch;
 
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readCanBeNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.read;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.readWithoutMarker;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.write;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.writeWithoutMarker;
 
 import com.google.common.base.Preconditions;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
@@ -93,9 +93,9 @@
   }
 
   private void writeObject(ObjectOutputStream out) throws IOException {
-    writeCanBeNull(out, oldId);
+    write(out, oldId);
     out.writeInt(parentNum == null ? 0 : parentNum);
-    writeNotNull(out, newId);
+    writeWithoutMarker(out, newId);
     Character c = PatchListKey.WHITESPACE_TYPES.get(whitespace);
     if (c == null) {
       throw new IOException("Invalid whitespace type: " + whitespace);
@@ -104,10 +104,10 @@
   }
 
   private void readObject(ObjectInputStream in) throws IOException {
-    oldId = readCanBeNull(in);
+    oldId = read(in);
     int n = in.readInt();
     parentNum = n == 0 ? null : Integer.valueOf(n);
-    newId = readNotNull(in);
+    newId = readWithoutMarker(in);
     char t = in.readChar();
     whitespace = PatchListKey.WHITESPACE_TYPES.inverse().get(t);
     if (whitespace == null) {
diff --git a/java/com/google/gerrit/server/patch/PatchList.java b/java/com/google/gerrit/server/patch/PatchList.java
index 16ede58..cf5df4a 100644
--- a/java/com/google/gerrit/server/patch/PatchList.java
+++ b/java/com/google/gerrit/server/patch/PatchList.java
@@ -18,10 +18,10 @@
 import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeBytes;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readCanBeNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.read;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.readWithoutMarker;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.write;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.writeWithoutMarker;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.common.Nullable;
@@ -173,8 +173,8 @@
   private void writeObject(ObjectOutputStream output) throws IOException {
     final ByteArrayOutputStream buf = new ByteArrayOutputStream();
     try (DeflaterOutputStream out = new DeflaterOutputStream(buf)) {
-      writeCanBeNull(out, oldId);
-      writeNotNull(out, newId);
+      write(out, oldId);
+      writeWithoutMarker(out, newId);
       writeVarInt32(out, isMerge ? 1 : 0);
       comparisonType.writeTo(out);
       writeVarInt32(out, insertions);
@@ -190,8 +190,8 @@
   private void readObject(ObjectInputStream input) throws IOException {
     final ByteArrayInputStream buf = new ByteArrayInputStream(readBytes(input));
     try (InflaterInputStream in = new InflaterInputStream(buf)) {
-      oldId = readCanBeNull(in);
-      newId = readNotNull(in);
+      oldId = read(in);
+      newId = readWithoutMarker(in);
       isMerge = readVarInt32(in) != 0;
       comparisonType = ComparisonType.readFrom(in);
       insertions = readVarInt32(in);
diff --git a/java/com/google/gerrit/server/patch/PatchListKey.java b/java/com/google/gerrit/server/patch/PatchListKey.java
index 73e82a1..e5be70c 100644
--- a/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.patch;
 
 import static com.google.common.base.Preconditions.checkState;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readCanBeNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.read;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.readWithoutMarker;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.write;
+import static org.eclipse.jgit.lib.ObjectIdSerializer.writeWithoutMarker;
 
 import com.google.common.collect.ImmutableBiMap;
 import com.google.gerrit.common.Nullable;
@@ -186,9 +186,9 @@
   }
 
   private void writeObject(ObjectOutputStream out) throws IOException {
-    writeCanBeNull(out, oldId);
+    write(out, oldId);
     out.writeInt(parentNum == null ? 0 : parentNum);
-    writeNotNull(out, newId);
+    writeWithoutMarker(out, newId);
     Character c = WHITESPACE_TYPES.get(whitespace);
     if (c == null) {
       throw new IOException("Invalid whitespace type: " + whitespace);
@@ -198,10 +198,10 @@
   }
 
   private void readObject(ObjectInputStream in) throws IOException {
-    oldId = readCanBeNull(in);
+    oldId = read(in);
     int n = in.readInt();
     parentNum = n == 0 ? null : Integer.valueOf(n);
-    newId = readNotNull(in);
+    newId = readWithoutMarker(in);
     char t = in.readChar();
     whitespace = WHITESPACE_TYPES.inverse().get(t);
     if (whitespace == null) {
diff --git a/java/com/google/gerrit/server/project/RuleEvalException.java b/java/com/google/gerrit/server/project/RuleEvalException.java
index 9dae11c..7e3c739 100644
--- a/java/com/google/gerrit/server/project/RuleEvalException.java
+++ b/java/com/google/gerrit/server/project/RuleEvalException.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.server.project;
 
-@SuppressWarnings("serial")
 public class RuleEvalException extends Exception {
+  private static final long serialVersionUID = 1L;
+
   public RuleEvalException(String message) {
     super(message);
   }
 
-  RuleEvalException(String message, Throwable cause) {
+  public RuleEvalException(String message, Throwable cause) {
     super(message, cause);
   }
 }
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index c1da015..cb01b81 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -14,38 +14,21 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.Accounts;
-import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.rules.PrologEnvironment;
-import com.google.gerrit.server.rules.StoredValues;
+import com.google.gerrit.server.rules.PrologRule;
+import com.google.gerrit.server.rules.SubmitRule;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import com.googlecode.prolog_cafe.exceptions.CompileException;
-import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
-import com.googlecode.prolog_cafe.lang.IntegerTerm;
-import com.googlecode.prolog_cafe.lang.ListTerm;
-import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.StructureTerm;
-import com.googlecode.prolog_cafe.lang.SymbolTerm;
-import com.googlecode.prolog_cafe.lang.Term;
-import com.googlecode.prolog_cafe.lang.VariableTerm;
-import java.io.StringReader;
-import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -55,9 +38,31 @@
  */
 public class SubmitRuleEvaluator {
   private static final Logger log = LoggerFactory.getLogger(SubmitRuleEvaluator.class);
-
   private static final String DEFAULT_MSG = "Error evaluating project rules, check server log";
 
+  private final ProjectCache projectCache;
+  private final PrologRule prologRule;
+  private final DynamicSet<SubmitRule> submitRules;
+  private final SubmitRuleOptions opts;
+
+  public interface Factory {
+    /** Returns a new {@link SubmitRuleEvaluator} with the specified options */
+    SubmitRuleEvaluator create(SubmitRuleOptions options);
+  }
+
+  @Inject
+  private SubmitRuleEvaluator(
+      ProjectCache projectCache,
+      PrologRule prologRule,
+      DynamicSet<SubmitRule> submitRules,
+      @Assisted SubmitRuleOptions options) {
+    this.projectCache = projectCache;
+    this.prologRule = prologRule;
+    this.submitRules = submitRules;
+
+    this.opts = options;
+  }
+
   public static List<SubmitRecord> defaultRuleError() {
     return createRuleError(DEFAULT_MSG);
   }
@@ -74,143 +79,25 @@
   }
 
   /**
-   * Exception thrown when the label term of a submit record unexpectedly didn't contain a user
-   * term.
-   */
-  private static class UserTermExpected extends Exception {
-    private static final long serialVersionUID = 1L;
-
-    UserTermExpected(SubmitRecord.Label label) {
-      super(String.format("A label with the status %s must contain a user.", label.toString()));
-    }
-  }
-
-  public interface Factory {
-    SubmitRuleEvaluator create(ChangeData cd);
-  }
-
-  private final AccountCache accountCache;
-  private final Accounts accounts;
-  private final Emails emails;
-  private final ProjectCache projectCache;
-  private final ChangeData cd;
-
-  private SubmitRuleOptions.Builder optsBuilder = SubmitRuleOptions.builder();
-  private SubmitRuleOptions opts;
-  private Change change;
-  private PatchSet patchSet;
-  private boolean logErrors = true;
-  private long reductionsConsumed;
-  private ProjectState projectState;
-
-  private Term submitRule;
-
-  @Inject
-  SubmitRuleEvaluator(
-      AccountCache accountCache,
-      Accounts accounts,
-      Emails emails,
-      ProjectCache projectCache,
-      @Assisted ChangeData cd) {
-    this.accountCache = accountCache;
-    this.accounts = accounts;
-    this.emails = emails;
-    this.projectCache = projectCache;
-    this.cd = cd;
-  }
-
-  /**
-   * @return immutable snapshot of options configured so far. If neither {@link #getSubmitRule()}
-   *     nor {@link #getSubmitType()} have been called yet, state within this instance is still
-   *     mutable, so may change before evaluation. The instance's options are frozen at evaluation
-   *     time.
-   */
-  public SubmitRuleOptions getOptions() {
-    if (opts != null) {
-      return opts;
-    }
-    return optsBuilder.build();
-  }
-
-  public SubmitRuleEvaluator setOptions(SubmitRuleOptions opts) {
-    checkNotStarted();
-    if (opts != null) {
-      optsBuilder = opts.toBuilder();
-    } else {
-      optsBuilder = SubmitRuleOptions.builder();
-    }
-    return this;
-  }
-
-  /**
-   * @param ps patch set of the change to evaluate. If not set, the current patch set will be loaded
-   *     from {@link #evaluate()} or {@link #getSubmitType}.
-   * @return this
-   */
-  public SubmitRuleEvaluator setPatchSet(PatchSet ps) {
-    checkArgument(
-        ps.getId().getParentKey().equals(cd.getId()),
-        "Patch set %s does not match change %s",
-        ps.getId(),
-        cd.getId());
-    patchSet = ps;
-    return this;
-  }
-
-  /**
-   * @param allow whether to allow {@link #evaluate()} on closed changes.
-   * @return this
-   */
-  public SubmitRuleEvaluator setAllowClosed(boolean allow) {
-    checkNotStarted();
-    optsBuilder.allowClosed(allow);
-    return this;
-  }
-
-  /**
-   * @param skip if true, submit filter will not be applied.
-   * @return this
-   */
-  public SubmitRuleEvaluator setSkipSubmitFilters(boolean skip) {
-    checkNotStarted();
-    optsBuilder.skipFilters(skip);
-    return this;
-  }
-
-  /**
-   * @param rule custom rule to use, or null to use refs/meta/config:rules.pl.
-   * @return this
-   */
-  public SubmitRuleEvaluator setRule(@Nullable String rule) {
-    checkNotStarted();
-    optsBuilder.rule(rule);
-    return this;
-  }
-
-  /**
-   * @param log whether to log error messages in addition to returning error records. If true, error
-   *     record messages will be less descriptive.
-   */
-  public SubmitRuleEvaluator setLogErrors(boolean log) {
-    logErrors = log;
-    return this;
-  }
-
-  /** @return Prolog reductions consumed during evaluation. */
-  public long getReductionsConsumed() {
-    return reductionsConsumed;
-  }
-
-  /**
    * Evaluate the submit rules.
    *
    * @return List of {@link SubmitRecord} objects returned from the evaluated rules, including any
    *     errors.
+   * @param cd ChangeData to evaluate
    */
-  public List<SubmitRecord> evaluate() {
-    initOptions();
+  public List<SubmitRecord> evaluate(ChangeData cd) {
+    Change change;
+    ProjectState projectState;
     try {
-      init();
+      change = cd.change();
+      if (change == null) {
+        throw new OrmException("Change not found");
+      }
+
+      projectState = projectCache.get(cd.project());
+      if (projectState == null) {
+        throw new NoSuchProjectException(cd.project());
+      }
     } catch (OrmException | NoSuchProjectException e) {
       return ruleError("Error looking up change " + cd.getId(), e);
     }
@@ -221,138 +108,16 @@
       return Collections.singletonList(rec);
     }
 
-    List<Term> results;
-    try {
-      results =
-          evaluateImpl(
-              "locate_submit_rule", "can_submit", "locate_submit_filter", "filter_submit_results");
-    } catch (RuleEvalException e) {
-      return ruleError(e.getMessage(), e);
-    }
-
-    if (results.isEmpty()) {
-      // This should never occur. A well written submit rule will always produce
-      // at least one result informing the caller of the labels that are
-      // required for this change to be submittable. Each label will indicate
-      // whether or not that is actually possible given the permissions.
-      return ruleError(
-          String.format(
-              "Submit rule '%s' for change %s of %s has no solution.",
-              getSubmitRuleName(), cd.getId(), getProjectName()));
-    }
-
-    return resultsToSubmitRecord(getSubmitRule(), results);
-  }
-
-  /**
-   * Convert the results from Prolog Cafe's format to Gerrit's common format.
-   *
-   * <p>can_submit/1 terminates when an ok(P) record is found. Therefore walk the results backwards,
-   * using only that ok(P) record if it exists. This skips partial results that occur early in the
-   * output. Later after the loop the out collection is reversed to restore it to the original
-   * ordering.
-   */
-  private List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
-    List<SubmitRecord> out = new ArrayList<>(results.size());
-    for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
-      Term submitRecord = results.get(resultIdx);
-      SubmitRecord rec = new SubmitRecord();
-      out.add(rec);
-
-      if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
-        return invalidResult(submitRule, submitRecord);
-      }
-
-      if ("ok".equals(submitRecord.name())) {
-        rec.status = SubmitRecord.Status.OK;
-
-      } else if ("not_ready".equals(submitRecord.name())) {
-        rec.status = SubmitRecord.Status.NOT_READY;
-
-      } else {
-        return invalidResult(submitRule, submitRecord);
-      }
-
-      // Unpack the one argument. This should also be a structure with one
-      // argument per label that needs to be reported on to the caller.
-      //
-      submitRecord = submitRecord.arg(0);
-
-      if (!(submitRecord instanceof StructureTerm)) {
-        return invalidResult(submitRule, submitRecord);
-      }
-
-      rec.labels = new ArrayList<>(submitRecord.arity());
-
-      for (Term state : ((StructureTerm) submitRecord).args()) {
-        if (!(state instanceof StructureTerm)
-            || 2 != state.arity()
-            || !"label".equals(state.name())) {
-          return invalidResult(submitRule, submitRecord);
-        }
-
-        SubmitRecord.Label lbl = new SubmitRecord.Label();
-        rec.labels.add(lbl);
-
-        lbl.label = state.arg(0).name();
-        Term status = state.arg(1);
-
-        try {
-          if ("ok".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.OK;
-            appliedBy(lbl, status);
-
-          } else if ("reject".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.REJECT;
-            appliedBy(lbl, status);
-
-          } else if ("need".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.NEED;
-
-          } else if ("may".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.MAY;
-
-          } else if ("impossible".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
-
-          } else {
-            return invalidResult(submitRule, submitRecord);
-          }
-        } catch (UserTermExpected e) {
-          return invalidResult(submitRule, submitRecord, e.getMessage());
-        }
-      }
-
-      if (rec.status == SubmitRecord.Status.OK) {
-        break;
-      }
-    }
-    Collections.reverse(out);
-
-    return out;
-  }
-
-  private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) {
-    return ruleError(
-        String.format(
-            "Submit rule %s for change %s of %s output invalid result: %s%s",
-            rule,
-            cd.getId(),
-            getProjectName(),
-            record,
-            (reason == null ? "" : ". Reason: " + reason)));
-  }
-
-  private List<SubmitRecord> invalidResult(Term rule, Term record) {
-    return invalidResult(rule, record, null);
-  }
-
-  private List<SubmitRecord> ruleError(String err) {
-    return ruleError(err, null);
+    // We evaluate all the plugin-defined evaluators,
+    // and then we collect the results in one list.
+    return StreamSupport.stream(submitRules.spliterator(), false)
+        .map(s -> s.evaluate(cd, opts))
+        .flatMap(Collection::stream)
+        .collect(Collectors.toList());
   }
 
   private List<SubmitRecord> ruleError(String err, Exception e) {
-    if (logErrors) {
+    if (opts.logErrors()) {
       if (e == null) {
         log.error(err);
       } else {
@@ -367,73 +132,24 @@
    * Evaluate the submit type rules to get the submit type.
    *
    * @return record from the evaluated rules.
+   * @param cd
    */
-  public SubmitTypeRecord getSubmitType() {
-    initOptions();
+  public SubmitTypeRecord getSubmitType(ChangeData cd) {
+    ProjectState projectState;
     try {
-      init();
-    } catch (OrmException | NoSuchProjectException e) {
+      projectState = projectCache.get(cd.project());
+      if (projectState == null) {
+        throw new NoSuchProjectException(cd.project());
+      }
+    } catch (NoSuchProjectException e) {
       return typeError("Error looking up change " + cd.getId(), e);
     }
 
-    List<Term> results;
-    try {
-      results =
-          evaluateImpl(
-              "locate_submit_type",
-              "get_submit_type",
-              "locate_submit_type_filter",
-              "filter_submit_type_results");
-    } catch (RuleEvalException e) {
-      return typeError(e.getMessage(), e);
-    }
-
-    if (results.isEmpty()) {
-      // Should never occur for a well written rule
-      return typeError(
-          "Submit rule '"
-              + getSubmitRuleName()
-              + "' for change "
-              + cd.getId()
-              + " of "
-              + getProjectName()
-              + " has no solution.");
-    }
-
-    Term typeTerm = results.get(0);
-    if (!(typeTerm instanceof SymbolTerm)) {
-      return typeError(
-          "Submit rule '"
-              + getSubmitRuleName()
-              + "' for change "
-              + cd.getId()
-              + " of "
-              + getProjectName()
-              + " did not return a symbol.");
-    }
-
-    String typeName = ((SymbolTerm) typeTerm).name();
-    try {
-      return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase()));
-    } catch (IllegalArgumentException e) {
-      return typeError(
-          "Submit type rule "
-              + getSubmitRule()
-              + " for change "
-              + cd.getId()
-              + " of "
-              + getProjectName()
-              + " output invalid result: "
-              + typeName);
-    }
-  }
-
-  private SubmitTypeRecord typeError(String err) {
-    return typeError(err, null);
+    return prologRule.getSubmitType(cd, opts);
   }
 
   private SubmitTypeRecord typeError(String err, Exception e) {
-    if (logErrors) {
+    if (opts.logErrors()) {
       if (e == null) {
         log.error(err);
       } else {
@@ -443,194 +159,4 @@
     }
     return SubmitTypeRecord.error(err);
   }
-
-  private List<Term> evaluateImpl(
-      String userRuleLocatorName,
-      String userRuleWrapperName,
-      String filterRuleLocatorName,
-      String filterRuleWrapperName)
-      throws RuleEvalException {
-    PrologEnvironment env = getPrologEnvironment();
-    try {
-      Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
-      List<Term> results = new ArrayList<>();
-      try {
-        for (Term[] template : env.all("gerrit", userRuleWrapperName, sr, new VariableTerm())) {
-          results.add(template[1]);
-        }
-      } catch (ReductionLimitException err) {
-        throw new RuleEvalException(
-            String.format(
-                "%s on change %d of %s", err.getMessage(), cd.getId().get(), getProjectName()));
-      } catch (RuntimeException err) {
-        throw new RuleEvalException(
-            String.format(
-                "Exception calling %s on change %d of %s", sr, cd.getId().get(), getProjectName()),
-            err);
-      } finally {
-        reductionsConsumed = env.getReductions();
-      }
-
-      Term resultsTerm = toListTerm(results);
-      if (!opts.skipFilters()) {
-        resultsTerm =
-            runSubmitFilters(resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
-      }
-      List<Term> r;
-      if (resultsTerm instanceof ListTerm) {
-        r = new ArrayList<>();
-        for (Term t = resultsTerm; t instanceof ListTerm; ) {
-          ListTerm l = (ListTerm) t;
-          r.add(l.car().dereference());
-          t = l.cdr().dereference();
-        }
-      } else {
-        r = Collections.emptyList();
-      }
-      submitRule = sr;
-      return r;
-    } finally {
-      env.close();
-    }
-  }
-
-  private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
-    PrologEnvironment env;
-    try {
-      if (opts.rule() == null) {
-        env = projectState.newPrologEnvironment();
-      } else {
-        env = projectState.newPrologEnvironment("stdin", new StringReader(opts.rule()));
-      }
-    } catch (CompileException err) {
-      String msg;
-      if (opts.rule() == null) {
-        msg = String.format("Cannot load rules.pl for %s: %s", getProjectName(), err.getMessage());
-      } else {
-        msg = err.getMessage();
-      }
-      throw new RuleEvalException(msg, err);
-    }
-    env.set(StoredValues.ACCOUNTS, accounts);
-    env.set(StoredValues.ACCOUNT_CACHE, accountCache);
-    env.set(StoredValues.EMAILS, emails);
-    env.set(StoredValues.REVIEW_DB, cd.db());
-    env.set(StoredValues.CHANGE_DATA, cd);
-    env.set(StoredValues.PROJECT_STATE, projectState);
-    return env;
-  }
-
-  private Term runSubmitFilters(
-      Term results,
-      PrologEnvironment env,
-      String filterRuleLocatorName,
-      String filterRuleWrapperName)
-      throws RuleEvalException {
-    PrologEnvironment childEnv = env;
-    for (ProjectState parentState : projectState.parents()) {
-      PrologEnvironment parentEnv;
-      try {
-        parentEnv = parentState.newPrologEnvironment();
-      } catch (CompileException err) {
-        throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
-      }
-
-      parentEnv.copyStoredValues(childEnv);
-      Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
-      try {
-        Term[] template =
-            parentEnv.once(
-                "gerrit", filterRuleWrapperName, filterRule, results, new VariableTerm());
-        results = template[2];
-      } catch (ReductionLimitException err) {
-        throw new RuleEvalException(
-            String.format(
-                "%s on change %d of %s",
-                err.getMessage(), cd.getId().get(), parentState.getName()));
-      } catch (RuntimeException err) {
-        throw new RuleEvalException(
-            String.format(
-                "Exception calling %s on change %d of %s",
-                filterRule, cd.getId().get(), parentState.getName()),
-            err);
-      } finally {
-        reductionsConsumed += env.getReductions();
-      }
-      childEnv = parentEnv;
-    }
-    return results;
-  }
-
-  private static Term toListTerm(List<Term> terms) {
-    Term list = Prolog.Nil;
-    for (int i = terms.size() - 1; i >= 0; i--) {
-      list = new ListTerm(terms.get(i), list);
-    }
-    return list;
-  }
-
-  private void appliedBy(SubmitRecord.Label label, Term status) throws UserTermExpected {
-    if (status instanceof StructureTerm && status.arity() == 1) {
-      Term who = status.arg(0);
-      if (isUser(who)) {
-        label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
-      } else {
-        throw new UserTermExpected(label);
-      }
-    }
-  }
-
-  private static boolean isUser(Term who) {
-    return who instanceof StructureTerm
-        && who.arity() == 1
-        && who.name().equals("user")
-        && who.arg(0) instanceof IntegerTerm;
-  }
-
-  public Term getSubmitRule() {
-    checkState(submitRule != null, "getSubmitRule() invalid before evaluation");
-    return submitRule;
-  }
-
-  public String getSubmitRuleName() {
-    return submitRule != null ? submitRule.toString() : "<unknown rule>";
-  }
-
-  private void checkNotStarted() {
-    checkState(opts == null, "cannot set options after starting evaluation");
-  }
-
-  private void initOptions() {
-    if (opts == null) {
-      opts = optsBuilder.build();
-      optsBuilder = null;
-    }
-  }
-
-  private void init() throws OrmException, NoSuchProjectException {
-    if (change == null) {
-      change = cd.change();
-      if (change == null) {
-        throw new OrmException("No change found");
-      }
-    }
-
-    if (projectState == null) {
-      projectState = projectCache.get(change.getProject());
-      if (projectState == null) {
-        throw new NoSuchProjectException(change.getProject());
-      }
-    }
-
-    if (patchSet == null) {
-      patchSet = cd.currentPatchSet();
-      if (patchSet == null) {
-        throw new OrmException("No patch set found");
-      }
-    }
-  }
-
-  private String getProjectName() {
-    return projectState.getName();
-  }
 }
diff --git a/java/com/google/gerrit/server/project/SubmitRuleOptions.java b/java/com/google/gerrit/server/project/SubmitRuleOptions.java
index 332aa75..a4340b2 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleOptions.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleOptions.java
@@ -25,17 +25,28 @@
  */
 @AutoValue
 public abstract class SubmitRuleOptions {
+  private static final SubmitRuleOptions defaults =
+      new AutoValue_SubmitRuleOptions.Builder()
+          .allowClosed(false)
+          .skipFilters(false)
+          .logErrors(true)
+          .rule(null)
+          .build();
+
+  public static SubmitRuleOptions defaults() {
+    return defaults;
+  }
+
   public static Builder builder() {
-    return new AutoValue_SubmitRuleOptions.Builder()
-        .allowClosed(false)
-        .skipFilters(false)
-        .rule(null);
+    return defaults.toBuilder();
   }
 
   public abstract boolean allowClosed();
 
   public abstract boolean skipFilters();
 
+  public abstract boolean logErrors();
+
   @Nullable
   public abstract String rule();
 
@@ -49,6 +60,8 @@
 
     public abstract SubmitRuleOptions.Builder rule(@Nullable String rule);
 
+    public abstract SubmitRuleOptions.Builder logErrors(boolean logErrors);
+
     public abstract SubmitRuleOptions build();
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 6dfc40b..fccb14a 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -943,7 +943,7 @@
       if (!lazyLoad) {
         return Collections.emptyList();
       }
-      records = submitRuleEvaluatorFactory.create(this).setOptions(options).evaluate();
+      records = submitRuleEvaluatorFactory.create(options).evaluate(this);
       submitRecords.put(options, records);
     }
     return records;
@@ -960,7 +960,8 @@
 
   public SubmitTypeRecord submitTypeRecord() {
     if (submitTypeRecord == null) {
-      submitTypeRecord = submitRuleEvaluatorFactory.create(this).getSubmitType();
+      submitTypeRecord =
+          submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults()).getSubmitType(this);
     }
     return submitTypeRecord;
   }
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 64c6208..0806edf 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gson.Gson;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -249,8 +250,8 @@
     }
 
     if (includeSubmitRecords) {
-      eventFactory.addSubmitRecords(
-          c, submitRuleEvaluatorFactory.create(d).setAllowClosed(true).evaluate());
+      SubmitRuleOptions options = SubmitRuleOptions.builder().allowClosed(true).build();
+      eventFactory.addSubmitRecords(c, submitRuleEvaluatorFactory.create(options).evaluate(d));
     }
 
     if (includeCommitMessage) {
diff --git a/java/com/google/gerrit/server/restapi/account/GetGroups.java b/java/com/google/gerrit/server/restapi/account/GetGroups.java
index 992a85a..486a151 100644
--- a/java/com/google/gerrit/server/restapi/account/GetGroups.java
+++ b/java/com/google/gerrit/server/restapi/account/GetGroups.java
@@ -28,6 +28,7 @@
 import com.google.inject.Singleton;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 @Singleton
 public class GetGroups implements RestReadView<AccountResource> {
@@ -44,8 +45,9 @@
   public List<GroupInfo> apply(AccountResource resource) throws OrmException {
     IdentifiedUser user = resource.getUser();
     Account.Id userId = user.getAccountId();
-    List<GroupInfo> groups = new ArrayList<>();
-    for (AccountGroup.UUID uuid : user.getEffectiveGroups().getKnownGroups()) {
+    Set<AccountGroup.UUID> knownGroups = user.getEffectiveGroups().getKnownGroups();
+    List<GroupInfo> visibleGroups = new ArrayList<>();
+    for (AccountGroup.UUID uuid : knownGroups) {
       GroupControl ctl;
       try {
         ctl = groupControlFactory.controlFor(uuid);
@@ -53,9 +55,9 @@
         continue;
       }
       if (ctl.isVisible() && ctl.canSeeMember(userId)) {
-        groups.add(json.format(ctl.getGroup()));
+        visibleGroups.add(json.format(ctl.getGroup()));
       }
     }
-    return groups;
+    return visibleGroups;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/Index.java b/java/com/google/gerrit/server/restapi/account/Index.java
index 20a381a..fc6b7b3 100644
--- a/java/com/google/gerrit/server/restapi/account/Index.java
+++ b/java/com/google/gerrit/server/restapi/account/Index.java
@@ -19,8 +19,8 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -32,14 +32,16 @@
 @Singleton
 public class Index implements RestModifyView<AccountResource, Input> {
 
-  private final AccountCache accountCache;
+  private final Provider<AccountIndexer> accountIndexer;
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> self;
 
   @Inject
   Index(
-      AccountCache accountCache, PermissionBackend permissionBackend, Provider<CurrentUser> self) {
-    this.accountCache = accountCache;
+      Provider<AccountIndexer> accountIndexer,
+      PermissionBackend permissionBackend,
+      Provider<CurrentUser> self) {
+    this.accountIndexer = accountIndexer;
     this.permissionBackend = permissionBackend;
     this.self = self;
   }
@@ -51,8 +53,7 @@
       permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT);
     }
 
-    // evicting the account from the cache, reindexes the account
-    accountCache.evict(rsrc.getUser().getAccountId());
+    accountIndexer.get().index(rsrc.getUser().getAccountId());
     return Response.none();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Mergeable.java b/java/com/google/gerrit/server/restapi/change/Mergeable.java
index 47d76b2..4f8064b 100644
--- a/java/com/google/gerrit/server/restapi/change/Mergeable.java
+++ b/java/com/google/gerrit/server/restapi/change/Mergeable.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -70,7 +71,7 @@
   private final Provider<ReviewDb> db;
   private final ChangeIndexer indexer;
   private final MergeabilityCache cache;
-  private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
+  private final SubmitRuleEvaluator submitRuleEvaluator;
 
   @Inject
   Mergeable(
@@ -89,7 +90,7 @@
     this.db = db;
     this.indexer = indexer;
     this.cache = cache;
-    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
+    submitRuleEvaluator = submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults());
   }
 
   public void setOtherBranches(boolean otherBranches) {
@@ -112,7 +113,7 @@
     }
 
     ChangeData cd = changeDataFactory.create(db.get(), resource.getNotes());
-    result.submitType = getSubmitType(cd, ps);
+    result.submitType = getSubmitType(cd);
 
     try (Repository git = gitManager.openRepository(change.getProject())) {
       ObjectId commit = toId(ps);
@@ -144,9 +145,8 @@
     return result;
   }
 
-  private SubmitType getSubmitType(ChangeData cd, PatchSet patchSet) throws OrmException {
-    SubmitTypeRecord rec =
-        submitRuleEvaluatorFactory.create(cd).setPatchSet(patchSet).getSubmitType();
+  private SubmitType getSubmitType(ChangeData cd) throws OrmException {
+    SubmitTypeRecord rec = submitRuleEvaluator.getSubmitType(cd);
     if (rec.status != SubmitTypeRecord.Status.OK) {
       throw new OrmException("Submit type rule failed: " + rec);
     }
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index 4ff3862..f9d7083 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -194,13 +194,20 @@
 
     Addition byAccountId =
         addByAccountId(reviewer, rsrc, state, notify, accountsToNotify, allowGroup, allowByEmail);
+
+    Addition wholeGroup = null;
+    if (byAccountId == null || !byAccountId.exactMatchFound) {
+      wholeGroup =
+          addWholeGroup(
+              reviewer, rsrc, state, notify, accountsToNotify, confirmed, allowGroup, allowByEmail);
+      if (wholeGroup != null && wholeGroup.exactMatchFound) {
+        return wholeGroup;
+      }
+    }
+
     if (byAccountId != null) {
       return byAccountId;
     }
-
-    Addition wholeGroup =
-        addWholeGroup(
-            reviewer, rsrc, state, notify, accountsToNotify, confirmed, allowGroup, allowByEmail);
     if (wholeGroup != null) {
       return wholeGroup;
     }
@@ -216,7 +223,8 @@
         null,
         CC,
         NotifyHandling.NONE,
-        ImmutableListMultimap.of());
+        ImmutableListMultimap.of(),
+        true);
   }
 
   @Nullable
@@ -229,9 +237,14 @@
       boolean allowGroup,
       boolean allowByEmail)
       throws OrmException, PermissionBackendException, IOException, ConfigInvalidException {
-    Account.Id accountId = null;
+    IdentifiedUser user = null;
+    boolean exactMatchFound = false;
     try {
-      accountId = accounts.parse(reviewer).getAccountId();
+      user = accounts.parse(reviewer);
+      if (reviewer.equalsIgnoreCase(user.getName())
+          || reviewer.equals(String.valueOf(user.getAccountId()))) {
+        exactMatchFound = true;
+      }
     } catch (UnprocessableEntityException | AuthException e) {
       // AuthException won't occur since the user is authenticated at this point.
       if (!allowGroup && !allowByEmail) {
@@ -242,13 +255,20 @@
       return null;
     }
 
-    ReviewerResource rrsrc = reviewerFactory.create(rsrc, accountId);
+    ReviewerResource rrsrc = reviewerFactory.create(rsrc, user.getAccountId());
     Account member = rrsrc.getReviewerUser().getAccount();
     PermissionBackend.ForRef perm =
         permissionBackend.user(rrsrc.getReviewerUser()).ref(rrsrc.getChange().getDest());
     if (isValidReviewer(member, perm)) {
       return new Addition(
-          reviewer, rsrc, ImmutableSet.of(member.getId()), null, state, notify, accountsToNotify);
+          reviewer,
+          rsrc,
+          ImmutableSet.of(member.getId()),
+          null,
+          state,
+          notify,
+          accountsToNotify,
+          exactMatchFound);
     }
     if (!member.isActive()) {
       if (allowByEmail && state == CC) {
@@ -328,7 +348,7 @@
       }
     }
 
-    return new Addition(reviewer, rsrc, reviewers, null, state, notify, accountsToNotify);
+    return new Addition(reviewer, rsrc, reviewers, null, state, notify, accountsToNotify, true);
   }
 
   @Nullable
@@ -357,7 +377,7 @@
       return fail(reviewer, MessageFormat.format(ChangeMessages.get().reviewerInvalid, reviewer));
     }
     return new Addition(
-        reviewer, rsrc, null, ImmutableList.of(adr), state, notify, accountsToNotify);
+        reviewer, rsrc, null, ImmutableList.of(adr), state, notify, accountsToNotify, true);
   }
 
   private boolean isValidReviewer(Account member, PermissionBackend.ForRef perm)
@@ -395,6 +415,7 @@
     final ReviewerState state;
     final ChangeNotes notes;
     final IdentifiedUser caller;
+    final boolean exactMatchFound;
 
     Addition(String reviewer) {
       result = new AddReviewerResult(reviewer);
@@ -404,6 +425,7 @@
       state = REVIEWER;
       notes = null;
       caller = null;
+      exactMatchFound = false;
     }
 
     protected Addition(
@@ -413,7 +435,8 @@
         @Nullable Collection<Address> reviewersByEmail,
         ReviewerState state,
         @Nullable NotifyHandling notify,
-        ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+        ListMultimap<RecipientType, Account.Id> accountsToNotify,
+        boolean exactMatchFound) {
       checkArgument(
           reviewers != null || reviewersByEmail != null,
           "must have either reviewers or reviewersByEmail");
@@ -427,6 +450,7 @@
       op =
           postReviewersOpFactory.create(
               rsrc, this.reviewers, this.reviewersByEmail, state, notify, accountsToNotify);
+      this.exactMatchFound = exactMatchFound;
     }
 
     void gatherResults() throws OrmException, PermissionBackendException {
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index b55ca5e..4545794 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -18,7 +18,10 @@
 import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -43,6 +46,7 @@
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeMessages;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyUtil;
 import com.google.gerrit.server.extensions.events.ChangeReverted;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mail.send.RevertedSender;
@@ -70,6 +74,7 @@
 import java.text.MessageFormat;
 import java.util.HashSet;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
@@ -102,6 +107,7 @@
   private final ChangeReverted changeReverted;
   private final ContributorAgreementsChecker contributorAgreements;
   private final ProjectCache projectCache;
+  private final NotifyUtil notifyUtil;
 
   @Inject
   Revert(
@@ -119,7 +125,8 @@
       ApprovalsUtil approvalsUtil,
       ChangeReverted changeReverted,
       ContributorAgreementsChecker contributorAgreements,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      NotifyUtil notifyUtil) {
     super(retryHelper);
     this.db = db;
     this.permissionBackend = permissionBackend;
@@ -135,13 +142,14 @@
     this.changeReverted = changeReverted;
     this.contributorAgreements = contributorAgreements;
     this.projectCache = projectCache;
+    this.notifyUtil = notifyUtil;
   }
 
   @Override
   public ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, ChangeResource rsrc, RevertInput input)
       throws IOException, OrmException, RestApiException, UpdateException, NoSuchChangeException,
-          PermissionBackendException, NoSuchProjectException {
+          PermissionBackendException, NoSuchProjectException, ConfigInvalidException {
     Change change = rsrc.getChange();
     if (change.getStatus() != Change.Status.MERGED) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
@@ -151,14 +159,14 @@
     permissionBackend.user(rsrc.getUser()).ref(change.getDest()).check(CREATE_CHANGE);
     projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
-    Change.Id revertId =
-        revert(updateFactory, rsrc.getNotes(), rsrc.getUser(), Strings.emptyToNull(input.message));
+    Change.Id revertId = revert(updateFactory, rsrc.getNotes(), rsrc.getUser(), input);
     return json.noOptions().format(rsrc.getProject(), revertId);
   }
 
   private Change.Id revert(
-      BatchUpdate.Factory updateFactory, ChangeNotes notes, CurrentUser user, String message)
-      throws OrmException, IOException, RestApiException, UpdateException {
+      BatchUpdate.Factory updateFactory, ChangeNotes notes, CurrentUser user, RevertInput input)
+      throws OrmException, IOException, RestApiException, UpdateException, ConfigInvalidException {
+    String message = Strings.emptyToNull(input.message);
     Change.Id changeIdToRevert = notes.getChangeId();
     PatchSet.Id patchSetId = notes.getChange().currentPatchSetId();
     PatchSet patch = psUtil.get(db.get(), notes, patchSetId);
@@ -213,11 +221,16 @@
       ObjectId id = oi.insert(revertCommitBuilder);
       RevCommit revertCommit = revWalk.parseCommit(id);
 
+      ListMultimap<RecipientType, Account.Id> accountsToNotify =
+          notifyUtil.resolveAccounts(input.notifyDetails);
+
       ChangeInserter ins =
           changeInserterFactory
               .create(changeId, revertCommit, notes.getChange().getDest().get())
               .setTopic(changeToRevert.getTopic());
       ins.setMessage("Uploaded patch set 1.");
+      ins.setNotify(input.notify);
+      ins.setAccountsToNotify(accountsToNotify);
 
       ReviewerSet reviewerSet = approvalsUtil.getReviewers(db.get(), notes);
 
@@ -235,7 +248,7 @@
       try (BatchUpdate bu = updateFactory.create(db.get(), project, user, now)) {
         bu.setRepository(git, revWalk, oi);
         bu.insertChange(ins);
-        bu.addOp(changeId, new NotifyOp(notes.getChange(), ins));
+        bu.addOp(changeId, new NotifyOp(changeToRevert, ins, input.notify, accountsToNotify));
         bu.addOp(changeToRevert.getId(), new PostRevertedMessageOp(computedChangeId));
         bu.execute();
       }
@@ -269,23 +282,31 @@
   private class NotifyOp implements BatchUpdateOp {
     private final Change change;
     private final ChangeInserter ins;
+    private final NotifyHandling notifyHandling;
+    private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
 
-    NotifyOp(Change change, ChangeInserter ins) {
+    NotifyOp(
+        Change change,
+        ChangeInserter ins,
+        NotifyHandling notifyHandling,
+        ListMultimap<RecipientType, Account.Id> accountsToNotify) {
       this.change = change;
       this.ins = ins;
+      this.notifyHandling = notifyHandling;
+      this.accountsToNotify = accountsToNotify;
     }
 
     @Override
     public void postUpdate(Context ctx) throws Exception {
       changeReverted.fire(change, ins.getChange(), ctx.getWhen());
-      Change.Id changeId = ins.getChange().getId();
       try {
-        RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), changeId);
+        RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
         cm.setFrom(ctx.getAccountId());
-        cm.setChangeMessage(ins.getChangeMessage().getMessage(), ctx.getWhen());
+        cm.setNotify(notifyHandling);
+        cm.setAccountsToNotify(accountsToNotify);
         cm.send();
       } catch (Exception err) {
-        log.error("Cannot send email for revert change " + changeId, err);
+        log.error("Cannot send email for revert change " + change.getId(), err);
       }
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerJson.java b/java/com/google/gerrit/server/restapi/change/ReviewerJson.java
index ef5b432..d74be7d 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerJson.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -49,7 +50,7 @@
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
   private final AccountLoader.Factory accountLoaderFactory;
-  private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
+  private final SubmitRuleEvaluator submitRuleEvaluator;
 
   @Inject
   ReviewerJson(
@@ -64,7 +65,7 @@
     this.changeDataFactory = changeDataFactory;
     this.approvalsUtil = approvalsUtil;
     this.accountLoaderFactory = accountLoaderFactory;
-    this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
+    submitRuleEvaluator = submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults());
   }
 
   public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs)
@@ -124,7 +125,7 @@
     // do not exist in the DB.
     PatchSet ps = cd.currentPatchSet();
     if (ps != null) {
-      for (SubmitRecord rec : submitRuleEvaluatorFactory.create(cd).evaluate()) {
+      for (SubmitRecord rec : submitRuleEvaluator.evaluate(cd)) {
         if (rec.labels == null) {
           continue;
         }
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
index eb356ed..2a18612 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.RulesCache;
 import com.google.gwtorm.server.OrmException;
@@ -70,24 +71,22 @@
       throw new AuthException("project rules are disabled");
     }
     input.filters = MoreObjects.firstNonNull(input.filters, filters);
-    SubmitRuleEvaluator evaluator =
-        submitRuleEvaluatorFactory.create(changeDataFactory.create(db.get(), rsrc.getNotes()));
 
-    List<SubmitRecord> records =
-        evaluator
-            .setPatchSet(rsrc.getPatchSet())
-            .setLogErrors(false)
-            .setSkipSubmitFilters(input.filters == Filters.SKIP)
-            .setRule(input.rule)
-            .evaluate();
+    SubmitRuleOptions opts =
+        SubmitRuleOptions.builder()
+            .skipFilters(input.filters == Filters.SKIP)
+            .rule(input.rule)
+            .logErrors(false)
+            .build();
+
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
+    List<SubmitRecord> records = submitRuleEvaluatorFactory.create(opts).evaluate(cd);
+
     List<Record> out = Lists.newArrayListWithCapacity(records.size());
     AccountLoader accounts = accountInfoFactory.create(true);
     for (SubmitRecord r : records) {
       out.add(new Record(r, accounts));
     }
-    if (!out.isEmpty()) {
-      out.get(0).prologReductionCount = evaluator.getReductionsConsumed();
-    }
     accounts.fill();
     return out;
   }
@@ -100,7 +99,6 @@
     Map<String, None> need;
     Map<String, AccountInfo> may;
     Map<String, None> impossible;
-    Long prologReductionCount;
 
     Record(SubmitRecord r, AccountLoader accounts) {
       this.status = r.status;
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
index d8d5d0a..c1be1ce 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.RulesCache;
 import com.google.gwtorm.server.OrmException;
@@ -64,19 +65,20 @@
       throw new AuthException("project rules are disabled");
     }
     input.filters = MoreObjects.firstNonNull(input.filters, filters);
-    SubmitRuleEvaluator evaluator =
-        submitRuleEvaluatorFactory.create(changeDataFactory.create(db.get(), rsrc.getNotes()));
 
-    SubmitTypeRecord rec =
-        evaluator
-            .setPatchSet(rsrc.getPatchSet())
-            .setLogErrors(false)
-            .setSkipSubmitFilters(input.filters == Filters.SKIP)
-            .setRule(input.rule)
-            .getSubmitType();
+    SubmitRuleOptions opts =
+        SubmitRuleOptions.builder()
+            .logErrors(false)
+            .skipFilters(input.filters == Filters.SKIP)
+            .rule(input.rule)
+            .build();
+
+    SubmitRuleEvaluator evaluator = submitRuleEvaluatorFactory.create(opts);
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
+    SubmitTypeRecord rec = evaluator.getSubmitType(cd);
+
     if (rec.status != SubmitTypeRecord.Status.OK) {
-      throw new BadRequestException(
-          String.format("rule %s produced invalid result: %s", evaluator.getSubmitRuleName(), rec));
+      throw new BadRequestException(String.format("rule produced invalid result: %s", rec));
     }
 
     return rec.type;
diff --git a/java/com/google/gerrit/server/restapi/config/GetSummary.java b/java/com/google/gerrit/server/restapi/config/GetSummary.java
index 26f069c..a382436 100644
--- a/java/com/google/gerrit/server/restapi/config/GetSummary.java
+++ b/java/com/google/gerrit/server/restapi/config/GetSummary.java
@@ -37,7 +37,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.eclipse.jgit.internal.storage.file.WindowCacheStatAccessor;
+import org.eclipse.jgit.storage.file.WindowCacheStats;
 import org.kohsuke.args4j.Option;
 
 @RequiresCapability(GlobalCapability.MAINTAIN_SERVER)
@@ -125,8 +125,8 @@
     long mTotal = r.totalMemory();
     long mInuse = mTotal - mFree;
 
-    int jgitOpen = WindowCacheStatAccessor.getOpenFiles();
-    long jgitBytes = WindowCacheStatAccessor.getOpenBytes();
+    int jgitOpen = WindowCacheStats.getOpenFiles();
+    long jgitBytes = WindowCacheStats.getOpenBytes();
 
     MemSummaryInfo memSummaryInfo = new MemSummaryInfo();
     memSummaryInfo.total = bytes(mTotal);
diff --git a/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java b/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
index 1f6feb4..2aa3fb9 100644
--- a/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetDiffPreferences.java
@@ -66,7 +66,7 @@
 
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
       DiffPreferencesInfo updatedPrefs = Preferences.updateDefaultDiffPreferences(md, input);
-      accountCache.evictAllNoReindex();
+      accountCache.evictAll();
       return updatedPrefs;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java b/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
index a531152..36bcdb3 100644
--- a/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetEditPreferences.java
@@ -66,7 +66,7 @@
 
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
       EditPreferencesInfo updatedPrefs = Preferences.updateDefaultEditPreferences(md, input);
-      accountCache.evictAllNoReindex();
+      accountCache.evictAll();
       return updatedPrefs;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/config/SetPreferences.java b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
index f3ba8f9..0815af7 100644
--- a/java/com/google/gerrit/server/restapi/config/SetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/SetPreferences.java
@@ -63,7 +63,7 @@
     Preferences.validateMy(input.my);
     try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
       GeneralPreferencesInfo updatedPrefs = Preferences.updateDefaultGeneralPreferences(md, input);
-      accountCache.evictAllNoReindex();
+      accountCache.evictAll();
       return updatedPrefs;
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
index 2c583c6..51fffbb 100644
--- a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
@@ -117,10 +117,12 @@
         if (includedGroup.isPresent()) {
           member = groupJson.format(new InternalGroupDescription(includedGroup.get()));
         } else {
-          GroupDescription.Basic groupDescription = groupBackend.get(includedGroupUUID);
           member = new GroupInfo();
           member.id = Url.encode(includedGroupUUID.get());
-          member.name = groupDescription.getName();
+          GroupDescription.Basic groupDescription = groupBackend.get(includedGroupUUID);
+          if (groupDescription != null) {
+            member.name = groupDescription.getName();
+          }
         }
 
         auditEvents.add(
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index eb44118..365fe89 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -47,6 +47,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
 import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
@@ -110,6 +111,7 @@
   private final Provider<IdentifiedUser> identifiedUser;
   private final Provider<PutConfig> putConfig;
   private final AllProjectsName allProjects;
+  private final AllUsersName allUsers;
   private final DynamicItem<ProjectNameLockManager> lockManager;
   private final String name;
 
@@ -131,6 +133,7 @@
       Provider<IdentifiedUser> identifiedUser,
       Provider<PutConfig> putConfig,
       AllProjectsName allProjects,
+      AllUsersName allUsers,
       DynamicItem<ProjectNameLockManager> lockManager,
       @Assisted String name) {
     this.projectsCollection = projectsCollection;
@@ -149,6 +152,7 @@
     this.identifiedUser = identifiedUser;
     this.putConfig = putConfig;
     this.allProjects = allProjects;
+    this.allUsers = allUsers;
     this.lockManager = lockManager;
     this.name = name;
   }
@@ -169,6 +173,10 @@
     String parentName =
         MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get());
     args.newParent = projectsCollection.get().parse(parentName, false).getNameKey();
+    if (args.newParent.equals(allUsers)) {
+      throw new ResourceConflictException(
+          String.format("Cannot inherit from '%s' project", allUsers.get()));
+    }
     args.createEmptyCommit = input.createEmptyCommit;
     args.permissionsOnly = input.permissionsOnly;
     args.projectDescription = Strings.emptyToNull(input.description);
diff --git a/java/com/google/gerrit/server/restapi/project/SetParent.java b/java/com/google/gerrit/server/restapi/project/SetParent.java
index 21fef97..2e51929 100644
--- a/java/com/google/gerrit/server/restapi/project/SetParent.java
+++ b/java/com/google/gerrit/server/restapi/project/SetParent.java
@@ -126,6 +126,11 @@
       throw new ResourceConflictException("cannot set parent of " + allProjects.get());
     }
 
+    if (allUsers.get().equals(newParent)) {
+      throw new ResourceConflictException(
+          String.format("Cannot inherit from '%s' project", allUsers.get()));
+    }
+
     newParent = Strings.emptyToNull(newParent);
     if (newParent != null) {
       ProjectState parent = cache.get(new Project.NameKey(newParent));
diff --git a/java/com/google/gerrit/server/rules/PrologModule.java b/java/com/google/gerrit/server/rules/PrologModule.java
index 1a8b46c..e6de229 100644
--- a/java/com/google/gerrit/server/rules/PrologModule.java
+++ b/java/com/google/gerrit/server/rules/PrologModule.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.rules;
 
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicSet;
 
@@ -22,6 +23,9 @@
   protected void configure() {
     install(new EnvironmentModule());
     bind(PrologEnvironment.Args.class);
+    factory(PrologRuleEvaluator.Factory.class);
+
+    bind(SubmitRule.class).annotatedWith(Exports.named("PrologRule")).to(PrologRule.class);
   }
 
   static class EnvironmentModule extends FactoryModule {
diff --git a/java/com/google/gerrit/server/rules/PrologRule.java b/java/com/google/gerrit/server/rules/PrologRule.java
new file mode 100644
index 0000000..44f18c5
--- /dev/null
+++ b/java/com/google/gerrit/server/rules/PrologRule.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.rules;
+
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.PrologRuleEvaluator.Factory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Collection;
+
+@Singleton
+public class PrologRule implements SubmitRule {
+
+  private final Factory factory;
+
+  @Inject
+  private PrologRule(PrologRuleEvaluator.Factory factory) {
+    this.factory = factory;
+  }
+
+  @Override
+  public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions opts) {
+    return getEvaluator(cd, opts).evaluate();
+  }
+
+  private PrologRuleEvaluator getEvaluator(ChangeData cd, SubmitRuleOptions opts) {
+    return factory.create(cd, opts);
+  }
+
+  public SubmitTypeRecord getSubmitType(ChangeData cd, SubmitRuleOptions opts) {
+    return getEvaluator(cd, opts).getSubmitType();
+  }
+}
diff --git a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
new file mode 100644
index 0000000..f2d3c19
--- /dev/null
+++ b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
@@ -0,0 +1,519 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.rules;
+
+import static com.google.gerrit.server.project.SubmitRuleEvaluator.createRuleError;
+import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultRuleError;
+import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultTypeError;
+
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RuleEvalException;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
+import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.ListTerm;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Evaluates a submit-like Prolog rule found in the rules.pl file of the current project and filters
+ * the results through rules found in the parent projects, all the way up to All-Projects.
+ */
+public class PrologRuleEvaluator {
+  private static final Logger log = LoggerFactory.getLogger(PrologRuleEvaluator.class);
+
+  public interface Factory {
+    /** Returns a new {@link PrologRuleEvaluator} with the specified options */
+    PrologRuleEvaluator create(ChangeData cd, SubmitRuleOptions options);
+  }
+
+  /**
+   * Exception thrown when the label term of a submit record unexpectedly didn't contain a user
+   * term.
+   */
+  private static class UserTermExpected extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    UserTermExpected(SubmitRecord.Label label) {
+      super(String.format("A label with the status %s must contain a user.", label.toString()));
+    }
+  }
+
+  private final AccountCache accountCache;
+  private final Accounts accounts;
+  private final Emails emails;
+  private final ChangeData cd;
+  private final ProjectState projectState;
+  private final SubmitRuleOptions opts;
+  private Term submitRule;
+
+  @AssistedInject
+  private PrologRuleEvaluator(
+      AccountCache accountCache,
+      Accounts accounts,
+      Emails emails,
+      ProjectCache projectCache,
+      @Assisted ChangeData cd,
+      @Assisted SubmitRuleOptions options) {
+    this.accountCache = accountCache;
+    this.accounts = accounts;
+    this.emails = emails;
+    this.cd = cd;
+    this.opts = options;
+
+    this.projectState = projectCache.get(cd.project());
+  }
+
+  private static Term toListTerm(List<Term> terms) {
+    Term list = Prolog.Nil;
+    for (int i = terms.size() - 1; i >= 0; i--) {
+      list = new ListTerm(terms.get(i), list);
+    }
+    return list;
+  }
+
+  private static boolean isUser(Term who) {
+    return who instanceof StructureTerm
+        && who.arity() == 1
+        && who.name().equals("user")
+        && who.arg(0) instanceof IntegerTerm;
+  }
+
+  private Term getSubmitRule() {
+    return submitRule;
+  }
+
+  /**
+   * Evaluate the submit rules.
+   *
+   * @return List of {@link SubmitRecord} objects returned from the evaluated rules, including any
+   *     errors.
+   */
+  public Collection<SubmitRecord> evaluate() {
+    Change change;
+    try {
+      change = cd.change();
+      if (change == null) {
+        throw new OrmException("No change found");
+      }
+
+      if (projectState == null) {
+        throw new NoSuchProjectException(cd.project());
+      }
+    } catch (OrmException | NoSuchProjectException e) {
+      return ruleError("Error looking up change " + cd.getId(), e);
+    }
+
+    if (!opts.allowClosed() && change.getStatus().isClosed()) {
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.CLOSED;
+      return Collections.singletonList(rec);
+    }
+
+    List<Term> results;
+    try {
+      results =
+          evaluateImpl(
+              "locate_submit_rule", "can_submit", "locate_submit_filter", "filter_submit_results");
+    } catch (RuleEvalException e) {
+      return ruleError(e.getMessage(), e);
+    }
+
+    if (results.isEmpty()) {
+      // This should never occur. A well written submit rule will always produce
+      // at least one result informing the caller of the labels that are
+      // required for this change to be submittable. Each label will indicate
+      // whether or not that is actually possible given the permissions.
+      return ruleError(
+          String.format(
+              "Submit rule '%s' for change %s of %s has no solution.",
+              getSubmitRuleName(), cd.getId(), projectState.getName()));
+    }
+
+    return resultsToSubmitRecord(getSubmitRule(), results);
+  }
+
+  private String getSubmitRuleName() {
+    return submitRule == null ? "<unknown>" : submitRule.name();
+  }
+
+  /**
+   * Convert the results from Prolog Cafe's format to Gerrit's common format.
+   *
+   * <p>can_submit/1 terminates when an ok(P) record is found. Therefore walk the results backwards,
+   * using only that ok(P) record if it exists. This skips partial results that occur early in the
+   * output. Later after the loop the out collection is reversed to restore it to the original
+   * ordering.
+   */
+  public List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
+    boolean foundOk = false;
+    List<SubmitRecord> out = new ArrayList<>(results.size());
+    for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
+      Term submitRecord = results.get(resultIdx);
+      SubmitRecord rec = new SubmitRecord();
+      out.add(rec);
+
+      if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
+        return invalidResult(submitRule, submitRecord);
+      }
+
+      if ("ok".equals(submitRecord.name())) {
+        rec.status = SubmitRecord.Status.OK;
+
+      } else if ("not_ready".equals(submitRecord.name())) {
+        rec.status = SubmitRecord.Status.NOT_READY;
+
+      } else {
+        return invalidResult(submitRule, submitRecord);
+      }
+
+      // Unpack the one argument. This should also be a structure with one
+      // argument per label that needs to be reported on to the caller.
+      //
+      submitRecord = submitRecord.arg(0);
+
+      if (!(submitRecord instanceof StructureTerm)) {
+        return invalidResult(submitRule, submitRecord);
+      }
+
+      rec.labels = new ArrayList<>(submitRecord.arity());
+
+      for (Term state : ((StructureTerm) submitRecord).args()) {
+        if (!(state instanceof StructureTerm)
+            || 2 != state.arity()
+            || !"label".equals(state.name())) {
+          return invalidResult(submitRule, submitRecord);
+        }
+
+        SubmitRecord.Label lbl = new SubmitRecord.Label();
+        rec.labels.add(lbl);
+
+        lbl.label = state.arg(0).name();
+        Term status = state.arg(1);
+
+        try {
+          if ("ok".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.OK;
+            appliedBy(lbl, status);
+
+          } else if ("reject".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.REJECT;
+            appliedBy(lbl, status);
+
+          } else if ("need".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.NEED;
+
+          } else if ("may".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.MAY;
+
+          } else if ("impossible".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
+
+          } else {
+            return invalidResult(submitRule, submitRecord);
+          }
+        } catch (UserTermExpected e) {
+          return invalidResult(submitRule, submitRecord, e.getMessage());
+        }
+      }
+
+      if (rec.status == SubmitRecord.Status.OK) {
+        foundOk = true;
+        break;
+      }
+    }
+    Collections.reverse(out);
+
+    // This transformation is required to adapt Prolog's behavior to the way Gerrit handles
+    // SubmitRecords, as defined in the SubmitRecord#allRecordsOK method.
+    // When several rules are defined in Prolog, they are all matched to a SubmitRecord. We want
+    // the change to be submittable when at least one result is OK.
+    if (foundOk) {
+      for (SubmitRecord record : out) {
+        record.status = SubmitRecord.Status.OK;
+      }
+    }
+
+    return out;
+  }
+
+  private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) {
+    return ruleError(
+        String.format(
+            "Submit rule %s for change %s of %s output invalid result: %s%s",
+            rule,
+            cd.getId(),
+            cd.project().get(),
+            record,
+            (reason == null ? "" : ". Reason: " + reason)));
+  }
+
+  private List<SubmitRecord> invalidResult(Term rule, Term record) {
+    return invalidResult(rule, record, null);
+  }
+
+  private List<SubmitRecord> ruleError(String err) {
+    return ruleError(err, null);
+  }
+
+  private List<SubmitRecord> ruleError(String err, Exception e) {
+    if (opts.logErrors()) {
+      if (e == null) {
+        log.error(err);
+      } else {
+        log.error(err, e);
+      }
+      return defaultRuleError();
+    }
+    return createRuleError(err);
+  }
+
+  /**
+   * Evaluate the submit type rules to get the submit type.
+   *
+   * @return record from the evaluated rules.
+   */
+  public SubmitTypeRecord getSubmitType() {
+    try {
+      if (projectState == null) {
+        throw new NoSuchProjectException(cd.project());
+      }
+    } catch (NoSuchProjectException e) {
+      return typeError("Error looking up change " + cd.getId(), e);
+    }
+
+    List<Term> results;
+    try {
+      results =
+          evaluateImpl(
+              "locate_submit_type",
+              "get_submit_type",
+              "locate_submit_type_filter",
+              "filter_submit_type_results");
+    } catch (RuleEvalException e) {
+      return typeError(e.getMessage(), e);
+    }
+
+    if (results.isEmpty()) {
+      // Should never occur for a well written rule
+      return typeError(
+          "Submit rule '"
+              + getSubmitRuleName()
+              + "' for change "
+              + cd.getId()
+              + " of "
+              + projectState.getName()
+              + " has no solution.");
+    }
+
+    Term typeTerm = results.get(0);
+    if (!(typeTerm instanceof SymbolTerm)) {
+      return typeError(
+          "Submit rule '"
+              + getSubmitRuleName()
+              + "' for change "
+              + cd.getId()
+              + " of "
+              + projectState.getName()
+              + " did not return a symbol.");
+    }
+
+    String typeName = typeTerm.name();
+    try {
+      return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase()));
+    } catch (IllegalArgumentException e) {
+      return typeError(
+          "Submit type rule "
+              + getSubmitRule()
+              + " for change "
+              + cd.getId()
+              + " of "
+              + projectState.getName()
+              + " output invalid result: "
+              + typeName);
+    }
+  }
+
+  private SubmitTypeRecord typeError(String err) {
+    return typeError(err, null);
+  }
+
+  private SubmitTypeRecord typeError(String err, Exception e) {
+    if (opts.logErrors()) {
+      if (e == null) {
+        log.error(err);
+      } else {
+        log.error(err, e);
+      }
+      return defaultTypeError();
+    }
+    return SubmitTypeRecord.error(err);
+  }
+
+  private List<Term> evaluateImpl(
+      String userRuleLocatorName,
+      String userRuleWrapperName,
+      String filterRuleLocatorName,
+      String filterRuleWrapperName)
+      throws RuleEvalException {
+    PrologEnvironment env = getPrologEnvironment();
+    try {
+      Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
+      List<Term> results = new ArrayList<>();
+      try {
+        for (Term[] template : env.all("gerrit", userRuleWrapperName, sr, new VariableTerm())) {
+          results.add(template[1]);
+        }
+      } catch (ReductionLimitException err) {
+        throw new RuleEvalException(
+            String.format(
+                "%s on change %d of %s",
+                err.getMessage(), cd.getId().get(), projectState.getName()));
+      } catch (RuntimeException err) {
+        throw new RuleEvalException(
+            String.format(
+                "Exception calling %s on change %d of %s",
+                sr, cd.getId().get(), projectState.getName()),
+            err);
+      }
+
+      Term resultsTerm = toListTerm(results);
+      if (!opts.skipFilters()) {
+        resultsTerm =
+            runSubmitFilters(resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
+      }
+      List<Term> r;
+      if (resultsTerm instanceof ListTerm) {
+        r = new ArrayList<>();
+        for (Term t = resultsTerm; t instanceof ListTerm; ) {
+          ListTerm l = (ListTerm) t;
+          r.add(l.car().dereference());
+          t = l.cdr().dereference();
+        }
+      } else {
+        r = Collections.emptyList();
+      }
+      submitRule = sr;
+      return r;
+    } finally {
+      env.close();
+    }
+  }
+
+  private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
+    PrologEnvironment env;
+    try {
+      if (opts.rule() == null) {
+        env = projectState.newPrologEnvironment();
+      } else {
+        env = projectState.newPrologEnvironment("stdin", new StringReader(opts.rule()));
+      }
+    } catch (CompileException err) {
+      String msg;
+      if (opts.rule() == null) {
+        msg =
+            String.format(
+                "Cannot load rules.pl for %s: %s", projectState.getName(), err.getMessage());
+      } else {
+        msg = err.getMessage();
+      }
+      throw new RuleEvalException(msg, err);
+    }
+    env.set(StoredValues.ACCOUNTS, accounts);
+    env.set(StoredValues.ACCOUNT_CACHE, accountCache);
+    env.set(StoredValues.EMAILS, emails);
+    env.set(StoredValues.REVIEW_DB, cd.db());
+    env.set(StoredValues.CHANGE_DATA, cd);
+    env.set(StoredValues.PROJECT_STATE, projectState);
+    return env;
+  }
+
+  private Term runSubmitFilters(
+      Term results,
+      PrologEnvironment env,
+      String filterRuleLocatorName,
+      String filterRuleWrapperName)
+      throws RuleEvalException {
+    PrologEnvironment childEnv = env;
+    ChangeData cd = env.get(StoredValues.CHANGE_DATA);
+    ProjectState projectState = env.get(StoredValues.PROJECT_STATE);
+    for (ProjectState parentState : projectState.parents()) {
+      PrologEnvironment parentEnv;
+      try {
+        parentEnv = parentState.newPrologEnvironment();
+      } catch (CompileException err) {
+        throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
+      }
+
+      parentEnv.copyStoredValues(childEnv);
+      Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
+      try {
+        Term[] template =
+            parentEnv.once(
+                "gerrit", filterRuleWrapperName, filterRule, results, new VariableTerm());
+        results = template[2];
+      } catch (ReductionLimitException err) {
+        throw new RuleEvalException(
+            String.format(
+                "%s on change %d of %s",
+                err.getMessage(), cd.getId().get(), parentState.getName()));
+      } catch (RuntimeException err) {
+        throw new RuleEvalException(
+            String.format(
+                "Exception calling %s on change %d of %s",
+                filterRule, cd.getId().get(), parentState.getName()),
+            err);
+      }
+      childEnv = parentEnv;
+    }
+    return results;
+  }
+
+  private void appliedBy(SubmitRecord.Label label, Term status) throws UserTermExpected {
+    if (status instanceof StructureTerm && status.arity() == 1) {
+      Term who = status.arg(0);
+      if (isUser(who)) {
+        label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
+      } else {
+        throw new UserTermExpected(label);
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/rules/SubmitRule.java b/java/com/google/gerrit/server/rules/SubmitRule.java
new file mode 100644
index 0000000..2a68683
--- /dev/null
+++ b/java/com/google/gerrit/server/rules/SubmitRule.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.rules;
+
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import java.util.Collection;
+
+/**
+ * Allows plugins to decide whether a change is ready to be submitted or not.
+ *
+ * <p>For a given {@link ChangeData}, each plugin is called and returns a {@link Collection} of
+ * {@link SubmitRecord}. This collection can be empty, or contain one or several values.
+ *
+ * <p>A Change can only be submitted if all the plugins give their consent.
+ *
+ * <p>Each {@link SubmitRecord} represents a decision made by the plugin. If the plugin rejects a
+ * change, it should hold valuable informations to help the end user understand and correct the
+ * blocking points.
+ *
+ * <p>It should be noted that each plugin can handle rules inheritance.
+ *
+ * <p>This interface should be used to write pre-submit validation rules. This includes both simple
+ * checks, coded in Java, and more complex fully fledged expression evaluators (think: Prolog,
+ * JavaCC, or even JavaScript rules).
+ */
+@ExtensionPoint
+public interface SubmitRule {
+  /** Returns a {@link Collection} of {@link SubmitRecord} status for the change. */
+  Collection<SubmitRecord> evaluate(ChangeData changeData, SubmitRuleOptions options);
+}
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index a3b4d90..e549e08 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -60,7 +60,7 @@
   }
 
   @Override
-  public synchronized void evictAllNoReindex() {
+  public synchronized void evictAll() {
     byId.clear();
   }
 
diff --git a/java/org/eclipse/jgit/BUILD b/java/org/eclipse/jgit/BUILD
index 65bc118..95fef28 100644
--- a/java/org/eclipse/jgit/BUILD
+++ b/java/org/eclipse/jgit/BUILD
@@ -39,8 +39,6 @@
     srcs = [
         "diff/EditDeserializer.java",
         "diff/ReplaceEdit.java",
-        "internal/storage/file/WindowCacheStatAccessor.java",
-        "lib/ObjectIdSerialization.java",
     ],
     visibility = ["//visibility:public"],
     deps = [
diff --git a/java/org/eclipse/jgit/internal/storage/file/WindowCacheStatAccessor.java b/java/org/eclipse/jgit/internal/storage/file/WindowCacheStatAccessor.java
deleted file mode 100644
index f8c4340..0000000
--- a/java/org/eclipse/jgit/internal/storage/file/WindowCacheStatAccessor.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2009 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 org.eclipse.jgit.internal.storage.file;
-
-// Hack to obtain visibility to package level methods only.
-// These aren't yet part of the public JGit API.
-
-public class WindowCacheStatAccessor {
-  public static int getOpenFiles() {
-    return WindowCache.getInstance().getOpenFiles();
-  }
-
-  public static long getOpenBytes() {
-    return WindowCache.getInstance().getOpenBytes();
-  }
-
-  private WindowCacheStatAccessor() {}
-}
diff --git a/java/org/eclipse/jgit/lib/ObjectIdSerialization.java b/java/org/eclipse/jgit/lib/ObjectIdSerialization.java
deleted file mode 100644
index c98da64..0000000
--- a/java/org/eclipse/jgit/lib/ObjectIdSerialization.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2009 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 org.eclipse.jgit.lib;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import org.eclipse.jgit.util.IO;
-
-public class ObjectIdSerialization {
-  public static void writeCanBeNull(OutputStream out, AnyObjectId id) throws IOException {
-    if (id != null) {
-      out.write((byte) 1);
-      writeNotNull(out, id);
-    } else {
-      out.write((byte) 0);
-    }
-  }
-
-  public static void writeNotNull(OutputStream out, AnyObjectId id) throws IOException {
-    id.copyRawTo(out);
-  }
-
-  public static ObjectId readCanBeNull(InputStream in) throws IOException {
-    switch (in.read()) {
-      case 0:
-        return null;
-      case 1:
-        return readNotNull(in);
-      default:
-        throw new IOException("Invalid flag before ObjectId");
-    }
-  }
-
-  public static ObjectId readNotNull(InputStream in) throws IOException {
-    final byte[] b = new byte[20];
-    IO.readFully(in, b, 0, 20);
-    return ObjectId.fromRaw(b);
-  }
-
-  private ObjectIdSerialization() {}
-}
diff --git a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
index 6c3a4b0..fce28de 100644
--- a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
+++ b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
@@ -71,7 +72,8 @@
   public void resetAllRefs() throws Exception {
     Ref matchingRef = createRef("refs/any/test");
 
-    try (ProjectResetter resetProject = builder().reset(project).build()) {
+    try (ProjectResetter resetProject =
+        builder().build(new ProjectResetter.Config().reset(project))) {
       updateRef(matchingRef);
     }
 
@@ -87,7 +89,10 @@
 
     Ref updatedNonMatchingRef;
     try (ProjectResetter resetProject =
-        builder().reset(project, "refs/match/*", "refs/another-match/*").build()) {
+        builder()
+            .build(
+                new ProjectResetter.Config()
+                    .reset(project, "refs/match/*", "refs/another-match/*"))) {
       updateRef(matchingRef);
       updateRef(anotherMatchingRef);
       updatedNonMatchingRef = updateRef(nonMatchingRef);
@@ -107,7 +112,10 @@
     Ref anotherMatchingRef;
     Ref nonMatchingRef;
     try (ProjectResetter resetProject =
-        builder().reset(project, "refs/match/*", "refs/another-match/*").build()) {
+        builder()
+            .build(
+                new ProjectResetter.Config()
+                    .reset(project, "refs/match/*", "refs/another-match/*"))) {
       matchingRef = createRef("refs/match/test");
       anotherMatchingRef = createRef("refs/another-match/test");
       nonMatchingRef = createRef("refs/no-match/test");
@@ -135,7 +143,11 @@
     Ref updatedNonMatchingRefProject1;
     Ref updatedNonMatchingRefProject2;
     try (ProjectResetter resetProject =
-        builder().reset(project, "refs/foo/*").reset(project2, "refs/bar/*").build()) {
+        builder()
+            .build(
+                new ProjectResetter.Config()
+                    .reset(project, "refs/foo/*")
+                    .reset(project2, "refs/bar/*"))) {
       updateRef(matchingRefProject1);
       updatedNonMatchingRefProject1 = updateRef(nonMatchingRefProject1);
 
@@ -162,7 +174,11 @@
     Ref matchingRefProject2;
     Ref nonMatchingRefProject2;
     try (ProjectResetter resetProject =
-        builder().reset(project, "refs/foo/*").reset(project2, "refs/bar/*").build()) {
+        builder()
+            .build(
+                new ProjectResetter.Config()
+                    .reset(project, "refs/foo/*")
+                    .reset(project2, "refs/bar/*"))) {
       matchingRefProject1 = createRef("refs/foo/test");
       nonMatchingRefProject1 = createRef("refs/bar/test");
 
@@ -183,7 +199,9 @@
   public void onlyDeleteNewlyCreatedWithOverlappingRefPatterns() throws Exception {
     Ref matchingRef;
     try (ProjectResetter resetProject =
-        builder().reset(project, "refs/match/*", "refs/match/test").build()) {
+        builder()
+            .build(
+                new ProjectResetter.Config().reset(project, "refs/match/*", "refs/match/test"))) {
       // This ref matches 2 ref pattern, ProjectResetter should try to delete it only once.
       matchingRef = createRef("refs/match/test");
     }
@@ -206,7 +224,8 @@
     Ref nonMetaConfig = createRef("refs/heads/master");
 
     try (ProjectResetter resetProject =
-        builder(null, null, projectCache).reset(project).reset(project2).build()) {
+        builder(null, null, null, projectCache)
+            .build(new ProjectResetter.Config().reset(project).reset(project2))) {
       updateRef(nonMetaConfig);
       updateRef(repo2, metaConfig);
     }
@@ -225,7 +244,8 @@
     EasyMock.replay(projectCache);
 
     try (ProjectResetter resetProject =
-        builder(null, null, projectCache).reset(project).reset(project2).build()) {
+        builder(null, null, null, projectCache)
+            .build(new ProjectResetter.Config().reset(project).reset(project2))) {
       createRef("refs/heads/master");
       createRef(repo2, RefNames.REFS_CONFIG);
     }
@@ -245,16 +265,22 @@
     EasyMock.expectLastCall();
     EasyMock.replay(accountCache);
 
+    AccountIndexer accountIndexer = EasyMock.createNiceMock(AccountIndexer.class);
+    accountIndexer.index(accountId);
+    EasyMock.expectLastCall();
+    EasyMock.replay(accountIndexer);
+
     // Non-user branch because it's not in All-Users.
     Ref nonUserBranch = createRef(RefNames.refsUsers(new Account.Id(2)));
 
     try (ProjectResetter resetProject =
-        builder(null, accountCache, null).reset(project).reset(allUsers).build()) {
+        builder(null, accountCache, accountIndexer, null)
+            .build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
       updateRef(nonUserBranch);
       updateRef(allUsersRepo, userBranch);
     }
 
-    EasyMock.verify(accountCache);
+    EasyMock.verify(accountCache, accountIndexer);
   }
 
   @Test
@@ -268,15 +294,21 @@
     EasyMock.expectLastCall();
     EasyMock.replay(accountCache);
 
+    AccountIndexer accountIndexer = EasyMock.createNiceMock(AccountIndexer.class);
+    accountIndexer.index(accountId);
+    EasyMock.expectLastCall();
+    EasyMock.replay(accountIndexer);
+
     try (ProjectResetter resetProject =
-        builder(null, accountCache, null).reset(project).reset(allUsers).build()) {
+        builder(null, accountCache, accountIndexer, null)
+            .build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
       // Non-user branch because it's not in All-Users.
       createRef(RefNames.refsUsers(new Account.Id(2)));
 
       createRef(allUsersRepo, RefNames.refsUsers(accountId));
     }
 
-    EasyMock.verify(accountCache);
+    EasyMock.verify(accountCache, accountIndexer);
   }
 
   @Test
@@ -296,17 +328,25 @@
     EasyMock.expectLastCall();
     EasyMock.replay(accountCache);
 
+    AccountIndexer accountIndexer = EasyMock.createNiceMock(AccountIndexer.class);
+    accountIndexer.index(accountId);
+    EasyMock.expectLastCall();
+    accountIndexer.index(accountId2);
+    EasyMock.expectLastCall();
+    EasyMock.replay(accountIndexer);
+
     // Non-user branch because it's not in All-Users.
     Ref nonUserBranch = createRef(RefNames.refsUsers(new Account.Id(3)));
 
     try (ProjectResetter resetProject =
-        builder(null, accountCache, null).reset(project).reset(allUsers).build()) {
+        builder(null, accountCache, accountIndexer, null)
+            .build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
       updateRef(nonUserBranch);
       updateRef(allUsersRepo, externalIds);
       createRef(allUsersRepo, RefNames.refsUsers(accountId2));
     }
 
-    EasyMock.verify(accountCache);
+    EasyMock.verify(accountCache, accountIndexer);
   }
 
   @Test
@@ -325,17 +365,25 @@
     EasyMock.expectLastCall();
     EasyMock.replay(accountCache);
 
+    AccountIndexer accountIndexer = EasyMock.createNiceMock(AccountIndexer.class);
+    accountIndexer.index(accountId);
+    EasyMock.expectLastCall();
+    accountIndexer.index(accountId2);
+    EasyMock.expectLastCall();
+    EasyMock.replay(accountIndexer);
+
     // Non-user branch because it's not in All-Users.
     Ref nonUserBranch = createRef(RefNames.refsUsers(new Account.Id(3)));
 
     try (ProjectResetter resetProject =
-        builder(null, accountCache, null).reset(project).reset(allUsers).build()) {
+        builder(null, accountCache, accountIndexer, null)
+            .build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
       updateRef(nonUserBranch);
       createRef(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
       createRef(allUsersRepo, RefNames.refsUsers(accountId2));
     }
 
-    EasyMock.verify(accountCache);
+    EasyMock.verify(accountCache, accountIndexer);
   }
 
   @Test
@@ -350,7 +398,8 @@
     EasyMock.replay(accountCreator);
 
     try (ProjectResetter resetProject =
-        builder(accountCreator, null, null).reset(project).reset(allUsers).build()) {
+        builder(accountCreator, null, null, null)
+            .build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
       createRef(allUsersRepo, RefNames.refsUsers(accountId));
     }
 
@@ -425,18 +474,20 @@
   }
 
   private ProjectResetter.Builder builder() {
-    return builder(null, null, null);
+    return builder(null, null, null, null);
   }
 
   private ProjectResetter.Builder builder(
       @Nullable AccountCreator accountCreator,
       @Nullable AccountCache accountCache,
+      @Nullable AccountIndexer accountIndexer,
       @Nullable ProjectCache projectCache) {
     return new ProjectResetter.Builder(
         repoManager,
         new AllUsersName(AllUsersNameProvider.DEFAULT),
         accountCreator,
         accountCache,
+        accountIndexer,
         projectCache);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index f8781b5..83d166a 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -1982,13 +1982,11 @@
     assertGroups(
         admin.username, ImmutableList.of("Anonymous Users", "Registered Users", "Administrators"));
 
-    // TODO: update when test user is fixed to be included in "Anonymous Users" and
-    //      "Registered Users" groups
-    assertGroups(user.username, ImmutableList.of());
+    assertGroups(user.username, ImmutableList.of("Anonymous Users", "Registered Users"));
 
     String group = createGroup("group");
     String newUser = createAccount("user1", group);
-    assertGroups(newUser, ImmutableList.of(group));
+    assertGroups(newUser, ImmutableList.of("Anonymous Users", "Registered Users", group));
   }
 
   @Test
@@ -2367,11 +2365,14 @@
   }
 
   private void assertGroups(String user, List<String> expected) throws Exception {
-    List<String> actual =
-        gApi.accounts().id(user).getGroups().stream().map(g -> g.name).collect(toList());
+    List<String> actual = getNamesOfGroupsOfUser(user);
     assertThat(actual).containsExactlyElementsIn(expected);
   }
 
+  private List<String> getNamesOfGroupsOfUser(String user) throws RestApiException {
+    return gApi.accounts().id(user).getGroups().stream().map(g -> g.name).collect(toList());
+  }
+
   private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
     int seq = 1;
     for (SshKeyInfo key : sshKeys) {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
new file mode 100644
index 0000000..310a1e19
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
@@ -0,0 +1,170 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.accounts;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountConfig;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.InternalAccountUpdate;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.index.account.AccountIndexer;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
+import com.google.gerrit.testing.InMemoryTestEnvironment;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class AccountIndexerIT {
+  @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
+
+  @Inject private AccountIndexer accountIndexer;
+  @Inject private GerritApi gApi;
+  @Inject private AccountCache accountCache;
+  @Inject private Provider<InternalAccountQuery> accountQueryProvider;
+  @Inject private GitRepositoryManager repoManager;
+  @Inject private AllUsersName allUsersName;
+  @Inject @GerritPersonIdent protected Provider<PersonIdent> serverIdent;
+
+  @Test
+  public void indexingUpdatesTheIndex() throws Exception {
+    Account.Id accountId = createAccount("foo");
+    String preferredEmail = "foo@example.com";
+    updateAccountWithoutCacheOrIndex(
+        accountId, newAccountUpdate().setPreferredEmail(preferredEmail).build());
+    assertThat(accountQueryProvider.get().byPreferredEmail(preferredEmail)).isEmpty();
+
+    accountIndexer.index(accountId);
+    List<AccountState> matchedAccountStates =
+        accountQueryProvider.get().byPreferredEmail(preferredEmail);
+    assertThat(matchedAccountStates).hasSize(1);
+    assertThat(matchedAccountStates.get(0).getAccount().getId()).isEqualTo(accountId);
+  }
+
+  @Test
+  public void indexCannotBeCorruptedByStaleCache() throws Exception {
+    Account.Id accountId = createAccount("foo");
+    loadAccountToCache(accountId);
+    String preferredEmail = "foo@example.com";
+    updateAccountWithoutCacheOrIndex(
+        accountId, newAccountUpdate().setPreferredEmail(preferredEmail).build());
+    assertThat(accountQueryProvider.get().byPreferredEmail(preferredEmail)).isEmpty();
+
+    accountIndexer.index(accountId);
+    List<AccountState> matchedAccountStates =
+        accountQueryProvider.get().byPreferredEmail(preferredEmail);
+    assertThat(matchedAccountStates).hasSize(1);
+    assertThat(matchedAccountStates.get(0).getAccount().getId()).isEqualTo(accountId);
+  }
+
+  @Test
+  public void indexingUpdatesStaleCache() throws Exception {
+    Account.Id accountId = createAccount("foo");
+    loadAccountToCache(accountId);
+    String status = "ooo";
+    updateAccountWithoutCacheOrIndex(accountId, newAccountUpdate().setStatus(status).build());
+    assertThat(accountCache.get(accountId).get().getAccount().getStatus()).isNull();
+
+    accountIndexer.index(accountId);
+    assertThat(accountCache.get(accountId).get().getAccount().getStatus()).isEqualTo(status);
+  }
+
+  @Test
+  public void reindexingStaleAccountUpdatesTheIndex() throws Exception {
+    Account.Id accountId = createAccount("foo");
+    String preferredEmail = "foo@example.com";
+    updateAccountWithoutCacheOrIndex(
+        accountId, newAccountUpdate().setPreferredEmail(preferredEmail).build());
+    assertThat(accountQueryProvider.get().byPreferredEmail(preferredEmail)).isEmpty();
+
+    accountIndexer.reindexIfStale(accountId);
+    List<AccountState> matchedAccountStates =
+        accountQueryProvider.get().byPreferredEmail(preferredEmail);
+    assertThat(matchedAccountStates).hasSize(1);
+    assertThat(matchedAccountStates.get(0).getAccount().getId()).isEqualTo(accountId);
+  }
+
+  @Test
+  public void notStaleAccountIsNotReindexed() throws Exception {
+    Account.Id accountId = createAccount("foo");
+    updateAccountWithoutCacheOrIndex(
+        accountId, newAccountUpdate().setPreferredEmail("foo@example.com").build());
+    accountIndexer.index(accountId);
+
+    boolean reindexed = accountIndexer.reindexIfStale(accountId);
+    assertWithMessage("Account should not have been reindexed").that(reindexed).isFalse();
+  }
+
+  @Test
+  public void indexStalenessIsNotDerivedFromCacheStaleness() throws Exception {
+    Account.Id accountId = createAccount("foo");
+    updateAccountWithoutCacheOrIndex(
+        accountId, newAccountUpdate().setPreferredEmail("foo@example.com").build());
+    reloadAccountToCache(accountId);
+
+    boolean reindexed = accountIndexer.reindexIfStale(accountId);
+    assertWithMessage("Account should have been reindexed").that(reindexed).isTrue();
+  }
+
+  private Account.Id createAccount(String name) throws RestApiException {
+    AccountInfo account = gApi.accounts().create(name).get();
+    return new Account.Id(account._accountId);
+  }
+
+  private void reloadAccountToCache(Account.Id accountId) {
+    accountCache.evict(accountId);
+    loadAccountToCache(accountId);
+  }
+
+  private void loadAccountToCache(Account.Id accountId) {
+    accountCache.get(accountId);
+  }
+
+  private static InternalAccountUpdate.Builder newAccountUpdate() {
+    return InternalAccountUpdate.builder();
+  }
+
+  private void updateAccountWithoutCacheOrIndex(
+      Account.Id accountId, InternalAccountUpdate accountUpdate)
+      throws IOException, ConfigInvalidException {
+    try (Repository allUsersRepo = repoManager.openRepository(allUsersName);
+        MetaDataUpdate md =
+            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo)) {
+      PersonIdent ident = serverIdent.get();
+      md.getCommitBuilder().setAuthor(ident);
+      md.getCommitBuilder().setCommitter(ident);
+
+      AccountConfig accountConfig = new AccountConfig(accountId, allUsersRepo).load();
+      accountConfig.setAccountUpdate(accountUpdate);
+      accountConfig.commit(md);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 8f27de3..eb40513 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -80,6 +80,7 @@
 import com.google.gerrit.extensions.api.changes.NotifyInfo;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.ReviewResult;
@@ -168,7 +169,6 @@
 import org.eclipse.jgit.transport.PushResult;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
 @NoHttpd
@@ -675,6 +675,37 @@
   }
 
   @Test
+  public void revertNotifications() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    sender.clear();
+    ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert().get();
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(2);
+    assertThat(sender.getMessages(revertChange.changeId, "newchange")).hasSize(1);
+    assertThat(sender.getMessages(r.getChangeId(), "revert")).hasSize(1);
+  }
+
+  @Test
+  public void suppressRevertNotifications() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    RevertInput revertInput = new RevertInput();
+    revertInput.notify = NotifyHandling.NONE;
+
+    sender.clear();
+    gApi.changes().id(r.getChangeId()).revert(revertInput).get();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
   public void revertPreservesReviewersAndCcs() throws Exception {
     PushOneCommit.Result r = createChange();
 
@@ -1506,7 +1537,6 @@
     assertThat(sender.getMessages()).hasSize(1);
   }
 
-  @Ignore
   @Test
   public void addReviewerThatIsNotPerfectMatch() throws Exception {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
@@ -1551,7 +1581,6 @@
     assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
   }
 
-  @Ignore
   @Test
   public void addGroupAsReviewersWhenANotPerfectMatchedUserExists() throws Exception {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
@@ -1560,16 +1589,17 @@
     String oldETag = rsrc.getETag();
     Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
 
-    //create a group named "us" with one user: testUser
-    TestAccount testUser = accountCreator.create("testUser", "testUser@test.com", "testUser");
-    String testGroup =
-        createGroupWithRealName(user.fullName.substring(0, user.fullName.length() / 2));
+    //create a group named "kobe" with one user: lee
+    TestAccount testUser = accountCreator.create("kobebryant", "kobebryant@test.com", "kobebryant");
+    TestAccount myGroupUser = accountCreator.create("lee", "lee@test.com", "lee");
+
+    String testGroup = createGroupWithRealName("kobe");
     GroupApi groupApi = gApi.groups().id(testGroup);
     groupApi.description("test group");
-    groupApi.addMembers(testUser.fullName);
+    groupApi.addMembers(myGroupUser.fullName);
 
     //ensure that user "user" is not in the group
-    groupApi.removeMembers(user.fullName);
+    groupApi.removeMembers(testUser.fullName);
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = testGroup;
@@ -1578,11 +1608,11 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(testUser.emailAddress);
-    assertThat(m.body()).contains("Hello " + testUser.fullName + ",\n");
+    assertThat(m.rcpt()).containsExactly(myGroupUser.emailAddress);
+    assertThat(m.body()).contains("Hello " + myGroupUser.fullName + ",\n");
     assertThat(m.body()).contains("I'd like you to do a code review.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
-    assertMailReplyTo(m, testUser.email);
+    assertMailReplyTo(m, myGroupUser.email);
     ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
 
     // When NoteDb is enabled adding a reviewer records that user as reviewer
@@ -1592,7 +1622,7 @@
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(testUser.getId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(myGroupUser.getId().get());
 
     // Ensure ETag and lastUpdatedOn are updated.
     rsrc = parseResource(r);
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 2d5703d..8aa03a4 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -40,7 +40,6 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.ProjectResetter;
-import com.google.gerrit.acceptance.ProjectResetter.Builder;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
@@ -166,11 +165,11 @@
   }
 
   @Override
-  protected ProjectResetter resetProjects(Builder resetter) throws IOException {
+  protected ProjectResetter.Config resetProjects() {
     // Don't reset All-Users since deleting users makes groups inconsistent (e.g. groups would
     // contain members that no longer exist) and as result of this the group consistency checker
     // that is executed after each test would fail.
-    return resetter.reset(allProjects, RefNames.REFS_CONFIG).build();
+    return new ProjectResetter.Config().reset(allProjects, RefNames.REFS_CONFIG);
   }
 
   @Test
@@ -281,7 +280,7 @@
 
     List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(g).auditLog();
     assertThat(auditEvents).hasSize(1);
-    assertAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, "Registered Users");
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, "Registered Users");
   }
 
   @Test
@@ -858,41 +857,41 @@
     GroupApi g = gApi.groups().create(name("group"));
     List<? extends GroupAuditEventInfo> auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(1);
-    assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, admin.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, admin.id);
 
     g.addMembers(user.username);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(2);
-    assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
 
     g.removeMembers(user.username);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(3);
-    assertAuditEvent(auditEvents.get(0), Type.REMOVE_USER, admin.id, user.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.REMOVE_USER, admin.id, user.id);
 
     String otherGroup = name("otherGroup");
     gApi.groups().create(otherGroup);
     g.addGroups(otherGroup);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(4);
-    assertAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
 
     g.removeGroups(otherGroup);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(5);
-    assertAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id, otherGroup);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id, otherGroup);
 
     // Add a removed member back again.
     g.addMembers(user.username);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(6);
-    assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
+    assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
 
     // Add a removed group back again.
     g.addGroups(otherGroup);
     auditEvents = g.auditLog();
     assertThat(auditEvents).hasSize(7);
-    assertAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
 
     Timestamp lastDate = null;
     for (GroupAuditEventInfo auditEvent : auditEvents) {
@@ -903,6 +902,52 @@
     }
   }
 
+  /**
+   * @Sandboxed is used by this test because it deletes a group reference which introduces an
+   * inconsistency for the group storage. Once group deletion is supported, this test should be
+   * updated to use the API instead.
+   */
+  @Test
+  @Sandboxed
+  @IgnoreGroupInconsistencies
+  public void getAuditLogAfterDeletingASubgroup() throws Exception {
+    assume().that(readGroupsFromNoteDb()).isTrue();
+
+    GroupInfo parentGroup = gApi.groups().create(name("parent-group")).get();
+
+    // Creates a subgroup and adds it to "parent-group" as a subgroup.
+    GroupInfo subgroup = gApi.groups().create(name("sub-group")).get();
+    gApi.groups().id(parentGroup.id).addGroups(subgroup.id);
+
+    // Deletes the subgroup.
+    deleteGroupRef(subgroup.id);
+
+    List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(parentGroup.id).auditLog();
+    assertThat(auditEvents).hasSize(2);
+    // Verify the unavailable subgroup's name is null.
+    assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, null);
+  }
+
+  private void deleteGroupRef(String groupId) throws Exception {
+    AccountGroup.UUID uuid = new AccountGroup.UUID(groupId);
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      RefUpdate ru = repo.updateRef(RefNames.refsGroups(uuid));
+      ru.setForceUpdate(true);
+      ru.setNewObjectId(ObjectId.zeroId());
+      assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+    }
+
+    // Reindex the group.
+    gApi.groups().id(uuid.get()).index();
+
+    // Verify "sub-group" has been deleted.
+    try {
+      gApi.groups().id(uuid.get()).get();
+      fail();
+    } catch (ResourceNotFoundException e) {
+    }
+  }
+
   // reindex is tested by {@link AbstractQueryGroupsTest#reindex}
   @Test
   public void reindexPermissions() throws Exception {
@@ -1076,7 +1121,9 @@
 
     // Use ProjectResetter to restore the group names ref
     try (ProjectResetter resetter =
-        projectResetter.builder().reset(allUsers, RefNames.REFS_GROUPNAMES).build()) {
+        projectResetter
+            .builder()
+            .build(new ProjectResetter.Config().reset(allUsers, RefNames.REFS_GROUPNAMES))) {
       // Manually delete group names ref
       try (Repository repo = repoManager.openRepository(allUsers);
           RevWalk rw = new RevWalk(repo)) {
@@ -1410,7 +1457,7 @@
     }
   }
 
-  private void assertAuditEvent(
+  private void assertMemberAuditEvent(
       GroupAuditEventInfo info,
       Type expectedType,
       Account.Id expectedUser,
@@ -1421,7 +1468,7 @@
     assertThat(((UserMemberAuditEventInfo) info).member._accountId).isEqualTo(expectedMember.get());
   }
 
-  private void assertAuditEvent(
+  private void assertSubgroupAuditEvent(
       GroupAuditEventInfo info,
       Type expectedType,
       Account.Id expectedUser,
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index c986c5e..84cfb0d 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -158,6 +158,38 @@
   }
 
   @Test
+  public void createProjectWithNonExistingParent() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.name = name("baz");
+    in.parent = "non-existing";
+
+    exception.expect(UnprocessableEntityException.class);
+    exception.expectMessage("Project Not Found: " + in.parent);
+    gApi.projects().create(in);
+  }
+
+  @Test
+  public void createProjectWithSelfAsParentNotPossible() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.name = name("baz");
+    in.parent = in.name;
+
+    exception.expect(UnprocessableEntityException.class);
+    exception.expectMessage("Project Not Found: " + in.parent);
+    gApi.projects().create(in);
+  }
+
+  @Test
+  public void createProjectUnderAllUsersNotAllowed() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.name = name("foo");
+    in.parent = allUsers.get();
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage(String.format("Cannot inherit from '%s' project", allUsers.get()));
+    gApi.projects().create(in);
+  }
+
+  @Test
   public void createAndDeleteBranch() throws Exception {
     assertThat(getRemoteHead(project.get(), "foo")).isNull();
 
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
index c1bc83e..ec4f327 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
@@ -90,6 +90,13 @@
   }
 
   @Test
+  public void setParentToAllUsersNotAllowed() throws Exception {
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage(String.format("Cannot inherit from '%s' project", allUsers.get()));
+    gApi.projects().name(project.get()).parent(allUsers.get());
+  }
+
+  @Test
   public void setParentForAllUsersMustBeAllProjects() throws Exception {
     gApi.projects().name(allUsers.get()).parent(allProjects.get());
 
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index e0c16e0f..2c95e27 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -785,7 +785,8 @@
   private ProjectResetter resetGroups() throws IOException {
     return projectResetter
         .builder()
-        .reset(allUsers, RefNames.REFS_GROUPS + "*", RefNames.REFS_GROUPNAMES)
-        .build();
+        .build(
+            new ProjectResetter.Config()
+                .reset(allUsers, RefNames.REFS_GROUPS + "*", RefNames.REFS_GROUPNAMES));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 7583830..2812c86 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -346,17 +346,17 @@
           "master",
           "Update git submodules\n\n"
               + "* Update "
-              + name("sub3")
+              + name("sub1")
               + " from branch 'master'\n  to "
-              + sub3HEAD
+              + sub1HEAD
               + "\n\n* Update "
               + name("sub2")
               + " from branch 'master'\n  to "
               + sub2HEAD
               + "\n\n* Update "
-              + name("sub1")
+              + name("sub3")
               + " from branch 'master'\n  to "
-              + sub1HEAD);
+              + sub3HEAD);
     }
 
     superRepo
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
index c1bbf2e..c78b47b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.project.ProjectState;
+import java.util.Arrays;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
@@ -114,4 +115,67 @@
 
     assertThat(state.getConfig(configName).get().toText()).isEqualTo(cfg.toText());
   }
+
+  @Test
+  public void withMergedInheritance() throws Exception {
+    String configName = "test.config";
+
+    Config parentCfg = new Config();
+    parentCfg.setString("s1", null, "k1", "parentValue1");
+    parentCfg.setString("s1", null, "k2", "parentValue2");
+    parentCfg.setString("s2", "ss", "k3", "parentValue3");
+    parentCfg.setString("s2", "ss", "k4", "parentValue4");
+
+    pushFactory
+        .create(
+            db,
+            admin.getIdent(),
+            testRepo,
+            "Create Project Level Config",
+            configName,
+            parentCfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    Project.NameKey childProject = createProject("child", project);
+    TestRepository<?> childTestRepo = cloneProject(childProject);
+    fetch(childTestRepo, RefNames.REFS_CONFIG + ":refs/heads/config");
+    childTestRepo.reset("refs/heads/config");
+
+    Config cfg = new Config();
+    cfg.setString("s1", null, "k1", "parentValue1");
+    cfg.setString("s1", null, "k2", "parentValue2");
+    cfg.setString("s2", "ss", "k3", "parentValue3");
+    cfg.setString("s2", "ss", "k4", "parentValue4");
+    cfg.setString("s1", null, "k1", "childValue1");
+    cfg.setString("s2", "ss", "k3", "childValue2");
+    cfg.setString("s3", null, "k5", "childValue3");
+    cfg.setString("s3", "ss", "k6", "childValue4");
+
+    pushFactory
+        .create(
+            db,
+            admin.getIdent(),
+            childTestRepo,
+            "Create Project Level Config",
+            configName,
+            cfg.toText())
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+
+    ProjectState state = projectCache.get(childProject);
+
+    Config expectedCfg = new Config();
+    expectedCfg.setStringList("s1", null, "k1", Arrays.asList("childValue1", "parentValue1"));
+    expectedCfg.setString("s1", null, "k2", "parentValue2");
+    expectedCfg.setStringList("s2", "ss", "k3", Arrays.asList("childValue2", "parentValue3"));
+    expectedCfg.setString("s2", "ss", "k4", "parentValue4");
+    expectedCfg.setString("s3", null, "k5", "childValue3");
+    expectedCfg.setString("s3", "ss", "k6", "childValue4");
+
+    assertThat(state.getConfig(configName).getWithInheritance(true).toText())
+        .isEqualTo(expectedCfg.toText());
+
+    assertThat(state.getConfig(configName).get().toText()).isEqualTo(cfg.toText());
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 417c9e3..a6c1153 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -2219,17 +2219,22 @@
     assume().that(notesMigration.readChanges()).isFalse();
     StagedChange sc = stageChange();
     revert(sc, sc.owner);
+
+    // email for the newly created revert change
     assertThat(sender)
         .sent("newchange", sc)
         .to(sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
 
+    // email for the change that is reverted
     assertThat(sender)
         .sent("revert", sc)
         .cc(sc.reviewer, sc.ccer, admin)
+        .cc(sc.reviewerByEmail)
+        .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
   }
 
   @Test
@@ -2237,18 +2242,23 @@
     assume().that(notesMigration.readChanges()).isTrue();
     StagedChange sc = stageChange();
     revert(sc, sc.owner);
+
+    // email for the newly created revert change
     assertThat(sender)
         .sent("newchange", sc)
         .to(sc.reviewer, sc.watchingProjectOwner, admin)
         .cc(sc.ccer)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
 
+    // email for the change that is reverted
     assertThat(sender)
         .sent("revert", sc)
         .cc(sc.reviewer, sc.ccer, admin)
+        .cc(sc.reviewerByEmail, sc.ccerByEmail)
+        .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
   }
 
   @Test
@@ -2256,19 +2266,24 @@
     assume().that(notesMigration.readChanges()).isFalse();
     StagedChange sc = stageChange();
     revert(sc, sc.owner, CC_ON_OWN_COMMENTS);
+
+    // email for the newly created revert change
     assertThat(sender)
         .sent("newchange", sc)
         .to(sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin)
         .cc(sc.owner)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
 
+    // email for the change that is reverted
     assertThat(sender)
         .sent("revert", sc)
         .to(sc.owner)
         .cc(sc.reviewer, sc.ccer, admin)
+        .cc(sc.reviewerByEmail)
+        .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
   }
 
   @Test
@@ -2276,19 +2291,24 @@
     assume().that(notesMigration.readChanges()).isTrue();
     StagedChange sc = stageChange();
     revert(sc, sc.owner, CC_ON_OWN_COMMENTS);
+
+    // email for the newly created revert change
     assertThat(sender)
         .sent("newchange", sc)
         .to(sc.reviewer, sc.watchingProjectOwner, admin)
         .cc(sc.owner, sc.ccer)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
 
+    // email for the change that is reverted
     assertThat(sender)
         .sent("revert", sc)
         .to(sc.owner)
         .cc(sc.reviewer, sc.ccer, admin)
+        .cc(sc.reviewerByEmail, sc.ccerByEmail)
+        .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
   }
 
   @Test
@@ -2296,17 +2316,23 @@
     assume().that(notesMigration.readChanges()).isFalse();
     StagedChange sc = stageChange();
     revert(sc, other);
+
+    // email for the newly created revert change
     assertThat(sender)
         .sent("newchange", sc)
         .to(sc.owner, sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
 
+    // email for the change that is reverted
     assertThat(sender)
         .sent("revert", sc)
-        .cc(sc.owner, sc.reviewer, sc.ccer, admin)
+        .to(sc.owner)
+        .cc(sc.reviewer, sc.ccer, admin)
+        .cc(sc.reviewerByEmail)
+        .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
   }
 
   @Test
@@ -2314,18 +2340,24 @@
     assume().that(notesMigration.readChanges()).isTrue();
     StagedChange sc = stageChange();
     revert(sc, other);
+
+    // email for the newly created revert change
     assertThat(sender)
         .sent("newchange", sc)
         .to(sc.owner, sc.reviewer, sc.watchingProjectOwner, admin)
         .cc(sc.ccer)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
 
+    // email for the change that is reverted
     assertThat(sender)
         .sent("revert", sc)
-        .cc(sc.owner, sc.reviewer, sc.ccer, admin)
+        .to(sc.owner)
+        .cc(sc.reviewer, sc.ccer, admin)
+        .cc(sc.reviewerByEmail, sc.ccerByEmail)
+        .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
   }
 
   @Test
@@ -2333,19 +2365,24 @@
     assume().that(notesMigration.readChanges()).isFalse();
     StagedChange sc = stageChange();
     revert(sc, other, CC_ON_OWN_COMMENTS);
+
+    // email for the newly created revert change
     assertThat(sender)
         .sent("newchange", sc)
         .to(sc.owner, sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin)
         .cc(other)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
 
+    // email for the change that is reverted
     assertThat(sender)
         .sent("revert", sc)
-        .to(other)
-        .cc(sc.owner, sc.reviewer, sc.ccer, admin)
+        .to(sc.owner)
+        .cc(other, sc.reviewer, sc.ccer, admin)
+        .cc(sc.reviewerByEmail)
+        .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
   }
 
   @Test
@@ -2353,19 +2390,24 @@
     assume().that(notesMigration.readChanges()).isTrue();
     StagedChange sc = stageChange();
     revert(sc, other, CC_ON_OWN_COMMENTS);
+
+    // email for the newly created revert change
     assertThat(sender)
         .sent("newchange", sc)
         .to(sc.owner, sc.reviewer, sc.watchingProjectOwner, admin)
         .cc(sc.ccer, other)
         .bcc(NEW_CHANGES, NEW_PATCHSETS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
 
+    // email for the change that is reverted
     assertThat(sender)
         .sent("revert", sc)
-        .to(other)
-        .cc(sc.owner, sc.reviewer, sc.ccer, admin)
+        .to(sc.owner)
+        .cc(other, sc.reviewer, sc.ccer, admin)
+        .cc(sc.reviewerByEmail, sc.ccerByEmail)
+        .bcc(sc.starrer)
         .bcc(ALL_COMMENTS)
-        .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email?
+        .noOneElse();
   }
 
   private StagedChange stageChange() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/BUILD b/javatests/com/google/gerrit/acceptance/server/rules/BUILD
new file mode 100644
index 0000000..2e96c0b
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/rules/BUILD
@@ -0,0 +1,10 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "server_rules",
+    labels = ["server"],
+    deps = [
+        "@prolog_runtime//jar",
+    ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
new file mode 100644
index 0000000..8fc32b4
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.rules;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.PrologRuleEvaluator;
+import com.google.gerrit.testing.TestChanges;
+import com.google.inject.Inject;
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.StructureTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+
+public class PrologRuleEvaluatorIT extends AbstractDaemonTest {
+  @Inject private PrologRuleEvaluator.Factory evaluatorFactory;
+
+  @Test
+  public void convertsPrologToSubmitRecord() {
+    PrologRuleEvaluator evaluator = makeEvaluator();
+
+    StructureTerm verifiedLabel = makeLabel("Verified", "may");
+    StructureTerm labels = new StructureTerm("label", verifiedLabel);
+
+    List<Term> terms = ImmutableList.of(makeTerm("ok", labels));
+    Collection<SubmitRecord> records = evaluator.resultsToSubmitRecord(null, terms);
+
+    assertThat(records).hasSize(1);
+  }
+
+  /**
+   * The Prolog behavior is everything but intuitive. Several submit_rules can be defined, and each
+   * will provide a different SubmitRecord answer when queried. The current implementation stops
+   * parsing the Prolog terms into SubmitRecord objects once it finds an OK record. This might lead
+   * to tangling results, as reproduced by this test.
+   *
+   * <p>Let's consider this rules.pl file (equivalent to the code in this test)
+   *
+   * <pre>{@code
+   * submit_rule(submit(R)) :-
+   *     gerrit:uploader(U),
+   *     R = label('Verified', reject(U)).
+   *
+   * submit_rule(submit(CR, V)) :-
+   *     gerrit:uploader(U),
+   *     V = label('Code-Review', ok(U)).
+   *
+   * submit_rule(submit(R)) :-
+   *     gerrit:uploader(U),
+   *     R = label('Any-Label-Name', reject(U)).
+   * }</pre>
+   *
+   * The first submit_rule always fails because the Verified label is rejected.
+   *
+   * <p>The second submit_rule is always valid, and provides two labels: OK and Code-Review.
+   *
+   * <p>The third submit_rule always fails because the Any-Label-Name label is rejected.
+   *
+   * <p>In this case, the last two SubmitRecords are used, the first one is discarded.
+   */
+  @Test
+  public void abortsEarlyWithOkayRecord() {
+    PrologRuleEvaluator evaluator = makeEvaluator();
+
+    SubmitRecord.Label submitRecordLabel1 = new SubmitRecord.Label();
+    submitRecordLabel1.label = "Verified";
+    submitRecordLabel1.status = SubmitRecord.Label.Status.REJECT;
+    submitRecordLabel1.appliedBy = admin.id;
+
+    SubmitRecord.Label submitRecordLabel2 = new SubmitRecord.Label();
+    submitRecordLabel2.label = "Code-Review";
+    submitRecordLabel2.status = SubmitRecord.Label.Status.OK;
+    submitRecordLabel2.appliedBy = admin.id;
+
+    SubmitRecord.Label submitRecordLabel3 = new SubmitRecord.Label();
+    submitRecordLabel3.label = "Any-Label-Name";
+    submitRecordLabel3.status = SubmitRecord.Label.Status.REJECT;
+    submitRecordLabel3.appliedBy = user.id;
+
+    List<Term> terms = new ArrayList<>();
+
+    StructureTerm label1 = makeLabel(submitRecordLabel1.label, "reject", admin);
+
+    StructureTerm label2 = makeLabel(submitRecordLabel2.label, "ok", admin);
+
+    StructureTerm label3 = makeLabel(submitRecordLabel3.label, "reject", user);
+
+    terms.add(makeTerm("not_ready", makeLabels(label1)));
+    terms.add(makeTerm("ok", makeLabels(label2)));
+    terms.add(makeTerm("not_ready", makeLabels(label3)));
+
+    // When
+    List<SubmitRecord> records = evaluator.resultsToSubmitRecord(null, terms);
+
+    // assert that
+    SubmitRecord record1Expected = new SubmitRecord();
+    record1Expected.status = SubmitRecord.Status.OK;
+    record1Expected.labels = new ArrayList<>();
+    record1Expected.labels.add(submitRecordLabel2);
+
+    SubmitRecord record2Expected = new SubmitRecord();
+    record2Expected.status = SubmitRecord.Status.OK;
+    record2Expected.labels = new ArrayList<>();
+    record2Expected.labels.add(submitRecordLabel3);
+
+    assertThat(records).hasSize(2);
+
+    assertThat(records.get(0)).isEqualTo(record1Expected);
+    assertThat(records.get(1)).isEqualTo(record2Expected);
+  }
+
+  private static Term makeTerm(String status, StructureTerm labels) {
+    return new StructureTerm(status, labels);
+  }
+
+  private static StructureTerm makeLabel(String name, String status) {
+    return new StructureTerm("label", new StructureTerm(name), new StructureTerm(status));
+  }
+
+  private static StructureTerm makeLabel(String name, String status, TestAccount account) {
+    StructureTerm user = new StructureTerm("user", new IntegerTerm(account.id.get()));
+    return new StructureTerm("label", new StructureTerm(name), new StructureTerm(status, user));
+  }
+
+  private static StructureTerm makeLabels(StructureTerm... labels) {
+    return new StructureTerm("label", labels);
+  }
+
+  private ChangeData makeChangeData() {
+    ChangeData cd = ChangeData.createForTest(project, new Change.Id(1), 1);
+    cd.setChange(TestChanges.newChange(project, admin.id));
+    return cd;
+  }
+
+  private PrologRuleEvaluator makeEvaluator() {
+    return evaluatorFactory.create(makeChangeData(), SubmitRuleOptions.defaults());
+  }
+}
diff --git a/javatests/com/google/gerrit/common/data/SubmitRecordTest.java b/javatests/com/google/gerrit/common/data/SubmitRecordTest.java
new file mode 100644
index 0000000..6a90f45
--- /dev/null
+++ b/javatests/com/google/gerrit/common/data/SubmitRecordTest.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import org.junit.Test;
+
+public class SubmitRecordTest {
+
+  private static SubmitRecord OK_RECORD;
+  private static SubmitRecord NOT_READY_RECORD;
+
+  static {
+    OK_RECORD = new SubmitRecord();
+    OK_RECORD.status = SubmitRecord.Status.OK;
+
+    NOT_READY_RECORD = new SubmitRecord();
+    NOT_READY_RECORD.status = SubmitRecord.Status.NOT_READY;
+  }
+
+  @Test
+  public void okIfAllOkay() {
+    Collection<SubmitRecord> submitRecords = new ArrayList<>();
+    submitRecords.add(OK_RECORD);
+
+    assertThat(SubmitRecord.allRecordsOK(submitRecords)).isTrue();
+  }
+
+  @Test
+  public void okWhenEmpty() {
+    Collection<SubmitRecord> submitRecords = new ArrayList<>();
+
+    assertThat(SubmitRecord.allRecordsOK(submitRecords)).isTrue();
+  }
+
+  @Test
+  public void emptyResultIfInvalid() {
+    Collection<SubmitRecord> submitRecords = new ArrayList<>();
+    submitRecords.add(NOT_READY_RECORD);
+    submitRecords.add(OK_RECORD);
+
+    assertThat(SubmitRecord.allRecordsOK(submitRecords)).isFalse();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index 031284d..95ac3e3 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Table;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.ReviewerSet;
@@ -30,6 +31,7 @@
 import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.TestTimeUtil;
 import java.sql.Timestamp;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import org.junit.After;
@@ -80,11 +82,32 @@
   @Test
   public void storedSubmitRecords() {
     assertStoredRecordRoundTrip(record(SubmitRecord.Status.CLOSED));
-    assertStoredRecordRoundTrip(
+
+    SubmitRecord r =
         record(
             SubmitRecord.Status.OK,
             label(SubmitRecord.Label.Status.MAY, "Label-1", null),
-            label(SubmitRecord.Label.Status.OK, "Label-2", 1)));
+            label(SubmitRecord.Label.Status.OK, "Label-2", 1));
+
+    assertStoredRecordRoundTrip(r);
+  }
+
+  @Test
+  public void storedSubmitRecordsWithRequirements() {
+    SubmitRecord r =
+        record(
+            SubmitRecord.Status.OK,
+            label(SubmitRecord.Label.Status.MAY, "Label-1", null),
+            label(SubmitRecord.Label.Status.OK, "Label-2", 1));
+
+    SubmitRequirement sr =
+        new SubmitRequirement(
+            "short reason",
+            "Full reason can be a long string with special symbols like < > \\ / ; :",
+            null);
+    r.requirements = Collections.singletonList(sr);
+
+    assertStoredRecordRoundTrip(r);
   }
 
   private static SubmitRecord record(SubmitRecord.Status status, SubmitRecord.Label... labels) {
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 4f14dda..019338e 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -62,6 +62,7 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
@@ -93,6 +94,8 @@
 
   @Inject protected AccountCache accountCache;
 
+  @Inject protected AccountIndexer accountIndexer;
+
   @Inject protected AccountManager accountManager;
 
   @Inject protected GerritApi gApi;
@@ -672,6 +675,7 @@
       accountManager.link(id, AuthRequest.forEmail(email));
     }
     accountCache.evict(id);
+    accountIndexer.index(id);
   }
 
   protected QueryRequest newQuery(Object query) throws RestApiException {
diff --git a/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java b/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
index a77f807..8dc4244 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_159_to_160_Test.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.testing.InMemoryTestEnvironment;
 import com.google.gerrit.testing.TestUpdateUI;
 import com.google.inject.Inject;
@@ -48,6 +49,7 @@
   @Rule public InMemoryTestEnvironment testEnv = new InMemoryTestEnvironment();
 
   @Inject private AccountCache accountCache;
+  @Inject private AccountIndexer accountIndexer;
   @Inject private AllUsersName allUsersName;
   @Inject private GerritApi gApi;
   @Inject private GitRepositoryManager repoManager;
@@ -95,6 +97,7 @@
 
     schema160.migrateData(db, new TestUpdateUI());
     accountCache.evict(accountId);
+    accountIndexer.index(accountId);
     testEnv.setApiUser(accountId);
 
     assertThat(metaRef(accountId)).isNotEqualTo(oldMetaId);
diff --git a/lib/guava.bzl b/lib/guava.bzl
index 0aa3cf7a..db85900 100644
--- a/lib/guava.bzl
+++ b/lib/guava.bzl
@@ -1,5 +1,5 @@
-GUAVA_VERSION = "23.6-jre"
+GUAVA_VERSION = "24.1-jre"
 
-GUAVA_BIN_SHA1 = "c0b638df79e7b2e1ed98f8d68ac62538a715ab1d"
+GUAVA_BIN_SHA1 = "96c528475465aeb22cce60605d230a7e67cebd7b"
 
 GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 7bee9e34..cf389850 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,8 +1,8 @@
 load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "MAVEN_CENTRAL", "maven_jar")
 
-_JGIT_VERS = "4.10.0.201712302008-r.24-gf3bb0e268"
+_JGIT_VERS = "4.11.0.201803080745-r.2-g61e4f1665"
 
-_DOC_VERS = "4.10.0.201712302008-r"  # Set to _JGIT_VERS unless using a snapshot
+_DOC_VERS = "4.11.0.201803080745-r"  # Set to _JGIT_VERS unless using a snapshot
 
 JGIT_DOC_URL = "http://download.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
 
@@ -26,28 +26,28 @@
         name = "jgit_lib",
         artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "1813447ae544b38e36bda9f1599df5f7cff6cfac",
-        src_sha1 = "f0bb1ac954ff529b4e1e01e65ceb0d46d949da9e",
+        sha1 = "38489eca0a4308087081d07774af86aa6a50b2ab",
+        src_sha1 = "e43c58829c72b5b18e16d1b2bbd1396ddd93098f",
         unsign = True,
     )
     maven_jar(
         name = "jgit_servlet",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "f7a88c3744f864587d50096ab99a58e09e4afd95",
+        sha1 = "f5be45e4f97f0bf0825e4ff8fcb2f47588dd7e92",
         unsign = True,
     )
     maven_jar(
         name = "jgit_archive",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "af579bcc932fa26f3c4d4ae00e812dc50d50a355",
+        sha1 = "f202f169b2e3a50be90b4123baa941136eda3ed6",
     )
     maven_jar(
         name = "jgit_junit",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "b5fc2330bf0418e3a0f773925c57497131f13380",
+        sha1 = "dd9f7e4cc41b4f47591ce51c4752ccfef012c553",
         unsign = True,
     )
 
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 32753c4..fa9df30 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 32753c4e226fe6297795c7d80166203719d6e48f
+Subproject commit fa9df3035c306069758712bfe9ae3425b119bb0c
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
index 8bceff1..9f5df11 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
@@ -24,7 +24,6 @@
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.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">
 
@@ -102,17 +101,7 @@
             <input
                 type="checkbox"
                 id="privateChangeCheckBox"
-                checked$="[[_repoConfig.private_by_default.inherited_value]]">
-          </span>
-        </section>
-        <section>
-          <label
-              class="title"
-              for="wipChangeCheckBox">WIP Change</label>
-          <span class="value">
-            <input
-                type="checkbox"
-                id="wipChangeCheckBox">
+                checked$="[[_formatBooleanString(_repoConfig.private_by_default)]]">
           </span>
         </section>
       </div>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
index eace34a..367d324 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -61,13 +61,11 @@
 
     handleCreateChange() {
       const isPrivate = this.$.privateChangeCheckBox.checked;
-      const isWip = this.$.wipChangeCheckBox.checked;
+      const isWip = true;
       return this.$.restAPI.createChange(this.repoName, this.branch,
           this.subject, this.topic, isPrivate, isWip)
           .then(changeCreated => {
-            if (!changeCreated) {
-              return;
-            }
+            if (!changeCreated) { return; }
             Gerrit.Nav.navigateToChange(changeCreated);
           });
     },
@@ -94,5 +92,21 @@
             return branches;
           });
     },
+
+    _formatBooleanString(config) {
+      if (config && config.configured_value === 'TRUE') {
+        return true;
+      } else if (config && config.configured_value === 'FALSE') {
+        return false;
+      } else if (config && config.configured_value === 'INHERIT') {
+        if (config && config.inherited_value) {
+          return true;
+        } else {
+          return false;
+        }
+      } else {
+        return false;
+      }
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
index afe3801..4a51d8f 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
@@ -55,9 +55,12 @@
         },
       });
       element = fixture('basic');
-      element.repoName = 'test-repo';
+      element.repoName = 'test-repo',
       element._repoConfig = {
-        private_by_default: {},
+        private_by_default: {
+          configured_value: 'FALSE',
+          inherited_value: false,
+        },
       };
     });
 
@@ -65,18 +68,57 @@
       sandbox.restore();
     });
 
-    test('new change created with private', () => {
+    test('new change created with default', done => {
+      const configInputObj = {
+        branch: 'test-branch',
+        subject: 'first change created with polygerrit ui',
+        topic: 'test-topic',
+        is_private: false,
+        work_in_progress: true,
+      };
+
+      const saveStub = sandbox.stub(element.$.restAPI,
+          'createChange', () => {
+            return Promise.resolve({});
+          });
+
+      element.branch = 'test-branch';
+      element.topic = 'test-topic';
+      element.subject = 'first change created with polygerrit ui';
+      assert.isFalse(element.$.privateChangeCheckBox.checked);
+
+      element.$.branchInput.bindValue = configInputObj.branch;
+      element.$.tagNameInput.bindValue = configInputObj.topic;
+      element.$.messageInput.bindValue = configInputObj.subject;
+
+      element.handleCreateChange().then(() => {
+        // Private change
+        assert.isFalse(saveStub.lastCall.args[4]);
+        // WIP Change
+        assert.isTrue(saveStub.lastCall.args[5]);
+        assert.isTrue(saveStub.called);
+        done();
+      });
+    });
+
+    test('new change created with private', done => {
       element._repoConfig = {
         private_by_default: {
-          inherited_value: true,
+          configured_value: 'TRUE',
+          inherited_value: false,
         },
       };
+      sandbox.stub(element, '_formatBooleanString', () => {
+        return Promise.resolve(true);
+      });
+      flushAsynchronousOperations();
 
       const configInputObj = {
         branch: 'test-branch',
-        topic: 'test-topic',
         subject: 'first change created with polygerrit ui',
-        work_in_progress: false,
+        topic: 'test-topic',
+        is_private: true,
+        work_in_progress: true,
       };
 
       const saveStub = sandbox.stub(element.$.restAPI,
@@ -94,34 +136,12 @@
       element.$.messageInput.bindValue = configInputObj.subject;
 
       element.handleCreateChange().then(() => {
-        assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
-      });
-    });
-
-    test('new change created with wip', () => {
-      const configInputObj = {
-        branch: 'test-branch',
-        topic: 'test-topic',
-        subject: 'first change created with polygerrit ui',
-      };
-
-      const saveStub = sandbox.stub(element.$.restAPI,
-          'createChange', () => {
-            return Promise.resolve({});
-          });
-
-      element.branch = 'test-branch';
-      element.topic = 'test-topic';
-      element.subject = 'first change created with polygerrit ui';
-      element.$.wipChangeCheckBox.checked = true;
-      assert.isFalse(element.$.privateChangeCheckBox.checked);
-
-      element.$.branchInput.bindValue = configInputObj.branch;
-      element.$.tagNameInput.bindValue = configInputObj.topic;
-      element.$.messageInput.bindValue = configInputObj.subject;
-
-      element.handleCreateChange().then(() => {
-        assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
+        // Private change
+        assert.isTrue(saveStub.lastCall.args[4]);
+        // WIP Change
+        assert.isTrue(saveStub.lastCall.args[5]);
+        assert.isTrue(saveStub.called);
+        done();
       });
     });
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
index c887367..5cca8b8 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
@@ -78,7 +78,7 @@
           </template>
         </div>
         <gr-button id="editBtn"
-            class$="[[_computeShowEditClass(_sections)]]"
+            class="visible"
             on-tap="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
         <gr-button id="saveBtn"
             primary
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index 2bebb96..d0af0f1 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -316,11 +316,6 @@
       });
     },
 
-    _computeShowEditClass(sections) {
-      if (!sections.length) { return ''; }
-      return 'visible';
-    },
-
     _computeShowSaveClass(editing) {
       if (!editing) { return ''; }
       return 'visible';
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index 46ff469..c4a7f63 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -85,7 +85,9 @@
         display: none;
       }
       .size gr-tooltip-content {
+        margin: -.4rem -.6rem;
         max-width: 2.5rem;
+        padding: .4rem .6rem;
       }
       a {
         color: var(--default-text-color);
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 1fa3c7f9..8073983 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
@@ -30,12 +30,14 @@
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
 <link rel="import" href="../../shared/gr-change-status/gr-change-status.html">
+<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-editable-content/gr-editable-content.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.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-tooltip-content/gr-tooltip-content.html">
 <link rel="import" href="../gr-change-actions/gr-change-actions.html">
 <link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
 <link rel="import" href="../gr-commit-info/gr-commit-info.html">
@@ -109,11 +111,6 @@
       .changeStatus {
         text-transform: capitalize;
       }
-      .container {
-        /* Needed to restrict the change view from allowing horizontal
-          scrolling */
-        width: 100vw;
-      }
       /* Strong specificity here is needed due to
          https://github.com/Polymer/polymer/issues/2531 */
       .container section.changeInfo {
@@ -556,7 +553,13 @@
           id="commentTabs"
           on-selected-changed="_handleTabChange">
         <paper-tab class="changeLog">Change Log</paper-tab>
-        <paper-tab class="commentThreads">Comment Threads</paper-tab>
+        <paper-tab
+            class="commentThreads">
+          <gr-tooltip-content
+              has-tooltip
+              title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]">
+            <span>Comment Threads</span></gr-tooltip-content>
+        </paper-tab>
       </paper-tabs>
       <gr-messages-list id="messageList"
           class$="hideOnMobileOverlay [[_computeShowMessages(_showMessagesView)]]"
@@ -574,6 +577,7 @@
           change="[[_change]]"
           change-num="[[_changeNum]]"
           class$="[[_computeShowThreads(_showMessagesView)]]"
+          logged-in="[[_loggedIn]]"
           on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
     </div>
     <gr-overlay id="downloadOverlay" with-backdrop>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index a2e9290..e552dd1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -96,6 +96,7 @@
         type: Object,
         value() { return document.body; },
       },
+      _commentThreads: Array,
       /** @type {?} */
       _serverConfig: {
         type: Object,
@@ -437,6 +438,19 @@
       });
     },
 
+    _computeTotalCommentCounts(unresolvedCount, changeComments) {
+      const draftCount = changeComments.computeDraftCount();
+      const unresolvedString = GrCountStringFormatter.computeString(
+          unresolvedCount, 'unresolved');
+      const draftString = GrCountStringFormatter.computePluralString(
+          draftCount, 'draft');
+
+      return unresolvedString +
+          // Add a comma and space if both unresolved and draft comments exist.
+          (unresolvedString && draftString ? ', ' : '') +
+          draftString;
+    },
+
     _handleCommentSave(e) {
       if (!e.target.comment.__draft) { return; }
 
@@ -595,6 +609,13 @@
     },
 
     _paramsChanged(value) {
+      // Change the content of the comment tabs back to messages list, but
+      // do not yet change the tab itself. The animation of tab switching will
+      // get messed up if changed here, because it requires the tabs to be on
+      // the streen, and they are hidden shortly after this. The tab switching
+      // animation will happen in post render tasks.
+      this._showMessagesView = true;
+
       if (value.view !== Gerrit.Nav.View.CHANGE) {
         this._initialLoadComplete = false;
         return;
@@ -630,15 +651,6 @@
       this._changeNum = value.changeNum;
       this.$.relatedChanges.clear();
 
-      // If the comment tabs were already rendered, but set to the wrong initial
-      // value, swap them here so the thread tab doesn't flash before being
-      // swapped out. If the selected tab is undefined, we have to wait until
-      // the page is finished rendering to set selected to 0, otherwise the
-      // animation will not show.
-      if (this.$.commentTabs.selected === 1) {
-        this.$.commentTabs.selected = 0;
-      }
-
       this._reload().then(() => {
         this._performPostLoadTasks();
       });
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index e514143..2101856 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -332,6 +332,30 @@
       });
     });
 
+    test('thread list modified', () => {
+      return element._reloadComments().then(() => {
+        let draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
+            .returns(1);
+        assert.equal(element._computeTotalCommentCounts(5,
+            element._changeComments), '5 unresolved, 1 draft');
+        assert.equal(element._computeTotalCommentCounts(0,
+            element._changeComments), '1 draft');
+        draftStub.restore();
+        draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
+            .returns(0);
+        assert.equal(element._computeTotalCommentCounts(0,
+            element._changeComments), '');
+        assert.equal(element._computeTotalCommentCounts(1,
+            element._changeComments), '1 unresolved');
+        draftStub.restore();
+        draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
+            .returns(2);
+        assert.equal(element._computeTotalCommentCounts(1,
+            element._changeComments), '1 unresolved, 2 drafts');
+        draftStub.restore();
+      });
+    });
+
     test('thread list and change log tabs', done => {
       element._changeNum = '1';
       element._patchRange = {
@@ -382,10 +406,15 @@
         assert.notEqual(getComputedStyle(element.$.threadList).display,
             'none');
 
-        // When the change is partially reloaded (ex: Shift+R), paramsChanged
-        // will set the tab, if it was previously set to the thread tab.
+        // When the change is partially reloaded (ex: Shift+R), the content
+        // is swapped out before the tab, so messages list will display even
+        // though the tab for comment threads is still temporarily selected.
         element._paramsChanged(element.params);
-        assert.equal(element.$.commentTabs.selected, 0);
+        assert.equal(element.$.commentTabs.selected, 1);
+        assert.notEqual(getComputedStyle(element.$.messageList).display,
+            'none');
+        assert.equal(getComputedStyle(element.$.threadList).display,
+            'none');
         done();
       });
     });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index ea70de9..55cea07 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -104,6 +104,7 @@
           <gr-autocomplete
               id="parentInput"
               query="[[_query]]"
+              no-debounce
               text="{{_inputText}}"
               on-tap="_handleEnterChangeNumberTap"
               on-commit="_handleBaseSelected"
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index ccfc368..7a43732 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -165,6 +165,7 @@
 
       test('input text change triggers function', () => {
         sandbox.spy(element, '_getRecentChanges');
+        element.$.parentInput.noDebounce = true;
         element._inputText = '1';
         assert.isTrue(element._getRecentChanges.calledOnce);
         element._inputText = '12';
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index fb7b89c..93f010a 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -149,7 +149,7 @@
           .then(res => {
             if (res.ok) {
               if (target) { target.disabled = false; }
-              this.set(['_change', 'revisions', sha, 'description'], desc);
+              this.set(['change', 'revisions', sha, 'description'], desc);
               this._patchsetDescription = desc;
             }
           }).catch(err => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index a92d783..f7056dc 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -134,6 +134,7 @@
         // The API stub should be called with an empty string for the new
         // description.
         assert.equal(putDescStub.lastCall.args[2], '');
+        assert.equal(element.change.revisions.rev1.description, '');
 
         flushAsynchronousOperations();
         // The editable label should now be visible and the chip hidden.
@@ -154,6 +155,7 @@
       }).then(() => {
         flushAsynchronousOperations();
         // The chip should be visible again, and the label hidden.
+        assert.equal(element.change.revisions.rev1.description, 'test2');
         assert.equal(getComputedStyle(label).display, 'none');
         assert.notEqual(getComputedStyle(chip).display, 'none');
       });
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
index 355b90a..b94b364 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
@@ -42,6 +42,12 @@
         min-height: 3.2em;
         padding: .5em var(--default-horizontal-margin);
       }
+      .toggleItem.draftToggle {
+        display: none;
+      }
+      .toggleItem.draftToggle.show {
+        display: flex;
+      }
       .toggleItem {
         align-items: center;
         display: flex;
@@ -62,8 +68,8 @@
         <paper-toggle-button
             id="unresolvedToggle"
             on-change="_toggleUnresolved"></paper-toggle-button>
-          Only Unresolved threads</div>
-      <div class="toggleItem">
+          Only unresolved threads</div>
+      <div class$="toggleItem draftToggle [[_computeShowDraftToggle(loggedIn)]]">
         <paper-toggle-button
             id="draftToggle"
             on-change="_toggleDrafts"></paper-toggle-button>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index 1ca667d..177bc9b 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -24,22 +24,28 @@
     is: 'gr-thread-list',
 
     properties: {
+      /** @type {?} */
       change: Object,
       threads: Array,
       changeNum: String,
+      loggedIn: Boolean,
       _sortedThreads: {
         type: Array,
         computed: '_computeSortedThreads(threads.*)',
       },
     },
 
+    _computeShowDraftToggle(loggedIn) {
+      return loggedIn ? 'show' : '';
+    },
+
     /**
      * Order as follows:
      *  - Unresolved threads with drafts (reverse chronological)
      *  - Unresolved threads without drafts (reverse chronological)
      *  - Resolved threads with drafts (reverse chronological)
      *  - Resolved threads without drafts (reverse chronological)
-     * @param {!Array} threads
+     * @param {!Object} changeRecord
      * @return {!Array}
      */
     _computeSortedThreads(changeRecord) {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
index c0be7d7..47c5b9c 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -186,6 +186,14 @@
       sandbox.restore();
     });
 
+    test('draft toggle only appears when logged in', () => {
+      assert.equal(getComputedStyle(element.$$('.draftToggle')).display,
+          'none');
+      element.loggedIn = true;
+      assert.notEqual(getComputedStyle(element.$$('.draftToggle')).display,
+          'none');
+    });
+
     test('there are five threads by default', () => {
       assert.equal(computeVisibleNumber(threadElements), 5);
     });
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index 94e3e4a19..d5b394c 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -47,7 +47,6 @@
           id="searchInput"
           text="{{_inputVal}}"
           query="[[query]]"
-          debounce-wait="200"
           on-commit="_handleInputCommit"
           allow-non-suggested-values
           multi
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index 67b4f51..cf66e57 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -178,7 +178,7 @@
     for (const path of Object.keys(paths)) {
       let commentsToAdd = this.getAllCommentsForPath(path, opt_patchNum);
       if (opt_includeDrafts) {
-        const drafts = this.getAllDraftsForPath(path)
+        const drafts = this.getAllDraftsForPath(path, opt_patchNum)
             .map(d => Object.assign({__draft: true}, d));
         commentsToAdd = commentsToAdd.concat(drafts);
       }
@@ -207,6 +207,7 @@
    *
    * @param {!string} path
    * @param {number=} opt_patchNum
+   * @param {boolean=} opt_includeDrafts
    * @return {!Array}
    */
   ChangeComments.prototype.getAllCommentsForPath = function(path,
@@ -323,18 +324,20 @@
   };
 
   /**
-   * Computes a string counting the number of commens in a given file and path.
+   * Computes a string counting the number of draft comments in the entire
+   * change, optionally filtered by path and/or patchNum.
    *
-   * @param {number} patchNum
+   * @param {number=} opt_patchNum
    * @param {string=} opt_path
    * @return {number}
    */
-  ChangeComments.prototype.computeDraftCount = function(patchNum, opt_path) {
+  ChangeComments.prototype.computeDraftCount = function(opt_patchNum,
+      opt_path) {
     if (opt_path) {
-      return this.getAllDraftsForPath(opt_path, patchNum).length;
+      return this.getAllDraftsForPath(opt_path, opt_patchNum).length;
     }
-    const allComments = this.getAllDrafts(patchNum);
-    return this._commentObjToArray(allComments).length;
+    const allDrafts = this.getAllDrafts(opt_patchNum);
+    return this._commentObjToArray(allDrafts).length;
   };
 
   /**
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
index 4cf79fa..2b06a10 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
@@ -248,6 +248,14 @@
                 updated: makeTime(3),
               },
             ],
+            'file/two': [
+              {
+                id: 5,
+                patch_set: 3,
+                line: 1,
+                updated: makeTime(3),
+              },
+            ],
           };
           element._changeComments._robotComments = {
             'file/one': [
@@ -340,13 +348,13 @@
           comments = element._changeComments.getCommentsBySideForPath(path,
               patchRange);
           assert.equal(comments.left.length, 0);
-          assert.equal(comments.right.length, 1);
+          assert.equal(comments.right.length, 2);
 
           patchRange.basePatchNum = 2;
           comments = element._changeComments.getCommentsBySideForPath(path,
               patchRange);
           assert.equal(comments.left.length, 1);
-          assert.equal(comments.right.length, 1);
+          assert.equal(comments.right.length, 2);
 
           patchRange.basePatchNum = PARENT;
           path = 'file/three';
@@ -429,6 +437,8 @@
               .computeDraftCount(1, 'file/one'), 0);
           assert.equal(element._changeComments
               .computeDraftCount(2, 'file/three'), 0);
+          assert.equal(element._changeComments
+              .computeDraftCount(), 3);
         });
 
         test('getAllPublishedComments', () => {
@@ -456,7 +466,7 @@
           comments = element._changeComments.getAllComments(true);
           assert.equal(Object.keys(comments).length, 4);
           assert.equal(Object.keys(comments[['file/one']]).length, 6);
-          assert.equal(Object.keys(comments[['file/two']]).length, 2);
+          assert.equal(Object.keys(comments[['file/two']]).length, 3);
           comments = element._changeComments.getAllComments(true, 2);
           assert.equal(Object.keys(comments).length, 4);
           assert.equal(Object.keys(comments[['file/one']]).length, 6);
@@ -627,6 +637,21 @@
             }, {
               comments: [
                 {
+                  id: 5,
+                  patch_set: 3,
+                  line: 1,
+                  __path: 'file/two',
+                  __draft: true,
+                  updated: '2013-02-26 15:03:43.986000000',
+                },
+              ],
+              rootId: 5,
+              patchNum: 3,
+              path: 'file/two',
+              line: 1,
+            }, {
+              comments: [
+                {
                   id: 11,
                   patch_set: 2,
                   side: 'PARENT',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index e1ebbf2..44d703c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -52,9 +52,6 @@
       :host([disabled]) .date {
         opacity: .5;
       }
-      :host([is-robot-comment]) {
-        background-color: #cfe8fc;
-      }
       .header {
         align-items: baseline;
         cursor: pointer;
@@ -101,6 +98,16 @@
         margin-left: 1em;
         --gr-button-color: #212121;
       }
+      .robotActions {
+        display: flex;
+        justify-content: flex-start;
+        padding-top: 0;
+      }
+      .robotActions .action {
+        /* Keep button text lined up with output text */
+        margin-left: -.3rem;
+        margin-right: 1em;
+      }
       .rightActions {
         display: flex;
         justify-content: flex-end;
@@ -149,7 +156,7 @@
         margin-top: -.4em;
       }
       .runIdInformation {
-        margin: 1em 0;
+        margin: .7em 0;
       }
       .robotRun {
         margin-left: .5em;
@@ -273,17 +280,24 @@
             disabled="{{disabled}}"
             rows="4"
             text="{{_messageText}}"></gr-textarea>
+        <!--The message class is needed to ensure selectability from
+        gr-diff-selection.-->
         <gr-formatted-text class="message"
             content="[[comment.message]]"
             no-trailing-margin="[[!comment.__draft]]"
             collapsed="[[collapsed]]"
             config="[[projectConfig.commentlinks]]"></gr-formatted-text>
-        <div hidden$="[[!comment.robot_run_id]]">
+        <div hidden$="[[!comment.robot_run_id]]" class="message">
           <div class="runIdInformation" hidden$="[[collapsed]]">
             Run ID:
-            <a class="robotRunLink" href$="[[comment.url]]">
-              <span class="robotRun">[[comment.robot_run_id]]</span>
-            </a>
+            <template is="dom-if" if="[[comment.url]]">
+              <a class="robotRunLink" href$="[[comment.url]]">
+                <span class="robotRun link">[[comment.robot_run_id]]</span>
+              </a>
+            </template>
+            <template is="dom-if" if="[[!comment.url]]">
+              <span class="robotRun text">[[comment.robot_run_id]]</span>
+            </template>
           </div>
         </div>
         <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
@@ -309,17 +323,17 @@
             </gr-button>
           </div>
         </div>
-        <div class="actions robotActions" hidden$="[[!_showRobotActions]]">
+        <div class="robotActions" hidden$="[[!_showRobotActions]]">
           <template is="dom-if" if="[[isRobotComment]]">
-            <gr-endpoint-decorator name="robot-comment-controls">
-              <gr-endpoint-param name="comment" value="[[comment]]">
-              </gr-endpoint-param>
-            </gr-endpoint-decorator>
             <gr-button link class="action fix"
                 on-tap="_handleFix"
                 disabled="[[robotButtonDisabled]]">
               Please Fix
             </gr-button>
+            <gr-endpoint-decorator name="robot-comment-controls">
+              <gr-endpoint-param name="comment" value="[[comment]]">
+              </gr-endpoint-param>
+            </gr-endpoint-decorator>
           </template>
         </div>
       </div>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index 8c78946..01f5ae7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -385,6 +385,23 @@
       element.draft = false;
       assert.isTrue(element.$$('.humanActions').hasAttribute('hidden'));
       assert.isFalse(element.$$('.robotActions').hasAttribute('hidden'));
+
+      // A robot comment with run ID should display plain text.
+      element.set(['comment', 'robot_run_id'], 'text');
+      element.editing = false;
+      element.collapsed = false;
+      flushAsynchronousOperations();
+      assert.isNotOk(element.$$('.robotRun.link'));
+      assert.notEqual(getComputedStyle(element.$$('.robotRun.text')).display,
+          'none');
+
+      // A robot comment with run ID and url should display a link.
+      element.set(['comment', 'url'], '/path/to/run');
+      flushAsynchronousOperations();
+      assert.notEqual(getComputedStyle(element.$$('.robotRun.link')).display,
+          'none');
+      assert.equal(getComputedStyle(element.$$('.robotRun.text')).display,
+          'none');
     });
 
     test('collapsible drafts', () => {
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 3bf8df7..22433fc 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
@@ -25,6 +25,7 @@
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
 <link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
+<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html">
 <link rel="import" href="../../shared/gr-icons/gr-icons.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">
@@ -54,6 +55,11 @@
           border-bottom: 1px solid #eee;
         }
       }
+      gr-fixed-panel {
+        background-color: #fff;
+        border-bottom: 1px #eee solid;
+        z-index: 1;
+      }
       header,
       .subHeader {
         align-items: center;
@@ -141,15 +147,6 @@
       .separator.hide {
         display: none;
       }
-      .sticky {
-        background: #fff;
-        border-bottom: 1px solid #eee;
-        left: 0;
-        position: sticky;
-        top: 0;
-        width: 100vw;
-        z-index: 1;
-      }
       gr-dropdown-list {
         --trigger-style: {
           text-transform: none;
@@ -208,136 +205,138 @@
         }
       }
     </style>
-    <div>
-      <div class="sticky">
-        <header class$="[[_computeContainerClass(_editMode)]]">
-          <h3>
-            <a href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">
-              [[_changeNum]]</a><span>:</span>
-            <span>[[_change.subject]]</span>
-            <span class="dash">—</span>
-            <input id="reviewed"
-                class="reviewed hideOnEdit"
-                type="checkbox"
-                on-change="_handleReviewedChange"
-                hidden$="[[!_loggedIn]]" hidden>
-            <div class="jumpToFileContainer">
-              <gr-dropdown-list
-                  id="dropdown"
-                  value="[[_path]]"
-                  on-value-change="_handleFileChange"
-                  items="[[_formattedFiles]]"
-                  initial-count="75">
-            </gr-dropdown-list>
-            </div>
-          </h3>
-          <div class="navLinks desktop">
-            <span class$="fileNum [[_computeFileNumClass(_fileNum, _formattedFiles)]]">
-              File [[_fileNum]] of [[_formattedFiles.length]]
-              <span class="separator"></span>
-            </span>
-            <a class="navLink"
-                href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
-              Prev</a>
-            <span class="separator"></span>
-            <a class="navLink"
-                href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">
-              Up</a>
-            <span class="separator"></span>
-            <a class="navLink"
-                href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
-              Next</a>
+    <gr-fixed-panel
+        class$="[[_computeContainerClass(_editMode)]]"
+        floating-disabled="[[_panelFloatingDisabled]]"
+        keep-on-scroll
+        ready-for-measure="[[!_loading]]">
+      <header>
+        <h3>
+          <a href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">
+            [[_changeNum]]</a><span>:</span>
+          <span>[[_change.subject]]</span>
+          <span class="dash">—</span>
+          <input id="reviewed"
+              class="reviewed hideOnEdit"
+              type="checkbox"
+              on-change="_handleReviewedChange"
+              hidden$="[[!_loggedIn]]" hidden>
+          <div class="jumpToFileContainer">
+            <gr-dropdown-list
+                id="dropdown"
+                value="[[_path]]"
+                on-value-change="_handleFileChange"
+                items="[[_formattedFiles]]"
+                initial-count="75">
+           </gr-dropdown-list>
           </div>
-        </header>
-        <div class="subHeader">
-          <div class="patchRangeLeft">
-            <gr-patch-range-select
-                id="rangeSelect"
-                change-num="[[_changeNum]]"
-                change-comments="[[_changeComments]]"
-                patch-num="[[_patchRange.patchNum]]"
-                base-patch-num="[[_patchRange.basePatchNum]]"
-                files-weblinks="[[_filesWeblinks]]"
-                available-patches="[[_allPatchSets]]"
-                revisions="[[_change.revisions]]"
-                revision-info="[[_revisionInfo]]"
-                on-patch-range-change="_handlePatchChange">
-            </gr-patch-range-select>
-            <span class="download desktop">
-              <span class="separator"></span>
-              <a
-                class="downloadLink"
-                download
-                href$="[[_computeDownloadLink(_changeNum, _patchRange, _path)]]">
-                Download
-              </a>
-            </span>
+        </h3>
+        <div class="navLinks desktop">
+          <span class$="fileNum [[_computeFileNumClass(_fileNum, _formattedFiles)]]">
+            File [[_fileNum]] of [[_formattedFiles.length]]
+            <span class="separator"></span>
+          </span>
+          <a class="navLink"
+              href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
+            Prev</a>
+          <span class="separator"></span>
+          <a class="navLink"
+              href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">
+            Up</a>
+          <span class="separator"></span>
+          <a class="navLink"
+              href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
+            Next</a>
+        </div>
+      </header>
+      <div class="subHeader">
+        <div class="patchRangeLeft">
+          <gr-patch-range-select
+              id="rangeSelect"
+              change-num="[[_changeNum]]"
+              change-comments="[[_changeComments]]"
+              patch-num="[[_patchRange.patchNum]]"
+              base-patch-num="[[_patchRange.basePatchNum]]"
+              files-weblinks="[[_filesWeblinks]]"
+              available-patches="[[_allPatchSets]]"
+              revisions="[[_change.revisions]]"
+              revision-info="[[_revisionInfo]]"
+              on-patch-range-change="_handlePatchChange">
+          </gr-patch-range-select>
+          <span class="download desktop">
+            <span class="separator"></span>
+            <a
+              class="downloadLink"
+              download
+              href$="[[_computeDownloadLink(_changeNum, _patchRange, _path)]]">
+              Download
+            </a>
+          </span>
+        </div>
+        <div class="rightControls">
+          <span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _isBlameSupported)]]">
+            <gr-button
+                link
+                disabled="[[_isBlameLoading]]"
+                on-tap="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button>
+            <span class="separator"></span>
+          </span>
+          <div class$="diffModeSelector [[_computeModeSelectHideClass(_isImageDiff)]]">
+            <span>Diff view:</span>
+            <gr-diff-mode-selector
+                id="modeSelect"
+                save-on-change="[[_loggedIn]]"
+                mode="{{changeViewState.diffMode}}"></gr-diff-mode-selector>
           </div>
-          <div class="rightControls">
-            <span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _isBlameSupported)]]">
+          <span id="diffPrefsContainer"
+              hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]" hidden>
+            <span class="preferences desktop">
               <gr-button
                   link
-                  disabled="[[_isBlameLoading]]"
-                  on-tap="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button>
-              <span class="separator"></span>
+                  class="prefsButton"
+                  has-tooltip
+                  title="Diff preferences"
+                  on-tap="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
             </span>
-            <div class$="diffModeSelector [[_computeModeSelectHideClass(_isImageDiff)]]">
-              <span>Diff view:</span>
-              <gr-diff-mode-selector
-                  id="modeSelect"
-                  save-on-change="[[_loggedIn]]"
-                  mode="{{changeViewState.diffMode}}"></gr-diff-mode-selector>
-            </div>
-            <span id="diffPrefsContainer"
-                hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]" hidden>
-              <span class="preferences desktop">
-                <gr-button
-                    link
-                    class="prefsButton"
-                    has-tooltip
-                    title="Diff preferences"
-                    on-tap="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
-              </span>
+          </span>
+          <gr-endpoint-decorator name="annotation-toggler">
+            <span hidden id="annotation-span">
+              <label for="annotation-checkbox" id="annotation-label"></label>
+              <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
             </span>
-            <gr-endpoint-decorator name="annotation-toggler">
-              <span hidden id="annotation-span">
-                <label for="annotation-checkbox" id="annotation-label"></label>
-                <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
-              </span>
-            </gr-endpoint-decorator>
-          </div>
-        </div>
-        <div class="fileNav mobile">
-          <a class="mobileNavLink"
-            href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
-            &lt;</a>
-          <div class="fullFileName mobile">[[computeDisplayPath(_path)]]
-          </div>
-          <a class="mobileNavLink"
-              href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
-            &gt;</a>
+          </gr-endpoint-decorator>
         </div>
       </div>
-      <div class="loading" hidden$="[[!_loading]]">Loading...</div>
-      <gr-diff
-          id="diff"
-          hidden
-          hidden$="[[_loading]]"
-          class$="[[_computeDiffClass(_panelFloatingDisabled)]]"
-          is-image-diff="{{_isImageDiff}}"
-          files-weblinks="{{_filesWeblinks}}"
-          change-num="[[_changeNum]]"
-          commit-range="[[_commitRange]]"
-          patch-range="[[_patchRange]]"
-          path="[[_path]]"
-          prefs="[[_prefs]]"
-          project-config="[[_projectConfig]]"
-          project-name="[[_change.project]]"
-          view-mode="[[_diffMode]]"
-          is-blame-loaded="{{_isBlameLoaded}}"
-          on-line-selected="_onLineSelected">
-      </gr-diff>
-    </div>
+      <div class="fileNav mobile">
+        <a class="mobileNavLink"
+          href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
+          &lt;</a>
+        <div class="fullFileName mobile">[[computeDisplayPath(_path)]]
+        </div>
+        <a class="mobileNavLink"
+            href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
+          &gt;</a>
+      </div>
+    </gr-fixed-panel>
+    <div class="loading" hidden$="[[!_loading]]">Loading...</div>
+    <gr-diff
+        id="diff"
+        hidden
+        hidden$="[[_loading]]"
+        class$="[[_computeDiffClass(_panelFloatingDisabled)]]"
+        is-image-diff="{{_isImageDiff}}"
+        files-weblinks="{{_filesWeblinks}}"
+        change-num="[[_changeNum]]"
+        commit-range="[[_commitRange]]"
+        patch-range="[[_patchRange]]"
+        path="[[_path]]"
+        prefs="[[_prefs]]"
+        project-config="[[_projectConfig]]"
+        project-name="[[_change.project]]"
+        view-mode="[[_diffMode]]"
+        is-blame-loaded="{{_isBlameLoaded}}"
+        on-line-selected="_onLineSelected">
+    </gr-diff>
     <gr-diff-preferences
         id="diffPreferences"
         prefs="{{_prefs}}"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 079600c..5ba62af 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -48,7 +48,6 @@
         display: none;
       }
       .diffContainer {
-        border-bottom: 1px solid #eee;
         display: flex;
         font: var(--font-size-small) var(--monospace-font-family);
         @apply --diff-container-styles;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
index bbb5111..6e6f7d9 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -60,12 +60,14 @@
 
   suite('edit button CUJ', () => {
     let navStubs;
+    let openAutoCcmplete;
 
     setup(() => {
       navStubs = [
         sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'),
         sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'),
       ];
+      openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete');
     });
 
     test('_isValidPath', () => {
@@ -83,8 +85,8 @@
         assert.isTrue(element._hideAllDialogs.called);
         assert.isTrue(element.$.openDialog.disabled);
         assert.isFalse(queryStub.called);
-        element.$.openDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        openAutoCcmplete.noDebounce = true;
+        openAutoCcmplete.text = 'src/test.cpp';
         assert.isTrue(queryStub.called);
         assert.isFalse(element.$.openDialog.disabled);
         MockInteractions.tap(element.$.openDialog.$$('gr-button[primary]'));
@@ -99,8 +101,8 @@
       MockInteractions.tap(element.$$('#open'));
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.openDialog.disabled);
-        element.$.openDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        openAutoCcmplete.noDebounce = true;
+        openAutoCcmplete.text = 'src/test.cpp';
         assert.isFalse(element.$.openDialog.disabled);
         MockInteractions.tap(element.$.openDialog.$$('gr-button'));
         for (const stub of navStubs) { assert.isFalse(stub.called); }
@@ -113,10 +115,13 @@
   suite('delete button CUJ', () => {
     let navStub;
     let deleteStub;
+    let deleteAutocomplete;
 
     setup(() => {
       navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
       deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit');
+      deleteAutocomplete =
+          element.$.deleteDialog.querySelector('gr-autocomplete');
     });
 
     test('delete', () => {
@@ -125,8 +130,8 @@
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.deleteDialog.disabled);
         assert.isFalse(queryStub.called);
-        element.$.deleteDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        deleteAutocomplete.noDebounce = true;
+        deleteAutocomplete.text = 'src/test.cpp';
         assert.isTrue(queryStub.called);
         assert.isFalse(element.$.deleteDialog.disabled);
         MockInteractions.tap(element.$.deleteDialog.$$('gr-button[primary]'));
@@ -148,8 +153,8 @@
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.deleteDialog.disabled);
         assert.isFalse(queryStub.called);
-        element.$.deleteDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        deleteAutocomplete.noDebounce = true;
+        deleteAutocomplete.text = 'src/test.cpp';
         assert.isTrue(queryStub.called);
         assert.isFalse(element.$.deleteDialog.disabled);
         MockInteractions.tap(element.$.deleteDialog.$$('gr-button[primary]'));
@@ -182,10 +187,13 @@
   suite('rename button CUJ', () => {
     let navStub;
     let renameStub;
+    let renameAutocomplete;
 
     setup(() => {
       navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
       renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
+      renameAutocomplete =
+          element.$.renameDialog.querySelector('gr-autocomplete');
     });
 
     test('rename', () => {
@@ -194,8 +202,8 @@
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.renameDialog.disabled);
         assert.isFalse(queryStub.called);
-        element.$.renameDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        renameAutocomplete.noDebounce = true;
+        renameAutocomplete.text = 'src/test.cpp';
         assert.isTrue(queryStub.called);
         assert.isTrue(element.$.renameDialog.disabled);
 
@@ -222,8 +230,8 @@
       return showDialogSpy.lastCall.returnValue.then(() => {
         assert.isTrue(element.$.renameDialog.disabled);
         assert.isFalse(queryStub.called);
-        element.$.renameDialog.querySelector('gr-autocomplete').text =
-            'src/test.cpp';
+        renameAutocomplete.noDebounce = true;
+        renameAutocomplete.text = 'src/test.cpp';
         assert.isTrue(queryStub.called);
         assert.isTrue(element.$.renameDialog.disabled);
 
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
index 46c69e4..8f2f166 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
@@ -24,6 +24,7 @@
 <link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
+<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-default-editor/gr-default-editor.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -34,13 +35,9 @@
       :host {
         background-color: var(--view-background-color);
       }
-      .sticky {
+      gr-fixed-panel {
         background-color: #ebf5fb;
         border-bottom: 1px #ddd solid;
-        position: -webkit-sticky;
-        position: sticky;
-        top: 0;
-        width: 100vw;
         z-index: 1;
       }
       header,
@@ -88,7 +85,7 @@
         }
       }
     </style>
-    <div class="sticky">
+    <gr-fixed-panel keep-on-scroll>
       <header>
         <span class="controlGroup">
           <span>Edit mode</span>
@@ -112,7 +109,7 @@
               on-tap="_saveEdit">Save</gr-button>
         </span>
       </header>
-    </div>
+    </gr-fixed-panel>
     <div class="textareaWrapper">
       <gr-endpoint-decorator id="editorEndpoint" name="editor">
         <gr-endpoint-param name="fileContent" value="[[_newContent]]"></gr-endpoint-param>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 2f2aa51..325072c 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -54,6 +54,7 @@
 <link rel="import" href="./settings/gr-cla-view/gr-cla-view.html">
 <link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html">
 <link rel="import" href="./settings/gr-settings-view/gr-settings-view.html">
+<link rel="import" href="./shared/gr-fixed-panel/gr-fixed-panel.html">
 <link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <script src="../scripts/util.js"></script>
@@ -62,44 +63,35 @@
   <template>
     <style include="shared-styles">
       :host {
-        /* This needs to be inline-flex so that the app container inherits the
-          width of its children. From here, can use position: sticky to display
-          items that we want to be 'floating' like the header and footer, with
-          a combination of: position: sticky, width=100vw, and left: 0. */
-        display: inline-flex;
+        display: flex;
         min-height: 100%;
         flex-direction: column;
       }
+      gr-fixed-panel {
+        /**
+         * This one should be greater that the z-index in gr-diff-view
+         * because gr-main-header contains overlay.
+         */
+        z-index: 10;
+      }
       gr-main-header,
       footer {
         color: var(--primary-text-color);
       }
+      gr-main-header {
+        background-color: var(--header-background-color);
+        padding: 0 var(--default-horizontal-margin);
+        border-bottom: 1px solid #ddd;
+      }
       gr-main-header.shadow {
         /* Make it obvious for shadow dom testing */
         border-bottom: 1px solid pink;
       }
-      gr-main-header {
-        /* The combination of left, width, position allow this to be
-          floating. */
-        left: 0;
-        width: 100vw;
-        background-color: var(--header-background-color);
-        padding: 0 var(--default-horizontal-margin);
-        position: sticky;
-        /* Needed for the menu dropdowns to display on top of the sticky
-          headers with z-index of 1.*/
-        z-index: 2;
-      }
       footer {
-        /* The combination of left, width, position allow this to be
-          floating. */
         background-color: var(--footer-background-color);
         display: flex;
-        left: 0;
-        width: 100vw;
         justify-content: space-between;
         padding: .5rem var(--default-horizontal-margin);
-        position: sticky;
         z-index: 100;
       }
       main {
@@ -138,11 +130,13 @@
         color: #b71c1c;
       }
     </style>
-    <gr-main-header
-        id="mainHeader"
-        search-query="{{params.query}}"
-        class$="[[_computeShadowClass(_isShadowDom)]]">
-    </gr-main-header>
+    <gr-fixed-panel id="header">
+      <gr-main-header
+          id="mainHeader"
+          search-query="{{params.query}}"
+          class$="[[_computeShadowClass(_isShadowDom)]]">
+      </gr-main-header>
+    </gr-fixed-panel>
     <main>
       <template is="dom-if" if="[[_showChangeListView]]" restamp="true">
         <gr-change-list-view
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 99c4d12..6c96fc1 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -182,6 +182,7 @@
       if (this.params.justRegistered) {
         this.$.registration.open();
       }
+      this.$.header.unfloat();
     },
 
     _computeShowGwtUiLink(config) {
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
index e3be10d..cf5e0b1 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -95,7 +95,6 @@
             <th>
               <gr-autocomplete
                   id="newProject"
-                  debounce-wait="200"
                   query="[[_query]]"
                   threshold="1"
                   placeholder="Project"></gr-autocomplete>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 66a50f0..475821d 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -15,6 +15,7 @@
   'use strict';
 
   const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+  const DEBOUNCE_WAIT_MS = 200;
 
   Polymer({
     is: 'gr-autocomplete',
@@ -130,12 +131,11 @@
       },
 
       /**
-       * The number of milliseconds to use as the debounce wait time. If null,
-       * no debouncing is used.
+       * When true, querying for suggestions is not debounced w/r/t keypresses
        */
-      debounceWait: {
-        type: Number,
-        value: null,
+      noDebounce: {
+        type: Boolean,
+        value: false,
       },
 
       /** @type {?} */
@@ -173,6 +173,7 @@
 
     detached() {
       this.unlisten(document.body, 'tap', '_handleBodyTap');
+      this.cancelDebouncer('update-suggestions');
     },
 
     get focusStart() {
@@ -255,10 +256,10 @@
         });
       };
 
-      if (this.debounceWait) {
-        this.debounce('update-suggestions', update, this.debounceWait);
-      } else {
+      if (this.noDebounce) {
         update();
+      } else {
+        this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS);
       }
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index dba0d6c..76f70f4 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -27,7 +27,7 @@
 
 <test-fixture id="basic">
   <template>
-    <gr-autocomplete></gr-autocomplete>
+    <gr-autocomplete no-debounce></gr-autocomplete>
   </template>
 </test-fixture>
 
@@ -235,7 +235,7 @@
       assert.isTrue(queryStub.called);
     });
 
-    test('debounceWait debounces the query', () => {
+    test('noDebounce=false debounces the query', () => {
       const queryStub = sandbox.spy(() => {
         return Promise.resolve([]);
       });
@@ -243,11 +243,11 @@
       const debounceStub = sandbox.stub(element, 'debounce',
           (name, cb) => { callback = cb; });
       element.query = queryStub;
-      element.debounceWait = 100;
+      element.noDebounce = false;
       element.text = 'a';
       assert.isFalse(queryStub.called);
       assert.isTrue(debounceStub.called);
-      assert.equal(debounceStub.lastCall.args[2], 100);
+      assert.equal(debounceStub.lastCall.args[2], 200);
       assert.isFunction(callback);
       callback();
       assert.isTrue(queryStub.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
new file mode 100644
index 0000000..967ff02
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
@@ -0,0 +1,50 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-fixed-panel">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: block;
+        min-height: var(--header-height);
+        position: relative;
+      }
+      header {
+        background: inherit;
+        border: inherit;
+        display: inline;
+        height: inherit;
+      }
+      .floating {
+        left: 0;
+        position: fixed;
+        width: 100%;
+        will-change: top;
+      }
+      .fixedAtTop {
+        border-bottom: 1px solid #a4a4a4;
+        box-shadow: 0 4px 4px rgba(0,0,0,0.1);
+      }
+    </style>
+    <header id="header" class$="[[_computeHeaderClass(_headerFloating, _topLast)]]">
+      <slot></slot>
+    </header>
+  </template>
+  <script src="gr-fixed-panel.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
new file mode 100644
index 0000000..f4f1a93
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -0,0 +1,194 @@
+// 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.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-fixed-panel',
+
+    properties: {
+      floatingDisabled: Boolean,
+      readyForMeasure: {
+        type: Boolean,
+        observer: '_readyForMeasureObserver',
+      },
+      keepOnScroll: {
+        type: Boolean,
+        value: false,
+      },
+      _isMeasured: {
+        type: Boolean,
+        value: false,
+      },
+
+      /**
+       * Initial offset from the top of the document, in pixels.
+       */
+      _topInitial: Number,
+
+      /**
+       * Current offset from the top of the window, in pixels.
+       */
+      _topLast: Number,
+
+      _headerHeight: Number,
+      _headerFloating: {
+        type: Boolean,
+        value: false,
+      },
+      _observer: {
+        type: Object,
+        value: null,
+      },
+      _webComponentsReady: Boolean,
+    },
+
+    attached() {
+      if (this.floatingDisabled) {
+        return;
+      }
+      // Enable content measure unless blocked by param.
+      if (this.readyForMeasure !== false) {
+        this.readyForMeasure = true;
+      }
+      this.listen(window, 'resize', 'update');
+      this.listen(window, 'scroll', '_updateOnScroll');
+      this._observer = new MutationObserver(this.update.bind(this));
+      this._observer.observe(this.$.header, {childList: true, subtree: true});
+    },
+
+    detached() {
+      this.unlisten(window, 'scroll', '_updateOnScroll');
+      this.unlisten(window, 'resize', 'update');
+      if (this._observer) {
+        this._observer.disconnect();
+      }
+    },
+
+    _readyForMeasureObserver(readyForMeasure) {
+      if (readyForMeasure) {
+        this.update();
+      }
+    },
+
+    _computeHeaderClass(headerFloating, topLast) {
+      const fixedAtTop = this.keepOnScroll && topLast === 0;
+      return [
+        headerFloating ? 'floating' : '',
+        fixedAtTop ? 'fixedAtTop' : '',
+      ].join(' ');
+    },
+
+    _getScrollY() {
+      return window.scrollY;
+    },
+
+    unfloat() {
+      if (this.floatingDisabled) {
+        return;
+      }
+      this.$.header.style.top = '';
+      this._headerFloating = false;
+      this.updateStyles({'--header-height': ''});
+    },
+
+    update() {
+      this.debounce('update', () => {
+        this._updateDebounced();
+      }, 100);
+    },
+
+    _updateOnScroll() {
+      this.debounce('update', () => {
+        this._updateDebounced();
+      });
+    },
+
+    _updateDebounced() {
+      if (this.floatingDisabled) {
+        return;
+      }
+      this._isMeasured = false;
+      this._maybeFloatHeader();
+      this._reposition();
+    },
+
+    _reposition() {
+      if (!this._headerFloating) {
+        return;
+      }
+      const header = this.$.header;
+      const scrollY = this._topInitial - this._getScrollY();
+      let newTop;
+      if (this.keepOnScroll) {
+        if (scrollY > 0) {
+          // Reposition to imitate natural scrolling.
+          newTop = scrollY;
+        } else {
+          newTop = 0;
+        }
+      } else if (scrollY > -this._headerHeight ||
+          this._topLast < -this._headerHeight) {
+        // Allow to scroll away, but ignore when far behind the edge.
+        newTop = scrollY;
+      } else {
+        newTop = -this._headerHeight;
+      }
+      if (this._topLast !== newTop) {
+        if (newTop === undefined) {
+          header.style.top = '';
+        } else {
+          header.style.top = newTop + 'px';
+        }
+        this._topLast = newTop;
+      }
+    },
+
+    _measure() {
+      if (this._isMeasured) {
+        return; // Already measured.
+      }
+      const rect = this.$.header.getBoundingClientRect();
+      if (rect.height === 0 && rect.width === 0) {
+        return; // Not ready for measurement yet.
+      }
+      const top = document.body.scrollTop + rect.top;
+      this._topLast = top;
+      this._headerHeight = rect.height;
+      this._topInitial =
+        this.getBoundingClientRect().top + document.body.scrollTop;
+      this._isMeasured = true;
+    },
+
+    _isFloatingNeeded() {
+      return this.keepOnScroll ||
+        document.body.scrollWidth > document.body.clientWidth;
+    },
+
+    _maybeFloatHeader() {
+      if (!this._isFloatingNeeded()) {
+        return;
+      }
+      this._measure();
+      if (this._isMeasured) {
+        this._floatHeader();
+      }
+    },
+
+    _floatHeader() {
+      this.updateStyles({'--header-height': this._headerHeight + 'px'});
+      this._headerFloating = true;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
new file mode 100644
index 0000000..408b7c9
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-fixed-panel</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-fixed-panel.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-fixed-panel>
+      <div style="height: 100px"></div>
+    </gr-fixed-panel>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-fixed-panel', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      element.readyForMeasure = true;
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('can be disabled with floatingDisabled', () => {
+      element.floatingDisabled = true;
+      sandbox.stub(element, '_reposition');
+      window.dispatchEvent(new CustomEvent('resize'));
+      element.flushDebouncer('update');
+      assert.isFalse(element._reposition.called);
+    });
+
+    test('header is the height of the content', () => {
+      assert.equal(element.getBoundingClientRect().height, 100);
+    });
+
+    test('scroll triggers _reposition', () => {
+      sandbox.stub(element, '_reposition');
+      window.dispatchEvent(new CustomEvent('scroll'));
+      element.flushDebouncer('update');
+      assert.isTrue(element._reposition.called);
+    });
+
+    suite('_reposition', () => {
+      const getHeaderTop = function() {
+        return element.$.header.style.top;
+      };
+
+      const emulateScrollY = function(distance) {
+        element._getScrollY.returns(distance);
+        element._updateDebounced();
+        element.flushDebouncer('scroll');
+      };
+
+      setup(() => {
+        element._headerTopInitial = 10;
+        sandbox.stub(element, '_getScrollY').returns(0);
+      });
+
+      test('scrolls header along with document', () => {
+        emulateScrollY(20);
+        assert.equal(getHeaderTop(), '-12px');
+      });
+
+      test('does not stick to the top by default', () => {
+        emulateScrollY(150);
+        assert.equal(getHeaderTop(), '-100px');
+      });
+
+      test('sticks to the top if enabled', () => {
+        element.keepOnScroll = true;
+        emulateScrollY(120);
+        assert.equal(getHeaderTop(), '0px');
+      });
+
+      test('drops a shadow when fixed to the top', () => {
+        element.keepOnScroll = true;
+        emulateScrollY(5);
+        assert.isFalse(element.$.header.classList.contains('fixedAtTop'));
+        emulateScrollY(120);
+        assert.isTrue(element.$.header.classList.contains('fixedAtTop'));
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
index 06aa43b..8e47485 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
@@ -42,7 +42,7 @@
       <!-- This is a custom PolyGerrit SVG -->
       <g id="side-by-side"><path d="M17.1578947,10.8888889 L2.84210526,10.8888889 C2.37894737,10.8888889 2,11.2888889 2,11.7777778 L2,17.1111111 C2,17.6 2.37894737,18 2.84210526,18 L17.1578947,18 C17.6210526,18 18,17.6 18,17.1111111 L18,11.7777778 C18,11.2888889 17.6210526,10.8888889 17.1578947,10.8888889 Z M17.1578947,2 L2.84210526,2 C2.37894737,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.37894737,9.11111111 2.84210526,9.11111111 L17.1578947,9.11111111 C17.6210526,9.11111111 18,8.71111111 18,8.22222222 L18,2.88888889 C18,2.4 17.6210526,2 17.1578947,2 Z M16.1973628,2 L2.78874238,2 C2.35493407,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.35493407,9.11111111 2.78874238,9.11111111 L16.1973628,9.11111111 C16.6311711,9.11111111 16.9861052,8.71111111 16.9861052,8.22222222 L16.9861052,2.88888889 C16.9861052,2.4 16.6311711,2 16.1973628,2 Z" id="Shape" transform="scale(1.2) translate(10.000000, 10.000000) rotate(-90.000000) translate(-10.000000, -10.000000)"/></g>
       <!-- This is a custom PolyGerrit SVG -->
-      <g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.2)"/></g>
+      <g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.12, 1.2)"/></g>
       <!-- This is a custom PolyGerrit SVG -->
       <g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></g>
       <!-- This is a custom PolyGerrit SVG -->