Merge "Add new slot for ported threads without a range"
diff --git a/.bazelversion b/.bazelversion
index 7c69a55d..fcdb2e1 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-3.7.0
+4.0.0
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index cd794b8..b7cdf8a 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -925,6 +925,18 @@
 ----
 
 
+[[PublicDomain]]
+PublicDomain
+
+* guice:aopalliance
+
+[[PublicDomain_license]]
+----
+This software has been placed in the public domain by its author(s).
+
+----
+
+
 [[antlr]]
 antlr
 
diff --git a/WORKSPACE b/WORKSPACE
index c35c190..6cb56ed 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -140,6 +140,12 @@
 )
 
 maven_jar(
+    name = "aopalliance",
+    artifact = "aopalliance:aopalliance:1.0",
+    sha1 = "0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8",
+)
+
+maven_jar(
     name = "javax_inject",
     artifact = "javax.inject:javax.inject:1",
     sha1 = "6975da39a7040257bd51d21a231b76c915872d38",
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java
index 1fa099e..ca13db9 100644
--- a/java/com/google/gerrit/entities/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -456,20 +456,14 @@
    */
   protected Timestamp lastUpdatedOn;
 
-  // DELETED: id = 6 (sortkey)
-
   protected Account.Id owner;
 
   /** The branch (and project) this change merges into. */
   protected BranchNameKey dest;
 
-  // DELETED: id = 9 (open)
-
   /** Current state code; see {@link Status}. */
   protected char status;
 
-  // DELETED: id = 11 (nbrPatchSets)
-
   /** The current patch set. */
   protected int currentPatchSetId;
 
@@ -479,9 +473,6 @@
   /** Topic name assigned by the user, if any. */
   @Nullable protected String topic;
 
-  // DELETED: id = 15 (lastSha1MergeTested)
-  // DELETED: id = 16 (mergeable)
-
   /**
    * First line of first patch set's commit message.
    *
@@ -553,12 +544,12 @@
     cherryPickOf = other.cherryPickOf;
   }
 
-  /** Legacy 32 bit integer identity for a change. */
+  /** 32 bit integer identity for a change. */
   public Change.Id getId() {
     return changeId;
   }
 
-  /** Legacy 32 bit integer identity for a change. */
+  /** 32 bit integer identity for a change. */
   public int getChangeId() {
     return changeId.get();
   }
diff --git a/java/com/google/gerrit/entities/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
index 522c60a..b6efcbf 100644
--- a/java/com/google/gerrit/entities/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -139,6 +139,11 @@
     return ref;
   }
 
+  /**
+   * Warning: Change refs have to manually be advertised in {@link
+   * com.google.gerrit.server.permissions.DefaultRefFilter}; this should be done when adding new
+   * change refs.
+   */
   public static String changeMetaRef(Change.Id id) {
     StringBuilder r = newStringBuilder().append(REFS_CHANGES);
     return shard(id.get(), r).append(META_SUFFIX).toString();
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index e8f1fe1..cf09ff3 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -512,6 +512,7 @@
   }
 
   public RobotCommentNotes getRobotCommentNotes() {
+    loadRobotComments();
     return robotCommentNotes;
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index af8c8c8..33bc039 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -47,7 +47,6 @@
 import com.google.gerrit.entities.converter.ChangeMessageProtoConverter;
 import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
 import com.google.gerrit.entities.converter.PatchSetProtoConverter;
-import com.google.gerrit.entities.converter.ProtoConverter;
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.proto.Protos;
 import com.google.gerrit.server.AssigneeStatusUpdate;
@@ -65,8 +64,6 @@
 import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
 import com.google.gson.Gson;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.MessageLite;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.List;
@@ -455,6 +452,9 @@
     abstract ChangeNotesState build();
   }
 
+  /**
+   * Convert ChangeNotesState (which is AutoValue based) to byte[] and back, using protocol buffers.
+   */
   enum Serializer implements CacheSerializer<ChangeNotesState> {
     INSTANCE;
 
@@ -482,13 +482,11 @@
       object.hashtags().forEach(b::addHashtag);
       object
           .patchSets()
-          .forEach(e -> b.addPatchSet(toByteString(e.getValue(), PatchSetProtoConverter.INSTANCE)));
+          .forEach(e -> b.addPatchSet(PatchSetProtoConverter.INSTANCE.toProto(e.getValue())));
       object
           .approvals()
           .forEach(
-              e ->
-                  b.addApproval(
-                      toByteString(e.getValue(), PatchSetApprovalProtoConverter.INSTANCE)));
+              e -> b.addApproval(PatchSetApprovalProtoConverter.INSTANCE.toProto(e.getValue())));
 
       object.reviewers().asTable().cellSet().forEach(c -> b.addReviewer(toReviewerSetEntry(c)));
       object
@@ -519,7 +517,7 @@
           .forEach(r -> b.addSubmitRecord(GSON.toJson(new StoredSubmitRecord(r))));
       object
           .changeMessages()
-          .forEach(m -> b.addChangeMessage(toByteString(m, ChangeMessageProtoConverter.INSTANCE)));
+          .forEach(m -> b.addChangeMessage(ChangeMessageProtoConverter.INSTANCE.toProto(m)));
       object.publishedComments().values().forEach(c -> b.addPublishedComment(GSON.toJson(c)));
       b.setUpdateCount(object.updateCount());
       if (object.mergedOn() != null) {
@@ -530,12 +528,6 @@
       return Protos.toByteArray(b.build());
     }
 
-    @VisibleForTesting
-    static <T> ByteString toByteString(T object, ProtoConverter<?, T> converter) {
-      MessageLite message = converter.toProto(object);
-      return Protos.toByteString(message);
-    }
-
     private static ChangeColumnsProto toChangeColumnsProto(ChangeColumns cols) {
       ChangeColumnsProto.Builder b =
           ChangeColumnsProto.newBuilder()
@@ -635,12 +627,12 @@
               .hashtags(proto.getHashtagList())
               .patchSets(
                   proto.getPatchSetList().stream()
-                      .map(bytes -> parseProtoFrom(PatchSetProtoConverter.INSTANCE, bytes))
+                      .map(msg -> PatchSetProtoConverter.INSTANCE.fromProto(msg))
                       .map(ps -> Maps.immutableEntry(ps.id(), ps))
                       .collect(toImmutableList()))
               .approvals(
                   proto.getApprovalList().stream()
-                      .map(bytes -> parseProtoFrom(PatchSetApprovalProtoConverter.INSTANCE, bytes))
+                      .map(msg -> PatchSetApprovalProtoConverter.INSTANCE.fromProto(msg))
                       .map(a -> Maps.immutableEntry(a.patchSetId(), a))
                       .collect(toImmutableList()))
               .reviewers(toReviewerSet(proto.getReviewerList()))
@@ -660,7 +652,7 @@
                       .collect(toImmutableList()))
               .changeMessages(
                   proto.getChangeMessageList().stream()
-                      .map(bytes -> parseProtoFrom(ChangeMessageProtoConverter.INSTANCE, bytes))
+                      .map(msg -> ChangeMessageProtoConverter.INSTANCE.fromProto(msg))
                       .collect(toImmutableList()))
               .publishedComments(
                   proto.getPublishedCommentList().stream()
@@ -671,12 +663,6 @@
       return b.build();
     }
 
-    private static <P extends MessageLite, T> T parseProtoFrom(
-        ProtoConverter<P, T> converter, ByteString byteString) {
-      P message = Protos.parseUnchecked(converter.getParser(), byteString);
-      return converter.fromProto(message);
-    }
-
     private static ChangeColumns toChangeColumns(Change.Id changeId, ChangeColumnsProto proto) {
       ChangeColumns.Builder b =
           ChangeColumns.builder()
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 6272cda..e88a840 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -211,6 +211,14 @@
                 refs.add(
                     new ObjectIdRef.PeeledNonTag(
                         Storage.PACKED, RefNames.patchSetRef(p.id()), p.commitId())));
