Merge "Convert gr-autocomplete tests to Karma tests"
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 97c2548..956a94d 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -279,36 +279,6 @@
 ----
 
 
-[[es6-promise]]
-es6-promise
-
-* es6-promise
-
-[[es6-promise_license]]
-----
-Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-----
-
-
 [[Polymer-2018]]
 Polymer-2018
 
@@ -747,37 +717,6 @@
 ----
 
 
-[[whatwg-fetch]]
-whatwg-fetch
-
-* whatwg-fetch
-
-[[whatwg-fetch_license]]
-----
-Copyright (c) 2014-2016 GitHub, Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
 [[font-roboto-local-fonts-roboto]]
 font-roboto-local-fonts-roboto
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index c1dfb94..d561596 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3221,36 +3221,6 @@
 ----
 
 
-[[es6-promise]]
-es6-promise
-
-* es6-promise
-
-[[es6-promise_license]]
-----
-Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-----
-
-
 [[Polymer-2018]]
 Polymer-2018
 
@@ -3689,37 +3659,6 @@
 ----
 
 
-[[whatwg-fetch]]
-whatwg-fetch
-
-* whatwg-fetch
-
-[[whatwg-fetch_license]]
-----
-Copyright (c) 2014-2016 GitHub, Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
 [[font-roboto-local-fonts-roboto]]
 font-roboto-local-fonts-roboto
 
diff --git a/README.md b/README.md
index a76dac6..084f2b0 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,8 @@
 [Gerrit](https://www.gerritcodereview.com) is a code review and project
 management tool for Git based projects.
 
-[![Build Status](https://gerrit-ci.gerritforge.com/job/Gerrit-master/badge/icon)](https://gerrit-ci.gerritforge.com/job/Gerrit-master/)
+[![Build Status](https://gerrit-ci.gerritforge.com/job/Gerrit-bazel-master/badge/icon)](https://gerrit-ci.gerritforge.com/job/Gerrit-bazel-master/)
+![Maven Central](https://img.shields.io/maven-central/v/com.google.gerrit/gerrit-war)
 
 ## Objective
 
diff --git a/WORKSPACE b/WORKSPACE
index 8f9ddf0..91eef76 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -60,8 +60,8 @@
 
 http_archive(
     name = "build_bazel_rules_nodejs",
-    sha256 = "d14076339deb08e5460c221fae5c5e9605d2ef4848eee1f0c81c9ffdc1ab31c1",
-    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.6.1/rules_nodejs-1.6.1.tar.gz"],
+    sha256 = "84abf7ac4234a70924628baa9a73a5a5cbad944c4358cf9abdb4aab29c9a5b77",
+    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.7.0/rules_nodejs-1.7.0.tar.gz"],
 )
 
 # File is specific to Polymer and copied from the Closure Github -- should be
@@ -82,6 +82,7 @@
 # https://github.com/google/closure-templates/pull/155
 rules_closure_dependencies(
     omit_aopalliance = True,
+    omit_bazel_skylib = True,
     omit_javax_inject = True,
     omit_rules_cc = True,
 )
@@ -91,10 +92,10 @@
 # Golang support for PolyGerrit local dev server.
 http_archive(
     name = "io_bazel_rules_go",
-    sha256 = "b34cbe1a7514f5f5487c3bfee7340a4496713ddf4f119f7a225583d6cafd793a",
+    sha256 = "a8d6b1b354d371a646d2f7927319974e0f9e52f73a2452d2b3877118169eb6bb",
     urls = [
-        "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/v0.21.1/rules_go-v0.21.1.tar.gz",
-        "https://github.com/bazelbuild/rules_go/releases/download/v0.21.1/rules_go-v0.21.1.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.23.3/rules_go-v0.23.3.tar.gz",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.23.3/rules_go-v0.23.3.tar.gz",
     ],
 )
 
@@ -106,8 +107,11 @@
 
 http_archive(
     name = "bazel_gazelle",
-    sha256 = "3c681998538231a2d24d0c07ed5a7658cb72bfb5fd4bf9911157c0e9ac6a2687",
-    urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.17.0/bazel-gazelle-0.17.0.tar.gz"],
+    sha256 = "cdb02a887a7187ea4d5a27452311a75ed8637379a1287d8eeb952138ea485f7d",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
+    ],
 )
 
 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 541e479..9d8bc57 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -39,6 +39,7 @@
     "//lib:gson",
     "//lib:guava-retrying",
     "//lib:jgit",
+    "//lib:jgit-ssh-jsch",
     "//lib:jsch",
     "//lib/commons:compress",
     "//lib/commons:lang",
diff --git a/java/com/google/gerrit/pgm/Init.java b/java/com/google/gerrit/pgm/Init.java
index 3593d8a..4e62a0f 100644
--- a/java/com/google/gerrit/pgm/Init.java
+++ b/java/com/google/gerrit/pgm/Init.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.IoUtil;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.PluginData;
+import com.google.gerrit.index.SchemaDefinitions;
 import com.google.gerrit.index.project.ProjectSchemaDefinitions;
 import com.google.gerrit.pgm.init.BaseInit;
 import com.google.gerrit.pgm.init.Browser;
@@ -31,6 +32,9 @@
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.index.GerritIndexStatus;
+import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
 import com.google.gerrit.server.ioutil.HostPlatform;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.gerrit.server.util.ReplicaUtil;
@@ -89,7 +93,7 @@
 
   @Inject Browser browser;
 
-  private boolean projectsIndexExists;
+  private GerritIndexStatus indexStatus;
 
   public Init() {
     super(new WarDistribution(), null);
@@ -103,7 +107,7 @@
 
   @Override
   protected boolean beforeInit(SiteInit init) throws Exception {
-    projectsIndexExists = new GerritIndexStatus(init.site).exists(ProjectSchemaDefinitions.NAME);
+    indexStatus = new GerritIndexStatus(init.site);
     ErrorLogFile.errorOnlyConsole();
 
     if (!skipPlugins) {
@@ -132,6 +136,12 @@
 
   @Override
   protected void afterInit(SiteRun run) throws Exception {
+    List<SchemaDefinitions<?>> schemaDefs =
+        ImmutableList.of(
+            AccountSchemaDefinitions.INSTANCE,
+            ChangeSchemaDefinitions.INSTANCE,
+            GroupSchemaDefinitions.INSTANCE,
+            ProjectSchemaDefinitions.INSTANCE);
     List<Module> modules = new ArrayList<>();
     modules.add(
         new AbstractModule() {
@@ -146,8 +156,12 @@
         });
     modules.add(new GerritServerConfigModule());
     Guice.createInjector(modules).injectMembers(this);
-    if (!ReplicaUtil.isReplica(run.flags.cfg) && !projectsIndexExists) {
-      reindexProjects();
+    if (!ReplicaUtil.isReplica(run.flags.cfg)) {
+      for (SchemaDefinitions<?> schemaDef : schemaDefs) {
+        if (!indexStatus.exists(schemaDef.getName())) {
+          reindex(schemaDef);
+        }
+      }
     }
     start(run);
   }
@@ -260,8 +274,7 @@
     }
   }
 
-  private void reindexProjects() throws Exception {
-    // Reindex all projects, so that we bootstrap the project index for new installations
+  private void reindex(SchemaDefinitions<?> schemaDef) throws Exception {
     List<String> reindexArgs =
         ImmutableList.of(
             "--site-path",
@@ -269,8 +282,9 @@
             "--threads",
             Integer.toString(1),
             "--index",
-            ProjectSchemaDefinitions.NAME);
-    getConsoleUI().message("Init complete, reindexing projects with:");
+            schemaDef.getName());
+    getConsoleUI()
+        .message(String.format("Init complete, reindexing %s with:", schemaDef.getName()));
     getConsoleUI().message(" reindex " + reindexArgs.stream().collect(joining(" ")));
     Reindex reindexPgm = new Reindex();
     reindexPgm.main(reindexArgs.stream().toArray(String[]::new));
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
index 5b3b34c..bca5338 100644
--- a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
+++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -50,7 +50,7 @@
     setHeader("Subject", "[Gerrit Code Review] HTTP password was " + operation);
     setMessageId(
         messageIdGenerator.fromReasonAccountIdAndTimestamp(
-            "HTTP password change", user.getAccountId(), TimeUtil.nowTs()));
+            "HTTP_password_change", user.getAccountId(), TimeUtil.now()));
     add(RecipientType.TO, Address.create(getEmail()));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
index 01e165d..3a411dc 100644
--- a/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
+++ b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
@@ -28,7 +28,7 @@
 import com.google.gerrit.server.update.RepoView;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -111,7 +111,7 @@
    * @return MessageId that depends on the reason, accountId, and timestamp.
    */
   public MessageId fromReasonAccountIdAndTimestamp(
-      String reason, Account.Id accountId, Timestamp timestamp) {
+      String reason, Account.Id accountId, Instant timestamp) {
     return new AutoValue_MessageIdGenerator_MessageId(
         reason + "-" + accountId.toString() + "-" + timestamp.toString());
   }
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index fdd6b81..8f63177 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -259,9 +259,12 @@
     return true;
   }
 
+  // All message ids must start with < and end with >. Also, they must have @domain and no spaces.
   private void addMessageId(OutgoingEmailValidationListener.Args va, String suffix) {
     if (messageId != null) {
-      va.headers.put(FieldName.MESSAGE_ID, new EmailHeader.String(messageId.id() + suffix));
+      String message = "<" + messageId.id() + suffix + "@" + getGerritHost() + ">";
+      message = message.replaceAll("\\s", "");
+      va.headers.put(FieldName.MESSAGE_ID, new EmailHeader.String(message));
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
index 057a871..8e08b1c 100644
--- a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.testing.FakeEmailSender;
+import java.net.URL;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Test;
 
@@ -43,23 +44,25 @@
     gApi.changes().id(result.getChangeId()).abandon();
     assertThat(getMessageId(sender))
         .isEqualTo(
-            repository
-                    .getRefDatabase()
-                    .exactRef(result.getChange().getId().toRefPrefix() + "meta")
-                    .getObjectId()
-                    .getName()
-                + "-HTML");
+            withPrefixAndSuffixForMessageId(
+                repository
+                        .getRefDatabase()
+                        .exactRef(result.getChange().getId().toRefPrefix() + "meta")
+                        .getObjectId()
+                        .getName()
+                    + "-HTML"));
     sender.clear();
 
     gApi.changes().id(result.getChangeId()).restore();
     assertThat(getMessageId(sender))
         .isEqualTo(
-            repository
-                    .getRefDatabase()
-                    .exactRef(result.getChange().getId().toRefPrefix() + "meta")
-                    .getObjectId()
-                    .getName()
-                + "-HTML");
+            withPrefixAndSuffixForMessageId(
+                repository
+                        .getRefDatabase()
+                        .exactRef(result.getChange().getId().toRefPrefix() + "meta")
+                        .getObjectId()
+                        .getName()
+                    + "-HTML"));
   }
 
   @Test
@@ -80,12 +83,13 @@
 
     assertThat(getMessageId(sender))
         .isEqualTo(
-            allUsersRepo
-                    .getRefDatabase()
-                    .exactRef(RefNames.refsUsers(admin.id()))
-                    .getObjectId()
-                    .getName()
-                + "-HTML");
+            withPrefixAndSuffixForMessageId(
+                allUsersRepo
+                        .getRefDatabase()
+                        .exactRef(RefNames.refsUsers(admin.id()))
+                        .getObjectId()
+                        .getName()
+                    + "-HTML"));
   }
 
   @Test
@@ -93,7 +97,8 @@
     sender.clear();
     String newPassword = gApi.accounts().self().generateHttpPassword();
     assertThat(newPassword).isNotNull();
-    assertThat(getMessageId(sender)).contains("HTTP password change-" + admin.id().toString());
+    assertThat(getMessageId(sender))
+        .containsMatch("<HTTP_password_change-" + admin.id().toString() + ".*@.*>");
   }
 
   @Test
@@ -124,4 +129,10 @@
             (Iterables.getOnlyElement(sender.getMessages()).headers().get("Message-ID")))
         .getString();
   }
