Merge "Highlight the entire line in comment context for line comments"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 3b9320c..3a5fb96 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -564,6 +564,12 @@
   }
 ----
 
+Historical state of the change can be retrieved by specifying the
+`meta=SHA1` parameter. This will use a historical NoteDb snapshot to
+populate ChangeInfo. If the SHA1 is not reachable as a NoteDb state,
+status code 412 is returned.
+
+
 [[get-change-detail]]
 === Get Change Detail
 --
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index a461b59..c7aedb6 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -47,6 +47,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
@@ -121,6 +122,7 @@
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
 
 /**
  * Produces {@link ChangeInfo} (which is serialized to JSON afterwards) from {@link ChangeData}.
@@ -283,6 +285,11 @@
     return format(changeDataFactory.create(change));
   }
 
+  public ChangeInfo format(Change change, @Nullable ObjectId metaRevId) {
+    ChangeNotes notes = notesFactory.createChecked(change.getProject(), change.getId(), metaRevId);
+    return format(changeDataFactory.create(notes));
+  }
+
   public ChangeInfo format(ChangeData cd) {
     return format(cd, Optional.empty(), true, getPluginInfos(cd));
   }
@@ -325,9 +332,13 @@
   }
 
   public ChangeInfo format(Project.NameKey project, Change.Id id) {
+    return format(project, id, null);
+  }
+
+  public ChangeInfo format(Project.NameKey project, Change.Id id, @Nullable ObjectId metaRevId) {
     ChangeNotes notes;
     try {
-      notes = notesFactory.createChecked(project, id);
+      notes = notesFactory.createChecked(project, id, metaRevId);
     } catch (StorageException e) {
       if (!has(CHECK)) {
         throw e;
diff --git a/java/com/google/gerrit/server/comment/CommentContextLoader.java b/java/com/google/gerrit/server/comment/CommentContextLoader.java
index 63bb8d0..c93f4b1 100644
--- a/java/com/google/gerrit/server/comment/CommentContextLoader.java
+++ b/java/com/google/gerrit/server/comment/CommentContextLoader.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.CommentContext;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.ContextLineInfo;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.ComparisonType;
@@ -158,12 +157,10 @@
 
   private static CommentContext createContext(Text src, Range commentRange, int contextPadding) {
     if (commentRange.start() < 1 || commentRange.end() - 1 > src.size()) {
-      throw new StorageException(
-          "Invalid comment range "
-              + commentRange
-              + ". Text only contains "
-              + src.size()
-              + " lines.");
+      // TODO(ghareeb): We should throw an exception in this case. See
+      // https://bugs.chromium.org/p/gerrit/issues/detail?id=14102 which is an example where the
+      // diff contains an extra line not in the original file.
+      return CommentContext.empty();
     }
     commentRange = adjustRange(commentRange, contextPadding, src.size());
     ImmutableMap.Builder<Integer, String> context =
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 9b5b4d4..a7c7757 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -37,6 +37,8 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 /** View of contents at a single ref related to some change. * */
 public abstract class AbstractChangeNotes<T> {
@@ -118,9 +120,14 @@
   private ObjectId revision;
   private boolean loaded;
 
-  protected AbstractChangeNotes(Args args, Change.Id changeId) {
+  protected AbstractChangeNotes(Args args, Change.Id changeId, @Nullable ObjectId metaSha1) {
     this.args = requireNonNull(args);
     this.changeId = requireNonNull(changeId);
+    this.revision = metaSha1;
+  }
+
+  protected AbstractChangeNotes(Args args, Change.Id changeId) {
+    this(args, changeId, null);
   }
 
   public Change.Id getChangeId() {
@@ -144,7 +151,7 @@
         Repository repo = args.repoManager.openRepository(getProjectName());
         // Call openHandle even if reading is disabled, to trigger
         // auto-rebuilding before this object may get passed to a ChangeUpdate.
-        LoadHandle handle = openHandle(repo)) {
+        LoadHandle handle = openHandle(repo, revision)) {
       revision = handle.id();
       onLoad(handle);
       loaded = true;
@@ -168,13 +175,17 @@
    * @param repo open repository.
    * @return handle for reading the entity.
    * @throws NoSuchChangeException change does not exist.
+   * @throws MissingMetaObjectException specified SHA1 isn't reachable from meta branch.
    * @throws IOException a repo-level error occurred.
    */
-  protected LoadHandle openHandle(Repository repo) throws NoSuchChangeException, IOException {
-    return openHandle(repo, readRef(repo));
-  }
+  protected LoadHandle openHandle(Repository repo, @Nullable ObjectId id)
+      throws NoSuchChangeException, IOException, MissingMetaObjectException {
+    if (id == null) {
+      id = readRef(repo);
+    } else {
+      verifyMetaId(repo, id);
+    }
 
-  protected LoadHandle openHandle(Repository repo, ObjectId id) {
     return new LoadHandle(repo, id);
   }
 
@@ -215,4 +226,20 @@
   protected final T self() {
     return (T) this;
   }
+
+  private void verifyMetaId(Repository repo, ObjectId id)
+      throws IOException, MissingMetaObjectException {
+    try (RevWalk rw = new RevWalk(repo)) {
+      Ref ref = repo.getRefDatabase().exactRef(getRefName());
+      RevCommit tip = rw.parseCommit(ref.getObjectId());
+      rw.markStart(tip);
+      for (RevCommit rev : rw) {
+        if (id.equals(rev)) {
+          return;
+        }
+      }
+    }
+
+    throw new MissingMetaObjectException(id.getName() + " not reachable from " + getRefName());
+  }
 }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index cf09ff3..77d7bc0 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -114,9 +114,14 @@
       return createChecked(c.getProject(), c.getId());
     }
 
-    public ChangeNotes createChecked(Project.NameKey project, Change.Id changeId) {
+    public ChangeNotes createChecked(
+        Project.NameKey project, Change.Id changeId, @Nullable ObjectId metaRevId) {
       Change change = newChange(project, changeId);
-      return new ChangeNotes(args, change, true, null).load();
+      return new ChangeNotes(args, change, true, null, metaRevId).load();
+    }
+
+    public ChangeNotes createChecked(Project.NameKey project, Change.Id changeId) {
+      return createChecked(project, changeId, null);
     }
 
     public static Change newChange(Project.NameKey project, Change.Id changeId) {
@@ -333,14 +338,23 @@
   private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals;
   private ImmutableSet<Comment.Key> commentKeys;
 
-  @VisibleForTesting
-  public ChangeNotes(Args args, Change change, boolean shouldExist, @Nullable RefCache refs) {
-    super(args, change.getId());
+  public ChangeNotes(
+      Args args,
+      Change change,
+      boolean shouldExist,
+      @Nullable RefCache refs,
+      @Nullable ObjectId metaSha1) {
+    super(args, change.getId(), metaSha1);
     this.change = new Change(change);
     this.shouldExist = shouldExist;
     this.refs = refs;
   }
 
+  @VisibleForTesting
+  public ChangeNotes(Args args, Change change, boolean shouldExist, @Nullable RefCache refs) {
+    this(args, change, shouldExist, refs, null);
+  }
+
   public Change getChange() {
     return change;
   }
diff --git a/java/com/google/gerrit/server/notedb/MissingMetaObjectException.java b/java/com/google/gerrit/server/notedb/MissingMetaObjectException.java
new file mode 100644
index 0000000..ffe9fa8
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/MissingMetaObjectException.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2021 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.notedb;
+
+/** Separate exception type to throw if requested meta SHA1 is not available. */
+public class MissingMetaObjectException extends RuntimeException {
+  MissingMetaObjectException(String msg) {
+    super(msg);
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
index 20fd675..842ed2a 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
@@ -93,10 +93,7 @@
       }
       IdentifiedUser deletedAssigneeUser = userFactory.create(currentAssigneeId);
       deletedAssignee = deletedAssigneeUser.state();
-      // noteDb
       update.removeAssignee();
-      // reviewDb
-      change.setAssignee(null);
       addMessage(ctx, update, deletedAssigneeUser);
       return true;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/GetChange.java b/java/com/google/gerrit/server/restapi/change/GetChange.java
index f28d2b3..2f1c61e 100644
--- a/java/com/google/gerrit/server/restapi/change/GetChange.java
+++ b/java/com/google/gerrit/server/restapi/change/GetChange.java
@@ -16,12 +16,15 @@
 
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Streams;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.PreconditionFailedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.DynamicOptions;
@@ -31,12 +34,15 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.PluginDefinedAttributesFactories;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.notedb.MissingMetaObjectException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.lib.ObjectId;
 import org.kohsuke.args4j.Option;
 
 public class GetChange
@@ -53,6 +59,9 @@
     options.add(o);
   }
 
+  @Option(name = "--meta", usage = "NoteDb meta SHA1")
+  String metaRevId = "";
+
   @Option(name = "-O", usage = "Output option flags, in hex")
   void setOptionFlagsHex(String hex) {
     options.addAll(ListOption.fromBits(ListChangesOption.class, Integer.parseInt(hex, 16)));
@@ -75,14 +84,35 @@
   }
 
   @Override
-  public Response<ChangeInfo> apply(ChangeResource rsrc) {
-    return Response.withMustRevalidate(newChangeJson().format(rsrc));
+  public Response<ChangeInfo> apply(ChangeResource rsrc)
+      throws BadRequestException, PreconditionFailedException {
+    try {
+      return Response.withMustRevalidate(newChangeJson().format(rsrc.getChange(), getMetaRevId()));
+    } catch (MissingMetaObjectException e) {
+      throw new PreconditionFailedException(e.getMessage());
+    }
   }
 
   Response<ChangeInfo> apply(RevisionResource rsrc) {
     return Response.withMustRevalidate(newChangeJson().format(rsrc));
   }
 
+  @Nullable
+  private ObjectId getMetaRevId() throws BadRequestException {
+    if (metaRevId.isEmpty()) {
+      return null;
+    }
+
+    // It might be interesting to also allow {SHA1}^^, so callers can walk back into history
+    // without having to fetch the entire /meta ref. If we do so, we have to be careful that
+    // the error messages can't be abused to fetch hidden data.
+    try {
+      return ObjectId.fromString(metaRevId);
+    } catch (InvalidObjectIdException e) {
+      throw new BadRequestException("invalid meta SHA1: " + metaRevId, e);
+    }
+  }
+
   private ChangeJson newChangeJson() {
     return json.create(options, this::createPluginDefinedInfos);
   }
diff --git a/java/com/google/gerrit/server/restapi/change/GetDetail.java b/java/com/google/gerrit/server/restapi/change/GetDetail.java
index e31d84b..15362d5 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDetail.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.PreconditionFailedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.DynamicOptions;
@@ -58,7 +60,8 @@
   }
 
   @Override
-  public Response<ChangeInfo> apply(ChangeResource rsrc) {
+  public Response<ChangeInfo> apply(ChangeResource rsrc)
+      throws BadRequestException, PreconditionFailedException {
     return delegate.apply(rsrc);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMetaIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMetaIT.java
index 3787a35..e025c52 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMetaIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMetaIT.java
@@ -20,8 +20,19 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.inject.AbstractModule;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Test;
 
@@ -55,4 +66,85 @@
               repo.exactRef(changeMetaRef(Change.id(before._number))).getObjectId().getName());
     }
   }
+
+  @Test
+  public void ChangeInfo_metaSha1_parameter() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    gApi.changes().id(changeId).setMessage("before\n\n" + "Change-Id: " + result.getChangeId());
+    ChangeInfo before = gApi.changes().id(changeId).get();
+    gApi.changes().id(changeId).setMessage("after\n\n" + "Change-Id: " + result.getChangeId());
+    ChangeInfo after = gApi.changes().id(changeId).get();
+    assertThat(after.metaRevId).isNotEqualTo(before.metaRevId);
+
+    RestResponse resp = adminRestSession.get("/changes/" + changeId + "/?meta=" + before.metaRevId);
+    resp.assertOK();
+
+    ChangeInfo got;
+    try (JsonReader jsonReader = new JsonReader(resp.getReader())) {
+      jsonReader.setLenient(true);
+      got = newGson().fromJson(jsonReader, ChangeInfo.class);
+    }
+    assertThat(got.subject).isEqualTo(before.subject);
+  }
+
+  @Test
+  public void metaUnreachableSha1() throws Exception {
+    PushOneCommit.Result ch1 = createChange();
+    PushOneCommit.Result ch2 = createChange();
+
+    ChangeInfo info2 = gApi.changes().id(ch2.getChangeId()).get();
+
+    RestResponse resp =
+        adminRestSession.get("/changes/" + ch1.getChangeId() + "/?meta=" + info2.metaRevId);
+
+    resp.assertStatus(412);
+  }
+
+  protected static class PluginDefinedSimpleAttributeModule extends AbstractModule {
+    static class MyMetaHash extends PluginDefinedInfo {
+      String myMetaRef;
+    };
+
+    static PluginDefinedInfo newMyMetaHash(ChangeData cd) {
+      MyMetaHash mmh = new MyMetaHash();
+      mmh.myMetaRef = cd.notes().getMetaId().name();
+      return mmh;
+    }
+
+    @Override
+    public void configure() {
+      DynamicSet.bind(binder(), ChangePluginDefinedInfoFactory.class)
+          .toInstance(
+              (cds, bp, p) -> {
+                Map<Change.Id, PluginDefinedInfo> out = new HashMap<>();
+                cds.forEach(cd -> out.put(cd.getId(), newMyMetaHash(cd)));
+                return out;
+              });
+    }
+  }
+
+  @Test
+  public void pluginDefinedAttribute() throws Exception {
+    try (AutoCloseable ignored =
+        installPlugin("my-plugin", PluginDefinedSimpleAttributeModule.class)) {
+      PushOneCommit.Result result = createChange();
+      String changeId = result.getChangeId();
+      gApi.changes().id(changeId).setMessage("before\n\n" + "Change-Id: " + result.getChangeId());
+      ChangeInfo before = gApi.changes().id(changeId).get();
+      gApi.changes().id(changeId).setMessage("after\n\n" + "Change-Id: " + result.getChangeId());
+      ChangeInfo after = gApi.changes().id(changeId).get();
+
+      RestResponse resp =
+          adminRestSession.get("/changes/" + changeId + "/?meta=" + before.metaRevId);
+      resp.assertOK();
+
+      Map<String, Object> changeInfo =
+          newGson().fromJson(resp.getReader(), new TypeToken<Map<String, Object>>() {}.getType());
+      List<Object> plugins = (List<Object>) changeInfo.get("plugins");
+      Map<String, Object> myplugin = (Map<String, Object>) plugins.get(0);
+
+      assertThat(myplugin.get("my_meta_ref")).isEqualTo(before.metaRevId);
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
index adfe56d..548e3fe 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
@@ -19,11 +19,9 @@
 import static com.google.gerrit.entities.Patch.COMMIT_MSG;
 import static com.google.gerrit.entities.Patch.MERGE_LIST;
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
-import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.MoreCollectors;
-import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -148,12 +146,10 @@
         CommentsUtil.newComment(COMMIT_MSG, Side.REVISION, 100, "comment", false);
     CommentsUtil.addComments(gApi, changeId, ps1, comment);
 
-    Throwable thrown =
-        assertThrows(
-            UncheckedExecutionException.class,
-            () -> gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList());
-
-    assertThat(thrown).hasCauseThat().hasMessageThat().contains("Invalid comment range");
+    List<CommentInfo> comments =
+        gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
+    assertThat(comments).hasSize(1);
+    assertThat(comments.get(0).contextLines).isEmpty();
   }
 
   @Test
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 7ee68b7..bd110c8 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -162,20 +162,15 @@
 export declare interface DiffPreferencesInfo {
   context: number;
   ignore_whitespace: IgnoreWhitespaceType;
-  intraline_difference?: boolean;
   line_length: number;
   show_line_endings?: boolean;
   show_tabs?: boolean;
   show_whitespace_errors?: boolean;
-  skip_uncommented?: boolean;
   syntax_highlighting?: boolean;
-  auto_hide_diff_table_header?: boolean;
   tab_size: number;
   font_size: number;
   // TODO: Missing documentation
   show_file_comment_button?: boolean;
-  // TODO: Missing documentation
-  theme?: string;
 }
 
 export declare interface RenderPreferences {
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 03d5000..be502f7 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -403,12 +403,10 @@
 // (Render mode being at least one of them).
 export function createDefaultDiffPrefs(): DiffPreferencesInfo {
   return {
-    auto_hide_diff_table_header: true,
     context: 10,
     cursor_blink_rate: 0,
     font_size: 12,
     ignore_whitespace: 'IGNORE_NONE',
-    intraline_difference: true,
     line_length: 100,
     line_wrapping: false,
     show_line_endings: true,
@@ -416,7 +414,6 @@
     show_whitespace_errors: true,
     syntax_highlighting: true,
     tab_size: 8,
-    theme: 'DEFAULT',
   };
 }
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index cd17271..c5c73c5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -115,6 +115,11 @@
          commit message box. Their top border should be on the same line. */
       margin-bottom: var(--spacing-s);
     }
+    .show-all-button iron-icon {
+      color: inherit;
+      --iron-icon-height: 18px;
+      --iron-icon-width: 18px;
+    }
   </style>
   <gr-external-style id="externalStyle" name="change-metadata">
     <template is="dom-if" if="[[_isNewChangeSummaryUiEnabled]]">
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 021d7c4..72ed6e6 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
@@ -55,6 +55,7 @@
 import {notUndefined} from '../../../types/types';
 import {uniqueDefinedAvatar} from '../../../utils/account-util';
 import {PrimaryTab} from '../../../constants/constants';
+import {CommentTabState} from '../../../types/events';
 
 export enum SummaryChipStyles {
   INFO = 'info',
@@ -71,6 +72,9 @@
   @property()
   styleType = SummaryChipStyles.UNDEFINED;
 
+  @property()
+  category?: CommentTabState;
+
   static get styles() {
     return [
       sharedStyles,
@@ -132,7 +136,9 @@
   private handleClick(e: MouseEvent) {
     e.stopPropagation();
     e.preventDefault();
-    fireShowPrimaryTab(this, PrimaryTab.COMMENT_THREADS);
+    fireShowPrimaryTab(this, PrimaryTab.COMMENT_THREADS, true, {
+      commentTab: this.category,
+    });
   }
 }
 
@@ -413,12 +419,14 @@
                 No Comments</gr-summary-chip
               ><gr-summary-chip
                 styleType=${SummaryChipStyles.WARNING}
+                category=${CommentTabState.DRAFTS}
                 icon="edit"
                 ?hidden=${!draftCount}
               >
                 ${pluralize(draftCount, 'draft')}</gr-summary-chip
               ><gr-summary-chip
                 styleType=${SummaryChipStyles.WARNING}
+                category=${CommentTabState.UNRESOLVED}
                 icon="message"
                 ?hidden=${!countUnresolvedComments}
               >
@@ -433,6 +441,7 @@
                 ${countUnresolvedComments} unresolved</gr-summary-chip
               ><gr-summary-chip
                 styleType=${SummaryChipStyles.CHECK}
+                category=${CommentTabState.SHOW_ALL}
                 icon="markChatRead"
                 ?hidden=${!countResolvedComments}
                 >${countResolvedComments} resolved</gr-summary-chip
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 61006f9..7b80977 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
@@ -152,11 +152,13 @@
   ShowAlertEventDetail,
   SwitchTabEvent,
   ThreadListModifiedEvent,
+  TabState,
 } from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrMessagesList} from '../gr-messages-list/gr-messages-list';
 import {GrThreadList} from '../gr-thread-list/gr-thread-list';
 import {
+  EventType,
   fireAlert,
   fireEvent,
   firePageError,
@@ -555,6 +557,9 @@
   @property({type: Boolean})
   _isNewChangeSummaryUiEnabled = false;
 
+  @property({type: String})
+  _tabState?: TabState;
+
   restApiService = appContext.restApiService;
 
   checksService = appContext.checksService;
@@ -694,7 +699,7 @@
     this.listen(window, 'scroll', '_handleScroll');
     this.listen(document, 'visibilitychange', '_handleVisibilityChange');
 
-    this.addEventListener('show-primary-tab', e =>
+    this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
       this._setActivePrimaryTab(e)
     );
     this.addEventListener('show-secondary-tab', e =>
@@ -853,6 +858,7 @@
         this._selectedTabPluginHeader = '';
       }
     }
+    this._tabState = e.detail.tabState;
   }
 
   /**
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 7fd985a..56c1d38 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
@@ -680,6 +680,7 @@
           change="[[_change]]"
           change-num="[[_changeNum]]"
           logged-in="[[_loggedIn]]"
+          comment-tab-state="[[_tabState.commentTab]]"
           only-show-robot-comments-with-human-reply=""
           unresolved-only
           show-comment-context
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index 2d38dcc..285b73f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -1374,12 +1374,10 @@
         line_length: 100,
         cursor_blink_rate: 0,
         line_wrapping: false,
-        intraline_difference: true,
         show_line_endings: true,
         show_tabs: true,
         show_whitespace_errors: true,
         syntax_highlighting: true,
-        auto_hide_diff_table_header: true,
         theme: 'DEFAULT',
         ignore_whitespace: 'IGNORE_NONE',
       };
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
index be48038..23627ae 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
@@ -148,7 +148,7 @@
               class="${classMap({
                 ['show-when-collapsed']: showWhenCollapsedPredicate(index),
               })}"
-              .currentChange="${this._changesEqual(change, this.change)}"
+              .isCurrentChange="${this._changesEqual(change, this.change)}"
               .change="${change}"
               .href="${GerritNav.getUrlForChangeById(
                 change._number,
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 28bccc6..f0ec7cd 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
@@ -43,6 +43,8 @@
 import {fireThreadListModifiedEvent} from '../../../utils/event-util';
 import {KnownExperimentId} from '../../../services/flags/flags';
 import {appContext} from '../../../services/app-context';
+import {assertNever} from '../../../utils/common-util';
+import {CommentTabState} from '../../../types/events';
 
 interface CommentThreadWithInfo {
   thread: CommentThread;
@@ -106,6 +108,9 @@
   @property({type: Boolean})
   _isNewChangeSummaryUiEnabled = false;
 
+  @property({type: Object, observer: '_commentTabStateChange'})
+  commentTabState?: CommentTabState;
+
   flagsService = appContext.flagsService;
 
   /** @override */
@@ -501,6 +506,26 @@
   filterRobotThreadsWithoutHumanReply(threads?: CommentThread[]) {
     return threads?.filter(t => !isRobotThread(t) || hasHumanReply(t));
   }
+
+  _commentTabStateChange(
+    newValue?: CommentTabState,
+    oldValue?: CommentTabState
+  ) {
+    if (!newValue || newValue === oldValue) return;
+    switch (newValue) {
+      case CommentTabState.UNRESOLVED:
+        this._handleOnlyUnresolved();
+        break;
+      case CommentTabState.DRAFTS:
+        this._handleOnlyDrafts();
+        break;
+      case CommentTabState.SHOW_ALL:
+        this._handleAllComments();
+        break;
+      default:
+        assertNever(newValue, 'Unsupported preferred state');
+    }
+  }
 }
 
 declare global {
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 1be2da1..8b1ba25 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
@@ -101,7 +101,7 @@
             name="filterComments"
             type="radio"
             on-click="_handleOnlyUnresolved"
-            checked$="[[unresolvedOnly]]"
+            checked="[[unresolvedOnly]]"
           />
           <label for="unresolvedRadio">
             Unresolved ([[_countUnresolved(threads)]])
@@ -112,7 +112,7 @@
             name="filterComments"
             type="radio"
             on-click="_handleOnlyDrafts"
-            checked$="[[_draftsOnly]]"
+            checked="[[_draftsOnly]]"
           />
           <label for="draftsRadio">
             Drafts ([[_countDrafts(threads)]])
@@ -123,7 +123,7 @@
             name="filterComments"
             type="radio"
             on-click="_handleAllComments"
-            checked$="[[_showAllComments(_draftsOnly, unresolvedOnly)]]"
+            checked="[[_showAllComments(_draftsOnly, unresolvedOnly)]]"
           />
           <label for="all">
             All ([[_countAllThreads(threads)]])
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index ec44f92..2215d5b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -161,12 +161,10 @@
         element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
         element.isImageDiff = true;
         element.prefs = {
-          auto_hide_diff_table_header: true,
           context: 10,
           cursor_blink_rate: 0,
           font_size: 12,
           ignore_whitespace: 'IGNORE_NONE',
-          intraline_difference: true,
           line_length: 100,
           line_wrapping: false,
           show_line_endings: true,
@@ -497,12 +495,11 @@
           line_length: 100,
           cursor_blink_rate: 0,
           line_wrapping: false,
-          intraline_difference: true,
+
           show_line_endings: true,
           show_tabs: true,
           show_whitespace_errors: true,
           syntax_highlighting: true,
-          auto_hide_diff_table_header: true,
           theme: 'DEFAULT',
           ignore_whitespace: 'IGNORE_NONE',
         };
@@ -959,11 +956,10 @@
     element = basicFixture.instantiate();
     element.prefs = {
       ignore_whitespace: ignore_whitespace || 'IGNORE_ALL',
-      auto_hide_diff_table_header: true,
       context: 10,
       cursor_blink_rate: 0,
       font_size: 12,
-      intraline_difference: true,
+
       line_length: 100,
       line_wrapping: false,
       show_line_endings: true,
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
index fbd675f..7cbcaef 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
@@ -396,12 +396,10 @@
     sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
 
     return element.getDiffPreferences().then(obj => {
-      assert.equal(obj.auto_hide_diff_table_header, true);
       assert.equal(obj.context, 10);
       assert.equal(obj.cursor_blink_rate, 0);
       assert.equal(obj.font_size, 12);
       assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
-      assert.equal(obj.intraline_difference, true);
       assert.equal(obj.line_length, 100);
       assert.equal(obj.line_wrapping, false);
       assert.equal(obj.show_line_endings, true);
@@ -409,7 +407,6 @@
       assert.equal(obj.show_whitespace_errors, true);
       assert.equal(obj.syntax_highlighting, true);
       assert.equal(obj.tab_size, 8);
-      assert.equal(obj.theme, 'DEFAULT');
     });
   });
 
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index aeacd9a..95801e7 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -1680,7 +1680,6 @@
   context?: number;
   expand_all_comments?: boolean;
   ignore_whitespace: IgnoreWhitespaceType;
-  intraline_difference?: boolean;
   line_length?: number;
   manual_review?: boolean;
   retain_header?: boolean;
@@ -1688,10 +1687,8 @@
   show_tabs?: boolean;
   show_whitespace_errors?: boolean;
   skip_deleted?: boolean;
-  skip_uncommented?: boolean;
   syntax_highlighting?: boolean;
   hide_top_menu?: boolean;
-  auto_hide_diff_table_header?: boolean;
   hide_line_numbers?: boolean;
   tab_size?: number;
   font_size?: number;
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 163c760..6b05fad 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -146,6 +146,18 @@
   value?: number;
   // scroll into the tab afterwards, from custom event
   scrollIntoView?: boolean;
+  // define state of tab after opening
+  tabState?: TabState;
+}
+
+export interface TabState {
+  commentTab?: CommentTabState;
+}
+
+export enum CommentTabState {
+  UNRESOLVED = 'unresolved',
+  DRAFTS = 'drafts',
+  SHOW_ALL = 'show all',
 }
 
 export type SwitchTabEvent = CustomEvent<SwitchTabEventDetail>;
diff --git a/polygerrit-ui/app/utils/event-util.ts b/polygerrit-ui/app/utils/event-util.ts
index ee3c833..cf590e0 100644
--- a/polygerrit-ui/app/utils/event-util.ts
+++ b/polygerrit-ui/app/utils/event-util.ts
@@ -17,7 +17,7 @@
 
 import {UrlEncodedCommentId} from '../types/common';
 import {FetchRequest} from '../types/types';
-import {DialogChangeEventDetail} from '../types/events';
+import {DialogChangeEventDetail, TabState} from '../types/events';
 
 export enum EventType {
   SHOW_ALERT = 'show-alert',
@@ -27,6 +27,7 @@
   TITLE_CHANGE = 'title-change',
   THREAD_LIST_MODIFIED = 'thread-list-modified',
   DIALOG_CHANGE = 'dialog-change',
+  SHOW_PRIMARY_TAB = 'show-primary-tab',
 }
 
 export function fireEvent(target: EventTarget, type: string) {
@@ -117,10 +118,15 @@
   );
 }
 
-export function fireShowPrimaryTab(target: EventTarget, tab: string) {
+export function fireShowPrimaryTab(
+  target: EventTarget,
+  tab: string,
+  scrollIntoView?: boolean,
+  tabState?: TabState
+) {
   target.dispatchEvent(
-    new CustomEvent('show-primary-tab', {
-      detail: {tab},
+    new CustomEvent(EventType.SHOW_PRIMARY_TAB, {
+      detail: {tab, scrollIntoView, tabState},
       composed: true,
       bubbles: true,
     })