+    if (changeNotes.getRobotCommentNotes() != null
+        && changeNotes.getRobotCommentNotes().getMetaId() != null) {
+      refs.add(
+          new ObjectIdRef.PeeledNonTag(
+              Storage.PACKED,
+              RefNames.robotCommentsRef(changeNotes.getChangeId()),
+              changeNotes.getRobotCommentNotes().getMetaId()));
+    }
   }
 
   /**
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index 162f324..c374691 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -562,20 +562,22 @@
      *
      * @param name name
      * @return the {@code OptionHandler} or {@code null}
-     *     <p>Note: this is cut & pasted from the parent class in arg4j, it was private and it
-     *     needed to be exposed.
+     *     <p>Note: this was originally cut & pasted from the parent class in arg4j, it was private
+     *     and it needed to be exposed.
      */
     @SuppressWarnings("rawtypes")
     public OptionHandler findOptionByName(String name) {
       for (OptionHandler h : optionsList) {
-        NamedOptionDef option = (NamedOptionDef) h.option;
-        if (name.equals(option.name())) {
-          return h;
-        }
-        for (String alias : option.aliases()) {
-          if (name.equals(alias)) {
+        if (h.option instanceof NamedOptionDef) {
+          NamedOptionDef option = (NamedOptionDef) h.option;
+          if (name.equals(option.name())) {
             return h;
           }
+          for (String alias : option.aliases()) {
+            if (name.equals(alias)) {
+              return h;
+            }
+          }
         }
       }
       return null;
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 9976fbc..5699a04 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -45,6 +45,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -58,6 +59,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.TestCommentHelper;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -86,6 +88,7 @@
   @Inject private PermissionBackend permissionBackend;
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private TestCommentHelper testCommentHelper;
 
   private AccountGroup.UUID admins;
   private AccountGroup.UUID nonInteractiveUsers;
@@ -1597,6 +1600,84 @@
   }
 
   @Test
+  public void advertiseMostRecentRefChangesWithRobotCommentRef() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
+
+    // user doesn't have refs/* permission.
+    requestScopeOperations.setApiUser(user.id());
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(Patch.COMMIT_MSG);
+    testCommentHelper.addRobotComment(cd1.getId(), input);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      PermissionBackend.ForProject forProject = newFilter(project, admin);
+      assertThat(
+              names(
+                  forProject.filter(
+                      repo.getRefDatabase().getRefs(),
+                      repo,
+                      RefFilterOptions.builder().setReturnMostRecentRefChanges(false).build())))
+          .containsExactlyElementsIn(
+              ImmutableList.of(
+                  "HEAD",
+                  RefNames.changeRefPrefix(cd1.getId()) + "robot-comments",
+                  psRef1,
+                  metaRef1,
+                  psRef2,
+                  metaRef2,
+                  psRef3,
+                  metaRef3,
+                  psRef4,
+                  metaRef4,
+                  "refs/heads/branch",
+                  "refs/heads/master",
+                  "refs/meta/config",
+                  "refs/tags/branch-tag",
+                  "refs/tags/master-tag",
+                  "refs/tags/tree-tag"));
+    }
+  }
+
+  @Test
+  public void advertiseMostRecentRefChangesWithRobotCommentRefWithReturnMostRecentRefChanges()
+      throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
+
+    // user doesn't have refs/* permission.
+    requestScopeOperations.setApiUser(user.id());
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(Patch.COMMIT_MSG);
+    testCommentHelper.addRobotComment(cd1.getId(), input);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      PermissionBackend.ForProject forProject = newFilter(project, admin);
+      assertThat(
+              names(
+                  forProject.filter(
+                      ImmutableList.of(),
+                      repo,
+                      RefFilterOptions.builder().setReturnMostRecentRefChanges(true).build())))
+          .containsExactlyElementsIn(
+              ImmutableList.of(
+                  RefNames.changeRefPrefix(cd1.getId()) + "robot-comments",
+                  psRef1,
+                  metaRef1,
+                  psRef2,
+                  metaRef2,
+                  psRef3,
+                  metaRef3,
+                  psRef4,
+                  metaRef4));
+    }
+  }
+
+  @Test
   public void fetchSingleChangeWithoutIndexAccess() throws Exception {
     PushOneCommit.Result change = createChange();
     String patchSetRef = change.getPatchSetId().toRefName();
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 248c7d1..9a6b82b 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -49,6 +49,7 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/metrics",
+        "//java/com/google/gerrit/proto",
         "//java/com/google/gerrit/proto/testing",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/account/externalids/testing",
@@ -82,5 +83,6 @@
         "//lib/truth:truth-java8-extension",
         "//lib/truth:truth-proto-extension",
         "//proto:cache_java_proto",
+        "//proto:entities_java_proto",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
index 29f520b..4902830 100644
--- a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
@@ -93,12 +93,14 @@
     Repository repo = repoManager.createRepository(someProjectKey);
     assertThat(repo.getDirectory()).isNotNull();
     assertThat(repo.getDirectory().exists()).isTrue();
-    assertThat(repo.getDirectory().getParent()).isEqualTo(alternateBasePath.toString());
+    assertThat(repo.getDirectory().getParent())
+        .isEqualTo(alternateBasePath.toRealPath().toString());
 
     repo = repoManager.openRepository(someProjectKey);
     assertThat(repo.getDirectory()).isNotNull();
     assertThat(repo.getDirectory().exists()).isTrue();
-    assertThat(repo.getDirectory().getParent()).isEqualTo(alternateBasePath.toString());
+    assertThat(repo.getDirectory().getParent())
+        .isEqualTo(alternateBasePath.toRealPath().toString());
 
     assertThat(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString())
         .isEqualTo(alternateBasePath.toString());
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index a1a1ca3..67181b7 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -17,7 +17,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
-import static com.google.gerrit.server.notedb.ChangeNotesState.Serializer.toByteString;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -40,6 +39,8 @@
 import com.google.gerrit.entities.converter.ChangeMessageProtoConverter;
 import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
 import com.google.gerrit.entities.converter.PatchSetProtoConverter;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
@@ -332,7 +333,8 @@
             .uploader(Account.id(2000))
             .createdOn(cols.createdOn())
             .build();
-    ByteString ps1Bytes = toByteString(ps1, PatchSetProtoConverter.INSTANCE);
+    Entities.PatchSet ps1Proto = PatchSetProtoConverter.INSTANCE.toProto(ps1);
+    ByteString ps1Bytes = Protos.toByteString(ps1Proto);
     assertThat(ps1Bytes.size()).isEqualTo(66);
 
     PatchSet ps2 =
@@ -342,7 +344,8 @@
             .uploader(Account.id(3000))
             .createdOn(cols.lastUpdatedOn())
             .build();
-    ByteString ps2Bytes = toByteString(ps2, PatchSetProtoConverter.INSTANCE);
+    Entities.PatchSet ps2Proto = PatchSetProtoConverter.INSTANCE.toProto(ps2);
+    ByteString ps2Bytes = Protos.toByteString(ps2Proto);
     assertThat(ps2Bytes.size()).isEqualTo(66);
     assertThat(ps2Bytes).isNotEqualTo(ps1Bytes);
 
@@ -352,8 +355,8 @@
             .setMetaId(SHA_BYTES)
             .setChangeId(ID.get())
             .setColumns(colsProto)
-            .addPatchSet(ps2Bytes)
-            .addPatchSet(ps1Bytes)
+            .addPatchSet(ps2Proto)
+            .addPatchSet(ps1Proto)
             .build());
   }
 
@@ -367,8 +370,8 @@
             .value(1)
             .granted(new Timestamp(1212L))
             .build();
-    ByteString a1Bytes = toByteString(a1, PatchSetApprovalProtoConverter.INSTANCE);
-    assertThat(a1Bytes.size()).isEqualTo(43);
+    Entities.PatchSetApproval psa1 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a1);
+    ByteString a1Bytes = Protos.toByteString(psa1);
 
     PatchSetApproval a2 =
         PatchSetApproval.builder()
@@ -378,7 +381,8 @@
             .value(-1)
             .granted(new Timestamp(3434L))
             .build();
-    ByteString a2Bytes = toByteString(a2, PatchSetApprovalProtoConverter.INSTANCE);
+    Entities.PatchSetApproval psa2 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a2);
+    ByteString a2Bytes = Protos.toByteString(psa2);
     assertThat(a2Bytes.size()).isEqualTo(49);
     assertThat(a2Bytes).isNotEqualTo(a1Bytes);
 
@@ -390,8 +394,8 @@
             .setMetaId(SHA_BYTES)
             .setChangeId(ID.get())
             .setColumns(colsProto)
-            .addApproval(a2Bytes)
-            .addApproval(a1Bytes)
+            .addApproval(psa2)
+            .addApproval(psa1)
             .build());
   }
 
@@ -722,7 +726,8 @@
             Account.id(1000),
             new Timestamp(1212L),
             PatchSet.id(ID, 1));
-    ByteString m1Bytes = toByteString(m1, ChangeMessageProtoConverter.INSTANCE);
+    Entities.ChangeMessage m1Proto = ChangeMessageProtoConverter.INSTANCE.toProto(m1);
+    ByteString m1Bytes = Protos.toByteString(m1Proto);
     assertThat(m1Bytes.size()).isEqualTo(35);
 
     ChangeMessage m2 =
@@ -731,7 +736,8 @@
             Account.id(2000),
             new Timestamp(3434L),
             PatchSet.id(ID, 2));
-    ByteString m2Bytes = toByteString(m2, ChangeMessageProtoConverter.INSTANCE);
+    Entities.ChangeMessage m2Proto = ChangeMessageProtoConverter.INSTANCE.toProto(m2);
+    ByteString m2Bytes = Protos.toByteString(m2Proto);
     assertThat(m2Bytes.size()).isEqualTo(35);
     assertThat(m2Bytes).isNotEqualTo(m1Bytes);
 
@@ -741,8 +747,8 @@
             .setMetaId(SHA_BYTES)
             .setChangeId(ID.get())
             .setColumns(colsProto)