+
+  // Each message-id must start with '<' and end with '>'. Also, it must contain no spaces and it
+  // must contain a '@'.
+  private String withPrefixAndSuffixForMessageId(String id) throws Exception {
+    return "<" + id + "@" + new URL(canonicalWebUrl.get()).getHost() + ">";
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
index 3228900..a8fd834 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.mail.MailMessage;
 import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.util.time.TimeUtil;
-import java.sql.Timestamp;
 import java.time.Instant;
 import javax.inject.Inject;
 import org.eclipse.jgit.lib.Repository;
@@ -38,7 +37,7 @@
     try (Repository repo = repoManager.openRepository(allUsers)) {
       String messageId = messageIdGenerator.fromAccountUpdate(admin.id()).id();
       String sha1 =
-          repo.getRefDatabase().getRef(RefNames.refsUsers(admin.id())).getObjectId().getName();
+          repo.getRefDatabase().findRef(RefNames.refsUsers(admin.id())).getObjectId().getName();
       assertThat(sha1).isEqualTo(messageId);
     }
   }
@@ -51,7 +50,7 @@
       String messageId = messageIdGenerator.fromChangeUpdate(project, patchsetId).id();
       String sha1 =
           repo.getRefDatabase()
-              .getRef(String.format("%smeta", patchsetId.changeId().toRefPrefix()))
+              .findRef(String.format("%smeta", patchsetId.changeId().toRefPrefix()))
               .getObjectId()
               .getName();
       assertThat(sha1).isEqualTo(messageId);
