Merge "Update upstream dependencies to match googlesource"
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/Documentation/user-search.txt b/Documentation/user-search.txt
index bbca364..d5b1e26 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -530,7 +530,7 @@
 
 A label name must be followed by either a score with optional operator,
 or a label status. The easiest way to explain this is by example.
-+
+
 First, some examples of scores with operators:
 
 `label:Code-Review=2`::
diff --git a/WORKSPACE b/WORKSPACE
index 3874578..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",
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 18e373f..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;
@@ -647,11 +653,11 @@
     return result;
   }
 
-  private boolean submittable(ChangeData cd) throws OrmException {
+  private boolean submittable(ChangeData cd) {
     return SubmitRecord.findOkRecord(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)).isPresent();
   }
 
-  private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException {
+  private List<SubmitRecord> submitRecords(ChangeData cd) {
     return cd.submitRecords(SUBMIT_RULE_OPTIONS_LENIENT);
   }
 
@@ -703,7 +709,7 @@
   }
 
   private Map<String, LabelWithStatus> initLabels(
-      ChangeData cd, LabelTypes labelTypes, boolean standard) throws OrmException {
+      ChangeData cd, LabelTypes labelTypes, boolean standard) {
     Map<String, LabelWithStatus> labels = new TreeMap<>(labelTypes.nameComparator());
     for (SubmitRecord rec : submitRecords(cd)) {
       if (rec.labels == null) {
@@ -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/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 79176e4..82fa596 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -566,7 +566,13 @@
     try (RevWalk revWalk = new RevWalk(repository)) {
       RefUpdate.Result res = ru.update(revWalk);
       if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) {
-        throw new IOException("update failed: " + ru);
+        throw new IOException(
+            "cannot update "
+                + ru.getName()
+                + " in "
+                + repository.getDirectory()
+                + ": "
+                + ru.getResult());
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 730bc10..73a3bc2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -327,8 +327,7 @@
     return allowClosed ? SUBMIT_RULE_OPTIONS_ALLOW_CLOSED : SUBMIT_RULE_OPTIONS;
   }
 
-  private static List<SubmitRecord> getSubmitRecords(ChangeData cd, boolean allowClosed)
-      throws OrmException {
+  private static List<SubmitRecord> getSubmitRecords(ChangeData cd, boolean allowClosed) {
     return cd.submitRecords(submitRuleOptions(allowClosed));
   }
 
@@ -396,13 +395,7 @@
     checkArgument(
         !cs.furtherHiddenChanges(), "cannot bypass submit rules for topic with hidden change");
     for (ChangeData cd : cs.changes()) {
-      List<SubmitRecord> records;
-      try {
-        records = new ArrayList<>(getSubmitRecords(cd, allowClosed));
-      } catch (OrmException e) {
-        log.warn("Error checking submit rules for change " + cd.getId(), e);
-        records = new ArrayList<>(1);
-      }
+      List<SubmitRecord> records = new ArrayList<>(getSubmitRecords(cd, allowClosed));
       SubmitRecord forced = new SubmitRecord();
       forced.status = SubmitRecord.Status.FORCED;
       records.add(forced);
@@ -836,13 +829,8 @@
   }
 
   private SubmitType getSubmitType(ChangeData cd) {
-    try {
-      SubmitTypeRecord str = cd.submitTypeRecord();
-      return str.isOk() ? str.type : null;
-    } catch (OrmException e) {
-      logError("Failed to get submit type for " + cd.getId(), e);
-      return null;
-    }
+    SubmitTypeRecord str = cd.submitTypeRecord();
+    return str.isOk() ? str.type : null;
   }
 
   private OpenRepo openRepo(Project.NameKey project) throws IntegrationException {
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 85e5d06..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
@@ -38,8 +38,9 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -105,6 +106,8 @@
   private final Map<QueryKey, List<ChangeData>> queryCache;
   private final Map<Branch.NameKey, Optional<RevCommit>> heads;
   private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final ProjectCache projectCache;
 
   private MergeOpRepoManager orm;
   private boolean closeOrm;
@@ -116,13 +119,17 @@
       Provider<InternalChangeQuery> queryProvider,
       Provider<MergeOpRepoManager> repoManagerProvider,
       PermissionBackend permissionBackend,
-      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
+      SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
+      ChangeControl.GenericFactory changeControlFactory,
+      ProjectCache projectCache) {
     this.cfg = cfg;
     this.changeDataFactory = changeDataFactory;
     this.queryProvider = queryProvider;
     this.repoManagerProvider = repoManagerProvider;
     this.permissionBackend = permissionBackend;
     this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
+    this.changeControlFactory = changeControlFactory;
+    this.projectCache = projectCache;
     queryCache = new HashMap<>();
     heads = new HashMap<>();
   }
@@ -138,7 +145,6 @@
       throws IOException, OrmException, PermissionBackendException {
     try {
       ChangeData cd = changeDataFactory.create(db, change.getProject(), change.getId());
-      cd.changeControl(user);
       ChangeSet cs =
           new ChangeSet(
               cd, permissionBackend.user(user).change(cd).database(db).test(ChangePermission.READ));
@@ -154,7 +160,8 @@
     }
   }
 
-  private SubmitType submitType(ChangeData cd, PatchSet ps, boolean visible) throws OrmException {
+  private SubmitType submitType(ChangeData cd, PatchSet ps, boolean visible)
+      throws OrmException, IOException {
     // Submit type prolog rules mean that the submit type can depend on the
     // submitting user and the content of the change.
     //
@@ -166,7 +173,7 @@
     // misleading (but still nonzero) count of the non visible changes that
     // would be submitted together with the visible ones.
     if (!visible) {
-      return cd.changeControl().getProject().getSubmitType();
+      return projectCache.checkedGet(cd.project()).getProject().getSubmitType();
     }
 
     SubmitTypeRecord str =
@@ -228,11 +235,6 @@
       List<RevCommit> visibleCommits = new ArrayList<>();
       List<RevCommit> nonVisibleCommits = new ArrayList<>();
       for (ChangeData cd : bc.get(b)) {
-        checkState(
-            cd.hasChangeControl(),
-            "completeChangeSet forgot to set changeControl for current user"
-                + " at ChangeData creation time");
-
         boolean visible = changes.ids().contains(cd.getId());
         if (visible && !canRead(db, user, cd)) {
           // We thought the change was visible, but it isn't.
@@ -240,15 +242,15 @@
           // 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
         // patch set.
         PatchSet ps = cd.currentPatchSet();
         boolean visiblePatchSet = visible;
-        if (!cd.changeControl().isPatchVisible(ps, cd)) {
-          Iterable<PatchSet> visiblePatchSets = cd.visiblePatchSets();
+        ChangeControl ctl = changeControlFactory.controlFor(cd.notes(), user);
+        if (!ctl.isPatchVisible(ps, cd)) {
+          Iterable<PatchSet> visiblePatchSets = ctl.getVisiblePatchSets(cd.patchSets(), db);
           if (Iterables.isEmpty(visiblePatchSets)) {
             visiblePatchSet = false;
           } else {
@@ -273,21 +275,19 @@
         // 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, user, b, visibleHashes);
-      for (ChangeData chd : cds) {
-        chd.changeControl(user);
-        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, user, b, nonVisibleHashes));
+      Iterables.addAll(nonVisibleChanges, byCommitsOnBranchNotMerged(or, db, b, nonVisibleHashes));
     }
 
     return new ChangeSet(visibleChanges, nonVisibleChanges);
@@ -320,7 +320,7 @@
   }
 
   private List<ChangeData> byCommitsOnBranchNotMerged(
-      OpenRepo or, ReviewDb db, CurrentUser user, Branch.NameKey branch, Set<String> hashes)
+      OpenRepo or, ReviewDb db, Branch.NameKey branch, Set<String> hashes)
       throws OrmException, IOException {
     if (hashes.isEmpty()) {
       return ImmutableList.of();
@@ -335,7 +335,6 @@
     Iterable<ChangeData> destChanges =
         query().byCommitsOnBranchNotMerged(or.repo, db, branch, hashes);
     for (ChangeData chd : destChanges) {
-      chd.changeControl(user);
       result.add(chd);
     }
     queryCache.put(k, result);
@@ -371,19 +370,10 @@
         continue;
       }
       for (ChangeData topicCd : query().byTopicOpen(topic)) {
-        try {
-          topicCd.changeControl(user);
-          if (canRead(db, user, topicCd)) {
-            visibleChanges.add(topicCd);
-          } else {
-            nonVisibleChanges.add(topicCd);
-          }
-        } catch (OrmException e) {
-          if (e.getCause() instanceof NoSuchChangeException) {
-            // Ignore and skip this change
-          } else {
-            throw e;
-          }
+        if (canRead(db, user, topicCd)) {
+          visibleChanges.add(topicCd);
+        } else {
+          nonVisibleChanges.add(topicCd);
         }
       }
       topicsSeen.add(topic);
@@ -396,7 +386,6 @@
         continue;
       }
       for (ChangeData topicCd : query().byTopicOpen(topic)) {
-        topicCd.changeControl(user);
         nonVisibleChanges.add(topicCd);
       }
       topicsSeen.add(topic);
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 804d3e2..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;
@@ -175,17 +176,16 @@
   private AccountGroup createGroup(CreateGroupArgs createGroupArgs)
       throws OrmException, ResourceConflictException, IOException {
 
-    // Do not allow creating groups with the same name as system groups
+    String nameLower = createGroupArgs.getGroupName().toLowerCase(Locale.US);
+
     for (String name : systemGroupBackend.getNames()) {
-      if (name.toLowerCase(Locale.US)
-          .equals(createGroupArgs.getGroupName().toLowerCase(Locale.US))) {
+      if (name.toLowerCase(Locale.US).equals(nameLower)) {
         throw new ResourceConflictException("group '" + name + "' already exists");
       }
     }
 
     for (String name : systemGroupBackend.getReservedNames()) {
-      if (name.toLowerCase(Locale.US)
-          .equals(createGroupArgs.getGroupName().toLowerCase(Locale.US))) {
+      if (name.toLowerCase(Locale.US).equals(nameLower)) {
         throw new ResourceConflictException("group name '" + name + "' is reserved");
       }
     }
@@ -199,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/group/SystemGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
index 56c0208..f0421a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -84,7 +84,8 @@
   }
 
   private final ImmutableSet<String> reservedNames;
-  private final SortedMap<String, GroupReference> names;
+  private final SortedMap<String, GroupReference> namesToGroups;
+  private final ImmutableSet<String> names;
   private final ImmutableMap<AccountGroup.UUID, GroupReference> uuids;
 
   @Inject
@@ -105,7 +106,9 @@
       u.put(ref.getUUID(), ref);
     }
     reservedNames = reservedNamesBuilder.build();
-    names = Collections.unmodifiableSortedMap(n);
+    namesToGroups = Collections.unmodifiableSortedMap(n);
+    names =
+        ImmutableSet.copyOf(namesToGroups.values().stream().map(r -> r.getName()).collect(toSet()));
     uuids = u.build();
   }
 
@@ -114,7 +117,7 @@
   }
 
   public Set<String> getNames() {
-    return names.values().stream().map(r -> r.getName()).collect(toSet());
+    return names;
   }
 
   public Set<String> getReservedNames() {
@@ -158,7 +161,7 @@
   @Override
   public Collection<GroupReference> suggest(String name, ProjectState project) {
     String nameLC = name.toLowerCase(Locale.US);
-    SortedMap<String, GroupReference> matches = names.tailMap(nameLC);
+    SortedMap<String, GroupReference> matches = namesToGroups.tailMap(nameLC);
     if (matches.isEmpty()) {
       return Collections.emptyList();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index a1c7f14..db71ef5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -658,8 +658,7 @@
     return Lists.transform(records, r -> GSON.toJson(new StoredSubmitRecord(r)).getBytes(UTF_8));
   }
 
-  private static Iterable<byte[]> storedSubmitRecords(ChangeData cd, SubmitRuleOptions opts)
-      throws OrmException {
+  private static Iterable<byte[]> storedSubmitRecords(ChangeData cd, SubmitRuleOptions opts) {
     return storedSubmitRecords(cd.submitRecords(opts));
   }
 
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/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 13fc362..b5a3d25 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
@@ -50,6 +51,7 @@
 import java.util.EnumSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 
 /** Access control management for a user accessing a single change. */
 public class ChangeControl {
@@ -214,6 +216,24 @@
     return isVisible(cd.db());
   }
 
+  /**
+   * @return patches for the change visible to the current user.
+   * @throws OrmException an error occurred reading the database.
+   */
+  public Collection<PatchSet> getVisiblePatchSets(Collection<PatchSet> patchSets, ReviewDb db)
+      throws OrmException {
+    // TODO(hiesel) These don't need to be migrated, just remove after support for drafts is removed
+    Predicate<? super PatchSet> predicate =
+        ps -> {
+          try {
+            return isPatchVisible(ps, db);
+          } catch (OrmException e) {
+            return false;
+          }
+        };
+    return patchSets.stream().filter(predicate).collect(toList());
+  }
+
   /** Can this user abandon this change? */
   private boolean canAbandon(ReviewDb db) throws OrmException {
     return (isOwner() // owner (aka creator) of the change can abandon
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
index 70271b7..ea2935d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -111,17 +111,17 @@
       return DashboardResource.projectDefault(myCtl);
     }
 
-    List<String> parts = Lists.newArrayList(Splitter.on(':').limit(2).split(id.get()));
-    if (parts.size() != 2) {
+    DashboardInfo info;
+    try {
+      info = newDashboardInfo(id.get());
+    } catch (InvalidDashboardId e) {
       throw new ResourceNotFoundException(id);
     }
 
     CurrentUser user = myCtl.getUser();
-    String ref = parts.get(0);
-    String path = parts.get(1);
     for (ProjectState ps : myCtl.getProjectState().tree()) {
       try {
-        return parse(ps.controlFor(user), ref, path, myCtl);
+        return parse(ps.controlFor(user), info, myCtl);
       } catch (AmbiguousObjectException | ConfigInvalidException | IncorrectObjectTypeException e) {
         throw new ResourceNotFoundException(id);
       } catch (ResourceNotFoundException e) {
@@ -131,13 +131,17 @@
     throw new ResourceNotFoundException(id);
   }
 
-  private DashboardResource parse(ProjectControl ctl, String ref, String path, ProjectControl myCtl)
+  public static String normalizeDashboardRef(String ref) {
+    if (!ref.startsWith(REFS_DASHBOARDS)) {
+      return REFS_DASHBOARDS + ref;
+    }
+    return ref;
+  }
+
+  private DashboardResource parse(ProjectControl ctl, DashboardInfo info, ProjectControl myCtl)
       throws ResourceNotFoundException, IOException, AmbiguousObjectException,
           IncorrectObjectTypeException, ConfigInvalidException, PermissionBackendException {
-    String id = ref + ":" + path;
-    if (!ref.startsWith(REFS_DASHBOARDS)) {
-      ref = REFS_DASHBOARDS + ref;
-    }
+    String ref = normalizeDashboardRef(info.ref);
     try {
       permissionBackend
           .user(ctl.getUser())
@@ -146,21 +150,21 @@
           .check(RefPermission.READ);
     } catch (AuthException e) {
       // Don't leak the project's existence
-      throw new ResourceNotFoundException(id);
+      throw new ResourceNotFoundException(info.id);
     }
     if (!Repository.isValidRefName(ref)) {
-      throw new ResourceNotFoundException(id);
+      throw new ResourceNotFoundException(info.id);
     }
 
     try (Repository git = gitManager.openRepository(ctl.getProject().getNameKey())) {
-      ObjectId objId = git.resolve(ref + ":" + path);
+      ObjectId objId = git.resolve(ref + ":" + info.path);
       if (objId == null) {
-        throw new ResourceNotFoundException(id);
+        throw new ResourceNotFoundException(info.id);
       }
       BlobBasedConfig cfg = new BlobBasedConfig(null, git, objId);
-      return new DashboardResource(myCtl, ref, path, cfg, false);
+      return new DashboardResource(myCtl, ref, info.path, cfg, false);
     } catch (RepositoryNotFoundException e) {
-      throw new ResourceNotFoundException(id);
+      throw new ResourceNotFoundException(info.id);
     }
   }
 
@@ -177,6 +181,26 @@
     return info;
   }
 
+  public static class InvalidDashboardId extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public InvalidDashboardId(String id) {
+      super(id);
+    }
+  }
+
+  static DashboardInfo newDashboardInfo(String id) throws InvalidDashboardId {
+    DashboardInfo info = new DashboardInfo();
+    List<String> parts = Lists.newArrayList(Splitter.on(':').limit(2).split(id));
+    if (parts.size() != 2) {
+      throw new InvalidDashboardId(id);
+    }
+    info.id = id;
+    info.ref = parts.get(0);
+    info.path = parts.get(1);
+    return info;
+  }
+
   static DashboardInfo parse(
       Project definingProject,
       String refName,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 2a3ff45..e3ee265 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -47,7 +47,6 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
@@ -93,7 +92,6 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.function.Predicate;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -603,10 +601,6 @@
     return visibleTo == user;
   }
 
-  public boolean hasChangeControl() {
-    return changeControl != null;
-  }
-
   public ChangeControl changeControl() throws OrmException {
     if (changeControl == null) {
       Change c = change();
@@ -619,36 +613,6 @@
     return changeControl;
   }
 
-  public ChangeControl changeControl(CurrentUser user) throws OrmException {
-    if (changeControl != null) {
-      CurrentUser oldUser = user;
-      if (sameUser(user, oldUser)) {
-        return changeControl;
-      }
-      throw new IllegalStateException("user already specified: " + changeControl.getUser());
-    }
-
-    if (change != null) {
-      changeControl = changeControlFactory.controlFor(db, change, user);
-    } else {
-      changeControl = changeControlFactory.controlFor(db, project(), legacyId, user);
-    }
-    return changeControl;
-  }
-
-  private static boolean sameUser(CurrentUser a, CurrentUser b) {
-    // TODO(dborowitz): This is a hack; general CurrentUser equality would be
-    // better.
-    if (a.isInternalUser() && b.isInternalUser()) {
-      return true;
-    } else if (a instanceof AnonymousUser && b instanceof AnonymousUser) {
-      return true;
-    } else if (a.isIdentifiedUser() && b.isIdentifiedUser()) {
-      return a.getAccountId().equals(b.getAccountId());
-    }
-    return false;
-  }
-
   void cacheVisibleTo(ChangeControl ctl) {
     visibleTo = ctl.getUser();
     changeControl = ctl;
@@ -816,22 +780,6 @@
     return patchSets;
   }
 
-  /**
-   * @return patches for the change visible to the current user.
-   * @throws OrmException an error occurred reading the database.
-   */
-  public Collection<PatchSet> visiblePatchSets() throws OrmException {
-    Predicate<? super PatchSet> predicate =
-        ps -> {
-          try {
-            return changeControl().isPatchVisible(ps, db);
-          } catch (OrmException e) {
-            return false;
-          }
-        };
-    return patchSets().stream().filter(predicate).collect(toList());
-  }
-
   public void setPatchSets(Collection<PatchSet> patchSets) {
     this.currentPatchSet = null;
     this.patchSets = patchSets;
@@ -1017,7 +965,7 @@
     return messages;
   }
 
-  public List<SubmitRecord> submitRecords(SubmitRuleOptions options) throws OrmException {
+  public List<SubmitRecord> submitRecords(SubmitRuleOptions options) {
     List<SubmitRecord> records = submitRecords.get(options);
     if (records == null) {
       if (!lazyLoad) {
@@ -1038,7 +986,7 @@
     submitRecords.put(options, records);
   }
 
-  public SubmitTypeRecord submitTypeRecord() throws OrmException {
+  public SubmitTypeRecord submitTypeRecord() {
     if (submitTypeRecord == null) {
       submitTypeRecord = submitRuleEvaluatorFactory.create(this).getSubmitType();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 1fe982f..7e7d456 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -275,7 +275,7 @@
           db,
           rw,
           c,
-          d.visiblePatchSets(),
+          d.changeControl().getVisiblePatchSets(d.patchSets(), db),
           includeApprovals ? d.approvals().asMap() : null,
           includeFiles,
           d.change(),
@@ -304,7 +304,7 @@
             db,
             rw,
             c,
-            d.visiblePatchSets(),
+            d.changeControl().getVisiblePatchSets(d.patchSets(), db),
             includeApprovals ? d.approvals().asMap() : null,
             includeFiles,
             d.change(),
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/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/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index a59075b..31faa54 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -926,6 +926,12 @@
           change.current_revision === edit.base_revision) {
         change.current_revision = edit.commit.commit;
         this._patchRange.patchNum = this.EDIT_NAME;
+        // Because edits are fibbed as revisions and added to the revisions
+        // array, and revision actions are always derived from the 'latest'
+        // patch set, we must copy over actions from the patch set base.
+        // Context: Issue 7243
+        change.revisions[edit.commit.commit].actions =
+            change.revisions[edit.base_revision].actions;
       }
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 1b2c34e..c1b1a90 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -1139,7 +1139,7 @@
       element._patchRange = {};
       const change = {
         current_revision: 'foo',
-        revisions: {foo: {commit: {}}},
+        revisions: {foo: {commit: {}, actions: {cherrypick: {enabled: true}}}},
       };
       let mockChange;
 
@@ -1159,17 +1159,21 @@
       assert.equal(mockChange.revisions.bar._number, element.EDIT_NAME);
       assert.equal(mockChange.current_revision, change.current_revision);
       assert.deepEqual(mockChange.revisions.bar.commit, {commit: 'bar'});
+      assert.notOk(mockChange.revisions.bar.actions);
 
       edit.base_revision = 'foo';
       element._processEdit(mockChange = _.cloneDeep(change), edit);
       assert.notDeepEqual(mockChange, change);
       assert.equal(mockChange.current_revision, 'bar');
+      assert.deepEqual(mockChange.revisions.bar.actions,
+          mockChange.revisions.foo.actions);
 
       // If _patchRange.patchNum is defined, do not load edit.
       element._patchRange.patchNum = 'baz';
       change.current_revision = 'baz';
       element._processEdit(mockChange = _.cloneDeep(change), edit);
       assert.equal(element._patchRange.patchNum, 'baz');
+      assert.notOk(mockChange.revisions.bar.actions);
     });
 
     test('_editLoaded set when patchNum is an edit', () => {
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/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>