-            .addChangeMessage(m2Bytes)
-            .addChangeMessage(m1Bytes)
+            .addChangeMessage(m2Proto)
+            .addChangeMessage(m1Proto)
             .build());
   }
 
@@ -1007,6 +1013,60 @@
                 .build());
   }
 
+  /* Transitional test. Remove once follow-up change is live without accidents. */
+  @Test
+  public void binaryCompatibility() throws Exception {
+    ChangeNotesState.Builder builder = newBuilder();
+    PatchSet ps1 =
+        PatchSet.builder()
+            .id(PatchSet.id(ID, 1))
+            .commitId(ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
+            .uploader(Account.id(2000))
+            .createdOn(cols.createdOn())
+            .build();
+    PatchSetApproval a1 =
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    ps1.id(), Account.id(2001), LabelId.create(LabelId.CODE_REVIEW)))
+            .value(1)
+            .granted(new Timestamp(1212L))
+            .build();
+
+    ChangeMessage m1 =
+        new ChangeMessage(
+            ChangeMessage.key(ID, "uuid1"),
+            Account.id(1000),
+            new Timestamp(1212L),
+            PatchSet.id(ID, 1));
+    ChangeNotesState state =
+        builder
+            .approvals(ImmutableMap.of(PatchSet.id(ID, 1), a1).entrySet())
+            .patchSets(ImmutableMap.of(ps1.id(), ps1).entrySet())
+            .changeMessages(ImmutableList.of(m1))
+            .build();
+
+    byte got[] = ChangeNotesState.Serializer.INSTANCE.serialize(state);
+    byte want[] =
+        new byte[] {
+          10, 20, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86,
+          120, 16, 123, 26, 89, 10, 41, 73, 97, 98, 99, 100, 97, 98, 99, 100, 97, 98, 99, 100, 97,
+          98, 99, 100, 97, 98, 99, 100, 97, 98, 99, 100, 97, 98, 99, 100, 97, 98, 99, 100, 97, 98,
+          99, 100, 97, 98, 99, 100, 16, -64, -60, 7, 24, -57, -88, 14, 32, -24, 7, 42, 17, 114, 101,
+          102, 115, 47, 104, 101, 97, 100, 115, 47, 109, 97, 115, 116, 101, 114, 66, 11, 84, 101,
+          115, 116, 32, 99, 104, 97, 110, 103, 101, -88, 1, 1, 50, 66, 10, 6, 10, 2, 8, 123, 16, 1,
+          18, 42, 10, 40, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97,
+          97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97,
+          26, 3, 8, -48, 15, 33, 64, -30, 1, 0, 0, 0, 0, 0, 58, 43, 10, 28, 10, 6, 10, 2, 8, 123,
+          16, 1, 18, 3, 8, -47, 15, 26, 13, 10, 11, 67, 111, 100, 101, 45, 82, 101, 118, 105, 101,
+          119, 16, 1, 25, -68, 4, 0, 0, 0, 0, 0, 0, 64, 0, 122, 35, 10, 11, 10, 2, 8, 123, 18, 5,
+          117, 117, 105, 100, 49, 18, 3, 8, -24, 7, 25, -68, 4, 0, 0, 0, 0, 0, 0, 42, 6, 10, 2, 8,
+          123, 16, 1
+        };
+
+    assertThat(got).isEqualTo(want);
+  }
+
   @Test
   public void commentFields() throws Exception {
     assertThatSerializedClass(Comment.Key.class)
diff --git a/lib/LICENSE-PublicDomain b/lib/LICENSE-PublicDomain
new file mode 100644
index 0000000..8a71ce0
--- /dev/null
+++ b/lib/LICENSE-PublicDomain
@@ -0,0 +1 @@
+This software has been placed in the public domain by its author(s).
diff --git a/lib/guice/BUILD b/lib/guice/BUILD
index 14179d6..f73984b 100644
--- a/lib/guice/BUILD
+++ b/lib/guice/BUILD
@@ -1,9 +1,4 @@
-load("@rules_java//java:defs.bzl", "java_import", "java_library")
-
-java_import(
-    name = "guice-library-no-aop",
-    jars = ["@guice-library-no-aop//file"],
-)
+load("@rules_java//java:defs.bzl", "java_library")
 
 java_library(
     name = "guice",
@@ -19,7 +14,8 @@
     name = "guice-library",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
-    exports = [":guice-library-no-aop"],
+    exports = ["@guice-library//jar"],
+    runtime_deps = ["aopalliance"],
 )
 
 java_library(
@@ -39,6 +35,12 @@
 )
 
 java_library(
+    name = "aopalliance",
+    data = ["//lib:LICENSE-PublicDomain"],
+    exports = ["@aopalliance//jar"],
+)
+
+java_library(
     name = "javax_inject",
     data = ["//lib:LICENSE-Apache2.0"],
     visibility = ["//visibility:public"],
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index f94486c..f596164 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -23,7 +23,7 @@
 flogger-system-backend
 guava
 guice-assistedinject
-guice-library-no-aop
+guice-library
 guice-servlet
 httpasyncclient
 httpcore-nio
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index 7008b48..841ee6e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -57,7 +57,6 @@
 import {DashboardViewState} from '../../../types/types';
 import {firePageError, fireTitleChange} from '../../../utils/event-util';
 import {GerritView} from '../../../services/router/router-model';
-import {KnownExperimentId} from '../../../services/flags/flags';
 
 const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;
 const RELOAD_DASHBOARD_INTERVAL_MS = 10 * 1000;
@@ -124,8 +123,6 @@
 
   private restApiService = appContext.restApiService;
 
-  private flagService = appContext.flagsService;
-
   private lastVisibleTimestampMs = 0;
 
   constructor() {
@@ -140,19 +137,17 @@
       e.stopPropagation();
       this._reload(this.params);
     });
-    if (this.flagService.isEnabled(KnownExperimentId.AUTO_RELOAD_DASHBOARD)) {
-      document.addEventListener('visibilitychange', () => {
-        if (document.visibilityState === 'visible') {
-          if (
-            Date.now() - this.lastVisibleTimestampMs >
-            RELOAD_DASHBOARD_INTERVAL_MS
-          )
-            this._reload(this.params);
-        } else {
-          this.lastVisibleTimestampMs = Date.now();
-        }
-      });
-    }
+    document.addEventListener('visibilitychange', () => {
+      if (document.visibilityState === 'visible') {
+        if (
+          Date.now() - this.lastVisibleTimestampMs >
+          RELOAD_DASHBOARD_INTERVAL_MS
+        )
+          this._reload(this.params);
+      } else {
+        this.lastVisibleTimestampMs = Date.now();
+      }
+    });
   }
 
   _loadPreferences() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index eca0b70..96714e3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -115,6 +115,7 @@
   getVotingRange,
 } from '../../../utils/label-util';
 import {CommentThread} from '../../../utils/comment-util';