@@ -74,7 +73,7 @@
   @Test
   public void fromReasonAccountIdAndTimestamp() throws Exception {
     String reason = "reason";
-    Timestamp timestamp = TimeUtil.nowTs();
+    Instant timestamp = TimeUtil.now();
     assertThat(
             messageIdGenerator.fromReasonAccountIdAndTimestamp(reason, admin.id(), timestamp).id())
         .isEqualTo(reason + "-" + admin.id().toString() + "-" + timestamp.toString());
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index 38b7e0e..cf349ab 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Injector;
 import org.eclipse.jgit.lib.Config;
-import org.junit.Before;
 
 public class ElasticReindexIT extends AbstractReindexTests {
 
@@ -39,10 +38,4 @@
   public void configureIndex(Injector injector) {
     createAllIndexes(injector);
   }
-
-  @Before
-  public void reindexFirstSinceElastic() throws Exception {
-    assertServerStartupFails();
-    runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 4ae77da..d7d67b8 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.testing.TestCommentHelper;
 import com.google.inject.Inject;
 import com.google.inject.Module;
+import java.net.URL;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.Collection;
@@ -300,7 +301,7 @@
 
     // ensure the message header contains a valid message id.
     assertThat(((EmailHeader.String) (message.headers().get("Message-ID"))).getString())
-        .isEqualTo("some id-REJECTION-HTML");
+        .containsMatch("<someid-REJECTION-HTML@" + new URL(canonicalWebUrl.get()).getHost() + ">");
   }
 
   @Test
diff --git a/lib/BUILD b/lib/BUILD
index f0c0aad..d3ef4b9 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -47,6 +47,13 @@
 )
 
 java_library(
+    name = "jgit-ssh-jsch",
+    data = ["//lib:LICENSE-jgit"],
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit.ssh.jsch:ssh-jsch"],
+)
+
+java_library(
     name = "jgit-archive",
     data = ["//lib:LICENSE-jgit"],
     visibility = ["//visibility:public"],
diff --git a/modules/jgit b/modules/jgit
index 75fccca..55371c5 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 75fcccaea39f7a2112886e04a94458d6b7b7c37f
+Subproject commit 55371c5f063370aeca85acab16f867734ff4226c
diff --git a/plugins/BUILD b/plugins/BUILD
index 5f9c142..a071bde 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -71,6 +71,7 @@
     "//lib/jackson:jackson-core",
     "//lib:jgit-servlet",
     "//lib:jgit",
+    "//lib:jgit-ssh-jsch",
     "//lib:jsr305",
     "//lib/log:api",
     "//lib/log:log4j",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index e211fb1..7357ab4 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit e211fb1bd21043e2574c438a687c8f492d538c97
+Subproject commit 7357ab473599d16ae33cc982bbd65472f08c2dd6
diff --git a/plugins/delete-project b/plugins/delete-project
index 02cced3..7671def 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 02cced37fd755a1123b1ec18af96503683d88f50
+Subproject commit 7671def07882aab89b19eb7496418588ea7375d9
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 021ce08..7bca96d 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -1,6 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary")
 load("//tools/bzl:genrule2.bzl", "genrule2")
-load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
 
 package(default_visibility = ["//visibility:public"])
 
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
index c46cf30..e9c16241 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
@@ -82,7 +82,7 @@
     <div id="mainContainer">
       <div class="header">
         <div class="name">
-          <h3>[[_computeSectionName(section.id)]]</h3>
+          <h3 class="heading-3">[[_computeSectionName(section.id)]]</h3>
           <gr-button
             id="editBtn"
             link=""
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
index c5577c2..47657ac 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
@@ -63,9 +63,9 @@
       Loading...
     </div>
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-      <h1 id="Title">[[_groupName]]</h1>
+      <h1 id="Title" class="heading-1">[[_groupName]]</h1>
       <div id="form">
-        <h3 id="members">Members</h3>
+        <h3 id="members" class="heading-3">Members</h3>
         <fieldset>
           <span class="value">
             <gr-autocomplete
@@ -112,7 +112,7 @@
             </tbody>
           </table>
         </fieldset>
-        <h3 id="includedGroups">Included Groups</h3>
+        <h3 id="includedGroups" class="heading-3">Included Groups</h3>
         <fieldset>
           <span class="value">
             <gr-autocomplete
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
index 0af87ad..7a843dc 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
@@ -37,18 +37,21 @@
       Loading...
     </div>
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-      <h1 id="Title">[[_groupName]]</h1>
-      <h2 id="configurations">General</h2>
+      <h1 id="Title" class="heading-1">[[_groupName]]</h1>
+      <h2 id="configurations" class="heading-2">General</h2>
       <div id="form">
         <fieldset>
-          <h3 id="groupUUID">Group UUID</h3>
+          <h3 id="groupUUID" class="heading-3">Group UUID</h3>
           <fieldset>
             <gr-copy-clipboard
               id="uuid"
               text="[[_getGroupUUID(_groupConfig.id)]]"
             ></gr-copy-clipboard>
           </fieldset>
-          <h3 id="groupName" class$="[[_computeHeaderClass(_rename)]]">
+          <h3
+            id="groupName"
+            class$="heading-3 [[_computeHeaderClass(_rename)]]"
+          >
             Group Name
           </h3>
           <fieldset>
@@ -72,7 +75,10 @@
               >
             </span>
           </fieldset>
-          <h3 id="groupOwner" class$="[[_computeHeaderClass(_owner)]]">
+          <h3
+            id="groupOwner"
+            class$="heading-3 [[_computeHeaderClass(_owner)]]"
+          >
             Owners
           </h3>
           <fieldset>
@@ -99,7 +105,7 @@
               >
             </span>
           </fieldset>
-          <h3 class$="[[_computeHeaderClass(_description)]]">
+          <h3 class$="heading-3 [[_computeHeaderClass(_description)]]">
             Description
           </h3>
           <fieldset>
@@ -123,7 +129,7 @@
               </gr-button>
             </span>
           </fieldset>
-          <h3 id="options" class$="[[_computeHeaderClass(_options)]]">
+          <h3 id="options" class$="heading-3 [[_computeHeaderClass(_options)]]">
             Group Options
           </h3>
           <fieldset>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
index 5f0739a..b46c7c0 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
@@ -62,7 +62,10 @@
       Loading...
     </div>
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-      <h3 id="inheritsFrom" class$="[[_computeShowInherit(_inheritsFrom)]]">
+      <h3
+        id="inheritsFrom"
+        class$="heading-3 [[_computeShowInherit(_inheritsFrom)]]"
+      >
         <span class="rightsText">Rights Inherit From</span>
         <a
           href$="[[_computeParentHref(_inheritsFrom.name)]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
index cf934b0..50aca6d 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
@@ -23,7 +23,7 @@
       margin-bottom: var(--spacing-xxl);
     }
   </style>
-  <h3>[[title]]</h3>
+  <h3 class="heading-3">[[title]]</h3>
   <gr-button
     title$="[[tooltip]]"
     disabled$="[[disabled]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
index b27c36b..66ec740 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
@@ -27,12 +27,12 @@
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
   <main class="gr-form-styles read-only">
-    <h1 id="Title">Repository Commands</h1>
+    <h1 id="Title" class="heading-1">Repository Commands</h1>
     <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
       Loading...
     </div>
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-      <h2 id="options">Command</h2>
+      <h2 id="options" class="heading-2">Command</h2>
       <div id="form">
         <gr-repo-command
           title="Create change"
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
index de36e73..ea8a8b9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
@@ -50,7 +50,7 @@
       /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
     </style>
     <div class="info">
-      <h1 id="Title" class$="name">
+      <h1 id="Title" class="heading-1">
         [[repo]]
         <hr />
       </h1>
@@ -63,7 +63,7 @@
     </div>
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
       <div id="downloadContent" class$="[[_computeHideClass(_schemes)]]">
-        <h2 id="download">Download</h2>
+        <h2 id="download" class="heading-2">Download</h2>
         <fieldset>
           <gr-download-commands
             id="downloadCommands"
@@ -73,12 +73,15 @@
           ></gr-download-commands>
         </fieldset>
       </div>
-      <h2 id="configurations" class$="[[_computeHeaderClass(_configChanged)]]">
+      <h2
+        id="configurations"
+        class$="heading-2 [[_computeHeaderClass(_configChanged)]]"
+      >
         Configurations
       </h2>
       <div id="form">
         <fieldset>
-          <h3 id="Description">Description</h3>
+          <h3 id="Description" class="heading-3">Description</h3>
           <fieldset>
             <iron-autogrow-textarea
               id="descriptionInput"
@@ -89,7 +92,7 @@
               disabled$="[[_readOnly]]"
             ></iron-autogrow-textarea>
           </fieldset>
-          <h3 id="Options">Repository Options</h3>
+          <h3 id="Options" class="heading-3">Repository Options</h3>
           <fieldset id="options">
             <section>
               <span class="title">State</span>
@@ -362,7 +365,7 @@
               </span>
             </section>
           </fieldset>
-          <h3 id="Options">Contributor Agreements</h3>
+          <h3 id="Options" class="heading-3">Contributor Agreements</h3>
           <fieldset id="agreements">
             <section>
               <span class="title">
@@ -407,7 +410,7 @@
             class$="pluginConfig [[_computeHideClass(_pluginData)]]"
             on-plugin-config-changed="_handlePluginConfigChanged"
           >
-            <h3>Plugins</h3>
+            <h3 class="heading-3">Plugins</h3>
             <template is="dom-repeat" items="[[_pluginData]]" as="data">
               <gr-repo-plugin-config
                 plugin-data="[[data]]"
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
index 4a357af..cc6223d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
@@ -48,12 +48,6 @@
       padding-top: var(--spacing-xl);
       vertical-align: top;
     }
-    #help h1 {
-      font-family: var(--header-font-family);
-      font-size: var(--font-size-h3);
-      font-weight: var(--font-weight-h3);
-      line-height: var(--line-height-h3);
-    }
     #help p {
       margin-bottom: var(--spacing-m);
       max-width: 35em;
@@ -73,7 +67,7 @@
     </p>
   </div>
   <div id="help">
