Merge "Set a maximum length on topic display"
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 22f785e..3644845 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -326,6 +326,8 @@
 A boolean indicating if reviewers and CCs that do not currently have a Gerrit
 account can be added to a change by providing their email address.
 
+This setting only takes affect for changes that are readable by anonymous users.
+
 Default is `INHERIT`, which means that this property is inherited from
 the parent project. If the property is not set in any parent project, the
 default value is `FALSE`.
diff --git a/WORKSPACE b/WORKSPACE
index 5f3d1e9..53a45a9 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -444,6 +444,7 @@
     sha1 = "18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3",
 )
 
+# When upgrading Lucene, make sure it's compatible with Elasticsearch
 LUCENE_VERS = "5.5.4"
 
 maven_jar(
@@ -918,6 +919,7 @@
     sha1 = "00d0003e99da3c4d830b12c099691ce910c84e39",
 )
 
+# When upgrading Elasticsearch, make sure it's compatible with Lucene
 maven_jar(
     name = "elasticsearch",
     artifact = "org.elasticsearch:elasticsearch:2.4.5",
@@ -1055,8 +1057,8 @@
 bower_archive(
     name = "iron-dropdown",
     package = "polymerelements/iron-dropdown",
-    sha1 = "63e3d669a09edaa31c4f05afc76b53b919ef0595",
-    version = "1.4.0",
+    sha1 = "ac96fe31cdf203a63426fa75131b43c98c0597d3",
+    version = "1.5.5",
 )
 
 bower_archive(
@@ -1069,15 +1071,15 @@
 bower_archive(
     name = "iron-overlay-behavior",
     package = "polymerelements/iron-overlay-behavior",
-    sha1 = "83181085fda59446ce74fd0d5ca30c223f38ee4a",
-    version = "1.7.6",
+    sha1 = "74cda9d7bf98e7a5e5004bc7ebdb6d208d49e11e",
+    version = "2.0.0",
 )
 
 bower_archive(
     name = "iron-selector",
     package = "polymerelements/iron-selector",
-    sha1 = "c57235dfda7fbb987c20ad0e97aac70babf1a1bf",
-    version = "1.5.2",
+    sha1 = "e0ee46c28523bf17730318c3b481a8ed4331c3b2",
+    version = "2.0.0",
 )
 
 bower_archive(
@@ -1095,6 +1097,20 @@
 )
 
 bower_archive(
+    name = "paper-item",
+    package = "polymerelements/paper-item",
+    sha1 = "803273ceb9ffebec8ecc9373ea638af4cd34af58",
+    version = "1.1.4",
+)
+
+bower_archive(
+    name = "paper-listbox",
+    package = "polymerelements/paper-listbox",
+    sha1 = "ccc1a90ab0a96878c7bf7c9c4cfe47c85b09c8e3",
+    version = "2.0.0",
+)
+
+bower_archive(
     name = "polymer",
     package = "polymer/polymer",
     sha1 = "566b5fe9a2a3eea2cf3417c67d975a6752d131eb",
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 4943b7e..1af8f7d 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1067,7 +1067,8 @@
 
   protected void grant(Project.NameKey project, String ref, String permission, boolean force)
       throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    InternalGroup adminGroup =
+        groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
     grant(project, ref, permission, force, adminGroup.getGroupUUID());
   }
 
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index faa674e..a8f7767 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -24,11 +24,12 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
-import com.google.gerrit.server.group.Groups;
 import com.google.gerrit.server.group.GroupsUpdate;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.ServerInitiated;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.SchemaFactory;
@@ -54,7 +55,7 @@
   private final Sequences sequences;
   private final AccountsUpdate.Server accountsUpdate;
   private final VersionedAuthorizedKeys.Accessor authorizedKeys;
-  private final Groups groups;
+  private final GroupCache groupCache;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
   private final SshKeyCache sshKeyCache;
   private final ExternalIdsUpdate.Server externalIdsUpdate;
@@ -66,7 +67,7 @@
       Sequences sequences,
       AccountsUpdate.Server accountsUpdate,
       VersionedAuthorizedKeys.Accessor authorizedKeys,
-      Groups groups,
+      GroupCache groupCache,
       @ServerInitiated Provider<GroupsUpdate> groupsUpdateProvider,
       SshKeyCache sshKeyCache,
       ExternalIdsUpdate.Server externalIdsUpdate,
@@ -76,7 +77,7 @@
     this.sequences = sequences;
     this.accountsUpdate = accountsUpdate;
     this.authorizedKeys = authorizedKeys;
-    this.groups = groups;
+    this.groupCache = groupCache;
     this.groupsUpdateProvider = groupsUpdateProvider;
     this.sshKeyCache = sshKeyCache;
     this.externalIdsUpdate = externalIdsUpdate;
@@ -121,7 +122,7 @@
       if (groupNames != null) {
         for (String n : groupNames) {
           AccountGroup.NameKey k = new AccountGroup.NameKey(n);
-          Optional<AccountGroup> group = groups.getGroup(db, k);
+          Optional<InternalGroup> group = groupCache.get(k);
           if (!group.isPresent()) {
             throw new NoSuchGroupException(n);
           }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 8ac063b..84f3533 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -91,6 +91,7 @@
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
 import com.google.gerrit.server.project.RefPattern;
@@ -1014,7 +1015,8 @@
     String userRef = RefNames.refsUsers(foo.id);
     accountIndexedCounter.clear();
 
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    InternalGroup adminGroup =
+        groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
     grant(allUsers, userRef, Permission.PUSH, false, adminGroup.getGroupUUID());
     grantLabel("Code-Review", -2, 2, allUsers, userRef, false, adminGroup.getGroupUUID(), false);
     grant(allUsers, userRef, Permission.SUBMIT, false, adminGroup.getGroupUUID());
@@ -1179,7 +1181,8 @@
     accountsUpdate.create().update(foo.id, a -> a.setPreferredEmail(noEmail));
     accountIndexedCounter.clear();
 
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    InternalGroup adminGroup =
+        groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
     grant(allUsers, userRef, Permission.PUSH, false, adminGroup.getGroupUUID());
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
@@ -1214,7 +1217,8 @@
     String userRef = RefNames.refsUsers(foo.id);
     accountIndexedCounter.clear();
 
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    InternalGroup adminGroup =
+        groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
     grant(allUsers, userRef, Permission.PUSH, false, adminGroup.getGroupUUID());
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
@@ -1273,7 +1277,8 @@
     String userRef = RefNames.refsUsers(foo.id);
     accountIndexedCounter.clear();
 
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    InternalGroup adminGroup =
+        groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
     grant(allUsers, userRef, Permission.PUSH, false, adminGroup.getGroupUUID());
 
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index d399e2b..a8a712c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -1225,7 +1225,7 @@
     Util.allow(
         cfg,
         Permission.READ,
-        groupCache.get(new AccountGroup.NameKey("Administrators")).getGroupUUID(),
+        groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID(),
         "refs/*");
     Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
     saveProjectConfig(p, cfg);
@@ -1302,7 +1302,7 @@
     Util.allow(
         cfg,
         Permission.READ,
-        groupCache.get(new AccountGroup.NameKey("Administrators")).getGroupUUID(),
+        groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID(),
         "refs/*");
     Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
     saveProjectConfig(p, cfg);
@@ -1349,7 +1349,7 @@
     Util.allow(
         cfg,
         Permission.READ,
-        groupCache.get(new AccountGroup.NameKey("Administrators")).getGroupUUID(),
+        groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID(),
         "refs/*");
     Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
     saveProjectConfig(p, cfg);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
index ccdd03b..dd891ce 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
@@ -19,7 +19,7 @@
 
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
 import java.util.Set;
 
 public class GroupAssert {
@@ -31,7 +31,7 @@
     assertWithMessage("unexpected groups: " + actual).that(actual).isEmpty();
   }
 
-  public static void assertGroupInfo(AccountGroup group, GroupInfo info) {
+  public static void assertGroupInfo(InternalGroup group, GroupInfo info) {
     if (info.name != null) {
       // 'name' is not set if returned in a map
       assertThat(info.name).isEqualTo(group.getName());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 1b5e544a..eb4df15 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -47,6 +47,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.GroupsUpdate;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.ServerInitiated;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.inject.Inject;
@@ -257,13 +258,13 @@
 
   @Test
   public void getGroup() throws Exception {
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    InternalGroup adminGroup = getFromCache("Administrators");
     testGetGroup(adminGroup.getGroupUUID().get(), adminGroup);
     testGetGroup(adminGroup.getName(), adminGroup);
     testGetGroup(adminGroup.getId().get(), adminGroup);
   }
 
-  private void testGetGroup(Object id, AccountGroup expectedGroup) throws Exception {
+  private void testGetGroup(Object id, InternalGroup expectedGroup) throws Exception {
     GroupInfo group = gApi.groups().id(id.toString()).get();
     assertGroupInfo(expectedGroup, group);
   }
@@ -559,7 +560,7 @@
 
   @Test
   public void allGroupInfoFieldsSetCorrectly() throws Exception {
-    AccountGroup adminGroup = getFromCache("Administrators");
+    InternalGroup adminGroup = getFromCache("Administrators");
     Map<String, GroupInfo> groups = gApi.groups().list().addGroup(adminGroup.getName()).getAsMap();
     assertThat(groups).hasSize(1);
     assertThat(groups).containsKey("Administrators");
@@ -683,8 +684,8 @@
     assertThat(gApi.groups().id(group).includedGroups()).isEmpty();
   }
 
-  private AccountGroup getFromCache(String name) throws Exception {
-    return groupCache.get(new AccountGroup.NameKey(name));
+  private InternalGroup getFromCache(String name) throws Exception {
+    return groupCache.get(new AccountGroup.NameKey(name)).orElse(null);
   }
 
   private void setCreatedOnToNull(AccountGroup.UUID groupUuid) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index b471efc..2f92e7a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import java.util.List;
 import org.junit.Before;
@@ -39,14 +40,15 @@
   private Project.NameKey secretProject;
   private Project.NameKey secretRefProject;
   private TestAccount privilegedUser;
-  private AccountGroup privilegedGroup;
+  private InternalGroup privilegedGroup;
 
   @Before
   public void setUp() throws Exception {
     normalProject = createProject("normal");
     secretProject = createProject("secret");
     secretRefProject = createProject("secretRef");
-    privilegedGroup = groupCache.get(new AccountGroup.NameKey(createGroup("privilegedGroup")));
+    privilegedGroup =
+        groupCache.get(new AccountGroup.NameKey(createGroup("privilegedGroup"))).orElse(null);
 
     privilegedUser = accountCreator.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden");
     gApi.groups().id(privilegedGroup.getGroupUUID().get()).addMembers(privilegedUser.username);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 84c2901..4dac61f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -82,7 +82,7 @@
 
   @Before
   public void setUp() throws Exception {
-    admins = groupCache.get(new AccountGroup.NameKey("Administrators")).getGroupUUID();
+    admins = groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID();
     setUpPermissions();
     setUpChanges();
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index 41f7d4a..b586ab2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -23,13 +23,14 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.group.InternalGroup;
 import org.junit.Test;
 
 public class FlushCacheIT extends AbstractDaemonTest {
 
   @Test
   public void flushCache() throws Exception {
-    AccountGroup group = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    InternalGroup group = groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
     assertWithMessage("Precondition: The group 'Administrators' was loaded by the group cache")
         .that(group)
         .isNotNull();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
index b6ac5e9..f67012a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -41,6 +41,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import java.util.HashMap;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -53,12 +54,12 @@
 
 public class AccessIT extends AbstractDaemonTest {
 
-  private final String PROJECT_NAME = "newProject";
+  private static final String PROJECT_NAME = "newProject";
 
-  private final String REFS_ALL = Constants.R_REFS + "*";
-  private final String REFS_HEADS = Constants.R_HEADS + "*";
+  private static final String REFS_ALL = Constants.R_REFS + "*";
+  private static final String REFS_HEADS = Constants.R_HEADS + "*";
 
-  private final String LABEL_CODE_REVIEW = "Code-Review";
+  private static final String LABEL_CODE_REVIEW = "Code-Review";
 
   private String newProjectName;
   private ProjectApi pApi;
@@ -394,7 +395,8 @@
 
   @Test
   public void addNonGlobalCapabilityToGlobalCapabilities() throws Exception {
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    InternalGroup adminGroup =
+        groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
 
     ProjectAccessInput accessInput = newProjectAccessInput();
     AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
@@ -423,7 +425,8 @@
 
   @Test
   public void removeGlobalCapabilityAsAdmin() throws Exception {
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    InternalGroup adminGroup =
+        groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
 
     ProjectAccessInput accessInput = newProjectAccessInput();
     AccessSectionInfo accessSectionInfo = newAccessSectionInfo();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 7640328..0409fbc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -191,7 +191,11 @@
     in.owners.add(SystemGroupBackend.REGISTERED_USERS.get()); // by UUID
     in.owners.add(
         Integer.toString(
-            groupCache.get(new AccountGroup.NameKey("Administrators")).getId().get())); // by ID
+            groupCache
+                .get(new AccountGroup.NameKey("Administrators"))
+                .orElse(null)
+                .getId()
+                .get())); // by ID
     gApi.projects().create(in);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
     Set<AccountGroup.UUID> expectedOwnerIds = Sets.newHashSetWithExpectedSize(3);
@@ -293,7 +297,7 @@
   }
 
   private AccountGroup.UUID groupUuid(String groupName) {
-    return groupCache.get(new AccountGroup.NameKey(groupName)).getGroupUUID();
+    return groupCache.get(new AccountGroup.NameKey(groupName)).orElse(null).getGroupUUID();
   }
 
   private void assertHead(String projectName, String expectedRef) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
index ce0787f..ca45e7c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gson.Gson;
@@ -310,6 +311,19 @@
     userSession.close();
   }
 
+  @Test
+  public void allChangeOptionsAreServedWithoutExceptions() throws Exception {
+    PushOneCommit.Result r = createChange();
+    // Merge the change so that the result has more data and potentially went through more
+    // computation while formatting the output, such as labels, reviewers etc.
+    merge(r);
+    for (ListChangesOption option : ListChangesOption.values()) {
+      assertThat(gApi.changes().query(r.getChangeId()).withOption(option).get())
+          .named("Option: " + option)
+          .hasSize(1);
+    }
+  }
+
   private List<ChangeAttribute> executeSuccessfulQuery(String params, SshSession session)
       throws Exception {
     String rawResponse = session.exec("gerrit query --format=JSON " + params);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index 1484809..0465902 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -46,6 +46,7 @@
   private final Account.Id ownerId;
   private final boolean mine;
   private ChangeTable table;
+  private ChangeTable.Section workInProgress;
   private ChangeTable.Section outgoing;
   private ChangeTable.Section incoming;
   private ChangeTable.Section closed;
@@ -72,11 +73,15 @@
         };
     table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
 
+    workInProgress = new ChangeTable.Section();
     outgoing = new ChangeTable.Section();
     incoming = new ChangeTable.Section();
     closed = new ChangeTable.Section();
 
     String who = mine ? "self" : ownerId.toString();
+    workInProgress.setTitleWidget(
+        new InlineHyperlink(
+            Util.C.workInProgress(), PageLinks.toChangeQuery(queryWorkInProgress(who))));
     outgoing.setTitleWidget(
         new InlineHyperlink(Util.C.outgoingReviews(), PageLinks.toChangeQuery(queryOutgoing(who))));
     incoming.setTitleWidget(
@@ -85,6 +90,7 @@
     closed.setTitleWidget(
         new InlineHyperlink(Util.C.recentlyClosed(), PageLinks.toChangeQuery(queryClosed(who))));
 
+    table.addSection(workInProgress);
     table.addSection(outgoing);
     table.addSection(incoming);
     table.addSection(closed);
@@ -92,8 +98,12 @@
     table.setSavePointerId("owner:" + ownerId);
   }
 
+  private static String queryWorkInProgress(String who) {
+    return "is:open is:wip owner:" + who;
+  }
+
   private static String queryOutgoing(String who) {
-    return "is:open owner:" + who;
+    return "is:open -is:wip owner:" + who;
   }
 
   private static String queryIncoming(String who) {
@@ -123,6 +133,7 @@
           }
         },
         mine ? MY_DASHBOARD_OPTIONS : DashboardTable.OPTIONS,
+        queryWorkInProgress(who),
         queryOutgoing(who),
         queryIncoming(who),
         queryClosed(who) + " -age:4w limit:10");
@@ -142,9 +153,10 @@
       return;
     }
 
-    ChangeList out = result.get(0);
-    ChangeList in = result.get(1);
-    ChangeList done = result.get(2);
+    ChangeList wip = result.get(0);
+    ChangeList out = result.get(1);
+    ChangeList in = result.get(2);
+    ChangeList done = result.get(3);
 
     if (mine) {
       setWindowTitle(Util.C.myDashboardTitle());
@@ -167,7 +179,8 @@
 
     Collections.sort(Natives.asList(out), outComparator());
 
-    table.updateColumnsForLabels(out, in, done);
+    table.updateColumnsForLabels(wip, out, in, done);
+    workInProgress.display(wip);
     outgoing.display(out);
     incoming.display(in);
     closed.display(done);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 402179c..aa6c4ec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -43,6 +43,8 @@
 
   String unknownDashboardTitle();
 
+  String workInProgress();
+
   String incomingReviews();
 
   String outgoingReviews();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index dd11a60..9860cb2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -12,6 +12,7 @@
 
 myDashboardTitle = My Reviews
 unknownDashboardTitle = Code Review Dashboard
+workInProgress Work in progress
 incomingReviews = Incoming reviews
 outgoingReviews = Outgoing reviews
 recentlyClosed = Recently closed
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateToNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateToNoteDb.java
index 5fbd411..dda8d14 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateToNoteDb.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateToNoteDb.java
@@ -20,8 +20,11 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.elasticsearch.ElasticIndexModule;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.pgm.util.BatchProgramModule;
 import com.google.gerrit.pgm.util.RuntimeShutdown;
 import com.google.gerrit.pgm.util.SiteProgram;
@@ -30,10 +33,11 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.index.DummyIndexModule;
+import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
+import com.google.inject.Module;
 import com.google.inject.Provider;
 import java.util.ArrayList;
 import java.util.List;
@@ -160,12 +164,23 @@
           public void configure() {
             install(dbInjector.getInstance(BatchProgramModule.class));
             bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
-            install(new DummyIndexModule());
+            install(getIndexModule());
             factory(ChangeResource.Factory.class);
           }
         });
   }
 
+  private Module getIndexModule() {
+    switch (IndexModule.getIndexType(dbInjector)) {
+      case LUCENE:
+        return LuceneIndexModule.singleVersionWithExplicitVersions(ImmutableMap.of(), threads);
+      case ELASTICSEARCH:
+        return ElasticIndexModule.singleVersionWithExplicitVersions(ImmutableMap.of(), threads);
+      default:
+        throw new IllegalStateException("unsupported index.type");
+    }
+  }
+
   private void stop() {
     try {
       LifecycleManager m = sysManager;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 4c6f670..2427ea2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -20,6 +20,7 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -33,8 +34,13 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.group.Groups;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.account.AccountIndexer;
+import com.google.gerrit.server.index.group.GroupField;
+import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
+import com.google.gerrit.server.query.group.InternalGroupQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -46,10 +52,10 @@
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
+import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -155,8 +161,9 @@
     private final SchemaFactory<ReviewDb> schema;
     private final AllUsersName allUsersName;
     private final Accounts accounts;
+    private final Provider<GroupIndex> groupIndexProvider;
+    private final Provider<InternalGroupQuery> groupQueryProvider;
     private final GroupCache groupCache;
-    private final Groups groups;
     private final GeneralPreferencesLoader loader;
     private final Provider<WatchConfig.Accessor> watchConfig;
     private final ExternalIds externalIds;
@@ -166,16 +173,18 @@
         SchemaFactory<ReviewDb> sf,
         AllUsersName allUsersName,
         Accounts accounts,
+        GroupIndexCollection groupIndexCollection,
+        Provider<InternalGroupQuery> groupQueryProvider,
         GroupCache groupCache,
-        Groups groups,
         GeneralPreferencesLoader loader,
         Provider<WatchConfig.Accessor> watchConfig,
         ExternalIds externalIds) {
       this.schema = sf;
       this.allUsersName = allUsersName;
       this.accounts = accounts;
+      this.groupIndexProvider = groupIndexCollection::getSearchIndex;
+      this.groupQueryProvider = groupQueryProvider;
       this.groupCache = groupCache;
-      this.groups = groups;
       this.loader = loader;
       this.watchConfig = watchConfig;
       this.externalIds = externalIds;
@@ -195,13 +204,7 @@
         return Optional.empty();
       }
 
-      Set<AccountGroup.UUID> internalGroups =
-          groups
-              .getGroupsWithMember(db, who)
-              .map(groupCache::get)
-              .map(AccountGroup::getGroupUUID)
-              .filter(Objects::nonNull)
-              .collect(toImmutableSet());
+      Set<AccountGroup.UUID> internalGroups = getGroupsWithMember(db, who);
 
       try {
         account.setGeneralPreferences(loader.load(who));
@@ -218,6 +221,21 @@
               externalIds.byAccount(who),
               watchConfig.get().getProjectWatches(who)));
     }
+
+    private ImmutableSet<AccountGroup.UUID> getGroupsWithMember(ReviewDb db, Account.Id memberId)
+        throws OrmException {
+      Stream<InternalGroup> internalGroupStream;
+      if (groupIndexProvider.get().getSchema().hasField(GroupField.MEMBER)) {
+        internalGroupStream = groupQueryProvider.get().byMember(memberId).stream();
+      } else {
+        internalGroupStream =
+            Groups.getGroupsWithMemberFromReviewDb(db, memberId)
+                .map(groupCache::get)
+                .flatMap(Streams::stream);
+      }
+
+      return internalGroupStream.map(InternalGroup::getGroupUUID).collect(toImmutableSet());
+    }
   }
 
   static class ByNameLoader extends CacheLoader<String, Optional<Account.Id>> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index cafdaed..ec756bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.GroupsUpdate;
-import com.google.gerrit.server.group.ServerInitiated;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.server.OrmException;
@@ -70,7 +69,7 @@
   private final Provider<InternalAccountQuery> accountQueryProvider;
   private final ExternalIds externalIds;
   private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
-  private final Provider<GroupsUpdate> groupsUpdateProvider;
+  private final GroupsUpdate.Factory groupsUpdateFactory;
 
   @Inject
   AccountManager(
@@ -87,7 +86,7 @@
       Provider<InternalAccountQuery> accountQueryProvider,
       ExternalIds externalIds,
       ExternalIdsUpdate.Server externalIdsUpdateFactory,
-      @ServerInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
+      GroupsUpdate.Factory groupsUpdateFactory) {
     this.schema = schema;
     this.sequences = sequences;
     this.accounts = accounts;
@@ -102,7 +101,7 @@
     this.accountQueryProvider = accountQueryProvider;
     this.externalIds = externalIds;
     this.externalIdsUpdateFactory = externalIdsUpdateFactory;
-    this.groupsUpdateProvider = groupsUpdateProvider;
+    this.groupsUpdateFactory = groupsUpdateFactory;
   }
 
   /** @return user identified by this external identity string */
@@ -253,9 +252,8 @@
               .getPermission(GlobalCapability.ADMINISTRATE_SERVER);
 
       AccountGroup.UUID uuid = admin.getRules().get(0).getGroup().getUUID();
-      GroupsUpdate groupsUpdate = groupsUpdateProvider.get();
       // The user initiated this request by logging in. -> Attribute all modifications to that user.
-      groupsUpdate.setCurrentUser(user);
+      GroupsUpdate groupsUpdate = groupsUpdateFactory.create(user);
       try {
         groupsUpdate.addGroupMember(db, uuid, newId);
       } catch (NoSuchGroupException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
index 82f1559..82bcce3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -22,9 +22,23 @@
 
 /** Tracks group objects in memory for efficient access. */
 public interface GroupCache {
-  AccountGroup get(AccountGroup.Id groupId);
+  /**
+   * Looks up an internal group by its ID.
+   *
+   * @param groupId the ID of the internal group
+   * @return an {@code Optional} of the internal group, or an empty {@code Optional} if no internal
+   *     group with this ID exists on this server or an error occurred during lookup
+   */
+  Optional<InternalGroup> get(AccountGroup.Id groupId);
 
-  AccountGroup get(AccountGroup.NameKey name);
+  /**
+   * Looks up an internal group by its name.
+   *
+   * @param name the name of the internal group
+   * @return an {@code Optional} of the internal group, or an empty {@code Optional} if no internal
+   *     group with this name exists on this server or an error occurred during lookup
+   */
+  Optional<InternalGroup> get(AccountGroup.NameKey name);
 
   /**
    * Looks up an internal group by its UUID.
@@ -39,11 +53,10 @@
   ImmutableList<AccountGroup> all();
 
   /** Notify the cache that a new group was constructed. */
-  void onCreateGroup(AccountGroup.NameKey newGroupName) throws IOException;
+  void onCreateGroup(AccountGroup group) throws IOException;
 
   void evict(AccountGroup.UUID groupUuid, AccountGroup.Id groupId, AccountGroup.NameKey groupName)
       throws IOException;
 
-  void evictAfterRename(AccountGroup.NameKey oldName, AccountGroup.NameKey newName)
-      throws IOException;
+  void evictAfterRename(AccountGroup.NameKey oldName) throws IOException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index 2901501..edbc2d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -15,20 +15,17 @@
 package com.google.gerrit.server.account;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.group.Groups;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.gerrit.server.query.group.InternalGroupQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -56,10 +53,10 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        cache(BYID_NAME, AccountGroup.Id.class, new TypeLiteral<Optional<AccountGroup>>() {})
+        cache(BYID_NAME, AccountGroup.Id.class, new TypeLiteral<Optional<InternalGroup>>() {})
             .loader(ByIdLoader.class);
 
-        cache(BYNAME_NAME, String.class, new TypeLiteral<Optional<AccountGroup>>() {})
+        cache(BYNAME_NAME, String.class, new TypeLiteral<Optional<InternalGroup>>() {})
             .loader(ByNameLoader.class);
 
         cache(BYUUID_NAME, String.class, new TypeLiteral<Optional<InternalGroup>>() {})
@@ -71,8 +68,8 @@
     };
   }
 
-  private final LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId;
-  private final LoadingCache<String, Optional<AccountGroup>> byName;
+  private final LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId;
+  private final LoadingCache<String, Optional<InternalGroup>> byName;
   private final LoadingCache<String, Optional<InternalGroup>> byUUID;
   private final SchemaFactory<ReviewDb> schema;
   private final Provider<GroupIndexer> indexer;
@@ -80,8 +77,8 @@
 
   @Inject
   GroupCacheImpl(
-      @Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId,
-      @Named(BYNAME_NAME) LoadingCache<String, Optional<AccountGroup>> byName,
+      @Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId,
+      @Named(BYNAME_NAME) LoadingCache<String, Optional<InternalGroup>> byName,
       @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID,
       SchemaFactory<ReviewDb> schema,
       Provider<GroupIndexer> indexer,
@@ -95,13 +92,12 @@
   }
 
   @Override
-  public AccountGroup get(AccountGroup.Id groupId) {
+  public Optional<InternalGroup> get(AccountGroup.Id groupId) {
     try {
-      Optional<AccountGroup> g = byId.get(groupId);
-      return g.isPresent() ? g.get() : missing(groupId);
+      return byId.get(groupId);
     } catch (ExecutionException e) {
       log.warn("Cannot load group " + groupId, e);
-      return missing(groupId);
+      return Optional.empty();
     }
   }
 
@@ -122,27 +118,22 @@
   }
 
   @Override
-  public void evictAfterRename(final AccountGroup.NameKey oldName, AccountGroup.NameKey newName)
-      throws IOException {
+  public void evictAfterRename(AccountGroup.NameKey oldName) throws IOException {
     if (oldName != null) {
       byName.invalidate(oldName.get());
     }
-    if (newName != null) {
-      byName.invalidate(newName.get());
-    }
-    indexer.get().index(get(newName).getGroupUUID());
   }
 
   @Override
-  public AccountGroup get(AccountGroup.NameKey name) {
+  public Optional<InternalGroup> get(AccountGroup.NameKey name) {
     if (name == null) {
-      return null;
+      return Optional.empty();
     }
     try {
-      return byName.get(name.get()).orElse(null);
+      return byName.get(name.get());
     } catch (ExecutionException e) {
       log.warn(String.format("Cannot look up group %s by name", name.get()), e);
-      return null;
+      return Optional.empty();
     }
   }
 
@@ -171,49 +162,35 @@
   }
 
   @Override
-  public void onCreateGroup(AccountGroup.NameKey newGroupName) throws IOException {
-    byName.invalidate(newGroupName.get());
-    indexer.get().index(get(newGroupName).getGroupUUID());
+  public void onCreateGroup(AccountGroup group) throws IOException {
+    indexer.get().index(group.getGroupUUID());
   }
 
-  private static AccountGroup missing(AccountGroup.Id key) {
-    AccountGroup.NameKey name = new AccountGroup.NameKey("Deleted Group" + key);
-    return new AccountGroup(name, key, null, TimeUtil.nowTs());
-  }
-
-  static class ByIdLoader extends CacheLoader<AccountGroup.Id, Optional<AccountGroup>> {
-    private final SchemaFactory<ReviewDb> schema;
-    private final Groups groups;
+  static class ByIdLoader extends CacheLoader<AccountGroup.Id, Optional<InternalGroup>> {
+    private final Provider<InternalGroupQuery> groupQueryProvider;
 
     @Inject
-    ByIdLoader(SchemaFactory<ReviewDb> sf, Groups groups) {
-      schema = sf;
-      this.groups = groups;
+    ByIdLoader(Provider<InternalGroupQuery> groupQueryProvider) {
+      this.groupQueryProvider = groupQueryProvider;
     }
 
     @Override
-    public Optional<AccountGroup> load(AccountGroup.Id key) throws Exception {
-      try (ReviewDb db = schema.open()) {
-        return groups.getGroup(db, key);
-      }
+    public Optional<InternalGroup> load(AccountGroup.Id key) throws Exception {
+      return groupQueryProvider.get().byId(key);
     }
   }
 
-  static class ByNameLoader extends CacheLoader<String, Optional<AccountGroup>> {
-    private final SchemaFactory<ReviewDb> schema;
-    private final Groups groups;
+  static class ByNameLoader extends CacheLoader<String, Optional<InternalGroup>> {
+    private final Provider<InternalGroupQuery> groupQueryProvider;
 
     @Inject
-    ByNameLoader(SchemaFactory<ReviewDb> sf, Groups groups) {
-      schema = sf;
-      this.groups = groups;
+    ByNameLoader(Provider<InternalGroupQuery> groupQueryProvider) {
+      this.groupQueryProvider = groupQueryProvider;
     }
 
     @Override
-    public Optional<AccountGroup> load(String name) throws Exception {
-      try (ReviewDb db = schema.open()) {
-        return groups.getGroup(db, new AccountGroup.NameKey(name));
-      }
+    public Optional<InternalGroup> load(String name) throws Exception {
+      return groupQueryProvider.get().byName(new AccountGroup.NameKey(name));
     }
   }
 
@@ -230,18 +207,7 @@
     @Override
     public Optional<InternalGroup> load(String uuid) throws Exception {
       try (ReviewDb db = schema.open()) {
-        AccountGroup.UUID groupUuid = new AccountGroup.UUID(uuid);
-        Optional<AccountGroup> accountGroup = groups.getGroup(db, groupUuid);
-
-        if (!accountGroup.isPresent()) {
-          return Optional.empty();
-        }
-
-        ImmutableSet<Account.Id> members =
-            groups.getMembers(db, groupUuid).collect(toImmutableSet());
-        ImmutableSet<AccountGroup.UUID> subgroups =
-            groups.getSubgroups(db, groupUuid).collect(toImmutableSet());
-        return accountGroup.map(group -> InternalGroup.create(group, members, subgroups));
+        return groups.getGroup(db, new AccountGroup.UUID(uuid));
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 5af4898..020a04d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -21,12 +21,15 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.util.Optional;
 
 /** Access control management for a group of accounts managed in Gerrit. */
 public class GroupControl {
@@ -71,11 +74,11 @@
     }
 
     public GroupControl controlFor(AccountGroup.Id groupId) throws NoSuchGroupException {
-      final AccountGroup group = groupCache.get(groupId);
-      if (group == null) {
-        throw new NoSuchGroupException(groupId);
-      }
-      return controlFor(GroupDescriptions.forAccountGroup(group));
+      Optional<InternalGroup> group = groupCache.get(groupId);
+      return group
+          .map(InternalGroupDescription::new)
+          .map(this::controlFor)
+          .orElseThrow(() -> new NoSuchGroupException(groupId));
     }
 
     public GroupControl controlFor(AccountGroup.UUID groupId) throws NoSuchGroupException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 10c002c..8d63fb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -19,22 +19,29 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.group.Groups;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.index.group.GroupField;
+import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.gerrit.server.index.group.GroupIndexCollection;
+import com.google.gerrit.server.query.group.InternalGroupQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 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;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Objects;
 import java.util.concurrent.ExecutionException;
+import java.util.stream.Stream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -158,26 +165,36 @@
   static class ParentGroupsLoader
       extends CacheLoader<AccountGroup.UUID, ImmutableList<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
+    private final Provider<GroupIndex> groupIndexProvider;
+    private final Provider<InternalGroupQuery> groupQueryProvider;
     private final GroupCache groupCache;
-    private final Groups groups;
 
     @Inject
-    ParentGroupsLoader(SchemaFactory<ReviewDb> sf, GroupCache groupCache, Groups groups) {
+    ParentGroupsLoader(
+        SchemaFactory<ReviewDb> sf,
+        GroupIndexCollection groupIndexCollection,
+        Provider<InternalGroupQuery> groupQueryProvider,
+        GroupCache groupCache) {
       schema = sf;
+      this.groupIndexProvider = groupIndexCollection::getSearchIndex;
+      this.groupQueryProvider = groupQueryProvider;
       this.groupCache = groupCache;
-      this.groups = groups;
     }
 
     @Override
     public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key) throws OrmException {
-      try (ReviewDb db = schema.open()) {
-        return groups
-            .getParentGroups(db, key)
-            .map(groupCache::get)
-            .map(AccountGroup::getGroupUUID)
-            .filter(Objects::nonNull)
-            .collect(toImmutableList());
+      Stream<InternalGroup> internalGroupStream;
+      if (groupIndexProvider.get().getSchema().hasField(GroupField.SUBGROUP)) {
+        internalGroupStream = groupQueryProvider.get().bySubgroup(key).stream();
+      } else {
+        try (ReviewDb db = schema.open()) {
+          internalGroupStream =
+              Groups.getParentGroupsFromReviewDb(db, key)
+                  .map(groupCache::get)
+                  .flatMap(Streams::stream);
+        }
       }
+      return internalGroupStream.map(InternalGroup::getGroupUUID).collect(toImmutableList());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
index d41f02c..f3393c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
@@ -16,8 +16,10 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import java.util.Optional;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.CmdLineParser;
 import org.kohsuke.args4j.OptionDef;
@@ -41,11 +43,11 @@
   @Override
   public final int parseArguments(Parameters params) throws CmdLineException {
     final String n = params.getParameter(0);
-    final AccountGroup group = groupCache.get(new AccountGroup.NameKey(n));
-    if (group == null) {
+    Optional<InternalGroup> group = groupCache.get(new AccountGroup.NameKey(n));
+    if (!group.isPresent()) {
       throw new CmdLineException(owner, "Group \"" + n + "\" does not exist");
     }
-    setter.addValue(group.getId());
+    setter.addValue(group.get().getId());
     return 1;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 63f8138..59a48fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -416,6 +416,12 @@
             | RuntimeException e) {
           if (has(CHECK)) {
             i = checkOnly(cd);
+          } else if (e instanceof NoSuchChangeException) {
+            log.info(
+                "NoSuchChangeException: Omitting corrupt change "
+                    + cd.getId()
+                    + " from results. Seems to be stale in the index.");
+            continue;
           } else {
             log.warn("Omitting corrupt change " + cd.getId() + " from results", e);
             continue;
@@ -1074,7 +1080,7 @@
     for (PatchSetApproval psa :
         approvalsUtil.byPatchSetUser(
             db.get(),
-            cd.notes(),
+            lazyLoad ? cd.notes() : notesFactory.createFromIndexedChange(cd.change()),
             user,
             cd.change().currentPatchSetId(),
             user.getAccountId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
index 227367e..32dc7bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -242,7 +242,6 @@
           // completeChangeSet computation, for example.
           visible = false;
         }
-        Collection<RevCommit> toWalk = visible ? visibleCommits : nonVisibleCommits;
 
         // Pick a revision to use for traversal.  If any of the patch sets
         // is visible, we use the most recent one.  Otherwise, use the current
@@ -276,16 +275,16 @@
         // Always include the input, even if merged. This allows
         // SubmitStrategyOp to correct the situation later, assuming it gets
         // returned by byCommitsOnBranchNotMerged below.
-        toWalk.add(commit);
+        if (visible) {
+          visibleCommits.add(commit);
+        } else {
+          nonVisibleCommits.add(commit);
+        }
       }
 
-      Set<String> emptySet = Collections.emptySet();
-      Set<String> visibleHashes = walkChangesByHashes(visibleCommits, emptySet, or, b);
-
-      List<ChangeData> cds = byCommitsOnBranchNotMerged(or, db, b, visibleHashes);
-      for (ChangeData chd : cds) {
-        visibleChanges.add(chd);
-      }
+      Set<String> visibleHashes =
+          walkChangesByHashes(visibleCommits, Collections.emptySet(), or, b);
+      Iterables.addAll(visibleChanges, byCommitsOnBranchNotMerged(or, db, b, visibleHashes));
 
       Set<String> nonVisibleHashes = walkChangesByHashes(nonVisibleCommits, visibleHashes, or, b);
       Iterables.addAll(nonVisibleChanges, byCommitsOnBranchNotMerged(or, db, b, nonVisibleHashes));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
index a1aefc5..e55397e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -56,6 +56,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -198,10 +199,8 @@
         new AccountGroup(createGroupArgs.getGroup(), groupId, uuid, TimeUtil.nowTs());
     group.setVisibleToAll(createGroupArgs.visibleToAll);
     if (createGroupArgs.ownerGroupId != null) {
-      AccountGroup ownerGroup = groupCache.get(createGroupArgs.ownerGroupId);
-      if (ownerGroup != null) {
-        group.setOwnerGroupUUID(ownerGroup.getGroupUUID());
-      }
+      Optional<InternalGroup> ownerGroup = groupCache.get(createGroupArgs.ownerGroupId);
+      ownerGroup.map(InternalGroup::getGroupUUID).ifPresent(group::setOwnerGroupUUID);
     }
     if (createGroupArgs.groupDescription != null) {
       group.setDescription(createGroupArgs.groupDescription);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
index ce287d0..5af7ebd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
@@ -152,7 +152,7 @@
       Account.Id accountId = m.getAccountId();
       String userName = accountCache.get(accountId).getUserName();
       AccountGroup.Id groupId = m.getAccountGroupId();
-      String groupName = groupCache.get(groupId).getName();
+      String groupName = getGroupName(groupId);
 
       descriptions.add(
           MessageFormat.format(
@@ -168,7 +168,7 @@
       AccountGroup.UUID groupUuid = m.getIncludeUUID();
       String groupName = groupBackend.get(groupUuid).getName();
       AccountGroup.Id targetGroupId = m.getGroupId();
-      String targetGroupName = groupCache.get(targetGroupId).getName();
+      String targetGroupName = getGroupName(targetGroupId);
 
       descriptions.add(
           MessageFormat.format(
@@ -178,6 +178,10 @@
     logOrmException(header, me, descriptions, e);
   }
 
+  private String getGroupName(AccountGroup.Id groupId) {
+    return groupCache.get(groupId).map(InternalGroup::getName).orElse("Deleted group " + groupId);
+  }
+
   private void logOrmException(String header, Account.Id me, Iterable<?> values, OrmException e) {
     StringBuilder message = new StringBuilder(header);
     message.append(" ");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Groups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Groups.java
index b835d22..a2660f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/Groups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Groups.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.group;
 
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.errors.NoSuchGroupException;
@@ -21,7 +24,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
@@ -45,6 +47,29 @@
 public class Groups {
 
   /**
+   * Returns the {@code InternalGroup} for the specified UUID if it exists.
+   *
+   * @param db the {@code ReviewDb} instance to use for lookups
+   * @param groupUuid the UUID of the group
+   * @return the found {@code InternalGroup} if it exists, or else an empty {@code Optional}
+   * @throws OrmDuplicateKeyException if multiple groups are found for the specified UUID
+   * @throws OrmException if the group couldn't be retrieved from ReviewDb
+   */
+  public Optional<InternalGroup> getGroup(ReviewDb db, AccountGroup.UUID groupUuid)
+      throws OrmException, NoSuchGroupException {
+    Optional<AccountGroup> accountGroup = getGroupFromReviewDb(db, groupUuid);
+
+    if (!accountGroup.isPresent()) {
+      return Optional.empty();
+    }
+
+    ImmutableSet<Account.Id> members = getMembers(db, groupUuid).collect(toImmutableSet());
+    ImmutableSet<AccountGroup.UUID> subgroups =
+        getSubgroups(db, groupUuid).collect(toImmutableSet());
+    return accountGroup.map(group -> InternalGroup.create(group, members, subgroups));
+  }
+
+  /**
    * Returns the {@code AccountGroup} for the specified UUID.
    *
    * @param db the {@code ReviewDb} instance to use for lookups
@@ -54,25 +79,13 @@
    * @throws OrmException if the group couldn't be retrieved from ReviewDb
    * @throws NoSuchGroupException if a group with such a UUID doesn't exist
    */
-  public AccountGroup getExistingGroup(ReviewDb db, AccountGroup.UUID groupUuid)
+  static AccountGroup getExistingGroupFromReviewDb(ReviewDb db, AccountGroup.UUID groupUuid)
       throws OrmException, NoSuchGroupException {
-    Optional<AccountGroup> group = getGroup(db, groupUuid);
+    Optional<AccountGroup> group = getGroupFromReviewDb(db, groupUuid);
     return group.orElseThrow(() -> new NoSuchGroupException(groupUuid));
   }
 
   /**
-   * Returns the {@code AccountGroup} for the specified ID if it exists.
-   *
-   * @param db the {@code ReviewDb} instance to use for lookups
-   * @param groupId the ID of the group
-   * @return the found {@code AccountGroup} if it exists, or else an empty {@code Optional}
-   * @throws OrmException if the group couldn't be retrieved from ReviewDb
-   */
-  public Optional<AccountGroup> getGroup(ReviewDb db, AccountGroup.Id groupId) throws OrmException {
-    return Optional.ofNullable(db.accountGroups().get(groupId));
-  }
-
-  /**
    * Returns the {@code AccountGroup} for the specified UUID if it exists.
    *
    * @param db the {@code ReviewDb} instance to use for lookups
@@ -81,8 +94,8 @@
    * @throws OrmDuplicateKeyException if multiple groups are found for the specified UUID
    * @throws OrmException if the group couldn't be retrieved from ReviewDb
    */
-  public Optional<AccountGroup> getGroup(ReviewDb db, AccountGroup.UUID groupUuid)
-      throws OrmException {
+  private static Optional<AccountGroup> getGroupFromReviewDb(
+      ReviewDb db, AccountGroup.UUID groupUuid) throws OrmException {
     List<AccountGroup> accountGroups = db.accountGroups().byUUID(groupUuid).toList();
     if (accountGroups.size() == 1) {
       return Optional.of(Iterables.getOnlyElement(accountGroups));
@@ -93,25 +106,6 @@
     }
   }
 
-  /**
-   * Returns the {@code AccountGroup} for the specified name if it exists.
-   *
-   * @param db the {@code ReviewDb} instance to use for lookups
-   * @param groupName the name of the group
-   * @return the found {@code AccountGroup} if it exists, or else an empty {@code Optional}
-   * @throws OrmException if the group couldn't be retrieved from ReviewDb
-   */
-  public Optional<AccountGroup> getGroup(ReviewDb db, AccountGroup.NameKey groupName)
-      throws OrmException {
-    AccountGroupName accountGroupName = db.accountGroupNames().get(groupName);
-    if (accountGroupName == null) {
-      return Optional.empty();
-    }
-
-    AccountGroup.Id groupId = accountGroupName.getId();
-    return Optional.ofNullable(db.accountGroups().get(groupId));
-  }
-
   public Stream<AccountGroup> getAll(ReviewDb db) throws OrmException {
     return Streams.stream(db.accountGroups().all());
   }
@@ -130,7 +124,7 @@
    */
   public boolean isMember(ReviewDb db, AccountGroup.UUID groupUuid, Account.Id accountId)
       throws OrmException, NoSuchGroupException {
-    AccountGroup group = getExistingGroup(db, groupUuid);
+    AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid);
     AccountGroupMember.Key key = new AccountGroupMember.Key(accountId, group.getId());
     return db.accountGroupMembers().get(key) != null;
   }
@@ -153,7 +147,7 @@
   public boolean isSubgroup(
       ReviewDb db, AccountGroup.UUID parentGroupUuid, AccountGroup.UUID subgroupUuid)
       throws OrmException, NoSuchGroupException {
-    AccountGroup parentGroup = getExistingGroup(db, parentGroupUuid);
+    AccountGroup parentGroup = getExistingGroupFromReviewDb(db, parentGroupUuid);
     AccountGroupById.Key key = new AccountGroupById.Key(parentGroup.getId(), subgroupUuid);
     return db.accountGroupById().get(key) != null;
   }
@@ -171,7 +165,7 @@
    */
   public Stream<Account.Id> getMembers(ReviewDb db, AccountGroup.UUID groupUuid)
       throws OrmException, NoSuchGroupException {
-    AccountGroup group = getExistingGroup(db, groupUuid);
+    AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid);
     ResultSet<AccountGroupMember> accountGroupMembers =
         db.accountGroupMembers().byGroup(group.getId());
     return Streams.stream(accountGroupMembers).map(AccountGroupMember::getAccountId);
@@ -193,7 +187,7 @@
    */
   public Stream<AccountGroup.UUID> getSubgroups(ReviewDb db, AccountGroup.UUID groupUuid)
       throws OrmException, NoSuchGroupException {
-    AccountGroup group = getExistingGroup(db, groupUuid);
+    AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid);
     ResultSet<AccountGroupById> accountGroupByIds = db.accountGroupById().byGroup(group.getId());
     return Streams.stream(accountGroupByIds).map(AccountGroupById::getIncludeUUID).distinct();
   }
@@ -209,8 +203,8 @@
    * @return a stream of the IDs of the groups of which the account is a member
    * @throws OrmException if an error occurs while reading from ReviewDb
    */
-  public Stream<AccountGroup.Id> getGroupsWithMember(ReviewDb db, Account.Id accountId)
-      throws OrmException {
+  public static Stream<AccountGroup.Id> getGroupsWithMemberFromReviewDb(
+      ReviewDb db, Account.Id accountId) throws OrmException {
     ResultSet<AccountGroupMember> accountGroupMembers =
         db.accountGroupMembers().byAccount(accountId);
     return Streams.stream(accountGroupMembers).map(AccountGroupMember::getAccountGroupId);
@@ -230,8 +224,8 @@
    * @return a stream of the IDs of the parent groups
    * @throws OrmException if an error occurs while reading from ReviewDb
    */
-  public Stream<AccountGroup.Id> getParentGroups(ReviewDb db, AccountGroup.UUID subgroupUuid)
-      throws OrmException {
+  public static Stream<AccountGroup.Id> getParentGroupsFromReviewDb(
+      ReviewDb db, AccountGroup.UUID subgroupUuid) throws OrmException {
     ResultSet<AccountGroupById> accountGroupByIds =
         db.accountGroupById().byIncludeUUID(subgroupUuid);
     return Streams.stream(accountGroupByIds).map(AccountGroupById::getGroupId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsUpdate.java
index d688e4c..736eeec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsUpdate.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.group;
 
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.server.group.Groups.getExistingGroupFromReviewDb;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
@@ -58,6 +59,17 @@
  */
 public class GroupsUpdate {
   public interface Factory {
+    /**
+     * Creates a {@code GroupsUpdate} which uses the identity of the specified user to mark database
+     * modifications executed by it. For NoteDb, this identity is used as author and committer for
+     * all related commits.
+     *
+     * <p><strong>Note</strong>: Please use this method with care and rather consider to use the
+     * correct annotation on the provider of a {@code GroupsUpdate} instead.
+     *
+     * @param currentUser the user to which modifications should be attributed, or {@code null} if
+     *     the Gerrit server identity should be used
+     */
     GroupsUpdate create(@Nullable IdentifiedUser currentUser);
   }
 
@@ -67,10 +79,8 @@
   private final AuditService auditService;
   private final AccountCache accountCache;
   private final RenameGroupOp.Factory renameGroupOpFactory;
-  private final PersonIdent serverIdent;
-
-  @Nullable private IdentifiedUser currentUser;
-  private PersonIdent committerIdent;
+  @Nullable private final IdentifiedUser currentUser;
+  private final PersonIdent committerIdent;
 
   @Inject
   GroupsUpdate(
@@ -88,33 +98,13 @@
     this.auditService = auditService;
     this.accountCache = accountCache;
     this.renameGroupOpFactory = renameGroupOpFactory;
-    this.serverIdent = serverIdent;
-
-    setCurrentUser(currentUser);
-  }
-
-  /**
-   * Uses the identity of the specified user to mark database modifications executed by this {@code
-   * GroupsUpdate}. For NoteDb, this identity is used as author and committer for all related
-   * commits.
-   *
-   * <p><strong>Note</strong>: Please use this method with care and rather consider to use the
-   * correct annotation on the provider of this class instead.
-   *
-   * @param currentUser the user to which modifications should be attributed, or {@code null} if the
-   *     Gerrit server identity should be used
-   */
-  public void setCurrentUser(@Nullable IdentifiedUser currentUser) {
     this.currentUser = currentUser;
-    setCommitterIdent(currentUser);
+    committerIdent = getCommitterIdent(serverIdent, currentUser);
   }
 
-  private void setCommitterIdent(@Nullable IdentifiedUser currentUser) {
-    if (currentUser != null) {
-      committerIdent = createPersonIdent(serverIdent, currentUser);
-    } else {
-      committerIdent = serverIdent;
-    }
+  private static PersonIdent getCommitterIdent(
+      PersonIdent serverIdent, @Nullable IdentifiedUser currentUser) {
+    return currentUser != null ? createPersonIdent(serverIdent, currentUser) : serverIdent;
   }
 
   private static PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
@@ -135,7 +125,7 @@
       throws OrmException, IOException {
     addNewGroup(db, group);
     addNewGroupMembers(db, group, memberIds);
-    groupCache.onCreateGroup(group.getNameKey());
+    groupCache.onCreateGroup(group);
   }
 
   /**
@@ -177,7 +167,7 @@
   public AccountGroup updateGroupInDb(
       ReviewDb db, AccountGroup.UUID groupUuid, Consumer<AccountGroup> groupConsumer)
       throws OrmException, NoSuchGroupException {
-    AccountGroup group = groups.getExistingGroup(db, groupUuid);
+    AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid);
     groupConsumer.accept(group);
     db.accountGroups().update(ImmutableList.of(group));
     return group;
@@ -196,7 +186,7 @@
    */
   public void renameGroup(ReviewDb db, AccountGroup.UUID groupUuid, AccountGroup.NameKey newName)
       throws OrmException, IOException, NameAlreadyUsedException, NoSuchGroupException {
-    AccountGroup group = groups.getExistingGroup(db, groupUuid);
+    AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid);
     AccountGroup.NameKey oldName = group.getNameKey();
 
     try {
@@ -221,8 +211,8 @@
 
     db.accountGroupNames().deleteKeys(ImmutableList.of(oldName));
 
+    groupCache.evictAfterRename(oldName);
     groupCache.evict(group.getGroupUUID(), group.getId(), group.getNameKey());
-    groupCache.evictAfterRename(oldName, newName);
 
     @SuppressWarnings("unused")
     Future<?> possiblyIgnoredError =
@@ -264,7 +254,7 @@
    */
   public void addGroupMembers(ReviewDb db, AccountGroup.UUID groupUuid, Set<Account.Id> accountIds)
       throws OrmException, IOException, NoSuchGroupException {
-    AccountGroup group = groups.getExistingGroup(db, groupUuid);
+    AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid);
     Set<Account.Id> newMemberIds = new HashSet<>();
     for (Account.Id accountId : accountIds) {
       boolean isMember = groups.isMember(db, groupUuid, accountId);
@@ -313,7 +303,7 @@
   public void removeGroupMembers(
       ReviewDb db, AccountGroup.UUID groupUuid, Set<Account.Id> accountIds)
       throws OrmException, IOException, NoSuchGroupException {
-    AccountGroup group = groups.getExistingGroup(db, groupUuid);
+    AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid);
     AccountGroup.Id groupId = group.getId();
     Set<AccountGroupMember> membersToRemove = new HashSet<>();
     for (Account.Id accountId : accountIds) {
@@ -357,7 +347,7 @@
   public void addSubgroups(
       ReviewDb db, AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> subgroupUuids)
       throws OrmException, NoSuchGroupException, IOException {
-    AccountGroup parentGroup = groups.getExistingGroup(db, parentGroupUuid);
+    AccountGroup parentGroup = getExistingGroupFromReviewDb(db, parentGroupUuid);
     AccountGroup.Id parentGroupId = parentGroup.getId();
     Set<AccountGroupById> newSubgroups = new HashSet<>();
     for (AccountGroup.UUID includedGroupUuid : subgroupUuids) {
@@ -400,7 +390,7 @@
   public void removeSubgroups(
       ReviewDb db, AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> subgroupUuids)
       throws OrmException, NoSuchGroupException, IOException {
-    AccountGroup parentGroup = groups.getExistingGroup(db, parentGroupUuid);
+    AccountGroup parentGroup = getExistingGroupFromReviewDb(db, parentGroupUuid);
     AccountGroup.Id parentGroupId = parentGroup.getId();
     Set<AccountGroupById> subgroupsToRemove = new HashSet<>();
     for (AccountGroup.UUID subgroupUuid : subgroupUuids) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/InternalGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/InternalGroup.java
index 228d86f..fafc591 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/InternalGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/InternalGroup.java
@@ -19,10 +19,12 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import java.io.Serializable;
 import java.sql.Timestamp;
 
 @AutoValue
-public abstract class InternalGroup {
+public abstract class InternalGroup implements Serializable {
+  private static final long serialVersionUID = 1L;
 
   public static InternalGroup create(
       AccountGroup accountGroup,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java
index 64539d1..14cb09a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail.receive;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.PeekingIterator;
 import com.google.gerrit.reviewdb.client.Comment;
@@ -28,6 +29,12 @@
 /** Provides functionality for parsing the HTML part of a {@link MailMessage}. */
 public class HtmlParser {
 
+  private static final ImmutableList<String> MAIL_PROVIDER_EXTRAS =
+      ImmutableList.of(
+          "gmail_extra", // "On 01/01/2017 User<user@gmail.com> wrote:"
+          "gmail_quote" // Used for quoting original content
+          );
+
   private HtmlParser() {}
 
   /**
@@ -96,7 +103,7 @@
         }
       } else if (!isInBlockQuote
           && elementName.equals("div")
-          && !e.className().startsWith("gmail")) {
+          && !MAIL_PROVIDER_EXTRAS.contains(e.className())) {
         // This is a comment typed by the user
         // Replace non-breaking spaces and trim string
         String content = e.ownText().replace('\u00a0', ' ').trim();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index 30cc3d3..73e82a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -71,12 +71,6 @@
     return new PatchListKey(otherCommitId, newId, whitespace, Algorithm.OPTIMIZED_DIFF);
   }
 
-  // Please keep this method for the moment even though it is unused.
-  public static PatchListKey againstCommitWithPureTreeDiff(
-      AnyObjectId otherCommitId, AnyObjectId newId, Whitespace whitespace) {
-    return new PatchListKey(otherCommitId, newId, whitespace, Algorithm.PURE_TREE_DIFF);
-  }
-
   /**
    * Old patch-set ID
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java
index 983d3b3..d02f6a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java
@@ -24,6 +24,10 @@
 import java.util.Locale;
 
 public class GroupPredicates {
+  public static Predicate<InternalGroup> id(AccountGroup.Id groupId) {
+    return new GroupPredicate(GroupField.ID, groupId.toString());
+  }
+
   public static Predicate<InternalGroup> uuid(AccountGroup.UUID uuid) {
     return new GroupPredicate(GroupField.UUID, GroupQueryBuilder.FIELD_UUID, uuid.get());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/InternalGroupQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
new file mode 100644
index 0000000..7a3a905
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/InternalGroupQuery.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.group;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.query.InternalQuery;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.index.group.GroupIndexCollection;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import java.util.List;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Query wrapper for the group index.
+ *
+ * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
+ * holding on to a single instance.
+ */
+public class InternalGroupQuery extends InternalQuery<InternalGroup> {
+  private static final Logger log = LoggerFactory.getLogger(InternalGroupQuery.class);
+
+  @Inject
+  InternalGroupQuery(
+      GroupQueryProcessor queryProcessor, GroupIndexCollection indexes, IndexConfig indexConfig) {
+    super(queryProcessor, indexes, indexConfig);
+  }
+
+  public Optional<InternalGroup> byName(AccountGroup.NameKey groupName) throws OrmException {
+    return getOnlyGroup(GroupPredicates.name(groupName.get()), "group name '" + groupName + "'");
+  }
+
+  public Optional<InternalGroup> byId(AccountGroup.Id groupId) throws OrmException {
+    return getOnlyGroup(GroupPredicates.id(groupId), "group id '" + groupId + "'");
+  }
+
+  public List<InternalGroup> byMember(Account.Id memberId) throws OrmException {
+    return query(GroupPredicates.member(memberId));
+  }
+
+  public List<InternalGroup> bySubgroup(AccountGroup.UUID subgroupId) throws OrmException {
+    return query(GroupPredicates.subgroup(subgroupId));
+  }
+
+  private Optional<InternalGroup> getOnlyGroup(
+      Predicate<InternalGroup> predicate, String groupDescription) throws OrmException {
+    List<InternalGroup> groups = query(predicate);
+    if (groups.isEmpty()) {
+      return Optional.empty();
+    }
+
+    if (groups.size() == 1) {
+      return Optional.of(Iterables.getOnlyElement(groups));
+    }
+
+    ImmutableList<AccountGroup.UUID> groupUuids =
+        groups.stream().map(InternalGroup::getGroupUUID).collect(toImmutableList());
+    log.warn(String.format("Ambiguous %s for groups %s.", groupDescription, groupUuids));
+    return Optional.empty();
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/GmailHtmlParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/GmailHtmlParserTest.java
index c0e8050..e210847 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/GmailHtmlParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/GmailHtmlParserTest.java
@@ -20,7 +20,7 @@
       String changeMessage, String c1, String c2, String c3, String f1, String f2, String fc1) {
     String email =
         ""
-            + "<div dir=\"ltr\">"
+            + "<div class=\"gmail_default\" dir=\"ltr\">"
             + (changeMessage != null ? changeMessage : "")
             + "<div class=\"gmail_extra\"><br><div class=\"gmail_quote\">"
             + "On Fri, Nov 18, 2016 at 11:15 AM, foobar (Gerrit) noreply@gerrit.com"
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/CommitsCollectionTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/CommitsCollectionTest.java
index 0d8080f..7f1b233 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/CommitsCollectionTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/CommitsCollectionTest.java
@@ -90,7 +90,7 @@
     // registered user.
     // See AccountManager#create().
     accountManager.authenticate(AuthRequest.forUser("admin")).getAccountId();
-    admins = groupCache.get(new AccountGroup.NameKey("Administrators")).getGroupUUID();
+    admins = groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID();
     setUpPermissions();
 
     Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
index 1c903c7..ffaf923 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.account.AccountLoader;
 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.ListMembers;
 import com.google.gerrit.server.ioutil.ColumnFormatter;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -31,6 +32,7 @@
 import com.google.inject.Inject;
 import java.io.PrintWriter;
 import java.util.List;
+import java.util.Optional;
 import org.kohsuke.args4j.Argument;
 
 /** Implements a command that allows the user to see the members of a group. */
@@ -68,16 +70,16 @@
     }
 
     void display(PrintWriter writer) throws OrmException {
-      AccountGroup group = groupCache.get(new AccountGroup.NameKey(name));
+      Optional<InternalGroup> group = groupCache.get(new AccountGroup.NameKey(name));
       String errorText = "Group not found or not visible\n";
 
-      if (group == null) {
+      if (!group.isPresent()) {
         writer.write(errorText);
         writer.flush();
         return;
       }
 
-      List<AccountInfo> members = apply(group.getGroupUUID());
+      List<AccountInfo> members = apply(group.get().getGroupUUID());
       ColumnFormatter formatter = new ColumnFormatter(writer, '\t');
       formatter.addColumn("id");
       formatter.addColumn("username");
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index 3ac2351..7657994 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -23,6 +23,11 @@
     version = "3.5.0",
     sha1 = "849ad3ee7c77506548b7b5db603a4e150b9431aa")
   bower_archive(
+    name = "font-roboto",
+    package = "PolymerElements/font-roboto",
+    version = "1.0.3",
+    sha1 = "edf478d20ae2fc0704d7c155e20162caaabdd5ae")
+  bower_archive(
     name = "iron-a11y-announcer",
     package = "PolymerElements/iron-a11y-announcer",
     version = "1.0.6",
@@ -45,8 +50,13 @@
   bower_archive(
     name = "iron-flex-layout",
     package = "PolymerElements/iron-flex-layout",
-    version = "2.0.0",
-    sha1 = "feae42cc5d2d948a50074f430cfb8ab28cb6dc9e")
+    version = "1.3.7",
+    sha1 = "4d4cf3232cf750a17a7df0a37476117f831ac633")
+  bower_archive(
+    name = "iron-menu-behavior",
+    package = "PolymerElements/iron-menu-behavior",
+    version = "2.0.1",
+    sha1 = "139528ee1e8d86257e2aa445de7761b8ec70ae91")
   bower_archive(
     name = "iron-meta",
     package = "PolymerElements/iron-meta",
@@ -54,7 +64,7 @@
     sha1 = "f77eba3f6f6817f10bda33918bde8f963d450041")
   bower_archive(
     name = "iron-resizable-behavior",
-    package = "PolymerElements/iron-resizable-behavior",
+    package = "polymerelements/iron-resizable-behavior",
     version = "1.0.6",
     sha1 = "719c2a8a1a784f8aefcdeef41fcc2e5a03518d9e")
   bower_archive(
@@ -70,14 +80,19 @@
   bower_archive(
     name = "mocha",
     package = "mocha",
-    version = "3.5.0",
-    sha1 = "09aa92f4f89949ed5f501a57f082e96510b18318")
+    version = "3.5.3",
+    sha1 = "c14f149821e4e96241b20f85134aa757b73038f1")
   bower_archive(
     name = "neon-animation",
     package = "polymerelements/neon-animation",
     version = "1.2.5",
     sha1 = "588d289f779d02b21ce5b676e257bbd6155649e8")
   bower_archive(
+    name = "paper-styles",
+    package = "PolymerElements/paper-styles",
+    version = "1.3.1",
+    sha1 = "4ee9c692366949a754e0e39f8031aa60ce66f24d")
+  bower_archive(
     name = "sinon-chai",
     package = "sinon-chai",
     version = "2.13.0",
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
index e36a759..906d6e3 100644
--- a/lib/js/bower_components.bzl
+++ b/lib/js/bower_components.bzl
@@ -30,6 +30,10 @@
     seed = True,
   )
   bower_component(
+    name = "font-roboto",
+    license = "//lib:LICENSE-polymer",
+  )
+  bower_component(
     name = "iron-a11y-announcer",
     license = "//lib:LICENSE-polymer",
     deps = [ ":polymer" ],
@@ -62,7 +66,6 @@
     name = "iron-dropdown",
     license = "//lib:LICENSE-polymer",
     deps = [
-      ":iron-a11y-keys-behavior",
       ":iron-behaviors",
       ":iron-overlay-behavior",
       ":iron-resizable-behavior",
@@ -92,6 +95,16 @@
     seed = True,
   )
   bower_component(
+    name = "iron-menu-behavior",
+    license = "//lib:LICENSE-polymer",
+    deps = [
+      ":iron-a11y-keys-behavior",
+      ":iron-flex-layout",
+      ":iron-selector",
+      ":polymer",
+    ],
+  )
+  bower_component(
     name = "iron-meta",
     license = "//lib:LICENSE-polymer",
     deps = [ ":polymer" ],
@@ -162,6 +175,37 @@
     seed = True,
   )
   bower_component(
+    name = "paper-item",
+    license = "//lib:LICENSE-polymer",
+    deps = [
+      ":iron-behaviors",
+      ":iron-flex-layout",
+      ":paper-styles",
+      ":polymer",
+    ],
+    seed = True,
+  )
+  bower_component(
+    name = "paper-listbox",
+    license = "//lib:LICENSE-polymer",
+    deps = [
+      ":iron-behaviors",
+      ":iron-menu-behavior",
+      ":paper-styles",
+      ":polymer",
+    ],
+    seed = True,
+  )
+  bower_component(
+    name = "paper-styles",
+    license = "//lib:LICENSE-polymer",
+    deps = [
+      ":font-roboto",
+      ":iron-flex-layout",
+      ":polymer",
+    ],
+  )
+  bower_component(
     name = "polymer-resin",
     license = "//lib:LICENSE-polymer",
     deps = [
diff --git a/plugins/hooks b/plugins/hooks
index 18f8c78..a96c0b9 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 18f8c78aad50509d39b489286b64d869752cccd6
+Subproject commit a96c0b937e412a44b00a7574fe0f7c5f010aabf5
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 31ab6aa..3e490eb 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -20,6 +20,8 @@
         "//lib/js:iron-selector",
         "//lib/js:moment",
         "//lib/js:page",
+        "//lib/js:paper-item",
+        "//lib/js:paper-listbox",
         "//lib/js:polymer",
         "//lib/js:polymer-resin",
         "//lib/js:promise-polyfill",
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 24673ff..161dfe7 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -266,7 +266,7 @@
       <section>
         <gr-button
             primary
-            disabled="[[_computeSendButtonDisabled(knownLatestState, _sendButtonLabel, diffDrafts, draft, _reviewersMutated, _labelsChanged)]]"
+            disabled="[[_computeSendButtonDisabled(knownLatestState, _sendButtonLabel, diffDrafts, draft, _reviewersMutated, _labelsChanged, _includeComments)]]"
             class="action send"
             on-tap="_sendTapHandler">[[_sendButtonLabel]]</gr-button>
         <template is="dom-if" if="[[canBeStarted]]">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 9f4a79a..5168e8f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -810,15 +810,15 @@
     },
 
     _computeSendButtonDisabled(knownLatestState, buttonLabel, drafts, text,
-        reviewersMutated, labelsChanged) {
+        reviewersMutated, labelsChanged, includeComments) {
       if (this._isState(knownLatestState, LatestPatchState.NOT_LATEST)) {
         return true;
       }
       if (buttonLabel === ButtonLabels.START_REVIEW) {
         return false;
       }
-      return !(drafts.length || text.length || reviewersMutated ||
-          labelsChanged);
+      const hasDrafts = includeComments && Object.keys(drafts).length;
+      return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged;
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 9a83259..278f2c6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -1014,15 +1014,19 @@
       const fn = element._computeSendButtonDisabled.bind(element);
       assert.isTrue(fn('not-latest'));
       assert.isFalse(fn('latest', 'Start review'));
-      assert.isTrue(fn('latest', 'Send', [], '', false, false));
-      // Mock nonempty comment draft array.
-      assert.isFalse(fn('latest', 'Send', ['test'], '', false, false));
+      assert.isTrue(fn('latest', 'Send', {}, '', false, false, false));
+      // Mock nonempty comment draft array, with seding comments.
+      assert.isFalse(fn('latest', 'Send', {file: ['draft']}, '', false, false,
+          true));
+      // Mock nonempty comment draft array, without seding comments.
+      assert.isTrue(fn('latest', 'Send', {file: ['draft']}, '', false, false,
+          false));
       // Mock nonempty change message.
-      assert.isFalse(fn('latest', 'Send', [], 'test', false, false));
+      assert.isFalse(fn('latest', 'Send', {}, 'test', false, false, false));
       // Mock reviewers mutated.
-      assert.isFalse(fn('latest', 'Send', [], '', true, false));
+      assert.isFalse(fn('latest', 'Send', {}, '', true, false, false));
       // Mock labels changed.
-      assert.isFalse(fn('latest', 'Send', [], '', false, true));
+      assert.isFalse(fn('latest', 'Send', {}, '', false, true, false));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index 72f557e..c29d494 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -58,8 +58,10 @@
   suite('gr-diff-builder tests', () => {
     let element;
     let builder;
+    let sandbox;
 
     setup(() => {
+      sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
         getLoggedIn() { return Promise.resolve(false); },
         getProjectConfig() { return Promise.resolve({}); },
@@ -74,6 +76,8 @@
           {content: []}, {left: [], right: []}, prefs, projectName);
     });
 
+    teardown(() => { sandbox.restore(); });
+
     test('context control buttons', () => {
       const section = {};
       const line = {contextGroup: {lines: []}};
@@ -141,11 +145,10 @@
       const text = (new Array(52)).join('a');
 
       const line = {text, highlights: []};
-      const newLineStub = sinon.stub(builder, '_addNewlines');
+      const newLineStub = sandbox.stub(builder, '_addNewlines');
       builder._createTextEl(line);
       flush(() => {
         assert.isFalse(newLineStub.called);
-        newLineStub.restore();
         done();
       });
     });
@@ -156,12 +159,11 @@
       const text = (new Array(52)).join('a');
 
       const line = {text, highlights: []};
-      const newLineStub = sinon.stub(builder, '_addNewlines');
+      const newLineStub = sandbox.stub(builder, '_addNewlines');
       builder._createTextEl(line);
 
       flush(() => {
         assert.isTrue(newLineStub.called);
-        newLineStub.restore();
         done();
       });
     });
@@ -358,15 +360,11 @@
       setup(() => {
         el = fixture('div-with-text');
         str = el.textContent;
-        annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
+        annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
         layer = document.createElement('gr-diff-builder')
             ._createIntralineLayer();
       });
 
-      teardown(() => {
-        annotateElementSpy.restore();
-      });
-
       test('annotate no highlights', () => {
         const line = {
           text: str,
@@ -513,21 +511,15 @@
     });
 
     suite('tab indicators', () => {
-      let sandbox;
       let element;
       let layer;
 
       setup(() => {
-        sandbox = sinon.sandbox.create();
         element = fixture('basic');
         element._showTabs = true;
         layer = element._createTabIndicatorLayer();
       });
 
-      teardown(() => {
-        sandbox.restore();
-      });
-
       test('does nothing with empty line', () => {
         const line = {text: ''};
         const el = document.createElement('div');
@@ -630,21 +622,15 @@
     });
 
     suite('trailing whitespace', () => {
-      let sandbox;
       let element;
       let layer;
 
       setup(() => {
-        sandbox = sinon.sandbox.create();
         element = fixture('basic');
         element._showTrailingWhitespace = true;
         layer = element._createTrailingWhitespaceLayer();
       });
 
-      teardown(() => {
-        sandbox.restore();
-      });
-
       test('does nothing with empty line', () => {
         const line = {text: ''};
         const el = document.createElement('div');
@@ -733,10 +719,8 @@
     suite('rendering', () => {
       let content;
       let outputEl;
-      let sandbox;
 
       setup(done => {
-        sandbox = sinon.sandbox.create();
         const prefs = {
           line_length: 10,
           show_tabs: true,
@@ -779,10 +763,6 @@
         element.render({left: [], right: []}, prefs).then(done);
       });
 
-      teardown(() => {
-        sandbox.restore();
-      });
-
       test('reporting', done => {
         const timeStub = element.$.reporting.time;
         const timeEndStub = element.$.reporting.timeEnd;
@@ -828,7 +808,7 @@
       });
 
       test('render-start and render are fired', done => {
-        const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
+        const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
         element.render({left: [], right: []}, {}).then(() => {
           const firedEventTypes = dispatchEventStub.getCalls()
               .map(c => { return c.args[0].type; });
@@ -929,7 +909,7 @@
       });
 
       test('_renderContentByRange', () => {
-        const spy = sinon.spy(builder, '_createTextEl');
+        const spy = sandbox.spy(builder, '_createTextEl');
         const start = 9;
         const end = 14;
         const count = end - start + 1;
@@ -940,8 +920,6 @@
         spy.getCalls().forEach((call, i) => {
           assert.equal(call.args[0].beforeNumber, start + i);
         });
-
-        spy.restore();
       });
 
       test('_getNextContentOnSide side-by-side left', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index ab91fc5..21552d9 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -20,6 +20,7 @@
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <script src="../../../bower_components/moment/moment.js"></script>
+<script src="../../../scripts/util.js"></script>
 
 <dom-module id="gr-date-formatter">
   <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
new file mode 100644
index 0000000..59f63fa
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -0,0 +1,174 @@
+<!--
+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="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="../../../bower_components/paper-item/paper-item.html">
+<link rel="import" href="../../../bower_components/paper-listbox/paper-listbox.html">
+
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-select/gr-select.html">
+
+
+<dom-module id="gr-dropdown-list">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: inline-block;
+      }
+      #trigger {
+        -moz-user-select: text;
+        -ms-user-select: text;
+        -webkit-user-select: text;
+        user-select: text;
+      }
+      .downArrow {
+        display: inline-block;
+        font-size: .6em;
+        user-select: none;
+        vertical-align: middle;
+      }
+      .dropdown-trigger {
+        cursor: pointer;
+        padding: 0;
+      }
+      .dropdown-content {
+        background-color: #fff;
+        box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
+        max-height: 70vh;
+        margin-top: 1.5em;
+        width: 266px;
+      }
+      paper-listbox {
+        --paper-listbox: {
+          padding: 0;
+        }
+      }
+      paper-item {
+        cursor: pointer;
+        flex-direction: column;
+        --paper-item: {
+          min-height: 0;
+          padding: 10px 16px;
+        }
+        --paper-item-selected: {
+          background-color: rgba(161,194,250,.12);
+        }
+        --paper-item-focused-before: {
+          background-color: #f2f2f2;
+        }
+        --paper-item-focused: {
+          background-color: #f2f2f2;
+        }
+      }
+      paper-item:not(:last-of-type) {
+        border-bottom: 1px solid #ddd;
+      }
+      gr-button {
+        color: black;
+        font: inherit;
+        padding: .3em 0;
+        text-decoration: none;
+      }
+      .bottomContent {
+        color: rgba(0,0,0,.54);
+        font-size: .85em;
+        line-height: 16px;
+      }
+      .bottomContent,
+      .topContent {
+        display: flex;
+        line-height: 16px;
+        justify-content: space-between;
+        flex-direction: row;
+        width: 100%;
+      }
+      gr-date-formatter {
+        color: rgba(0,0,0,.54);
+        margin-left: 2em;
+      }
+      gr-select {
+        display: none;
+      }
+      @media only screen and (max-width: 50em) {
+        gr-select {
+          display: inline;
+        }
+        gr-button,
+        iron-dropdown {
+          display: none;
+        }
+        select {
+          max-width: 5.25em;
+        }
+      }
+    </style>
+    <gr-button
+        link
+        id="trigger"
+        class="dropdown-trigger"
+        on-tap="_showDropdownTapHandler">
+      <span>[[text]]</span>
+      <span
+          class="downArrow"
+          on-tap="_showDropdownTapHandler">&#9660;</span>
+    </gr-button>
+    <iron-dropdown
+        id="dropdown"
+        vertical-align="top"
+        allow-outside-scroll="true">
+      <paper-listbox
+          class="dropdown-content"
+          slot="dropdown-content"
+          attr-for-selected="value"
+          on-tap="_handleDropdownTap"
+          selected="{{value}}">
+        <template is="dom-repeat" items="[[items]]">
+            <paper-item
+                disabled="[[item.disabled]]"
+                value="[[item.value]]">
+              <div class="topContent">
+                <div>[[item.text]]</div>
+                <template is="dom-if" if="[[item.date]]">
+                    <gr-date-formatter
+                        date-str="[[item.date]]"></gr-date-formatter>
+                </template>
+              </div>
+              <template is="dom-if" if="[[item.bottomText]]">
+                <div class="bottomContent">
+                  <div>[[item.bottomText]]</div>
+                </div>
+              </template>
+          </paper-item>
+          </template>
+      </paper-listbox>
+    </iron-dropdown>
+    <gr-select bind-value="{{value}}">
+      <select>
+        <template is="dom-repeat" items="[[items]]">
+          <option
+              disabled$="[[item.disabled]]"
+              value="[[item.value]]">
+            [[_computeMobileText(item)]]
+          </option>
+        </template>
+      </select>
+    </gr-select>
+  </template>
+  <script src="gr-dropdown-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
new file mode 100644
index 0000000..27c6ba8
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -0,0 +1,102 @@
+// 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';
+
+  /**
+   * fired when the selected value of the dropdown changes
+   *
+   * @event {change}
+   */
+
+  const Defs = {};
+
+  /**
+   * Requred values are text and value. mobileText and triggerText will
+   * fall back to text if not provided.
+   *
+   * If bottomText is not provided, nothing will display on the second
+   * line.
+   *
+   * If date is not provided, nothing will be displayed in its place.
+   *
+   * @typedef {{
+   *    text: string,
+   *    value: (string|number),
+   *    bottomText: (string|undefined),
+   *    triggerText: (string|undefined),
+   *    mobileText: (string|undefined),
+   *    date: (!Date|undefined),
+   * }}
+   */
+  Defs.item;
+
+  Polymer({
+    is: 'gr-dropdown-list',
+
+    properties: {
+      /** @type {!Array<!Defs.item>} */
+      items: Object,
+      text: String,
+      value: {
+        type: String,
+        notify: true,
+      },
+    },
+
+    observers: [
+      '_handleValueChange(value, items)',
+    ],
+
+    /**
+     * Handle a click on the iron-dropdown element.
+     * @param {!Event} e
+     */
+    _handleDropdownTap(e) {
+      // async is needed so that that the click event is fired before the
+      // dropdown closes (This was a bug for touch devices).
+      this.async(() => {
+        this.$.dropdown.close();
+      }, 1);
+    },
+
+    /**
+     * Handle a click on the button to open the dropdown.
+     * @param {!Event} e
+     */
+    _showDropdownTapHandler(e) {
+      this._open();
+    },
+
+    /**
+     * Open the dropdown.
+     */
+    _open() {
+      this.$.dropdown.open();
+    },
+
+    _computeMobileText(item) {
+      return item.mobileText ? item.mobileText : item.text;
+    },
+
+    _handleValueChange(value, items) {
+      if (!value) { return; }
+      const selectedObj = items.find(item => {
+        return item.value + '' === value + '';
+      });
+      this.text = selectedObj.triggerText? selectedObj.triggerText :
+          selectedObj.text;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
new file mode 100644
index 0000000..d3c6d83
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
@@ -0,0 +1,167 @@
+<!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-dropdown-list</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-dropdown-list.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-dropdown-list></gr-dropdown-list>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-dropdown-list tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        getConfig() { return Promise.resolve({}); },
+      });
+      element = fixture('basic');
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('tap on trigger opens menu', () => {
+      sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
+      assert.isFalse(element.$.dropdown.opened);
+      MockInteractions.tap(element.$.trigger);
+      assert.isTrue(element.$.dropdown.opened);
+    });
+
+    test('_computeMobileText', () => {
+      const item = {
+        value: 1,
+        text: 'text',
+      };
+      assert.equal(element._computeMobileText(item), item.text);
+      item.mobileText = 'mobile text';
+      assert.equal(element._computeMobileText(item), item.mobileText);
+    });
+
+    test('options are selected and laid out correctly', () => {
+      element.value = 2;
+      element.items = [
+        {
+          value: 1,
+          text: 'Top Text 1',
+        },
+        {
+          value: 2,
+          bottomText: 'Bottom Text 2',
+          triggerText: 'Button Text 2',
+          text: 'Top Text 2',
+          mobileText: 'Mobile Text 2',
+        },
+        {
+          value: 3,
+          disabled: true,
+          bottomText: 'Bottom Text 3',
+          triggerText: 'Button Text 3',
+          date: '2017-08-18 23:11:42.569000000',
+          text: 'Top Text 3',
+          mobileText: 'Mobile Text 3',
+        },
+      ];
+      assert.equal(element.$$('paper-listbox').selected, element.value);
+      assert.equal(element.text, 'Button Text 2');
+      flushAsynchronousOperations();
+      const items = Polymer.dom(element.root).querySelectorAll('paper-item');
+      const mobileItems = Polymer.dom(element.root).querySelectorAll('option');
+      assert.equal(items.length, 3);
+      assert.equal(mobileItems.length, 3);
+
+      // First Item
+      // The first item should be disabled, has no bottom text, and no date.
+      assert.isFalse(!!items[0].disabled);
+      assert.isFalse(mobileItems[0].disabled);
+      assert.isFalse(items[0].classList.contains('iron-selected'));
+      assert.isFalse(mobileItems[0].selected);
+
+      assert.isNotOk(Polymer.dom(items[0]).querySelector('gr-date-formatter'));
+      assert.isNotOk(Polymer.dom(items[0]).querySelector('.bottomContent'));
+      assert.equal(items[0].value, element.items[0].value);
+      assert.equal(mobileItems[0].value, element.items[0].value);
+      assert.equal(Polymer.dom(items[0]).querySelector('.topContent div')
+          .innerText, element.items[0].text);
+
+      // Since no mobile specific text, it should fall back to text.
+      assert.equal(mobileItems[0].text, element.items[0].text);
+
+
+      // Second Item
+      // The second item should have top text, bottom text, and no date.
+      assert.isFalse(!!items[1].disabled);
+      assert.isFalse(mobileItems[1].disabled);
+      assert.isTrue(items[1].classList.contains('iron-selected'));
+      assert.isTrue(mobileItems[1].selected);
+
+      assert.isNotOk(Polymer.dom(items[1]).querySelector('gr-date-formatter'));
+      assert.isOk(Polymer.dom(items[1]).querySelector('.bottomContent'));
+      assert.equal(items[1].value, element.items[1].value);
+      assert.equal(mobileItems[1].value, element.items[1].value);
+      assert.equal(Polymer.dom(items[1]).querySelector('.topContent div')
+          .innerText, element.items[1].text);
+
+      // Since there is mobile specific text, it should that.
+      assert.equal(mobileItems[1].text, element.items[1].mobileText);
+
+      // Since this item is selected, and it has triggerText defined, that
+      // should be used.
+      assert.equal(element.text, element.items[1].triggerText);
+
+      // Third item
+      // The third item should be disabled, and have a date, and bottom content.
+      assert.isTrue(!!items[2].disabled);
+      assert.isTrue(mobileItems[2].disabled);
+      assert.isFalse(items[2].classList.contains('iron-selected'));
+      assert.isFalse(mobileItems[2].selected);
+
+      assert.isOk(Polymer.dom(items[2]).querySelector('gr-date-formatter'));
+      assert.isOk(Polymer.dom(items[2]).querySelector('.bottomContent'));
+      assert.equal(items[2].value, element.items[2].value);
+      assert.equal(mobileItems[2].value, element.items[2].value);
+      assert.equal(Polymer.dom(items[2]).querySelector('.topContent div')
+          .innerText, element.items[2].text);
+
+      // Since there is mobile specific text, it should that.
+      assert.equal(mobileItems[2].text, element.items[2].mobileText);
+
+      // Select a new item.
+      MockInteractions.tap(items[0]);
+      flushAsynchronousOperations();
+      assert.equal(element.value, 1);
+      assert.isTrue(items[0].classList.contains('iron-selected'));
+      assert.isTrue(mobileItems[0].selected);
+
+      // Since no triggerText, the fallback is used.
+      assert.equal(element.text, element.items[0].text);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 3f2879f..7c8b6ff 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -130,6 +130,7 @@
     'shared/gr-cursor-manager/gr-cursor-manager_test.html',
     'shared/gr-date-formatter/gr-date-formatter_test.html',
     'shared/gr-download-commands/gr-download-commands_test.html',
+    'shared/gr-dropdown-list/gr-dropdown-list_test.html',
     'shared/gr-editable-content/gr-editable-content_test.html',
     'shared/gr-editable-label/gr-editable-label_test.html',
     'shared/gr-formatted-text/gr-formatted-text_test.html',
diff --git a/tools/checkstyle.xml b/tools/checkstyle.xml
deleted file mode 100644
index cb24e8f..0000000
--- a/tools/checkstyle.xml
+++ /dev/null
@@ -1,114 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
-
-<!--
-    This configuration file was written by the eclipse-cs plugin configuration editor
--->
-<!--
-    Checkstyle-Configuration: Google Checks for Gerrit
-    Description:
-Checkstyle configuration based on the Google coding conventions (https://google-styleguide.googlecode.com/svn-history/r130/trunk/javaguide.html),
-edited to remove noisy warnings.
--->
-<module name="Checker">
-  <property name="severity" value="warning"/>
-  <property name="charset" value="UTF-8"/>
-  <module name="TreeWalker">
-    <module name="FileContentsHolder"/>
-    <module name="OuterTypeFilename"/>
-    <module name="LineLength">
-      <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
-      <property name="max" value="150"/>
-      <property name="tabWidth" value="2"/>
-    </module>
-    <module name="OneTopLevelClass"/>
-    <module name="NoLineWrap"/>
-    <module name="EmptyBlock">
-      <property name="option" value="TEXT"/>
-      <property name="tokens" value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
-    </module>
-    <module name="NeedBraces"/>
-    <module name="LeftCurly">
-      <property name="maxLineLength" value="150"/>
-    </module>
-    <module name="RightCurly">
-      <property name="option" value="alone"/>
-      <property name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO, STATIC_INIT, INSTANCE_INIT"/>
-    </module>
-    <module name="WhitespaceAround">
-      <property name="severity" value="ignore"/>
-      <property name="allowEmptyConstructors" value="true"/>
-      <property name="allowEmptyMethods" value="true"/>
-      <property name="allowEmptyTypes" value="true"/>
-      <property name="allowEmptyLoops" value="true"/>
-      <message key="ws.notFollowed" value="WhitespaceAround: ''{0}'' is not followed by whitespace."/>
-      <message key="ws.notPreceded" value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
-      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
-    </module>
-    <module name="OneStatementPerLine"/>
-    <module name="MultipleVariableDeclarations"/>
-    <module name="ArrayTypeStyle"/>
-    <module name="UpperEll"/>
-    <module name="ModifierOrder"/>
-    <module name="EmptyLineSeparator">
-      <property name="severity" value="ignore"/>
-      <property name="allowNoEmptyLineBetweenFields" value="true"/>
-      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
-    </module>
-    <module name="SeparatorWrap">
-      <property name="severity" value="ignore"/>
-      <property name="option" value="nl"/>
-      <property name="tokens" value="DOT"/>
-      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
-    </module>
-    <module name="SeparatorWrap">
-      <property name="severity" value="ignore"/>
-      <property name="option" value="EOL"/>
-      <property name="tokens" value="COMMA"/>
-      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
-    </module>
-    <module name="NoFinalizer"/>
-    <module name="GenericWhitespace">
-      <property name="severity" value="ignore"/>
-      <message key="ws.followed" value="GenericWhitespace ''{0}'' is followed by whitespace."/>
-      <message key="ws.illegalFollow" value="GenericWhitespace ''{0}'' should followed by whitespace."/>
-      <message key="ws.preceded" value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
-      <message key="ws.notPreceded" value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
-      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
-    </module>
-    <module name="Indentation">
-      <property name="severity" value="ignore"/>
-      <property name="basicOffset" value="2"/>
-      <property name="caseIndent" value="2"/>
-      <property name="arrayInitIndent" value="2"/>
-      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
-    </module>
-    <module name="MethodParamPad">
-      <property name="severity" value="ignore"/>
-      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
-    </module>
-    <module name="OperatorWrap">
-      <property name="severity" value="ignore"/>
-      <property name="option" value="NL"/>
-      <property name="tokens" value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR "/>
-      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
-    </module>
-    <module name="RedundantImport"/>
-    <module name="RedundantModifier"/>
-    <module name="ExplicitInitialization"/>
-    <module name="ArrayTrailingComma"/>
-  </module>
-  <module name="FileTabCharacter">
-    <property name="severity" value="ignore"/>
-    <property name="eachLine" value="true"/>
-    <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
-  </module>
-  <module name="SuppressWithNearbyCommentFilter">
-    <property name="commentFormat" value="CS IGNORE (\w+) FOR NEXT (\d+) LINES\. REASON\: \w+"/>
-    <property name="checkFormat" value="$1"/>
-    <property name="influenceFormat" value="$2"/>
-  </module>
-  <module name="SuppressionFilter">
-    <property name="file" value="${samedir}/checkstyle_suppressions.xml"/>
-  </module>
-</module>
diff --git a/tools/checkstyle_suppressions.xml b/tools/checkstyle_suppressions.xml
deleted file mode 100644
index 5f5d9ee..0000000
--- a/tools/checkstyle_suppressions.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0"?>
-
-<!DOCTYPE suppressions PUBLIC
-  "-//Puppy Crawl//DTD Suppressions 1.1//EN"
-  "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
-<suppressions>
-  <suppress files="[/\\].apt_generated[/\\]" checks=".*"/>
-</suppressions>
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py
index 9e7895d..04d7c24 100755
--- a/tools/js/bower2bazel.py
+++ b/tools/js/bower2bazel.py
@@ -35,6 +35,7 @@
 package_licenses = {
   "es6-promise": "es6-promise",
   "fetch": "fetch",
+  "font-roboto": "polymer",
   "iron-a11y-announcer": "polymer",
   "iron-a11y-keys-behavior": "polymer",
   "iron-autogrow-textarea": "polymer",
@@ -44,6 +45,7 @@
   "iron-flex-layout": "polymer",
   "iron-form-element-behavior": "polymer",
   "iron-input": "polymer",
+  "iron-menu-behavior": "polymer",
   "iron-meta": "polymer",
   "iron-overlay-behavior": "polymer",
   "iron-resizable-behavior": "polymer",
@@ -52,6 +54,9 @@
   "moment": "moment",
   "neon-animation": "polymer",
   "page": "page.js",
+  "paper-item": "polymer",
+  "paper-listbox": "polymer",
+  "paper-styles": "polymer",
   "polymer": "polymer",
   "polymer-resin": "polymer",
   "promise-polyfill": "promise-polyfill",