Merge "Avoid real name in change messages"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 47162c3..94c4552 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5416,7 +5416,13 @@
 login as the 'Gerrit Code Review' user, required for the link:cmd-suexec.html[suexec]
 command.
 
-The format is one Base-64 encoded public key per line.
+The format is one Base-64 encoded public key per line with optional comment, e.g.:
+----
+# Comments allowed at start of line
+AAAAC3...51R== john@example.net
+# Another comment
+AAAAB5...21S== jane@example.net
+----
 
 === Configurable Parameters
 
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index dd48b93..32edadb 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -35,34 +35,38 @@
 @Singleton
 public class ChangeMessagesUtil {
   public static final String AUTOGENERATED_TAG_PREFIX = "autogenerated:";
+  public static final String AUTOGENERATED_BY_GERRIT_TAG_PREFIX =
+      AUTOGENERATED_TAG_PREFIX + "gerrit:";
 
-  public static final String TAG_ABANDON = AUTOGENERATED_TAG_PREFIX + "gerrit:abandon";
+  public static final String TAG_ABANDON = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "abandon";
   public static final String TAG_CHERRY_PICK_CHANGE =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:cherryPickChange";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "cherryPickChange";
   public static final String TAG_DELETE_ASSIGNEE =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:deleteAssignee";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteAssignee";
   public static final String TAG_DELETE_REVIEWER =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:deleteReviewer";
-  public static final String TAG_DELETE_VOTE = AUTOGENERATED_TAG_PREFIX + "gerrit:deleteVote";
-  public static final String TAG_MERGED = AUTOGENERATED_TAG_PREFIX + "gerrit:merged";
-  public static final String TAG_MOVE = AUTOGENERATED_TAG_PREFIX + "gerrit:move";
-  public static final String TAG_RESTORE = AUTOGENERATED_TAG_PREFIX + "gerrit:restore";
-  public static final String TAG_REVERT = AUTOGENERATED_TAG_PREFIX + "gerrit:revert";
-  public static final String TAG_SET_ASSIGNEE = AUTOGENERATED_TAG_PREFIX + "gerrit:setAssignee";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteReviewer";
+  public static final String TAG_DELETE_VOTE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteVote";
+  public static final String TAG_MERGED = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "merged";
+  public static final String TAG_MOVE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "move";
+  public static final String TAG_RESTORE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "restore";
+  public static final String TAG_REVERT = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "revert";
+  public static final String TAG_SET_ASSIGNEE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setAssignee";
   public static final String TAG_UPDATE_ATTENTION_SET =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:updateAttentionSet";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "updateAttentionSet";
   public static final String TAG_SET_DESCRIPTION =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:setPsDescription";
-  public static final String TAG_SET_HASHTAGS = AUTOGENERATED_TAG_PREFIX + "gerrit:setHashtag";
-  public static final String TAG_SET_PRIVATE = AUTOGENERATED_TAG_PREFIX + "gerrit:setPrivate";
-  public static final String TAG_SET_READY = AUTOGENERATED_TAG_PREFIX + "gerrit:setReadyForReview";
-  public static final String TAG_SET_TOPIC = AUTOGENERATED_TAG_PREFIX + "gerrit:setTopic";
-  public static final String TAG_SET_WIP = AUTOGENERATED_TAG_PREFIX + "gerrit:setWorkInProgress";
-  public static final String TAG_UNSET_PRIVATE = AUTOGENERATED_TAG_PREFIX + "gerrit:unsetPrivate";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setPsDescription";
+  public static final String TAG_SET_HASHTAGS = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setHashtag";
+  public static final String TAG_SET_PRIVATE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setPrivate";
+  public static final String TAG_SET_READY =
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setReadyForReview";
+  public static final String TAG_SET_TOPIC = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setTopic";
+  public static final String TAG_SET_WIP = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setWorkInProgress";
+  public static final String TAG_UNSET_PRIVATE =
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "unsetPrivate";
   public static final String TAG_UPLOADED_PATCH_SET =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:newPatchSet";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "newPatchSet";
   public static final String TAG_UPLOADED_WIP_PATCH_SET =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:newWipPatchSet";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "newWipPatchSet";
 
   public static ChangeMessage newMessage(ChangeContext ctx, String body, @Nullable String tag) {
     return newMessage(ctx.getChange().currentPatchSetId(), ctx.getUser(), ctx.getWhen(), body, tag);
@@ -122,6 +126,10 @@
     return tag != null && tag.startsWith(AUTOGENERATED_TAG_PREFIX);
   }
 
+  public static boolean isAutogeneratedByGerrit(@Nullable String tag) {
+    return tag != null && tag.startsWith(AUTOGENERATED_BY_GERRIT_TAG_PREFIX);
+  }
+
   public static ChangeMessageInfo createChangeMessageInfo(
       ChangeMessage message, AccountLoader accountLoader) {
     PatchSet.Id patchNum = message.getPatchSetId();
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 450cbe0..e8b44aa 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -272,7 +272,9 @@
   }
 
   private static boolean isAutoGenerated(ChangeMessage cm) {
-    return ChangeMessagesUtil.isAutogenerated(cm.getTag());
+    // Ignore Gerrit auto-generated messages, allowing to link against human change messages that
+    // have an auto-generated tag
+    return ChangeMessagesUtil.isAutogeneratedByGerrit(cm.getTag());
   }
 
   private static boolean isAfter(CommentInfo c, ChangeMessage cm) {
diff --git a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 916775d..6997d96 100644
--- a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -18,6 +18,7 @@
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.base.Splitter;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.common.FileUtil;
@@ -38,6 +39,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Locale;
 import java.util.Set;
 import org.apache.sshd.common.SshException;
@@ -197,9 +199,15 @@
             continue;
           }
 
+          List<String> parts = Splitter.on(' ').splitToList(line);
+          if (parts.size() > 2) {
+            throw new IllegalArgumentException(
+                "Invalid peer key file format, only <key [comment]> lines supported");
+          }
           try {
             byte[] bin =
-                BaseEncoding.base64().decode(new String(line.getBytes(ISO_8859_1), ISO_8859_1));
+                BaseEncoding.base64()
+                    .decode(new String(parts.get(0).getBytes(ISO_8859_1), ISO_8859_1));
             keys.add(new ByteArrayBuffer(bin).getRawPublicKey());
           } catch (RuntimeException | SshException e) {
             logBadKey(path, line, e);
diff --git a/javatests/com/google/gerrit/integration/ssh/BUILD b/javatests/com/google/gerrit/integration/ssh/BUILD
new file mode 100644
index 0000000..dc8e68c
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/ssh/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = ["PeerKeysAuthIT.java"],
+    group = "peer-keys-auth",
+    labels = ["ssh"],
+)
diff --git a/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java b/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java
new file mode 100644
index 0000000..a219cc2
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2020 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.integration.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.server.PeerDaemonUser;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import org.junit.Test;
+
+@NoHttpd
+@UseSsh
+public class PeerKeysAuthIT extends StandaloneSiteTest {
+  private static final String[] SSH_KEYGEN_CMD =
+      new String[] {"ssh-keygen", "-t", "rsa", "-q", "-P", "", "-f", "id_rsa"};
+  private static final String[] SSH_COMMAND =
+      new String[] {
+        "ssh",
+        "-o",
+        "UserKnownHostsFile=/dev/null",
+        "-o",
+        "StrictHostKeyChecking=no",
+        "-o",
+        "IdentitiesOnly=yes",
+        "-i"
+      };
+
+  @Inject private @TestSshServerAddress InetSocketAddress sshAddress;
+
+  @Test
+  public void test() throws Exception {
+    try (ServerContext ctx = startServer()) {
+      ctx.getInjector().injectMembers(this);
+      // Generate private/public key for user
+      execute(ImmutableList.<String>builder().add(SSH_KEYGEN_CMD).build());
+
+      String[] parts =
+          new String(Files.readAllBytes(sitePaths.data_dir.resolve("id_rsa.pub")), UTF_8)
+              .split(" ");
+
+      // Loose algorithm at index 0, verify the format: "key comment"
+      Files.write(
+          sitePaths.peer_keys, String.format("%s %s", parts[1], parts[2]).getBytes(ISO_8859_1));
+      assertContent(execGerritVersionCommand());
+
+      // Only preserve the key material: no algorithm and no comment
+      Files.write(sitePaths.peer_keys, parts[1].getBytes(ISO_8859_1));
+      assertContent(execGerritVersionCommand());
+
+      // Wipe out the content of the peer keys file
+      Files.delete(sitePaths.peer_keys);
+      assertThrows(IOException.class, () -> execGerritVersionCommand());
+    }
+  }
+
+  private String execGerritVersionCommand() throws Exception {
+    return execute(
+        ImmutableList.<String>builder()
+            .add(SSH_COMMAND)
+            .add(sitePaths.data_dir.resolve("id_rsa").toString())
+            .add("-p " + sshAddress.getPort())
+            .add(PeerDaemonUser.USER_NAME + "@" + sshAddress.getHostName())
+            .add("suexec")
+            .add("--as")
+            .add("admin")
+            .add("--")
+            .add("gerrit")
+            .add("version")
+            .build());
+  }
+
+  private String execute(ImmutableList<String> cmd) throws Exception {
+    return execute(cmd, sitePaths.data_dir.toFile(), ImmutableMap.of());
+  }
+
+  private static void assertContent(String result) {
+    assertThat(result).contains("gerrit version " + Version.getVersion());
+  }
+}
diff --git a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
index 5a28404..daefd7c 100644
--- a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
@@ -14,77 +14,126 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.collect.MoreCollectors.onlyElement;
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 @RunWith(JUnit4.class)
 public class ListChangeCommentsTest {
-
   @Test
-  public void commentsLinkedToChangeMessages() {
-    /**
-     * Human comments should not be linked to auto-generated messages. A comment is linked to the
-     * nearest next change message in timestamp
-     */
-    CommentInfo c1 = getNewCommentInfo("c1", Timestamp.valueOf("2018-01-01 09:01:00"));
-    CommentInfo c2 = getNewCommentInfo("c2", Timestamp.valueOf("2018-01-01 09:01:15"));
-    CommentInfo c3 = getNewCommentInfo("c3", Timestamp.valueOf("2018-01-01 09:01:25"));
+  public void commentsLinkedToChangeMessagesIgnoreGerritAutoGenTaggedMessages() {
+    /* Comments should not be linked to Gerrit's autogenerated messages */
+    List<CommentInfo> comments = createComments("c1", "00", "c2", "10", "c3", "25");
+    List<ChangeMessage> changeMessages =
+        createChangeMessages("cm1", "00", "cm2", "16", "cm3", "30");
 
-    ChangeMessage cm1 =
-        getNewChangeMessage("cm1key", "cm1", Timestamp.valueOf("2018-01-01 00:00:00"), null);
-    ChangeMessage cmIgnore =
-        getNewChangeMessage(
-            "cm2key",
-            "cm2",
-            Timestamp.valueOf("2018-01-01 09:01:15"),
-            ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX);
-    ChangeMessage cm2 =
-        getNewChangeMessage("cm2key", "cm2", Timestamp.valueOf("2018-01-01 09:01:16"), null);
-    ChangeMessage cm3 =
-        getNewChangeMessage("cm3key", "cm3", Timestamp.valueOf("2018-01-01 09:01:27"), null);
-
-    assertThat(c1.changeMessageId).isNull();
-    assertThat(c2.changeMessageId).isNull();
-    assertThat(c3.changeMessageId).isNull();
-
-    ImmutableList<CommentInfo> comments = ImmutableList.of(c1, c2, c3);
-    ImmutableList<ChangeMessage> changeMessages = ImmutableList.of(cm1, cmIgnore, cm2, cm3);
+    changeMessages.add(
+        newChangeMessage("ignore", "cmAutoGenByGerrit", "15", ChangeMessagesUtil.TAG_MERGED));
 
     CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, true);
 
-    assertThat(c1.changeMessageId).isEqualTo(changeMessageKey(cm2));
-    /** comment 2 ignored the auto-generated change message */
-    assertThat(c2.changeMessageId).isEqualTo(changeMessageKey(cm2));
-    assertThat(c3.changeMessageId).isEqualTo(changeMessageKey(cm3));
+    assertThat(getComment(comments, "c1").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cm1").getKey().uuid());
+    /* comment 2 ignored the auto-generated message because it has a Gerrit tag */
+    assertThat(getComment(comments, "c2").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cm2").getKey().uuid());
+    assertThat(getComment(comments, "c3").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cm3").getKey().uuid());
+
+    // Make sure no comment is linked to the auto-gen message
+    assertThat(comments.stream().map(c -> c.changeMessageId).collect(Collectors.toSet()))
+        .doesNotContain(getChangeMessage(changeMessages, "cmAutoGenByGerrit"));
   }
 
-  private static CommentInfo getNewCommentInfo(String message, Timestamp ts) {
+  @Test
+  public void commentsLinkedToChangeMessagesAllowLinkingToAutoGenTaggedMessages() {
+    /* Human comments are allowed to be linked to autogenerated messages */
+    List<CommentInfo> comments = createComments("c1", "00", "c2", "10", "c3", "25");
+    List<ChangeMessage> changeMessages =
+        createChangeMessages("cm1", "00", "cm2", "16", "cm3", "30");
+
+    changeMessages.add(
+        newChangeMessage(
+            "cmAutoGen", "cmAutoGen", "15", ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX));
+
+    CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, true);
+
+    assertThat(getComment(comments, "c1").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cm1").getKey().uuid());
+    /* comment 2 did not ignore the auto-generated change message */
+    assertThat(getComment(comments, "c2").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cmAutoGen").getKey().uuid());
+    assertThat(getComment(comments, "c3").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cm3").getKey().uuid());
+  }
+
+  /**
+   * Create a list of comments from the specified args args should be passed as consecutive pairs of
+   * messages and timestamps example: (m1, t1, m2, t2, ...)
+   */
+  private static List<CommentInfo> createComments(String... args) {
+    List<CommentInfo> comments = new ArrayList<>();
+    for (int i = 0; i < args.length; i += 2) {
+      String message = args[i];
+      String ts = args[i + 1];
+      comments.add(newCommentInfo(message, ts));
+    }
+    return comments;
+  }
+
+  /**
+   * Create a list of change messages from the specified args args should be passed as consecutive
+   * pairs of messages and timestamps example: (m1, t1, m2, t2, ...). the tag parameter for the
+   * created change messages will be null.
+   */
+  private static List<ChangeMessage> createChangeMessages(String... args) {
+    List<ChangeMessage> changeMessages = new ArrayList<>();
+    for (int i = 0; i < args.length; i += 2) {
+      String key = args[i] + "Key";
+      String message = args[i];
+      String ts = args[i + 1];
+      changeMessages.add(newChangeMessage(key, message, ts, null));
+    }
+    return changeMessages;
+  }
+
+  /** Create a new CommentInfo with a given message and timestamp */
+  private static CommentInfo newCommentInfo(String message, String ts) {
     CommentInfo c = new CommentInfo();
     c.message = message;
-    c.updated = ts;
+    c.updated = Timestamp.valueOf("2000-01-01 00:00:" + ts);
     return c;
   }
 
-  private static ChangeMessage getNewChangeMessage(
-      String id, String message, Timestamp ts, String tag) {
+  /** Create a new change message with an id, message, timestamp and tag */
+  private static ChangeMessage newChangeMessage(String id, String message, String ts, String tag) {
     ChangeMessage.Key key = ChangeMessage.key(Change.id(1), id);
-    ChangeMessage cm = new ChangeMessage(key, null, ts, null);
+    ChangeMessage cm =
+        new ChangeMessage(key, null, Timestamp.valueOf("2000-01-01 00:00:" + ts), null);
     cm.setMessage(message);
     cm.setTag(tag);
     return cm;
   }
 
-  private static String changeMessageKey(ChangeMessage changeMessage) {
-    return changeMessage.getKey().uuid();
+  /** Return the change message from the list of messages that has specific message text */
+  private static ChangeMessage getChangeMessage(List<ChangeMessage> messages, String messageText) {
+    return messages.stream().filter(m -> m.getMessage().equals(messageText)).collect(onlyElement());
+  }
+
+  /** Return the comment from the list of comments that has specific message text */
+  private CommentInfo getComment(List<CommentInfo> comments, String messageText) {
+    return comments.stream().filter(c -> c.message.equals(messageText)).collect(onlyElement());
   }
 }
diff --git a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
deleted file mode 100644
index 88c8835..0000000
--- a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * @license
- * Copyright (C) 2019 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.
- */
-
-/** @polymerBehavior Gerrit.FireBehavior */
-export const FireBehavior = {
-  /**
-   * Dispatches a custom event with an optional detail value.
-   *
-   * @param {string} type Name of event type.
-   * @param {*=} detail Detail value containing event-specific
-   *   payload.
-   * @param {{ bubbles: (boolean|undefined), cancelable: (boolean|undefined),
-   *     composed: (boolean|undefined) }=}
-   *  options Object specifying options.  These may include:
-   *  `bubbles` (boolean, defaults to `true`),
-   *  `cancelable` (boolean, defaults to false), and
-   *  `composed` (boolean, defaults to true).
-   * @return {!Event} The new event that was fired.
-   * @override
-   */
-  fire(type, detail, options) {
-    console.warn('\'fire\' is deprecated, please use dispatchEvent instead!');
-    options = options || {};
-    detail = (detail === null || detail === undefined) ? {} : detail;
-    const event = new Event(type, {
-      bubbles: options.bubbles === undefined ? true : options.bubbles,
-      cancelable: Boolean(options.cancelable),
-      composed: options.composed === undefined ? true: options.composed,
-    });
-    event.detail = detail;
-    this.dispatchEvent(event);
-    return event;
-  },
-};
-
-// TODO(dmfilippov) Remove the following lines with assignments
-// Plugins can use the behavior because it was accessible with
-// the global Gerrit... variable. To avoid breaking changes in plugins
-// temporary assign global variables.
-window.Gerrit = window.Gerrit || {};
-window.Gerrit.FireBehavior = FireBehavior;
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
index 09e3b72..ca31ee8 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
@@ -171,6 +171,7 @@
   TOGGLE_FILE_REVIEWED: 'TOGGLE_FILE_REVIEWED',
   TOGGLE_ALL_INLINE_DIFFS: 'TOGGLE_ALL_INLINE_DIFFS',
   TOGGLE_INLINE_DIFF: 'TOGGLE_INLINE_DIFF',
+  TOGGLE_HIDE_ALL_COMMENT_THREADS: 'TOGGLE_HIDE_ALL_COMMENT_THREADS',
 
   OPEN_FIRST_FILE: 'OPEN_FIRST_FILE',
   OPEN_LAST_FILE: 'OPEN_LAST_FILE',
@@ -251,6 +252,8 @@
     'Expand all comment threads');
 _describe(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, ShortcutSection.DIFFS,
     'Collapse all comment threads');
+_describe(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, ShortcutSection.DIFFS,
+    'Hide/Display all comment threads');
 _describe(Shortcut.LEFT_PANE, ShortcutSection.DIFFS, 'Select left pane');
 _describe(Shortcut.RIGHT_PANE, ShortcutSection.DIFFS, 'Select right pane');
 _describe(Shortcut.TOGGLE_LEFT_PANE, ShortcutSection.DIFFS,
@@ -290,6 +293,8 @@
     'Go to selected file');
 _describe(Shortcut.TOGGLE_ALL_INLINE_DIFFS, ShortcutSection.FILE_LIST,
     'Show/hide all inline diffs');
+_describe(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, ShortcutSection.FILE_LIST,
+    'Hide/Display all comment threads');
 _describe(Shortcut.TOGGLE_INLINE_DIFF, ShortcutSection.FILE_LIST,
     'Show/hide selected inline diff');
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 74b62a7..894f0cc 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -275,6 +275,8 @@
       [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
       [this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
       [this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
+      [this.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]:
+        '_handleToggleHideAllCommentThreads',
       [this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
       [this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
       [this.Shortcut.NEXT_LINE]: '_handleCursorNext',
@@ -808,6 +810,15 @@
     this._toggleInlineDiffs();
   }
 
+  _handleToggleHideAllCommentThreads(e) {
+    if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+      return;
+    }
+
+    e.preventDefault();
+    this.toggleClass('hideComments');
+  }
+
   _handleCursorNext(e) {
     if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
       return;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
index 4ac4bb1..df7cb00 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
@@ -310,6 +310,9 @@
         display: none;
       }
     }
+    :host(.hideComments) {
+      --gr-comment-thread-display: none;
+    }
   </style>
   <div
     id="container"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index b808bd9..e37c84a 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -295,6 +295,19 @@
     this.addEventListener('comment-editing-changed', e => {
       this._commentEditing = e.detail;
     });
+
+    // Plugins on reply-reviewers endpoint can take advantage of these
+    // events to add / remove reviewers
+
+    this.addEventListener('add-reviewer', e => {
+      // Only support account type, see more from:
+      // elements/shared/gr-account-list/gr-account-list.js#addAccountItem
+      this.$.reviewers.addAccountItem({account: e.detail.reviewer});
+    });
+
+    this.addEventListener('remove-reviewer', e => {
+      this.$.reviewers.removeAccount(e.detail.reviewer);
+    });
   }
 
   /** @override */
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
index de11712..79f38a6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
@@ -97,13 +97,10 @@
     }
     .textareaContainer,
     #textarea,
-    gr-endpoint-decorator {
+    gr-endpoint-decorator[name='reply-text'] {
       display: flex;
       width: 100%;
     }
-    gr-endpoint-decorator[name='reply-label-scores'] {
-      display: block;
-    }
     .previewContainer gr-formatted-text {
       background: var(--table-header-background-color);
       padding: var(--spacing-l);
@@ -140,20 +137,25 @@
   </style>
   <div class="container" tabindex="-1">
     <section class="peopleContainer">
-      <div class="peopleList">
-        <div class="peopleListLabel">Reviewers</div>
-        <gr-account-list
-          id="reviewers"
-          accounts="{{_reviewers}}"
-          removable-values="[[change.removable_reviewers]]"
-          filter="[[filterReviewerSuggestion]]"
-          pending-confirmation="{{_reviewerPendingConfirmation}}"
-          placeholder="Add reviewer..."
-          on-account-text-changed="_handleAccountTextEntry"
-          suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"
-        >
-        </gr-account-list>
-      </div>
+      <gr-endpoint-decorator name="reply-reviewers">
+        <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
+        <div class="peopleList">
+          <div class="peopleListLabel">Reviewers</div>
+          <gr-account-list
+            id="reviewers"
+            accounts="{{_reviewers}}"
+            removable-values="[[change.removable_reviewers]]"
+            filter="[[filterReviewerSuggestion]]"
+            pending-confirmation="{{_reviewerPendingConfirmation}}"
+            placeholder="Add reviewer..."
+            on-account-text-changed="_handleAccountTextEntry"
+            suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"
+          >
+          </gr-account-list>
+          <gr-endpoint-slot name="right"></gr-endpoint-slot>
+        </div>
+        <gr-endpoint-slot name="below"></gr-endpoint-slot>
+      </gr-endpoint-decorator>
       <div class="peopleList">
         <div class="peopleListLabel">CC</div>
         <gr-account-list
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 5c77b0e..19abdaf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -298,6 +298,8 @@
       [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
       [this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile',
       [this.Shortcut.TOGGLE_BLAME]: '_handleToggleBlame',
+      [this.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]:
+          '_handleToggleHideAllCommentThreads',
 
       // Final two are actually handled by gr-comment-thread.
       [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
@@ -740,6 +742,7 @@
     this._initCursor(this.params);
 
     this._changeNum = value.changeNum;
+    this.classList.remove('hideComments');
     this._path = value.path;
     this._patchRange = {
       patchNum: value.patchNum,
@@ -1277,6 +1280,12 @@
     this._toggleBlame();
   }
 
+  _handleToggleHideAllCommentThreads(e) {
+    if (this.shouldSuppressKeyboardShortcut(e) ||
+      this.modifierPressed(e)) { return; }
+    this.toggleClass('hideComments');
+  }
+
   _computeBlameLoaderClass(isImageDiff, path) {
     return !this.isMagicPath(path) && !isImageDiff ? 'show' : '';
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
index e735153..3d30f77 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
@@ -196,6 +196,9 @@
         }
       }
     }
+    :host(.hideComments) {
+      --gr-comment-thread-display: none;
+    }
   </style>
   <gr-fixed-panel
     class$="[[_computeContainerClass(_editMode)]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 98371dc..21d1144 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -71,6 +71,7 @@
     kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
     kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
     kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
+    kb.bindShortcut(kb.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
     kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
     kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
     kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b');
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 10ca963..ecb40a5 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -350,6 +350,8 @@
         this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
     this.bindShortcut(
         this.Shortcut.TOGGLE_BLAME, 'b');
+    this.bindShortcut(
+        this.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
 
     this.bindShortcut(
         this.Shortcut.OPEN_FIRST_FILE, ']');
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
index 52c4f4e..3f8aa44 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
@@ -14,13 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-attribute-helper">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrAttributeHelper(element) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
index ea60dc6..89d8ec2 100644
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
@@ -14,13 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-api">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrChangeMetadataApi(plugin) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index 41d5fd5..4822163 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -14,14 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-dom-hooks">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 
 /** @constructor */
 export function GrDomHooksManager(plugin) {
@@ -61,13 +54,18 @@
 }
 
 GrDomHook.prototype._createPlaceholder = function(hookName) {
-  Polymer({
-    is: hookName,
-    properties: {
-      plugin: Object,
-      content: Object,
-    },
-  });
+  class HookPlaceholder extends PolymerElement {
+    static get is() { return hookName; }
+
+    static get properties() {
+      return {
+        plugin: Object,
+        content: Object,
+      };
+    }
+  }
+
+  customElements.define(HookPlaceholder.is, HookPlaceholder);
 };
 
 GrDomHook.prototype.handleInstanceDetached = function(instance) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
index da85b9e..466f84a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
@@ -14,13 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-event-helper">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrEventHelper(element) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
index 738b276..e2dd047 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -16,13 +16,6 @@
  */
 import './gr-plugin-popup.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-popup-interface">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /**
  * Plugin popup API.
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
index 445356d..04408f8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
@@ -15,13 +15,6 @@
  * limitations under the License.
  */
 import './gr-plugin-repo-command.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-repo-api">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrRepoApi(plugin) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
index 35396cf..8050cd6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
@@ -16,13 +16,6 @@
  */
 import '../../settings/gr-settings-view/gr-settings-item.js';
 import '../../settings/gr-settings-view/gr-settings-menu-item.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-settings-api">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrSettingsApi(plugin) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
index ae8b8ab..1e37603 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
@@ -14,10 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-Polymer({
-  _template: html`
+
+class CustomPluginHeader extends PolymerElement {
+  static get is() {
+    return 'gr-custom-plugin-header';
+  }
+
+  static get properties() {
+    return {
+      logoUrl: String,
+      title: String,
+    };
+  }
+
+  static get template() {
+    return html`
     <style>
       img {
         width: 1em;
@@ -32,12 +45,8 @@
       <img src="[[logoUrl]]" hidden\$="[[!logoUrl]]">
       <span class="title">[[title]]</span>
     </span>
-`,
+`;
+  }
+}
 
-  is: 'gr-custom-plugin-header',
-
-  properties: {
-    logoUrl: String,
-    title: String,
-  },
-});
+customElements.define(CustomPluginHeader.is, CustomPluginHeader);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
index be427ec..48b14d3 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
@@ -15,13 +15,6 @@
  * limitations under the License.
  */
 import './gr-custom-plugin-header.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-theme-api">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrThemeApi(plugin) {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index abe5206..6f5f9e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -158,10 +158,10 @@
   }
 
   _handleAdd(e) {
-    this._addAccountItem(e.detail.value);
+    this.addAccountItem(e.detail.value);
   }
 
-  _addAccountItem(item) {
+  addAccountItem(item) {
     // Append new account or group to the accounts property. We add our own
     // internal properties to the account/group here, so we clone the object
     // to avoid cluttering up the shared change object.
@@ -248,11 +248,11 @@
 
   _handleRemove(e) {
     const toRemove = e.detail.account;
-    this._removeAccount(toRemove);
+    this.removeAccount(toRemove);
     this.$.entry.focus();
   }
 
-  _removeAccount(toRemove) {
+  removeAccount(toRemove) {
     if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
       return;
     }
@@ -286,7 +286,7 @@
     }
     switch (e.detail.keyCode) {
       case 8: // Backspace
-        this._removeAccount(this.accounts[this.accounts.length - 1]);
+        this.removeAccount(this.accounts[this.accounts.length - 1]);
         break;
       case 37: // Left arrow
         if (this.accountChips[this.accountChips.length - 1]) {
@@ -305,7 +305,7 @@
       case 13: // Enter
       case 32: // Spacebar
       case 46: // Delete
-        this._removeAccount(chip.account);
+        this.removeAccount(chip.account);
         // Splice from this array to avoid inconsistent ordering of
         // event handling.
         chips.splice(index, 1);
@@ -345,7 +345,7 @@
   submitEntryText() {
     const text = this.$.entry.getText();
     if (!text.length) { return true; }
-    const wasSubmitted = this._addAccountItem(text);
+    const wasSubmitted = this.addAccountItem(text);
     if (wasSubmitted) { this.$.entry.clear(); }
     return wasSubmitted;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
index 2efc6e8..41158a8 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -351,7 +351,7 @@
     element.readonly = true;
     const acct = makeAccount();
     element.accounts = [acct];
-    element._removeAccount(acct);
+    element.removeAccount(acct);
     assert.equal(element.accounts.length, 1);
   });
 
@@ -537,7 +537,7 @@
       element.accounts = [makeAccount(), makeAccount()];
       flush(() => {
         const focusSpy = sandbox.spy(element.accountChips[1], 'focus');
-        const removeSpy = sandbox.spy(element, '_removeAccount');
+        const removeSpy = sandbox.spy(element, 'removeAccount');
         MockInteractions.pressAndReleaseKeyOn(
             element.accountChips[0], 8); // Backspace
         assert.isTrue(focusSpy.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
index 7c5c5c4c..2bb5b66ea 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
@@ -37,7 +37,7 @@
     #container {
       background-color: var(--comment-background-color);
       color: var(--comment-text-color);
-      display: block;
+      display: var(--gr-comment-thread-display, block);
       margin: 0 var(--spacing-s) var(--spacing-s);
       white-space: normal;
       box-shadow: var(--elevation-level-2);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
index 7402e22..a4adbac 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -14,13 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-etag-decorator">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 // Limit cache size because /change/detail responses may be large.
 const MAX_CACHE_SIZE = 30;
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index c89378c..9b4bbaf 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -17,14 +17,7 @@
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-select">
-  <slot></slot>
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
 /**
  * @extends PolymerElement
@@ -33,6 +26,12 @@
     LegacyElementMixin(PolymerElement)) {
   static get is() { return 'gr-select'; }
 
+  static get template() {
+    return html`
+      <slot></slot>
+    `;
+  }
+
   static get properties() {
     return {
       bindValue: {
diff --git a/polygerrit-ui/app/samples/add-from-favorite-reviewers.js b/polygerrit-ui/app/samples/add-from-favorite-reviewers.js
new file mode 100644
index 0000000..76b2787
--- /dev/null
+++ b/polygerrit-ui/app/samples/add-from-favorite-reviewers.js
@@ -0,0 +1,145 @@
+/**
+ * @license
+ * Copyright (C) 2020 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.
+ */
+/**
+ * This plugin will a button to quickly add favorite reviewers to
+ * reviewers in reply dialog.
+ */
+
+const onToggleButtonClicks = [];
+function toggleButtonClicked(expanded) {
+  onToggleButtonClicks.forEach(cb => {
+    cb(expanded);
+  });
+}
+
+class ReviewerShortcut extends Polymer.Element {
+  static get is() { return 'reviewer-shortcut'; }
+
+  static get properties() {
+    return {
+      change: Object,
+      expanded: {
+        type: Boolean,
+        value: false,
+      },
+    };
+  }
+
+  static get template() {
+    return Polymer.html`
+      <button on-click="toggleControlContent">
+        [[computeButtonText(expanded)]]
+      </button>
+    `;
+  }
+
+  toggleControlContent() {
+    this.expanded = !this.expanded;
+    toggleButtonClicked(this.expanded);
+  }
+
+  computeButtonText(expanded) {
+    return expanded ? 'Collapse' : 'Add favorite reviewers';
+  }
+}
+
+customElements.define(ReviewerShortcut.is, ReviewerShortcut);
+
+class ReviewerShortcutContent extends Polymer.Element {
+  static get is() { return 'reviewer-shortcut-content'; }
+
+  static get properties() {
+    return {
+      change: Object,
+      hidden: {
+        type: Boolean,
+        value: true,
+        reflectToAttribute: true,
+      },
+    };
+  }
+
+  static get template() {
+    return Polymer.html`
+      <style>
+      :host([hidden]) {
+        display: none;
+      }
+      :host {
+        display: block;
+      }
+      </style>
+      <ul>
+        <li><button on-click="addApple">Apple</button></li>
+        <li><button on-click="addBanana">Banana</button></li>
+        <li><button on-click="addCherry">Cherry</button></li>
+      </ul>
+    `;
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    onToggleButtonClicks.push(expanded => {
+      this.hidden = !expanded;
+    });
+  }
+
+  addApple() {
+    this.dispatchEvent(new CustomEvent('add-reviewer', {detail: {
+      reviewer: {
+        display_name: 'Apple',
+        email: 'apple@gmail.com',
+        name: 'Apple',
+        _account_id: 0,
+      },
+    },
+    composed: true, bubbles: true}));
+  }
+
+  addBanana() {
+    this.dispatchEvent(new CustomEvent('add-reviewer', {detail: {
+      reviewer: {
+        display_name: 'Banana',
+        email: 'banana@gmail.com',
+        name: 'B',
+        _account_id: 1,
+      },
+    },
+    composed: true, bubbles: true}));
+  }
+
+  addCherry() {
+    this.dispatchEvent(new CustomEvent('add-reviewer', {detail: {
+      reviewer: {
+        display_name: 'Cherry',
+        email: 'cherry@gmail.com',
+        name: 'C',
+        _account_id: 2,
+      },
+    },
+    composed: true, bubbles: true}));
+  }
+}
+
+customElements.define(ReviewerShortcutContent.is, ReviewerShortcutContent);
+
+Gerrit.install(plugin => {
+  plugin.registerCustomComponent(
+      'reply-reviewers', ReviewerShortcut.is, {slot: 'right'});
+  plugin.registerCustomComponent(
+      'reply-reviewers', ReviewerShortcutContent.is, {slot: 'below'});
+});
\ No newline at end of file