-    <h1>Push your first change for code review</h1>
+    <h2 class="heading-3">Push your first change for code review</h2>
     <p>
       Pushing a change for review is easy, but a little different from other git
       code review tools. Click on the \`Create Change' button and follow the
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
index f6fb1d0..75af51e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
@@ -24,7 +24,7 @@
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
   <div class="info">
-    <h1 class$="name">
+    <h1 class="heading-1">
       [[repo]]
       <hr />
     </h1>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
index 5a5d590..8207284 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
@@ -21,12 +21,6 @@
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
   <style include="dashboard-header-styles">
-    .name {
-      display: inline-block;
-    }
-    .name hr {
-      width: 100%;
-    }
     .status.hide,
     .name.hide,
     .dashboardLink.hide {
@@ -39,7 +33,7 @@
     aria-label="Account avatar"
   ></gr-avatar>
   <div class="info">
-    <h1 class="name">
+    <h1 class="heading-1">
       [[_computeDetail(_accountDetails, 'name')]]
     </h1>
     <hr />
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index f8580e0..dc207dc 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -1019,10 +1019,7 @@
     this.$.fileList.collapseAllDiffs();
   }
 
-  _paramsChanged(value, oldValue) {
-    const paramsChanged = JSON.stringify(oldValue) !== JSON.stringify(value);
-    if (!paramsChanged) return;
-
+  _paramsChanged(value) {
     if (value.view !== GerritNav.View.CHANGE) {
       this._initialLoadComplete = false;
       return;
@@ -1138,6 +1135,14 @@
     this.viewState.numFilesShown = numFilesShown;
   }
 
+  _handleMessageAnchorTap(e) {
+    const hash = MSG_PREFIX + e.detail.id;
+    const url = GerritNav.getUrlForChange(this._change,
+        this._patchRange.patchNum, this._patchRange.basePatchNum,
+        this._editMode, hash);
+    history.replaceState(null, '', url);
+  }
+
   _maybeScrollToMessage(hash) {
     if (hash.startsWith(MSG_PREFIX)) {
       this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length));
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
index 50b6d2d..f5a0463 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
@@ -711,6 +711,7 @@
             change-comments="[[_changeComments]]"
             project-name="[[_change.project]]"
             show-reply-buttons="[[_loggedIn]]"
+            on-message-anchor-tap="_handleMessageAnchorTap"
             on-reply="_handleMessageReply"
           ></gr-messages-list>
         </template>
@@ -725,6 +726,7 @@
             change-comments="[[_changeComments]]"
             project-name="[[_change.project]]"
             show-reply-buttons="[[_loggedIn]]"
+            on-message-anchor-tap="_handleMessageAnchorTap"
             on-reply="_handleMessageReply"
           ></gr-messages-list-experimental>
         </template>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index 192ba8f..1362fe3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
@@ -322,6 +322,20 @@
   const getCustomCssValue =
       cssParam => util.getComputedStyleValue(cssParam, element);
 
+  test('_handleMessageAnchorTap', () => {
+    element._changeNum = '1';
+    element._patchRange = {
+      basePatchNum: 'PARENT',
+      patchNum: 1,
+    };
+    const getUrlStub = sandbox.stub(GerritNav, 'getUrlForChange');
+    const replaceStateStub = sandbox.stub(history, 'replaceState');
+    element._handleMessageAnchorTap({detail: {id: 'a12345'}});
+
+    assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
+    assert.isTrue(replaceStateStub.called);
+  });
+
   suite('plugins adding to file tab', () => {
     setup(done => {
       // Resolving it here instead of during setup() as other tests depend
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
index 9569c03..0446e4e 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
@@ -56,16 +56,12 @@
     .archives a:last-of-type {
       margin-right: 0;
     }
-    .title {
-      flex: 1;
-      font-weight: var(--font-weight-bold);
-    }
     .hidden {
       display: none;
     }
   </style>
   <section>
-    <h3 class="title">
+    <h3 class="heading-3">
       Patch set [[patchNum]] of [[_computePatchSetQuantity(change.revisions)]]
     </h3>
   </section>
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 adcf2e2..5e9825a 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
@@ -70,6 +70,8 @@
   U: 'Unchanged',
 };
 
+const FILE_ROW_CLASS = 'file-row';
+
 /**
  * Type for FileInfo
  *
@@ -676,29 +678,55 @@
   }
 
   /**
+   * Returns true if the event e is a click on an element.
+   *
+   * The click is: mouse click or pressing Enter or Space key
+   * P.S> Screen readers sends click event as well
+   */
+  _isClickEvent(e) {
+    if (e.type === 'click') {
+      return true;
+    }
+    const isSpaceOrEnter = (e.key === 'Enter' || e.key === ' ');
+    return e.type === 'keydown' && isSpaceOrEnter;
+  }
+
+  _fileActionClick(e, fileAction) {
+    if (this._isClickEvent(e)) {
+      const fileRow = this._getFileRowFromEvent(e);
+      if (!fileRow) {
+        return;
+      }
+      // Prevent default actions (e.g. scrolling for space key)
+      e.preventDefault();
+      // Prevent _handleFileListClick handler call
+      e.stopPropagation();
+      this.$.fileCursor.setCursor(fileRow.element);
+      fileAction(fileRow.file);
+    }
+  }
+
+  _reviewedClick(e) {
+    this._fileActionClick(e,
+        file => this._reviewFile(file.path));
+  }
+
+  _expandedClick(e) {
+    this._fileActionClick(e,
+        file => this._toggleFileExpanded(file));
+  }
+
+  /**
    * Handle all events from the file list dom-repeat so event handleers don't
    * have to get registered for potentially very long lists.
    */
   _handleFileListClick(e) {
-    // Traverse upwards to find the row element if the target is not the row.
-    let row = e.target;
-    while (!row.classList.contains('row') && row.parentElement) {
-      row = row.parentElement;
-    }
-
-    // No action needed for item without a valid file
-    if (!row.dataset.file) {
+    const fileRow = this._getFileRowFromEvent(e);
+    if (!fileRow) {
       return;
     }
-
-    const file = JSON.parse(row.dataset.file);
+    const file = fileRow.file;
     const path = file.path;
-    // Handle checkbox mark as reviewed.
-    if (e.target.classList.contains('markReviewed')) {
-      e.preventDefault();
-      return this._reviewFile(path);
-    }
-
     // If a path cannot be interpreted from the click target (meaning it's not
     // somewhere in the row, e.g. diff content) or if the user clicked the
     // link, defer to the native behavior.
@@ -708,9 +736,28 @@
     if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
 
     e.preventDefault();
+    this.$.fileCursor.setCursor(fileRow.element);
     this._toggleFileExpanded(file);
   }
 
+  _getFileRowFromEvent(e) {
+    // Traverse upwards to find the row element if the target is not the row.
+    let row = e.target;
+    while (!row.classList.contains(FILE_ROW_CLASS) && row.parentElement) {
+      row = row.parentElement;
+    }
+
+    // No action needed for item without a valid file
+    if (!row.dataset.file) {
+      return null;
+    }
+
+    return {
+      file: JSON.parse(row.dataset.file),
+      element: row,
+    };
+  }
+
   /**
    * Generates file range from file info object.
    *
@@ -1066,7 +1113,7 @@
     if (this._files && this._files.length > 0) {
       flush();
       this.$.fileCursor.stops = Array.from(
-          dom(this.root).querySelectorAll('.file-row'));
+          dom(this.root).querySelectorAll(`.${FILE_ROW_CLASS}`));
       this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
     }
   }
@@ -1129,10 +1176,29 @@
       FileStatus[statusCode] : 'Status Unknown';
   }
 
+  /**
+   * Converts any boolean-like variable to the string 'true' or 'false'
+   *
+   * This method is useful when you bind aria-checked attribute to a boolean
+   * value. The aria-checked attribute is string attribute. Binding directly
+   * to boolean variable causes problem on gerrit-CI.
+   *
+   * @param {object} val
+   * @return {string} 'true' if val is true-like, otherwise false
+   */
+  _booleanToString(val) {
+    return val ? 'true' : 'false';
+  }
+
   _isFileExpanded(path, expandedFilesRecord) {
     return expandedFilesRecord.base.some(f => f.path === path);
   }
 
+  _isFileExpandedStr(path, expandedFilesRecord) {
+    return this._booleanToString(
+        this._isFileExpanded(path, expandedFilesRecord));
+  }
+
   _computeExpandedFiles(expandedCount, totalCount) {
     if (expandedCount === 0) {
       return GrFileListConstants.FilesExpandedState.NONE;
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 d1238f3..4ac4bb1 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
@@ -28,6 +28,20 @@
       min-height: calc(var(--line-height-normal) + 2 * var(--spacing-s));
       padding: var(--spacing-xs) var(--spacing-l);
     }
+    /* The class defines a content visible only to screen readers */
+    .noCommentsScreenReaderText {
+      opacity: 0;
+      max-width: 1px;
+      overflow: hidden;
+      display: none;
+    }
+    div[role='gridcell']
+      > div.comments
+      > span:empty
+      + span:empty
+      + span.noCommentsScreenReaderText {
+      display: inline;
+    }
     :host(.loading) .row {
       opacity: 0.5;
     }
@@ -164,6 +178,9 @@
       color: #c62828;
       font-weight: var(--font-weight-bold);
     }
+    .show-hide-icon:focus {
+      outline: none;
+    }
     .show-hide {
       margin-left: var(--spacing-s);
       width: 1.9em;
@@ -200,27 +217,24 @@
       margin-left: var(--spacing-xxl);
       width: 15em;
     }
-    .reviewed label {
+    .reviewedSwitch {
       color: var(--link-color);
       opacity: 0;
       justify-content: flex-end;
       width: 100%;
     }
-    .reviewed label:hover {
+    .reviewedSwitch:hover {
       cursor: pointer;
       opacity: 100;
     }
     .row:focus {
       outline: none;
     }
-    .row:hover .reviewed label,
-    .row:focus .reviewed label,
-    .row.expanded .reviewed label {
+    .row:hover .reviewedSwitch,
+    .row:focus-within .reviewedSwitch,
+    .row.expanded .reviewedSwitch {
       opacity: 100;
     }
-    .reviewed input {
-      display: none;
-    }
     .reviewedLabel {
       color: var(--deemphasized-text-color);
       margin-right: var(--spacing-l);
@@ -233,6 +247,9 @@
     .editFileControls {
       width: 7em;
     }
+    .markReviewed:focus {
+      outline: none;
+    }
     .markReviewed,
     .pathLink {
       display: inline-block;
@@ -255,10 +272,8 @@
         padding: 0px;
       }
     }
-    .pathLink:hover gr-copy-clipboard,
-    .pathLink:focus gr-copy-clipboard,
-    .oldPath:focus gr-copy-clipboard,
-    .oldPath:hover gr-copy-clipboard {
+    .row:focus-within gr-copy-clipboard,
+    .row:hover gr-copy-clipboard {
       visibility: visible;
     }
 
@@ -296,8 +311,13 @@
       }
     }
   </style>
-  <div id="container" on-click="_handleFileListClick">
-    <div class="header-row row">
+  <div
+    id="container"
+    on-click="_handleFileListClick"
+    role="grid"
+    aria-label="Files list"
+  >
+    <div class="header-row row" role="row">
       <!-- endpoint: change-view-file-list-header-prepend -->
       <template is="dom-if" if="[[_showPrependedDynamicColumns]]">
         <template
@@ -305,14 +325,14 @@
           items="[[_dynamicPrependedHeaderEndpoints]]"
           as="headerEndpoint"
         >
-          <gr-endpoint-decorator name$="[[headerEndpoint]]">
+          <gr-endpoint-decorator name$="[[headerEndpoint]]" role="columnheader">
           </gr-endpoint-decorator>
         </template>
       </template>
-      <div class="path">File</div>
-      <div class="comments">Comments</div>
-      <div class="sizeBars">Size</div>
-      <div class="header-stats">Delta</div>
+      <div class="path" role="columnheader">File</div>
+      <div class="comments" role="columnheader">Comments</div>
+      <div class="sizeBars" role="columnheader">Size</div>
+      <div class="header-stats" role="columnheader">Delta</div>
       <!-- endpoint: change-view-file-list-header -->
       <template is="dom-if" if="[[_showDynamicColumns]]">
         <template
@@ -320,14 +340,18 @@
           items="[[_dynamicHeaderEndpoints]]"
           as="headerEndpoint"
         >
-          <gr-endpoint-decorator name$="[[headerEndpoint]]">
+          <gr-endpoint-decorator name$="[[headerEndpoint]]" role="columnheader">
           </gr-endpoint-decorator>
         </template>
       </template>
       <!-- Empty div here exists to keep spacing in sync with file rows. -->
-      <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div>
-      <div class="editFileControls showOnEdit"></div>
-      <div class="show-hide"></div>
+      <div
+        class="reviewed hideOnEdit"
+        hidden$="[[!_loggedIn]]"
+        aria-hidden="true"
+      ></div>
+      <div class="editFileControls showOnEdit" aria-hidden="true"></div>
+      <div class="show-hide" aria-hidden="true"></div>
     </div>
 
     <template
@@ -344,6 +368,7 @@
           class$="file-row row [[_computePathClass(file.__path, _expandedFiles.*)]]"
           data-file$="[[_computeFileRange(file)]]"
           tabindex="-1"
+          role="row"
         >
           <!-- endpoint: change-view-file-list-content-prepend -->
           <template is="dom-if" if="[[_showPrependedDynamicColumns]]">
@@ -352,7 +377,7 @@
               items="[[_dynamicPrependedContentEndpoints]]"
               as="contentEndpoint"
             >
-              <gr-endpoint-decorator name="[[contentEndpoint]]">
+              <gr-endpoint-decorator name="[[contentEndpoint]]" role="gridcell">
                 <gr-endpoint-param name="changeNum" value="[[changeNum]]">
                 </gr-endpoint-param>
                 <gr-endpoint-param name="patchRange" value="[[patchRange]]">
@@ -366,6 +391,7 @@
           <span
             data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"
             class="path"
+            role="gridcell"
           >
             <a
               class="pathLink"
@@ -406,66 +432,114 @@
               </div>
             </template>
           </span>
-          <div class="comments desktop">
-            <span class="drafts">
-              [[_computeDraftsString(changeComments, patchRange, file.__path)]]
-            </span>
-            [[_computeCommentsString(changeComments, patchRange, file.__path)]]
+          <div role="gridcell">
+            <div class="comments desktop">
+              <span class="drafts"
+                ><!-- This comments ensure that span is empty when the function
+                returns empty string.                
+              -->[[_computeDraftsString(changeComments, patchRange,
+                file.__path)]]<!-- This comments ensure that span is empty when
+                the function returns empty string.
+           --></span
+              >
+              <span
+                ><!--
+              -->[[_computeCommentsString(changeComments, patchRange,
+                file.__path)]]<!--
+           --></span
+              >
+              <span class="noCommentsScreenReaderText">
+                <!-- Screen readers read the following content only if 2 other
+              spans in the parent div is empty. The content is not visible on
+              the page.
+              Without this span, screen readers don't navigate correctly inside
+              table, because empty div doesn't rendered. For example, VoiceOver
+              jumps back to the whole table.
+              We can use &nbsp instead, but it sounds worse.                            
+              -->
+                No comments
+              </span>
+            </div>
+            <div class="comments mobile">
+              <span class="drafts"
+                ><!-- This comments ensure that span is empty when the function
+                returns empty string.
+              -->[[_computeDraftsStringMobile(changeComments, patchRange,
+                file.__path)]]<!-- This comments ensure that span is empty when
+                the function returns empty string.
+           --></span
+              >
+              <span
+                ><!--
+             -->[[_computeCommentsStringMobile(changeComments, patchRange,
+                file.__path)]]<!--
+           --></span
+              >
+              <span class="noCommentsScreenReaderText">
+                <!-- The same as for desktop comments -->
+                No comments
+              </span>
+            </div>
           </div>
-          <div class="comments mobile">
-            <span class="drafts">
-              [[_computeDraftsStringMobile(changeComments, patchRange,
-              file.__path)]]
-            </span>
-            [[_computeCommentsStringMobile(changeComments, patchRange,
-            file.__path)]]
+          <div role="gridcell">
+            <!-- The content must be in a separate div. It guarantees, that
+              gridcell always visible for screen readers.
+              For example, without a nested div screen readers pronounce the
+              "Commit message" row content with incorrect column headers.
+            -->
+            <div
+              class$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]"
+              aria-label="A bar that represents the addition and deletion ratio for the current file"
+            >
+              <svg width="61" height="8">
+                <rect
+                  x$="[[_computeBarAdditionX(file, _sizeBarLayout)]]"
+                  y="0"
+                  height="8"
+                  fill="var(--positive-green-text-color)"
+                  width$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]"
+                ></rect>
+                <rect
+                  x$="[[_computeBarDeletionX(_sizeBarLayout)]]"
+                  y="0"
+                  height="8"
+                  fill="var(--negative-red-text-color)"
+                  width$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]"
+                ></rect>
+              </svg>
+            </div>
           </div>
-          <div
-            class$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]"
-            aria-label="A bar that represents the addition and deletion ratio for the current file"
-            tabindex="0"
-          >
-            <svg width="61" height="8">
-              <rect
-                x$="[[_computeBarAdditionX(file, _sizeBarLayout)]]"
-                y="0"
-                height="8"
-                fill="var(--positive-green-text-color)"
-                width$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]"
-              ></rect>
-              <rect
-                x$="[[_computeBarDeletionX(_sizeBarLayout)]]"
-                y="0"
-                height="8"
-                fill="var(--negative-red-text-color)"
-                width$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]"
-              ></rect>
-            </svg>
-          </div>
-          <div class$="[[_computeClass('stats', file.__path)]]">
-            <span
-              class="added"
-              tabindex="0"
-              aria-label$="[[file.lines_inserted]] lines added"
-              hidden$="[[file.binary]]"
-            >
-              +[[file.lines_inserted]]
-            </span>
-            <span
-              class="removed"
-              tabindex="0"
-              aria-label$="[[file.lines_deleted]] lines removed"
-              hidden$="[[file.binary]]"
-            >
-              -[[file.lines_deleted]]
-            </span>
-            <span
-              class$="[[_computeBinaryClass(file.size_delta)]]"
-              hidden$="[[!file.binary]]"
-            >
-              [[_formatBytes(file.size_delta)]] [[_formatPercentage(file.size,
-              file.size_delta)]]
-            </span>
+          <div class="stats" role="gridcell">
+            <!-- The content must be in a separate div. It guarantees, that
+            gridcell always visible for screen readers.
+            For example, without a nested div screen readers pronounce the
+            "Commit message" row content with incorrect column headers.
+            -->
+            <div class$="[[_computeClass('', file.__path)]]">
+              <span
+                class="added"
+                tabindex="0"
+                aria-label$="[[file.lines_inserted]] lines added"
+                hidden$="[[file.binary]]"
+              >
+                +[[file.lines_inserted]]
+              </span>
+              <span
+                class="removed"
+                tabindex="0"
+                aria-label$="[[file.lines_deleted]] lines removed"
+                hidden$="[[file.binary]]"
+              >
+                -[[file.lines_deleted]]
+              </span>
+              <span
+                class$="[[_computeBinaryClass(file.size_delta)]]"
+                hidden$="[[!file.binary]]"
+              >
+                [[_formatBytes(file.size_delta)]] [[_formatPercentage(file.size,
+                file.size_delta)]]
+              </span>
+            </div>
           </div>
           <!-- endpoint: change-view-file-list-content -->
           <template is="dom-if" if="[[_showDynamicColumns]]">
@@ -474,7 +548,7 @@
               items="[[_dynamicContentEndpoints]]"
               as="contentEndpoint"
             >
-              <div class$="[[_computeClass('', file.__path)]]">
+              <div class$="[[_computeClass('', file.__path)]]" role="gridcell">
                 <gr-endpoint-decorator name="[[contentEndpoint]]">
                   <gr-endpoint-param name="changeNum" value="[[changeNum]]">
                   </gr-endpoint-param>
@@ -486,25 +560,44 @@
               </div>
             </template>
           </template>
-          <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]" hidden="">
+          <div
+            class="reviewed hideOnEdit"
+            role="gridcell"
+            hidden$="[[!_loggedIn]]"
+          >
             <span
               class$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]"
+              aria-hidden$="[[!file.isReviewed]]"
               >Reviewed</span
             >
-            <label>
-              <input
-                class="reviewed"
-                type="checkbox"
-                checked="[[file.isReviewed]]"
-              />
+            <!-- Do not use input type="checkbox" with hidden input and
+                  visible label here. Screen readers don't read/interract
+                  correctly with such input. 
+              -->
+            <span
+              class="reviewedSwitch"
+              role="switch"
+              tabindex="0"
+              on-click="_reviewedClick"
+              on-keydown="_reviewedClick"
+              aria-label="Reviewed"
+              aria-checked$="[[_booleanToString(file.isReviewed)]]"
+            >
+              <!-- Trick with tabindex to avoid outline on mouse focus, but
+                preserve focus outline for keyboard navigation -->
               <span
+                tabindex="-1"
                 class="markReviewed"
                 title$="[[_reviewedTitle(file.isReviewed)]]"
                 >[[_computeReviewedText(file.isReviewed)]]</span
               >
-            </label>
+            </span>
           </div>
-          <div class="editFileControls showOnEdit">
+          <div
+            class="editFileControls showOnEdit"
+            role="gridcell"
+            aria-hidden$="[[!editMode]]"
+          >
             <template is="dom-if" if="[[editMode]]">
               <gr-edit-file-controls
                 class$="[[_computeClass('', file.__path)]]"
@@ -512,25 +605,32 @@
               ></gr-edit-file-controls>
             </template>
           </div>
-          <div class="show-hide">
-            <label
+          <div class="show-hide" role="gridcell">
+            <!-- Do not use input type="checkbox" with hidden input and
+                visible label here. Screen readers don't read/interract
+                correctly with such input. 
+            -->
+            <span
               class="show-hide"
               data-path$="[[file.__path]]"
               data-expand="true"
+              role="switch"
+              tabindex="0"
+              aria-checked$="[[_isFileExpandedStr(file.__path, _expandedFiles.*)]]"
+              aria-label="Expand file"
+              on-click="_expandedClick"
+              on-keydown="_expandedClick"
             >
-              <input
-                type="checkbox"
-                class="show-hide"
-                checked$="[[_isFileExpanded(file.__path, _expandedFiles.*)]]"
-                data-path$="[[file.__path]]"
-                data-expand="true"
-              />
+              <!-- Trick with tabindex to avoid outline on mouse focus, but
+              preserve focus outline for keyboard navigation -->
               <iron-icon
+                class="show-hide-icon"
+                tabindex="-1"
                 id="icon"
                 icon="[[_computeShowHideIcon(file.__path, _expandedFiles.*)]]"
               >
               </iron-icon>
-            </label>
+            </span>
           </div>
         </div>
         <template
@@ -560,14 +660,14 @@
       <span
         class="added"
         tabindex="0"
-        aria-label$="[[_patchChange.inserted]] lines added"
+        aria-label$="Total [[_patchChange.inserted]] lines added"
       >
         +[[_patchChange.inserted]]
       </span>
       <span
         class="removed"
         tabindex="0"
-        aria-label$="[[_patchChange.deleted]] lines removed"
+        aria-label$="Total [[_patchChange.deleted]] lines removed"
       >
         -[[_patchChange.deleted]]
       </span>
@@ -590,12 +690,18 @@
   </div>
   <div class="row totalChanges" hidden$="[[_hideBinaryChangeTotals]]">
     <div class="total-stats">
-      <span class="added" aria-label="Total lines added">
+      <span
+        class="added"
+        aria-label$="Total bytes inserted: [[_formatBytes(_patchChange.size_delta_inserted)]] "
+      >
         [[_formatBytes(_patchChange.size_delta_inserted)]]
         [[_formatPercentage(_patchChange.total_size,
         _patchChange.size_delta_inserted)]]
       </span>
-      <span class="removed" aria-label="Total lines removed">
+      <span
+        class="removed"
+        aria-label$="Total bytes removed: [[_formatBytes(_patchChange.size_delta_deleted)]]"
+      >
         [[_formatBytes(_patchChange.size_delta_deleted)]]
         [[_formatPercentage(_patchChange.total_size,
         _patchChange.size_delta_deleted)]]
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index f038f6c..8e279ae 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -836,36 +836,42 @@
         patchNum: '2',
       };
       element.$.fileCursor.setCursorAtIndex(0);
+      const reviewSpy = sandbox.spy(element, '_reviewFile');
+      const toggleExpandSpy = sandbox.spy(element, '_toggleFileExpanded');
 
       flushAsynchronousOperations();
       const fileRows =
           dom(element.root).querySelectorAll('.row:not(.header-row)');
-      const checkSelector = 'input.reviewed[type="checkbox"]';
+      const checkSelector = 'span.reviewedSwitch[role="switch"]';
       const commitMsg = fileRows[0].querySelector(checkSelector);
       const fileAdded = fileRows[1].querySelector(checkSelector);
       const myFile = fileRows[2].querySelector(checkSelector);
 
-      assert.isTrue(commitMsg.checked);
-      assert.isFalse(fileAdded.checked);
-      assert.isTrue(myFile.checked);
+      assert.equal(commitMsg.getAttribute('aria-checked'), 'true');
+      assert.equal(fileAdded.getAttribute('aria-checked'), 'false');
+      assert.equal(myFile.getAttribute('aria-checked'), 'true');
 
       const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel');
-      const markReviewLabel = commitMsg.nextElementSibling;
+      const markReviewLabel = fileRows[0].querySelector('.markReviewed');
       assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
       assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
 
-      const clickSpy = sandbox.spy(element, '_handleFileListClick');
+      const clickSpy = sandbox.spy(element, '_reviewedClick');
       MockInteractions.tap(markReviewLabel);
-      assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
-      assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
+      // assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
+      // assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
       assert.equal(markReviewLabel.textContent, 'MARK REVIEWED');
       assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+      assert.isTrue(reviewSpy.calledOnce);
 
       MockInteractions.tap(markReviewLabel);
       assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
       assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
       assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
       assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+      assert.isTrue(reviewSpy.calledTwice);
+
+      assert.isFalse(toggleExpandSpy.called);
     });
 
     test('_computeFileStatusLabel', () => {
@@ -906,13 +912,6 @@
       assert.isTrue(clickSpy.calledTwice);
       assert.isTrue(toggleExpandSpy.calledOnce);
       assert.isFalse(reviewStub.called);
-
-      // Click the reviewed checkbox, resulting in a call to _reviewFile, but
-      // no additional call to _toggleFileExpanded.
-      row.querySelector('.markReviewed').click();
-      assert.isTrue(clickSpy.calledThrice);
-      assert.isTrue(toggleExpandSpy.calledOnce);
-      assert.isTrue(reviewStub.calledOnce);
     });
 
     test('_handleFileListClick editMode', () => {
@@ -975,12 +974,12 @@
           dom(element.root).querySelectorAll('.row:not(.header-row)');
       // Because the label surrounds the input, the tap event is triggered
       // there first.
-      const showHideLabel = fileRows[0].querySelector('label.show-hide');
       const showHideCheck = fileRows[0].querySelector(
-          'input.show-hide[type="checkbox"]');
-      assert.isNotOk(showHideCheck.checked);
+          'span.show-hide[role="switch"]');
+      const showHideLabel = showHideCheck.querySelector('.show-hide-icon');
+      assert.equal(showHideCheck.getAttribute('aria-checked'), 'false');
       MockInteractions.tap(showHideLabel);
-      assert.isOk(showHideCheck.checked);
+      assert.equal(showHideCheck.getAttribute('aria-checked'), 'true');
       assert.notEqual(
           element._expandedFiles.findIndex(f => f.path === 'myfile.txt'),
           -1);
@@ -1001,7 +1000,7 @@
 
       // Tap on a file to generate the diff.
       const row = dom(element.root)
-          .querySelectorAll('.row:not(.header-row) label.show-hide')[0];
+          .querySelectorAll('.row:not(.header-row) span.show-hide')[0];
 
       MockInteractions.tap(row);
       flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
index 2faac52..4670e6a 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
@@ -67,7 +67,7 @@
     }
   </style>
   <header>
-    <h1 id="title">Included In:</h1>
+    <h1 id="title" class="heading-1">Included In:</h1>
     <span class="closeButtonContainer">
       <gr-button id="closeButton" link="" on-click="_handleCloseTap"
         >Close</gr-button
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index be53652..e065008 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -35,7 +35,6 @@
 
 const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
 const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
-const MSG_PREFIX = '#message-';
 
 /**
  * @extends PolymerElement
@@ -153,10 +152,6 @@
         value: false,
       },
       _isCleanerLogExperimentEnabled: Boolean,
-      _changeMessageUrl: {
-        type: String,
-        computed: '_computeChangeMessageUrl(message)',
-      },
     };
   }
 
@@ -411,10 +406,13 @@
     return classes.join(' ');
   }
 
-  _computeChangeMessageUrl(message) {
-    if (!message) return '';
-    const hash = MSG_PREFIX + message.id;
-    return hash;
+  _handleAnchorClick(e) {
+    e.preventDefault();
+    this.dispatchEvent(new CustomEvent('message-anchor-tap', {
+      bubbles: true,
+      composed: true,
+      detail: {id: this.message.id},
+    }));
   }
 
   _handleReplyTap(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
index baaf5a3..de4a72a 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
@@ -181,9 +181,6 @@
         content: 'PS ';
       }
     }
-    .message-link {
-      text-decoration: none;
-    }
   </style>
   <div class$="[[_computeClass(_expanded)]]">
     <div class="contentContainer">
@@ -303,20 +300,13 @@
           </span>
         </template>
         <template is="dom-if" if="[[message.id]]">
-          <span class="date">
+          <span class="date" on-click="_handleAnchorClick">
             <gr-date-formatter
               has-tooltip=""
               show-date-and-time=""
               date-str="[[message.date]]"
             ></gr-date-formatter>
           </span>
-          <a class="message-link" href="[[_changeMessageUrl]]">
-            <iron-icon
-              id="icon"
-              class="link-icon"
-              icon="gr-icons:link"
-            ></iron-icon>
-          </a>
         </template>
         <iron-icon
           id="expandToggle"
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 0bd53c7..332ca52 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -222,6 +222,26 @@
       });
     });
 
+    test('clicking on date link fires event', () => {
+      element.message = {
+        type: 'REVIEWER_UPDATE',
+        updated: '2016-01-12 20:24:49.448000000',
+        reviewer: {},
+        id: '47c43261_55aa2c41',
+        expanded: false,
+      };
+      flushAsynchronousOperations();
+      const stub = sinon.stub();
+      element.addEventListener('message-anchor-tap', stub);
+      const dateEl = element.shadowRoot
+          .querySelector('.date');
+      assert.ok(dateEl);
+      MockInteractions.tap(dateEl);
+
+      assert.isTrue(stub.called);
+      assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
+    });
+
     suite('compute messages', () => {
       test('empty', () => {
         assert.equal(element._computeMessageContent('', '', true), '');
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
index 6aa7ffc..2321deb 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
@@ -21,9 +21,6 @@
     :host {
       display: block;
     }
-    h3 {
-      margin: var(--spacing-m) 0 0;
-    }
     section {
       margin-bottom: 1.4em; /* Same as line height for collapse purposes */
     }
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 7286678..de11712 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
@@ -108,9 +108,6 @@
       background: var(--table-header-background-color);
       padding: var(--spacing-l);
     }
-    .draftsContainer h3 {
-      margin-top: var(--spacing-xs);
-    }
     #checkingStatusLabel,
     #notLatestLabel {
       margin-left: var(--spacing-l);
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
index 78b576e..e850fbe 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
@@ -55,7 +55,7 @@
     }
   </style>
   <header>
-    <h3>Keyboard shortcuts</h3>
+    <h3 class="heading-3">Keyboard shortcuts</h3>
     <gr-button link="" on-click="_handleCloseTap">Close</gr-button>
   </header>
   <main>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
index 8308942..69e2989 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
@@ -393,13 +393,12 @@
    * @param {number=} opt_patchNum
    * @return {string}
    */
-  getUrlForChangeById(changeNum, project, opt_patchNum, opt_messageHash) {
+  getUrlForChangeById(changeNum, project, opt_patchNum) {
     return this._getUrlFor({
       view: GerritNav.View.CHANGE,
       changeNum,
       project,
       patchNum: opt_patchNum,
-      messageHash: opt_messageHash,
     });
   },
 
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
index 2d371e2..b1a951b7 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
@@ -61,8 +61,8 @@
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
   <main>
-    <h1>New Contributor Agreement</h1>
-    <h3>Select an agreement type:</h3>
+    <h1 class="heading-1">New Contributor Agreement</h1>
+    <h3 class="heading-3">Select an agreement type:</h3>
     <template
       is="dom-repeat"
       items="[[_serverConfig.auth.contributor_agreements]]"
@@ -92,7 +92,7 @@
       id="claNewAgreement"
       class$="[[_computeShowAgreementsClass(_showAgreements)]]"
     >
-      <h3 class="smallHeading">Review the agreement:</h3>
+      <h3 class="heading-3">Review the agreement:</h3>
       <div id="agreementsUrl" class="agreementsUrl">
         <a href$="[[_agreementsUrl]]" target="blank" rel="noopener">
           Please review the agreement.</a
@@ -101,7 +101,7 @@
       <div
         class$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]"
       >
-        <h3 class="smallHeading">Complete the agreement:</h3>
+        <h3 class="heading-3">Complete the agreement:</h3>
         <iron-input
           bind-value="{{_agreementsText}}"
           placeholder="Enter 'I agree' here"
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
index e26faab..26200d4 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
@@ -23,6 +23,6 @@
       margin-bottom: var(--spacing-xxl);
     }
   </style>
-  <h2 id="[[anchor]]">[[title]]</h2>
+  <h2 id="[[anchor]]" class="heading-2">[[title]]</h2>
   <slot></slot>
 `;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