+import {ShowAlertEventDetail} from '../../../types/events';
 
 const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
 const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -1753,7 +1754,7 @@
     return fetchChangeUpdates(change, this.restApiService).then(result => {
       if (!result.isLatest) {
         this.dispatchEvent(
-          new CustomEvent('show-alert', {
+          new CustomEvent<ShowAlertEventDetail>('show-alert', {
             detail: {
               message:
                 'Cannot set label: a newer patch has been ' +
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 86b714c..6e99a7d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -52,6 +52,7 @@
 import {AccountInfo} from '../../../types/common';
 import {notUndefined} from '../../../types/types';
 import {uniqueDefinedAvatar} from '../../../utils/account-util';
+import {PrimaryTab} from '../../../constants/constants';
 
 export enum SummaryChipStyles {
   INFO = 'info',
@@ -115,11 +116,21 @@
   render() {
     const chipClass = `summaryChip font-small ${this.styleType}`;
     const grIcon = this.icon ? `gr-icons:${this.icon}` : '';
-    return html`<div class="${chipClass}" role="button">
+    return html`<div
+      class="${chipClass}"
+      role="button"
+      @click="${this.handleClick}"
+    >
       ${this.icon && html`<iron-icon icon="${grIcon}"></iron-icon>`}
       <slot></slot>
     </div>`;
   }
+
+  private handleClick(e: MouseEvent) {
+    e.stopPropagation();
+    e.preventDefault();
+    fireShowPrimaryTab(this, PrimaryTab.COMMENT_THREADS);
+  }
 }
 
 @customElement('gr-checks-chip')
@@ -219,16 +230,13 @@
   private handleClick(e: MouseEvent) {
     e.stopPropagation();
     e.preventDefault();
-    fireShowPrimaryTab(this, 'checks');
+    fireShowPrimaryTab(this, PrimaryTab.CHECKS);
   }
 }
 
 /** What is the maximum number of expanded checks chips? */
 const DETAILS_QUOTA = 3;
 
-/** What is the maximum number of links renderend within one chip? */
-const MAX_LINKS_PER_CHIP = 3;
-
 @customElement('gr-change-summary')
 export class GrChangeSummary extends GrLitElement {
   private readonly newChangeSummaryUiEnabled = appContext.flagsService.isEnabled(
@@ -273,9 +281,11 @@
         }
         td.key {
           padding-right: var(--spacing-l);
+          padding-bottom: var(--spacing-m);
         }
         td.value {
           padding-right: var(--spacing-l);
+          padding-bottom: var(--spacing-m);
         }
         iron-icon.launch {
           color: var(--gray-foreground);
@@ -320,15 +330,13 @@
     if (runs.length <= this.detailsQuota) {
       this.detailsQuota -= runs.length;
       return runs.map(run => {
-        const links = resultFilter(run)
+        const allLinks = resultFilter(run)
           .reduce((links, result) => {
             return links.concat(result.links ?? []);
           }, [] as Link[])
-          .filter(link => link.primary)
-          .slice(0, MAX_LINKS_PER_CHIP);
-        const count = resultFilter(run).length;
-        const countText = count > 1 ? ` ${count}` : '';
-        const text = `${run.checkName}${countText}`;
+          .filter(link => link.primary);
+        const links = allLinks.length === 1 ? allLinks : [];
+        const text = `${run.checkName}`;
         return html`<gr-checks-chip
           class="${icon}"
           .icon="${icon}"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index e361f9f..4a92869 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -146,6 +146,7 @@
   CustomKeyboardEvent,
   EditableContentSaveEvent,
   OpenFixPreviewEvent,
+  ShowAlertEventDetail,
   SwitchTabEvent,
   ThreadListModifiedEvent,
 } from '../../../types/events';
@@ -371,13 +372,6 @@
   })
   _hideEditCommitMessage?: boolean;
 
-  @property({
-    type: Boolean,
-    computed:
-      '_computeHideShowAllContainer(_hideEditCommitMessage, _commitCollapsible)',
-  })
-  _hideShowAllContainer = false;
-
   @property({type: String})
   _diffAgainst?: string;
 
@@ -931,13 +925,6 @@
     return changeStatuses(change, options);
   }
 
-  _computeHideShowAllContainer(
-    _hideEditCommitMessage?: boolean,
-    _commitCollapsible?: boolean
-  ) {
-    return !_commitCollapsible && _hideEditCommitMessage;
-  }
-
   _computeHideEditCommitMessage(
     loggedIn: boolean,
     editing: boolean,
@@ -2379,6 +2366,9 @@
   }
 
   _computeCommitMessageCollapsed(collapsed?: boolean, collapsible?: boolean) {
+    if (this._isNewChangeSummaryUiEnabled) {
+      return false;
+    }
     return collapsible && collapsed;
   }
 
@@ -2387,9 +2377,6 @@
   }
 
   _computeCollapseText(collapsed: boolean) {
-    if (this._isNewChangeSummaryUiEnabled) {
-      return collapsed ? 'Show all' : 'Show less';
-    }
     // Symbols are up and down triangles.
     return collapsed ? '\u25bc Show more' : '\u25b2 Show less';
   }
@@ -2590,11 +2577,12 @@
 
         this._cancelUpdateCheckTimer();
         this.dispatchEvent(
-          new CustomEvent('show-alert', {
+          new CustomEvent<ShowAlertEventDetail>('show-alert', {
             detail: {
               message: toastMessage,
               // Persist this alert.
               dismissOnNavigation: true,
+              showDismiss: true,
               action: 'Reload',
               callback: () => {
                 this._reload(
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index ed33916..3c37ec7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -109,25 +109,6 @@
       /* Account for border and padding and rounding errors. */
       max-width: calc(72ch + 2px + 2 * var(--spacing-m) + 0.4px);
     }
-    .show-all-container {
-      background-color: var(--view-background-color);
-      display: flex;
-      justify-content: flex-end;
-      margin-bottom: 8px;
-      border-top-width: 1px;
-      border-top-style: solid;
-      border-radius: 0 0 4px 4px;
-      border-color: var(--border-color);
-      box-shadow: var(--elevation-level-1);
-    }
-    .show-all-container .show-all-button {
-      margin-right: auto;
-    }
-    .show-all-container iron-icon {
-      color: inherit;
-      --iron-icon-height: 18px;
-      --iron-icon-width: 18px;
-    }
     .commitMessage gr-linked-text {
       word-break: break-word;
     }
@@ -139,9 +120,6 @@
     .new-change-summary-true #commitMessageEditor {
       --collapsed-max-height: 300px;
     }
-    .new-change-summary-true gr-linked-text {
-      min-height: 160px;
-    }
     .editCommitMessage {
       margin-top: var(--spacing-l);
 
@@ -493,9 +471,11 @@
               >
                 <gr-editable-content
                   id="commitMessageEditor"
-                  editing="[[_editingCommitMessage]]"
+                  editing="{{_editingCommitMessage}}"
                   content="{{_latestCommitMessage}}"
                   storage-key="[[_computeCommitMessageKey(_change._number, _change.current_revision)]]"
+                  hide-edit-commit-message="[[_hideEditCommitMessage]]"
+                  commit-collapsible="[[_commitCollapsible]]"
                   remove-zero-width-space=""
                   collapsed$="[[_computeCommitMessageCollapsed(_commitCollapsed, _commitCollapsible)]]"
                 >
@@ -506,37 +486,6 @@
                     remove-zero-width-space=""
                   ></gr-linked-text>
                 </gr-editable-content>
-                <template is="dom-if" if="[[_isNewChangeSummaryUiEnabled]]">
-                  <div
-                    class="show-all-container"
-                    hidden$="[[_hideShowAllContainer]]"
-                  >
-                    <gr-button
-                      link=""
-                      class="show-all-button"
-                      on-click="_toggleCommitCollapsed"
-                      hidden$="[[!_commitCollapsible]]"
-                      ><iron-icon
-                        icon="gr-icons:expand-more"
-                        hidden$="[[!_commitCollapsed]]"
-                      ></iron-icon
-                      ><iron-icon
-                        icon="gr-icons:expand-less"
-                        hidden$="[[_commitCollapsed]]"
-                      ></iron-icon>
-                      [[_computeCollapseText(_commitCollapsed)]]
-                    </gr-button>
-                    <gr-button
-                      link=""
-                      class="edit-commit-message"
-                      title="Edit commit message"
-                      on-click="_handleEditCommitMessage"
-                      hidden$="[[_hideEditCommitMessage]]"
-                      ><iron-icon icon="gr-icons:edit"></iron-icon>
-                      Edit</gr-button
-                    >
-                  </div>
-                </template>
                 <template is="dom-if" if="[[!_isNewChangeSummaryUiEnabled]]">
                   <gr-button
                     link=""
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index 1e72ae9..af72b67 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
@@ -47,6 +47,9 @@
     .patchInfoOldPatchSet .container.latestPatchContainer {
       display: initial;
     }
+    .editMode.patchInfoOldPatchSet .container.latestPatchContainer {
+      display: none;
+    }
     .latestPatchContainer a {
       text-decoration: none;
     }
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index c856f68..ca3161f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -991,7 +991,8 @@
       const notIsReviewerAndHasDraftOrLabel = (r: AccountInfo) =>
         !(r._account_id === currentUser._account_id && (hasDrafts || hasVote));
       reviewers.base
-        .filter(r => r._pendingAdd && r._account_id)
+        .filter(r => r._account_id)
+        .filter(r => r._pendingAdd || (this.canBeStarted && isOwner))
         .filter(notIsReviewerAndHasDraftOrLabel)
         .forEach(r => newAttention.add(r._account_id!));
       // Add owner and uploader, if someone else replies.
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index 4547e53..2b92ca6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -314,6 +314,39 @@
     assert.sameMembers([...element._newAttentionSet], [2, 5]);
   });
 
+  test('computeNewAttention when sending wip change for review', () => {
+    const reviewers = {base: [
+      {_account_id: 2},
+      {_account_id: 3},
+    ]};
+    const change = {
+      owner: {_account_id: 1},
+      status: 'NEW',
+      attention_set: {},
+    };
+    element.change = change;
+    element._reviewers = reviewers.base;
+    flush();
+
+    // For an active change there is no reason to add anyone to the set.
+    let user = {_account_id: 1};
+    element._computeNewAttention(user, reviewers, [], change, [], false);
+    assert.sameMembers([...element._newAttentionSet], []);
+
+    // If the change is "work in progress" and the owner sends a reply, then
+    // add all reviewers.
+    element.canBeStarted = true;
+    flush();
+    user = {_account_id: 1};
+    element._computeNewAttention(user, reviewers, [], change, [], false);
+    assert.sameMembers([...element._newAttentionSet], [2, 3]);
+
+    // ... but not when someone else replies.
+    user = {_account_id: 4};
+    element._computeNewAttention(user, reviewers, [], change, [], false);
+    assert.sameMembers([...element._newAttentionSet], []);
+  });
+
   test('computeNewAttentionAccounts', () => {
     element._reviewers = [
       {_account_id: 123, display_name: 'Ernie'},
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index 4976503..7cea964 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -31,9 +31,17 @@
   PolymerDeepPropertyChange,
 } from '@polymer/polymer/interfaces';
 import {ChangeInfo} from '../../../types/common';
-import {CommentThread, isDraft, UIRobot} from '../../../utils/comment-util';
+import {
+  CommentThread,
+  isDraft,
+  UIRobot,
+  isUnresolved,
+  isDraftThread,
+} from '../../../utils/comment-util';
 import {pluralize} from '../../../utils/string-util';
 import {fireThreadListModifiedEvent} from '../../../utils/event-util';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {appContext} from '../../../services/app-context';
 
 interface CommentThreadWithInfo {
   thread: CommentThread;
@@ -91,6 +99,19 @@
   @property({type: Boolean})
   hideToggleButtons = false;
 
+  @property({type: Boolean})
+  _isNewChangeSummaryUiEnabled = false;
+
+  flagsService = appContext.flagsService;
+
+  /** @override */
+  ready() {
+    super.ready();
+    this._isNewChangeSummaryUiEnabled = this.flagsService.isEnabled(
+      KnownExperimentId.NEW_CHANGE_SUMMARY_UI
+    );
+  }
+
   _computeShowDraftToggle(loggedIn?: boolean) {
     return loggedIn ? 'show' : '';
   }
@@ -431,6 +452,37 @@
     return !!side;
   }
 
+  _handleOnlyUnresolved() {
+    this.unresolvedOnly = true;
+    this._draftsOnly = false;
+  }
+
+  _handleOnlyDrafts() {
+    this._draftsOnly = true;
+    this.unresolvedOnly = false;
+  }
+
+  _handleAllComments() {
+    this._draftsOnly = false;
+    this.unresolvedOnly = false;
+  }
+
+  _showAllComments(draftsOnly?: boolean, unresolvedOnly?: boolean) {
+    return !draftsOnly && !unresolvedOnly;
+  }
+
+  _countUnresolved(threads?: CommentThread[]) {
+    return threads?.filter(isUnresolved).length ?? 0;
+  }
+
+  _countAllThreads(threads?: CommentThread[]) {
+    return threads?.length ?? 0;
+  }
+
+  _countDrafts(threads?: CommentThread[]) {
+    return threads?.filter(isDraftThread).length ?? 0;
+  }
+
   /**
    * Work around a issue on iOS when clicking turns into double tap
    */
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index 2c21b4a..a4546a2 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
@@ -65,27 +65,72 @@
       box-shadow: none;
       padding-left: var(--spacing-m);
     }
+    .header .categoryRadio {
+      height: 18px;
+      width: 18px;
+    }
+    .header label {
+      padding-left: 8px;
+      margin-right: 16px;
+    }
   </style>
   <template is="dom-if" if="[[!hideToggleButtons]]">
     <div class="header">
-      <div class="toggleItem">
-        <paper-toggle-button
-          id="unresolvedToggle"
-          checked="{{!unresolvedOnly}}"
-          on-tap="_onTapUnresolvedToggle"
-          >All comments</paper-toggle-button
+      <template is="dom-if" if="[[!_isNewChangeSummaryUiEnabled]]">
+        <div class="toggleItem">
+          <paper-toggle-button
+            id="unresolvedToggle"
+            checked="{{!unresolvedOnly}}"
+            on-tap="_onTapUnresolvedToggle"
+            >All comments</paper-toggle-button
+          >
+        </div>
+        <div
+          class$="toggleItem draftToggle [[_computeShowDraftToggle(loggedIn)]]"
         >
-      </div>
-      <div
-        class$="toggleItem draftToggle [[_computeShowDraftToggle(loggedIn)]]"
-      >
-        <paper-toggle-button
-          id="draftToggle"
-          checked="{{_draftsOnly}}"
-          on-tap="_onTapUnresolvedToggle"
-          >Comments with drafts</paper-toggle-button
-        >
-      </div>
+          <paper-toggle-button
+            id="draftToggle"
+            checked="{{_draftsOnly}}"
+            on-tap="_onTapUnresolvedToggle"
+            >Comments with drafts</paper-toggle-button
+          >
+        </div>
+      </template>
+      <template is="dom-if" if="[[_isNewChangeSummaryUiEnabled]]">
+          <input
+            class="categoryRadio"
+            id="unresolvedRadio"
+            name="filterComments"
+            type="radio"
+            on-click="_handleOnlyUnresolved"
+            checked$="[[unresolvedOnly]]"
+          />
+          <label for="unresolvedRadio">
+            Unresolved ([[_countUnresolved(threads)]])
+          </label>
+          <input
+            class="categoryRadio"
+            id="draftsRadio"
+            name="filterComments"
+            type="radio"
+            on-click="_handleOnlyDrafts"
+            checked$="[[_draftsOnly]]"
+          />
+          <label for="draftsRadio">
+            Drafts ([[_countDrafts(threads)]])
+          </label>
+          <input
+            class="categoryRadio"
+            id="allRadio"
+            name="filterComments"
+            type="radio"
+            on-click="_handleAllComments"
+            checked$="[[_showAllComments(_draftsOnly, unresolvedOnly)]]"
+          />
+          <label for="all">
+            All ([[_countAllThreads(threads)]])
+          </label>
+      </template>
     </div>
   </template>
   <div id="threads">
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 4bb11b3..68ff67b 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -339,7 +339,10 @@
     if (runs.length === 0) return;
     return html`
       <h3 class="categoryHeader heading-3">
-        <iron-icon icon="gr-icons:check-circle" class="success"></iron-icon>
+        <iron-icon
+          icon="gr-icons:check-circle-outline"
+          class="success"
+        ></iron-icon>
         Success
       </h3>
       <table class="resultsTable">
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 82c346a..6638c5f 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 import {html} from 'lit-html';
+import {classMap} from 'lit-html/directives/class-map';
 import {css, customElement, property} from 'lit-element';
 import {GrLitElement} from '../lit/gr-lit-element';
 import {CheckRun, RunStatus} from '../../api/checks';
@@ -33,12 +34,116 @@
   updateStateSetResults,
 } from '../../services/checks/checks-model';
 
-function renderRun(run: CheckRun) {
-  const icon = iconForRun(run);
-  return html`<div class="runChip ${icon}">
-    <iron-icon icon="gr-icons:${icon}" class="${icon}"></iron-icon>
-    <span>${run.checkName}</span>
-  </div>`;
+/* The RunSelectedEvent is only used locally to communicate from <gr-checks-run>
+   to its <gr-checks-runs> parent. */
+
+interface RunSelectedEventDetail {
+  checkName: string;
+}
+
+type RunSelectedEvent = CustomEvent<RunSelectedEventDetail>;
+
+declare global {
+  interface HTMLElementEventMap {
+    'run-selected': RunSelectedEvent;
+  }
+}
+
+function fireRunSelected(target: EventTarget, checkName: string) {
+  target.dispatchEvent(
+    new CustomEvent('run-selected', {
+      detail: {checkName},
+      composed: false,
+      bubbles: false,
+    })
+  );
+}
+
+@customElement('gr-checks-run')
+export class GrChecksRun extends GrLitElement {
+  static get styles() {
+    return [
+      sharedStyles,
+      css`
+        :host {
+          display: block;
+          --thick-border: 6px;
+        }
+        .chip {
+          display: block;
+          font-weight: var(--font-weight-bold);
+          border: 1px solid var(--border-color);
+          border-radius: var(--border-radius);
+          padding: var(--spacing-s) var(--spacing-m);
+          margin-top: var(--spacing-s);
+          cursor: default;
+        }
+        .chip.error {
+          border-left: var(--thick-border) solid var(--error-foreground);
+        }
+        .chip.warning {
+          border-left: var(--thick-border) solid var(--warning-foreground);
+        }
+        .chip.info-outline {
+          border-left: var(--thick-border) solid var(--info-foreground);
+        }
+        .chip.check-circle-outline {
+          border-left: var(--thick-border) solid var(--success-foreground);
+        }
+        .chip.timelapse {
+          border-left: var(--thick-border) solid var(--border-color);
+        }
+        .chip.placeholder {
+          border-left: var(--thick-border) solid var(--border-color);
+        }
+        .chip.error iron-icon {
+          color: var(--error-foreground);
+        }
+        .chip.warning iron-icon {
+          color: var(--warning-foreground);
+        }
+        .chip.info-outline iron-icon {
+          color: var(--info-foreground);
+        }
+        .chip.check-circle-outline iron-icon {
+          color: var(--success-foreground);
+        }
+        /* Additional 'div' for increased specificity. */
+        div.chip.selected {
+          border: 1px solid var(--selected-foreground);
+          background-color: var(--selected-background);
+          padding-left: calc(var(--spacing-m) + var(--thick-border) - 1px);
+        }
+        div.chip.selected iron-icon {
+          color: var(--selected-foreground);
+        }
+      `,
+    ];
+  }
+
+  @property()
+  run!: CheckRun;
+
+  @property()
+  selected = false;
+
+  render() {
+    const icon = this.selected ? 'check-circle' : iconForRun(this.run);
+    const classes = {chip: true, [icon]: true, selected: this.selected};
+
+    return html`
+      <div @click="${this._handleChipClick}" class="${classMap(classes)}">
+        <iron-icon icon="gr-icons:${icon}"></iron-icon>
+        <span>${this.run.checkName}</span>
+      </div>
+    `;
+  }
+
+  _handleChipClick(e: MouseEvent) {
+    e.stopPropagation();
+    e.preventDefault();
+    fireRunSelected(this, this.run.checkName);
+  }
 }
 
 @customElement('gr-checks-runs')
@@ -46,6 +151,8 @@
   @property()
   runs: CheckRun[] = [];
 
+  private selectedRuns = new Set<string>();
+
   constructor() {
     super();
     this.subscribe('runs', allRuns$);
@@ -63,43 +170,6 @@
           padding-top: var(--spacing-l);
           text-transform: capitalize;
         }
-        .runChip {
-          font-weight: var(--font-weight-bold);
-          border: 1px solid var(--border-color);
-          border-radius: var(--border-radius);
-          padding: var(--spacing-s) var(--spacing-m);
-          margin-top: var(--spacing-s);
-        }
-        .runChip.error {
-          border-left: 6px solid var(--error-foreground);
-        }
-        .runChip.warning {
-          border-left: 6px solid var(--warning-foreground);
-        }
-        .runChip.info-outline {
-          border-left: 6px solid var(--info-foreground);
-        }
-        .runChip.check-circle {
-          border-left: 6px solid var(--success-foreground);
-        }
-        .runChip.timelapse {
-          border-left: 6px solid var(--border-color);
-        }
-        .runnable .runChip.placeholder iron-icon {
-          display: none;
-        }
-        .runChip.error iron-icon {
-          color: var(--error-foreground);
-        }
-        .runChip.warning iron-icon {
-          color: var(--warning-foreground);
-        }
-        .runChip.info-outline iron-icon {
-          color: var(--info-foreground);
-        }
-        .runChip.check-circle iron-icon {
-          color: var(--success-foreground);
-        }
         .testing {
           margin-top: var(--spacing-xxl);
           color: var(--deemphasized-text-color);
@@ -175,14 +245,34 @@
     return html`
       <div class="${status.toLowerCase()}">
         <h3 class="statusHeader heading-3">${status.toLowerCase()}</h3>
-        ${runs.map(renderRun)}
+        ${runs.map(run => this.renderRun(run))}
       </div>
     `;
   }
+
+  renderRun(run: CheckRun) {
+    const selected = this.selectedRuns.has(run.checkName);
+    return html`<gr-checks-run
+      .run="${run}"
+      .selected="${selected}"
+      @run-selected="${this.handleRunSelected}"
+    ></gr-checks-run>`;
+  }
+
+  handleRunSelected(e: RunSelectedEvent) {
+    const checkName = e.detail.checkName;
+    if (this.selectedRuns.has(checkName)) {
+      this.selectedRuns.delete(checkName);
+    } else {
+      this.selectedRuns.add(checkName);
+    }
+    this.requestUpdate();
+  }
 }
 
 declare global {
   interface HTMLElementTagNameMap {
+    'gr-checks-run': GrChecksRun;
     'gr-checks-runs': GrChecksRuns;
   }
 }
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index 71d92d0..264034c 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -33,7 +33,11 @@
 import {ErrorType, FixIronA11yAnnouncer} from '../../../types/types';
 import {AccountId} from '../../../types/common';
 import {EventType} from '../../../utils/event-util';
-import {NetworkErrorEvent, ServerErrorEvent} from '../../../types/events';
+import {
+  NetworkErrorEvent,
+  ServerErrorEvent,
+  ShowAlertEvent,
+} from '../../../types/events';
 
 const HIDE_ALERT_TIMEOUT_MS = 5000;
 const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
@@ -274,12 +278,14 @@
     return err;
   }
 
-  _handleShowAlert(e: CustomEvent) {
+  _handleShowAlert(e: ShowAlertEvent) {
     this._showAlert(
       e.detail.message,
       e.detail.action,
       e.detail.callback,
-      e.detail.dismissOnNavigation
+      e.detail.dismissOnNavigation,
+      undefined,
+      e.detail.showDismiss
     );
   }
 
@@ -299,7 +305,8 @@
     actionText?: string,
     actionCallback?: () => void,
     dismissOnNavigation?: boolean,
-    type?: ErrorType
+    type?: ErrorType,
+    showDismiss?: boolean
   ) {
     if (this._alertElement) {
       // check priority before hiding
@@ -317,7 +324,7 @@
         HIDE_ALERT_TIMEOUT_MS
       );
     }
-    const el = this._createToastAlert();
+    const el = this._createToastAlert(showDismiss);
     el.show(text, actionText, actionCallback);
     this._alertElement = el;
     this.fire('iron-announce', {text: `Alert: ${text}`}, {bubbles: true});
@@ -363,9 +370,10 @@
     }
   }
 
-  _createToastAlert() {
+  _createToastAlert(showDismiss?: boolean) {
     const el = document.createElement('gr-alert');
     el.toast = true;
+    el.showDismiss = !!showDismiss;
     return el;
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index 5cef814..3f8fe6b 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -31,6 +31,7 @@
 import {hasOwnProperty} from '../../../utils/common-util';
 import {fireEvent} from '../../../utils/event-util';
 import {isInvolved} from '../../../utils/change-util';
+import {ShowAlertEventDetail} from '../../../types/events';
 
 @customElement('gr-account-label')
 export class GrAccountLabel extends GestureEventListeners(
@@ -213,7 +214,7 @@
     if (!this.account._account_id) return;
 
     this.dispatchEvent(
-      new CustomEvent('show-alert', {
+      new CustomEvent<ShowAlertEventDetail>('show-alert', {
         detail: {
           message: 'Saving attention set update ...',
           dismissOnNavigation: true,
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
index e5806f0..a0fddcd 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
@@ -62,6 +62,9 @@
   @property({type: Boolean})
   _hideActionButton?: boolean;
 
+  @property({type: Boolean})
+  showDismiss = false;
+
   @property()
   _boundTransitionEndHandler?: (
     this: HTMLElement,
@@ -105,6 +108,10 @@
     }
   }
 
+  _handleDismissTap() {
+    this.hide();
+  }
+
   _hasZeroTransitionDuration() {
     const style = window.getComputedStyle(this);
     // transitionDuration is always given in seconds.
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.ts
index d2aed40..b66a1dd 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.ts
@@ -74,6 +74,10 @@
       hidden$="[[_hideActionButton]]"
       on-click="_handleActionTap"
       >[[actionText]]</gr-button
+    ><template is="dom-if" if="[[showDismiss]]"
+      ><gr-button link="" class="action" on-click="_handleDismissTap"
+        >Dismiss</gr-button
+      ></template
     >
   </div>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index 2537c1a..e825d21 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -25,6 +25,8 @@
 import {customElement, property} from '@polymer/decorators';
 import {htmlTemplate} from './gr-editable-content_html';
 import {fireAlert, fireEvent} from '../../../utils/event-util';
+import {appContext} from '../../../services/app-context';
+import {KnownExperimentId} from '../../../services/flags/flags';
 
 const RESTORED_MESSAGE = 'Content restored from a previous edit.';
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -67,7 +69,7 @@
   @property({type: Boolean, reflectToAttribute: true})
   disabled = false;
 
-  @property({type: Boolean, observer: '_editingChanged'})
+  @property({type: Boolean, observer: '_editingChanged', notify: true})
   editing = false;
 
   @property({type: Boolean})
@@ -77,6 +79,29 @@
   @property({type: String})
   storageKey?: string;
 
+  /** If false, then the "Show more" button was used to expand. */
+  @property({type: Boolean})
+  _commitCollapsed = true;
+
+  @property({type: Boolean})
+  commitCollapsible = true;
+
+  @property({
+    type: Boolean,
+    computed:
+      '_computeHideShowAllContainer(hideEditCommitMessage, _hideShowAllButton, editing)',
+  })
+  _hideShowAllContainer = false;
+
+  @property({
+    type: Boolean,
+    computed: '_computeHideShowAllButton(commitCollapsible, editing)',
+  })
+  _hideShowAllButton = false;
+
+  @property({type: Boolean})
+  hideEditCommitMessage?: boolean;
+
   @property({
     type: Boolean,
     computed: '_computeSaveDisabled(disabled, content, _newContent)',
@@ -86,8 +111,21 @@
   @property({type: String, observer: '_newContentChanged'})
   _newContent?: string;
 
+  @property({type: Boolean})
+  _isNewChangeSummaryUiEnabled = false;
+
   private readonly storage = new GrStorage();
 
+  private readonly flagsService = appContext.flagsService;
+
+  /** @override */
+  ready() {
+    super.ready();
+    this._isNewChangeSummaryUiEnabled = this.flagsService.isEnabled(
+      KnownExperimentId.NEW_CHANGE_SUMMARY_UI
+    );
+  }
+
   _contentChanged() {
     /* A changed content means that either a different change has been loaded
      * or new content was saved. Either way, let's reset the component.
@@ -186,4 +224,37 @@
     this.editing = false;
     fireEvent(this, 'editable-content-cancel');
   }
+
+  _computeCollapseText(collapsed: boolean) {
+    return collapsed ? 'Show all' : 'Show less';
+  }
+
+  _toggleCommitCollapsed() {
+    this._commitCollapsed = !this._commitCollapsed;
+    if (this._commitCollapsed) {
+      window.scrollTo(0, 0);
+    }
+  }
+
+  _computeHideShowAllContainer(
+    hideEditCommitMessage?: boolean,
+    _hideShowAllButton?: boolean,
+    editing?: boolean
+  ) {
+    if (editing) return false;
+    return _hideShowAllButton && hideEditCommitMessage;
+  }
+
+  _computeHideShowAllButton(commitCollapsible?: boolean, editing?: boolean) {
+    return !commitCollapsible || editing;
+  }
+
+  _computeCommitMessageCollapsed(collapsed?: boolean, collapsible?: boolean) {
+    return collapsible && collapsed;
+  }
+
+  _handleEditCommitMessage() {
+    this.editing = true;
+    this.focusTextarea();
+  }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
index fa18761..6605394 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
@@ -31,13 +31,19 @@
       box-shadow: var(--elevation-level-1);
       padding: var(--spacing-m);
     }
-    :host([collapsed]) .viewer {
+    :host([collapsed]) .viewer,
+    .viewer[collapsed] {
       max-height: var(--collapsed-max-height, 300px);
       overflow: hidden;
     }
+    .editor.new-change-summary-true iron-autogrow-textarea,
+    .viewer.new-change-summary-true {
+      min-height: 160px;
+    }
     .editor iron-autogrow-textarea {
       background-color: var(--view-background-color);
       width: 100%;
+      display: block;
 
       /* You have to also repeat everything from shared-styles here, because
            you can only *replace* --iron-autogrow-textarea vars as a whole. */
@@ -52,23 +58,103 @@
       display: flex;
       justify-content: space-between;
     }
+    .show-all-container {
+      background-color: var(--view-background-color);
+      display: flex;
+      justify-content: flex-end;
+      margin-bottom: 8px;
+      border-top-width: 1px;
+      border-top-style: solid;
+      border-radius: 0 0 4px 4px;
+      border-color: var(--border-color);
+      box-shadow: var(--elevation-level-1);
+    }
+    .show-all-container .show-all-button {
+      margin-right: auto;
+    }
+    .show-all-container iron-icon {
+      color: inherit;
+      --iron-icon-height: 18px;
+      --iron-icon-width: 18px;
+    }
+    .cancel-button {
+      margin-right: var(--spacing-l);
+    }
+    .save-button {
+      margin-right: var(--spacing-xs);
+    }
   </style>
-  <div class="viewer" hidden$="[[editing]]">
+  <div
+    class$="viewer new-change-summary-[[_isNewChangeSummaryUiEnabled]]"
+    hidden$="[[editing]]"
+    collapsed$="[[_computeCommitMessageCollapsed(_commitCollapsed, commitCollapsible)]]"
+  >
     <slot></slot>
   </div>
-  <div class="editor" hidden$="[[!editing]]">
-    <iron-autogrow-textarea
-      autocomplete="on"
-      bind-value="{{_newContent}}"
-      disabled="[[disabled]]"
-    ></iron-autogrow-textarea>
-    <div class="editButtons">
-      <gr-button primary="" on-click="_handleSave" disabled="[[_saveDisabled]]"
-        >Save</gr-button
-      >
-      <gr-button on-click="_handleCancel" disabled="[[disabled]]"
-        >Cancel</gr-button
-      >
+  <div
+    class$="editor new-change-summary-[[_isNewChangeSummaryUiEnabled]]"
+    hidden$="[[!editing]]"
+  >
+    <div>
+      <iron-autogrow-textarea
+        autocomplete="on"
+        bind-value="{{_newContent}}"
+        disabled="[[disabled]]"
+      ></iron-autogrow-textarea>
+      <div class="editButtons" hidden$="[[_isNewChangeSummaryUiEnabled]]">
+        <gr-button
+          primary=""
+          on-click="_handleSave"
+          disabled="[[_saveDisabled]]"
+          >Save</gr-button
+        >
+        <gr-button on-click="_handleCancel" disabled="[[disabled]]"
+          >Cancel</gr-button
+        >
+      </div>
     </div>
   </div>
+  <template is="dom-if" if="[[_isNewChangeSummaryUiEnabled]]">
+    <div class="show-all-container" hidden$="[[_hideShowAllContainer]]">
+      <gr-button
+        link=""
+        class="show-all-button"
+        on-click="_toggleCommitCollapsed"
+        hidden$="[[_hideShowAllButton]]"
+        ><iron-icon
+          icon="gr-icons:expand-more"
+          hidden$="[[!_commitCollapsed]]"
+        ></iron-icon
+        ><iron-icon
+          icon="gr-icons:expand-less"
+          hidden$="[[_commitCollapsed]]"
+        ></iron-icon>
+        [[_computeCollapseText(_commitCollapsed)]]
+      </gr-button>
+      <gr-button
+        link=""
+        class="edit-commit-message"
+        title="Edit commit message"
+        on-click="_handleEditCommitMessage"
+        hidden$="[[hideEditCommitMessage]]"
+        ><iron-icon icon="gr-icons:edit"></iron-icon> Edit</gr-button
+      >
+      <div class="editButtons" hidden$="[[!editing]]">
+        <gr-button
+          link=""
+          class="cancel-button"
+          on-click="_handleCancel"
+          disabled="[[disabled]]"
+          >Cancel</gr-button
+        >
+        <gr-button
+          class="save-button"
+          primary=""
+          on-click="_handleSave"
+          disabled="[[_saveDisabled]]"
+          >Save</gr-button
+        >
+      </div>
+    </div>
+  </template>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
index a0481ae..b99b119 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
@@ -105,7 +105,7 @@
 
       assert.equal(element._newContent, 'stored content');
       assert.isTrue(dispatchSpy.called);
-      assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert');
+      assert.equal(dispatchSpy.firstCall.args[0].type, 'show-alert');
     });
 
     test('editing toggled to true, has no stored data', () => {
@@ -114,7 +114,7 @@
       element.editing = true;
 
       assert.equal(element._newContent, 'current content');
-      assert.isFalse(dispatchSpy.called);
+      assert.equal(dispatchSpy.firstCall.args[0].type, 'editing-changed');
     });
 
     test('edits are cached', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
index 78b6cda..94f99d3 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
@@ -28,12 +28,7 @@
   pushScrollLock,
   removeScrollLock,
 } from '@polymer/iron-overlay-behavior/iron-scroll-manager';
-
-interface ShowAlertEventDetail {
-  message: string;
-  dismissOnNavigation?: boolean;
-}
-
+import {ShowAlertEventDetail} from '../../../types/events';
 interface ReloadEventDetail {
   clearPatchset?: boolean;
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index e499b8b..0a3ef5b 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -79,8 +79,10 @@
       <g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path></g>
       <!-- This is a custom PolyGerrit SVG -->
       <g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
-      <!-- This SVG is a copy from material.io https://material.io/icons/#check-circle-->
-      <g id="check-circle"><path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none"/><path d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></g>
+      <!-- This SVG is a copy from material.io https://material.io/icons/#check_circle-->
+      <g id="check-circle"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></g>
+      <!-- This SVG is a copy from material.io https://material.io/icons/#check_circle_outline-->
+      <g id="check-circle-outline"><path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none"/><path d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></g>
       <!-- This is a custom PolyGerrit SVG -->
       <g id="robot"><path d="M4.137453,5.61015591 L4.54835569,1.5340419 C4.5717665,1.30180904 4.76724872,1.12504213 5.00065859,1.12504213 C5.23327176,1.12504213 5.42730868,1.30282046 5.44761309,1.53454578 L5.76084628,5.10933916 C6.16304484,5.03749412 6.57714381,5 7,5 L17,5 C20.8659932,5 24,8.13400675 24,12 L24,15.1250421 C24,18.9910354 20.8659932,22.1250421 17,22.1250421 L7,22.1250421 C3.13400675,22.1250421 2.19029351e-15,18.9910354 0,15.1250421 L0,12 C-3.48556243e-16,9.15382228 1.69864167,6.70438358 4.137453,5.61015591 Z M5.77553049,6.12504213 C3.04904264,6.69038358 1,9.10590202 1,12 L1,15.1250421 C1,18.4387506 3.6862915,21.1250421 7,21.1250421 L17,21.1250421 C20.3137085,21.1250421 23,18.4387506 23,15.1250421 L23,12 C23,8.6862915 20.3137085,6 17,6 L7,6 C6.60617231,6 6.2212068,6.03794347 5.84855971,6.11037415 L5.84984496,6.12504213 L5.77553049,6.12504213 Z M6.93003717,6.95027711 L17.1232083,6.95027711 C19.8638332,6.95027711 22.0855486,9.17199258 22.0855486,11.9126175 C22.0855486,14.6532424 19.8638332,16.8749579 17.1232083,16.8749579 L6.93003717,16.8749579 C4.18941226,16.8749579 1.9676968,14.6532424 1.9676968,11.9126175 C1.9676968,9.17199258 4.18941226,6.95027711 6.93003717,6.95027711 Z M7.60124392,14.0779303 C9.03787127,14.0779303 10.2024878,12.9691885 10.2024878,11.6014862 C10.2024878,10.2337839 9.03787127,9.12504213 7.60124392,9.12504213 C6.16461657,9.12504213 5,10.2337839 5,11.6014862 C5,12.9691885 6.16461657,14.0779303 7.60124392,14.0779303 Z M16.617997,14.1098288 C18.0638768,14.1098288 19.2359939,12.9939463 19.2359939,11.6174355 C19.2359939,10.2409246 18.0638768,9.12504213 16.617997,9.12504213 C15.1721172,9.12504213 14,10.2409246 14,11.6174355 C14,12.9939463 15.1721172,14.1098288 16.617997,14.1098288 Z M9.79751216,18.1250421 L15,18.1250421 L15,19.1250421 C15,19.6773269 14.5522847,20.1250421 14,20.1250421 L10.7975122,20.1250421 C10.2452274,20.1250421 9.79751216,19.6773269 9.79751216,19.1250421 L9.79751216,18.1250421 Z"></path></g>
       <!-- This is a custom PolyGerrit SVG -->
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
index ffdf710..2149fe4 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
@@ -16,6 +16,7 @@
  */
 
 import {RevisionInfo, ChangeInfo, RequestPayload} from '../../../types/common';
+import {ShowAlertEventDetail} from '../../../types/events';
 import {PluginApi} from '../../plugins/gr-plugin-types';
 import {UIActionInfo} from './gr-change-actions-js-api';
 
@@ -117,7 +118,7 @@
       .then(onSuccess)
       .catch((error: unknown) => {
         document.dispatchEvent(
-          new CustomEvent('show-alert', {
+          new CustomEvent<ShowAlertEventDetail>('show-alert', {
             detail: {
               message: `Plugin network error: ${error}`,
             },
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index bdca0ed..aacef0e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -27,6 +27,7 @@
 import {PluginApi} from '../../plugins/gr-plugin-types';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {hasOwnProperty} from '../../../utils/common-util';
+import {ShowAlertEventDetail} from '../../../types/events';
 
 enum PluginState {
   /** State that indicates the plugin is pending to be loaded. */
@@ -248,7 +249,7 @@
   _failToLoad(message: string, pluginUrl?: string) {
     // Show an alert with the error
     document.dispatchEvent(
-      new CustomEvent('show-alert', {
+      new CustomEvent<ShowAlertEventDetail>('show-alert', {
         detail: {
           message: `Plugin install error: ${message} from ${pluginUrl}`,
         },
diff --git a/polygerrit-ui/app/services/checks/checks-util.ts b/polygerrit-ui/app/services/checks/checks-util.ts
index 344983f..86b9b47 100644
--- a/polygerrit-ui/app/services/checks/checks-util.ts
+++ b/polygerrit-ui/app/services/checks/checks-util.ts
@@ -46,7 +46,7 @@
   switch (status) {
     // Note that this is only for COMPLETED without results!
     case RunStatus.COMPLETED:
-      return 'check-circle';
+      return 'check-circle-outline';
     case RunStatus.RUNNABLE:
       return 'placeholder';
     case RunStatus.RUNNING:
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 5edbf87..bf62d0d0 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -31,5 +31,4 @@
   CI_REBOOT_CHECKS = 'UiFeature__ci_reboot_checks',
   NEW_CHANGE_SUMMARY_UI = 'UiFeature__new_change_summary_ui',
   PORTING_COMMENTS = 'UiFeature__porting_comments',
-  AUTO_RELOAD_DASHBOARD = 'UiFeature__auto_reload_dashboard',
 }
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 15099e2..c3b0681 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -74,6 +74,8 @@
     --warning-background: var(--orange-50);
     --info-foreground: var(--blue-700);
     --info-background: var(--blue-50);
+    --selected-foreground: var(--blue-700);
+    --selected-background: var(--blue-50);
     --info-deemphasized-foreground: var(--gray-300);
     --info-deemphasized-background: var(--gray-50);
     --success-foreground: var(--green-700);
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index e9b2900..163c760 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -180,6 +180,10 @@
 
 export interface ShowAlertEventDetail {
   message: string;
+  dismissOnNavigation?: boolean;
+  showDismiss?: boolean;
+  action?: string;
+  callback?: () => void;
 }
 
 export type ShowAlertEvent = CustomEvent<ShowAlertEventDetail>;
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 5b332ea..ad76b79 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -47,6 +47,26 @@
 }
 
 /**
+ * Throws an error with the provided error message if the condition is false.
+ */
+export function check(
+  condition: boolean,
+  errorMessage: string
+): asserts condition {
+  if (!condition) throw new Error(errorMessage);
+}
+
+/**
+ * Throws an error if the property is not defined.
+ */
+export function checkProperty(
+  condition: boolean,
+  propertyName: string
+): asserts condition {
+  check(condition, `missing required property '${propertyName}'`);
+}
+
+/**
  * Returns true, if both sets contain the same members.
  */
 export function areSetsEqual<T>(a: Set<T>, b: Set<T>): boolean {
diff --git a/proto/BUILD b/proto/BUILD
index 57be265..7aa761d 100644
--- a/proto/BUILD
+++ b/proto/BUILD
@@ -4,6 +4,7 @@
 proto_library(
     name = "cache_proto",
     srcs = ["cache.proto"],
+    deps = [":entities_proto"],
 )
 
 java_proto_library(
diff --git a/proto/cache.proto b/proto/cache.proto
index e827956..4fd037d 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -18,7 +18,9 @@
 
 option java_package = "com.google.gerrit.server.cache.proto";
 
-// Serialized form of com.google.gerrit.server.change.CHangeKindCacheImpl.Key.
+import "proto/entities.proto";
+
+// Serialized form of com.google.gerrit.server.change.ChangeKindCacheImpl.Key.
 // Next ID: 4
 message ChangeKindKeyProto {
   bytes prior = 1;
@@ -140,11 +142,9 @@
 
   repeated string hashtag = 5;
 
-  // Raw PatchSet proto as produced by PatchSetProtoConverter.
-  repeated bytes patch_set = 6;
+  repeated devtools.gerritcodereview.PatchSet patch_set = 6;
 
-  // Raw PatchSetApproval proto as produced by PatchSetApprovalProtoConverter.
-  repeated bytes approval = 7;
+  repeated devtools.gerritcodereview.PatchSetApproval approval = 7;
 
   // Next ID: 4
   message ReviewerSetEntryProto {
@@ -184,8 +184,7 @@
   // com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord.
   repeated string submit_record = 14;
 
-  // Raw ChangeMessage proto as produced by ChangeMessageProtoConverter.
-  repeated bytes change_message = 15;
+  repeated devtools.gerritcodereview.ChangeMessage change_message = 15;
 
   // JSON produced from com.google.gerrit.entities.Comment.
   repeated string published_comment = 16;
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 3eb1e4b..e06fba3 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -142,34 +142,24 @@
         sha1 = GUAVA_BIN_SHA1,
     )
 
-    GUICE_VERS = "4.2.3"
+    GUICE_VERS = "5.0.0-BETA-1"
 
-    GUICE_LIBRARY_SHA256 = "5168f5e7383f978c1b4154ac777b78edd8ac214bb9f9afdb92921c8d156483d3"
-
-    http_file(
-        name = "guice-library-no-aop",
-        canonical_id = "guice-library-no-aop-" + GUICE_VERS + ".jar-" + GUICE_LIBRARY_SHA256,
-        downloaded_file_path = "guice-library-no-aop.jar",
-        sha256 = GUICE_LIBRARY_SHA256,
-        urls = [
-            "https://repo1.maven.org/maven2/com/google/inject/guice/" +
-            GUICE_VERS +
-            "/guice-" +
-            GUICE_VERS +
-            "-no_aop.jar",
-        ],
+    maven_jar(
+        name = "guice-library",
+        artifact = "com.google.inject:guice:" + GUICE_VERS,
+        sha1 = "c5572be8a8b75ea50e0fdf54fa1f75a3141ab936",
     )
 
     maven_jar(
         name = "guice-assistedinject",
         artifact = "com.google.inject.extensions:guice-assistedinject:" + GUICE_VERS,
-        sha1 = "acbfddc556ee9496293ed1df250cc378f331d854",
+        sha1 = "4d06eba0e08151b52d9e25a14e4f01eedf998bc3",
     )
 
     maven_jar(
         name = "guice-servlet",
         artifact = "com.google.inject.extensions:guice-servlet:" + GUICE_VERS,
-        sha1 = "8d6e7e35eac4fb5e7df19c55b3bc23fa51b10a11",
+        sha1 = "373b9a4f1b6683d9a991410660d2c9adb9f06737",
     )
 
     # Keep this version of Soy synchronized with the version used in Gitiles.