index b22d412..e32a551 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
@@ -21,6 +21,12 @@
     :host {
       color: var(--primary-text-color);
     }
+    h2 {
+      font-family: var(--header-font-family);
+      font-size: var(--font-size-h2);
+      font-weight: var(--font-weight-h2);
+      line-height: var(--line-height-h2);
+    }
     .newEmailInput {
       width: 20em;
     }
@@ -94,7 +100,7 @@
       </ul>
     </gr-page-nav>
     <main class="gr-form-styles">
-      <h1>User Settings</h1>
+      <h1 class="heading-1">User Settings</h1>
       <section class="darkToggle">
         <div class="toggle">
           <paper-toggle-button
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
index b32f871..54b404d 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
@@ -60,7 +60,7 @@
     }
   </style>
   <div class="container" on-keydown="_handleKeydown">
-    <header class="font-h3"><slot name="header"></slot></header>
+    <header class="heading-3"><slot name="header"></slot></header>
     <main>
       <div class="overflow-container">
         <slot name="main"></slot>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
index 9c8788b..bbf6a96 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
@@ -70,7 +70,7 @@
           <gr-avatar account="[[account]]" image-size="56"></gr-avatar>
         </div>
         <div class="account">
-          <h3 class="name">[[account.name]]</h3>
+          <h3 class="name heading-3">[[account.name]]</h3>
           <div class="email">[[account.email]]</div>
         </div>
       </div>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
index 254fd25..85caa61 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
@@ -102,8 +102,6 @@
       <g id="pets"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/><path d="M0 0h24v24H0z" fill="none"/></g>
       <!-- This SVG is a copy from material.io https://material.io/icons/#visibility-->
       <g id="ready"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></g>
-      <!-- This SVG is a copy from material.io https://material.io/resources/icons/?icon=link&style=baseline-->
-      <g id="link"><path d="M0 0h24v24H0z" fill="none"/><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></g>
     </defs>
   </svg>
 </iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 2d951d4..de4f12d 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -14,15 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-/* NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 */
 /* NB: Order is important, because of namespaced classes. */
 
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 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';
-import 'es6-promise/lib/es6-promise.js';
-import 'whatwg-fetch/fetch.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index 4c259cd..1141d6b 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -261,14 +261,6 @@
     }
   },
   {
-    name: "es6-promise",
-    license: {
-      name: "es6-promise",
-      type: LicenseTypes.Mit,
-      packageLicenseFile: "LICENSE"
-    }
-  },
-  {
     name: "isarray",
     license: SharedLicenses.IsArray
   },
@@ -291,14 +283,6 @@
   {
     name: "polymer-bridges",
     license: SharedLicenses.Polymer2018
-  },
-  {
-    name: "whatwg-fetch",
-    license: {
-      name: "whatwg-fetch",
-      type: LicenseTypes.Mit,
-      packageLicenseFile: "LICENSE"
-    }
   }
 ];
 
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index a9668c3..32560ff 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -25,12 +25,10 @@
     "@polymer/polymer": "^3.3.0",
     "@webcomponents/shadycss": "^1.9.2",
     "@webcomponents/webcomponentsjs": "^1.3.3",
-    "es6-promise": "^3.3.1",
     "page": "^1.11.5",
     "polymer-bridges": "file:../../polymer-bridges/",
     "ba-linkify": "file:../../lib/ba-linkify/src/",
-    "polymer-resin": "^2.0.1",
-    "whatwg-fetch": "^3.0.0"
+    "polymer-resin": "^2.0.1"
   },
   "license": "Apache-2.0",
   "private": true
diff --git a/polygerrit-ui/app/styles/shared-styles.js b/polygerrit-ui/app/styles/shared-styles.js
index f5e048f..3e81761 100644
--- a/polygerrit-ui/app/styles/shared-styles.js
+++ b/polygerrit-ui/app/styles/shared-styles.js
@@ -103,19 +103,19 @@
         font-weight: var(--font-weight-normal);
         line-height: var(--line-height-small);
       }
-      h1, .font-h1 {
+      .heading-1 {
         font-family: var(--header-font-family);
         font-size: var(--font-size-h1);
         font-weight: var(--font-weight-h1);
         line-height: var(--line-height-h1);
       }
-      h2, .font-h2 {
+      .heading-2 {
         font-family: var(--header-font-family);
         font-size: var(--font-size-h2);
         font-weight: var(--font-weight-h2);
         line-height: var(--line-height-h2);
       }
-      h3, .font-h3 {
+      .heading-3 {
         font-family: var(--header-font-family);
         font-size: var(--font-size-h3);
         font-weight: var(--font-weight-h3);
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 821b724..8fb3eea 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -328,11 +328,6 @@
 "ba-linkify@file:../../lib/ba-linkify/src":
   version "1.0.0"
 
-es6-promise@^3.3.1:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
-  integrity sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=
-
 isarray@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -362,8 +357,3 @@
   dependencies:
     "@polymer/polymer" "^3.0.2"
     "@webcomponents/webcomponentsjs" "^2.0.3"
-
-whatwg-fetch@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
-  integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index c9ac0fe..2b473bc 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -23,6 +23,7 @@
     "//lib/bouncycastle:bcprov",
     "//lib/bouncycastle:bcpg",
     "//lib/log:impl-log4j",
+    "//lib:jgit-ssh-jsch",
     "//prolog:gerrit-prolog-common",
     "//resources:log4j-config",
 ]
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 9915a6e..f360fa5 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -181,6 +181,8 @@
         classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/src')
         classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/resources')
         classpathentry('src', 'modules/jgit/org.eclipse.jgit.junit/src')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/src')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/resources')
 
     def classpathentry(kind, path, src=None, out=None, exported=None, excluding=None):
         e = doc.createElement('classpathentry')