Merge "Fix gr-registration-dialog"
diff --git a/Documentation/cmd-convert-ref-storage.txt b/Documentation/cmd-convert-ref-storage.txt
new file mode 100644
index 0000000..aae385f
--- /dev/null
+++ b/Documentation/cmd-convert-ref-storage.txt
@@ -0,0 +1,58 @@
+= gerrit convert-ref-storage
+
+== NAME
+gerrit convert-ref-storage - Convert ref storage to reftable (experimental).
+
+A reftable file is a portable binary file format customized for reference storage.
+References are sorted, enabling linear scans, binary search lookup, and range scans.
+
+See also link:https://www.git-scm.com/docs/reftable for more details[reftable,role=external,window=_blank]
+
+== SYNOPSIS
+[verse]
+--
+_ssh_ -p <port> <host> _gerrit convert-ref-storage_
+  [--format <format>]
+  [--backup | -b]
+  [--reflogs | -r]
+  [--project <PROJECT> | -p <PROJECT>]
+--
+
+== DESCRIPTION
+Convert ref storage to reftable.
+
+== ACCESS
+Administrators
+
+== OPTIONS
+--project::
+-p::
+	Required; Name of the project for which the ref format should be changed.
+
+--format::
+	Format to convert to: `reftable` or `refdir`.
+	Default: reftable.
+
+--backup::
+-b::
+	Create backup of old ref storage format.
+	Default: true.
+
+--reflogs::
+-r::
+	Write reflogs to reftable.
+	Default: true.
+
+== EXAMPLES
+
+Convert ref format for project "core" to reftable:
+----
+$ ssh -p 29418 review.example.com gerrit convert-ref-format -p core
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-community.txt b/Documentation/dev-community.txt
index 70fda87..7488f74 100644
--- a/Documentation/dev-community.txt
+++ b/Documentation/dev-community.txt
@@ -43,7 +43,7 @@
 ** link:dev-contributing.html#mentorship[Mentorship]
 * link:dev-design-docs.html[Design Docs]
 * link:dev-readme.html[Developer Setup]
-* link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui[Polymer Frontend Developer Setup]
+* link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui[TypeScript Frontend Developer Setup]
 * link:dev-crafting-changes.html[Crafting Changes]
 * link:dev-starter-projects.html[Starter Projects]
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index f2a3e12..a66d3b5 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1450,7 +1450,7 @@
 
 By implementing the `com.google.gerrit.server.restapi.change.OnPostReview`
 interface plugins can extend the change message that is being posted when the
-[post review](rest-api-changes.html#set-review) REST endpoint is invoked.
+link:rest-api-changes.html#set-review[post review] REST endpoint is invoked.
 
 This is useful if certain approvals have a special meaning (e.g. custom logic
 that is implemented in Prolog submit rules, signal for triggering an action
@@ -1458,6 +1458,8 @@
 in the change message. This makes the effect of a given approval more
 transparent to the user.
 
+[[ui_extension]]
+== UI Extension
 
 [[actions]]
 === Actions
diff --git a/Documentation/images/user-checks-overview.png b/Documentation/images/user-checks-overview.png
new file mode 100644
index 0000000..7a9864e
--- /dev/null
+++ b/Documentation/images/user-checks-overview.png
Binary files differ
diff --git a/Documentation/pg-plugin-checks-api.txt b/Documentation/pg-plugin-checks-api.txt
new file mode 100644
index 0000000..4e93da1
--- /dev/null
+++ b/Documentation/pg-plugin-checks-api.txt
@@ -0,0 +1,44 @@
+:linkattrs:
+= Gerrit Code Review - JavaScript Plugin Checks API
+
+This API is provided by link:pg-plugin-dev.html#plugin-checks[plugin.checks()].
+It allows plugins to contribute to the "Checks" tab and summary:
+
+image::images/user-checks-overview.png[width=800]
+
+Each plugin can link:#register[register] a checks provider that will be called
+when a change page is loaded. Such a call would return a list of `Runs` and each
+run can contain a list of `Results`.
+
+The details of the ChecksApi are documented in the
+link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/api/checks.ts[source code].
+Note that this link points to the `master` branch and might thus reflect a
+newer version of the API than your Gerrit installation.
+
+If no plugins are registered with the ChecksApi, then the Checks tab will be
+hidden.
+
+You can read about the motivation, the use cases and the original plans in the
+link:https://www.gerritcodereview.com/design-docs/ci-reboot.html[design doc].
+
+Here are some examples of open source plugins that make use of the Checks API:
+
+* link:https://gerrit.googlesource.com/plugins/checks/+/master/gr-checks/plugin.js[Gerrit Checks Plugin]
+* link:https://chromium.googlesource.com/infra/gerrit-plugins/buildbucket/+/master/src/main/resources/static/buildbucket.js[Chromium Buildbucket Plugin]
+* link:https://chromium.googlesource.com/infra/gerrit-plugins/code-coverage/+/master/src/main/resources/static/chromium-coverage.js[Chromium Coverage Plugin]
+
+[[register]]
+== register
+`checksApi.register(provider, config?)`
+
+.Params
+- *provider* Must implement a `fetch()` interface that returns a
+  `Promise<FetchResponse>` with runs and results. See also documentation in the
+  link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/api/checks.ts[source code].
+- *config* Optional configuration values for the checks provider.
+
+[[announceUpdate]]
+== announceUpdate
+`checksApi.announceUpdate()`
+
+Tells Gerrit to call `provider.fetch()`.
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index 9c565da..dc7986f 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -21,6 +21,7 @@
   located in `gerrit-site/plugins` folder, where `pluginname` is an alphanumeric
   plugin name.
 
+=== Examples
 Here's a recommended starter `myplugin.js`:
 
 ``` js
@@ -29,6 +30,10 @@
 });
 ```
 
+You can find more elaborate examples in the
+link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/samples/[polygerrit-ui/app/samples/]
+directory of the source tree.
+
 [[low-level-api-concepts]]
 == Low-level DOM API concepts
 
@@ -96,9 +101,9 @@
 `plugin.registerStyleModule(endpointName, moduleName)`. A style must be defined
 as a standalone `<dom-module>` defined in the same .js file.
 
-See `samples/theme-plugin.js` for examples.
-
-Note: TODO: Insert link to the full styling API.
+See
+link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/samples/theme-plugin.js[samples/theme-plugin.js]
+for an example.
 
 ``` js
 const styleElement = document.createElement('dom-module');
@@ -141,8 +146,9 @@
 binding,role=external,window=_blank] for plugins that don't use Polymer. Can be used to bind element
 attribute changes to callbacks.
 
-See `samples/bind-parameters.js` for examples on both Polymer data bindings
-and `attibuteHelper` usage.
+See
+link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/samples/bind-parameters.js[samples/bind-parameters.js]
+for an example.
 
 === hook
 `plugin.hook(endpointName, opt_options)`
@@ -369,6 +375,12 @@
 Returns an instance of the
 link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/api/change-reply.ts[ChangeReplyPluginApi].
 
+[[checks]]
+=== checks
+`plugin.checks()`
+
+Returns an instance of the link:pg-plugin-checks-api.html[ChecksApi].
+
 === getPluginName
 `plugin.getPluginName()`
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index dab8117..d3635b3 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7058,6 +7058,9 @@
 |`web_links`   |optional|
 Links to the file in external sites as a list of
 link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
+|`edit_web_links`   |optional|
+Links to edit the file in external sites as a list of
+link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
 |==========================
 
 [[diff-info]]
diff --git a/Documentation/user-attention-set.txt b/Documentation/user-attention-set.txt
index 5053d10..1f67fc7 100644
--- a/Documentation/user-attention-set.txt
+++ b/Documentation/user-attention-set.txt
@@ -149,7 +149,7 @@
 instead.
 
 The "Assignee" feature can be turned on/off with the
-link:config-gerrit.html#change.enableAttentionSet[enableAssignee] config option.
+link:config-gerrit.html#change.enableAssignee[enableAssignee] config option.
 
 === Bold Changes / Mark Reviewed
 
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index b05050d..003df28 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1284,6 +1284,7 @@
     assertThat(diff.metaB.lines).isEqualTo(expectedLines.size());
     assertThat(diff.metaB.name).isEqualTo(path);
     assertThat(diff.metaB.webLinks).isNull();
+    assertThat(diff.metaB.editWebLinks).isNull();
 
     assertThat(diff.content).hasSize(1);
     DiffInfo.ContentEntry contentEntry = diff.content.get(0);
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 35f8ce6..6c6bab0 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.webui.EditWebLink;
 import com.google.gerrit.extensions.webui.FileHistoryWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.server.ExceptionHook;
@@ -75,6 +76,7 @@
   private final DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners;
   private final DynamicSet<FileHistoryWebLink> fileHistoryWebLinks;
   private final DynamicSet<PatchSetWebLink> patchSetWebLinks;
+  private final DynamicSet<EditWebLink> editWebLinks;
   private final DynamicSet<RevisionCreatedListener> revisionCreatedListeners;
   private final DynamicSet<GroupBackend> groupBackends;
   private final DynamicSet<AccountActivationValidationListener>
@@ -109,6 +111,7 @@
       DynamicSet<GitReferenceUpdatedListener> refUpdatedListeners,
       DynamicSet<FileHistoryWebLink> fileHistoryWebLinks,
       DynamicSet<PatchSetWebLink> patchSetWebLinks,
+      DynamicSet<EditWebLink> editWebLinks,
       DynamicSet<RevisionCreatedListener> revisionCreatedListeners,
       DynamicSet<GroupBackend> groupBackends,
       DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners,
@@ -139,6 +142,7 @@
     this.refUpdatedListeners = refUpdatedListeners;
     this.fileHistoryWebLinks = fileHistoryWebLinks;
     this.patchSetWebLinks = patchSetWebLinks;
+    this.editWebLinks = editWebLinks;
     this.revisionCreatedListeners = revisionCreatedListeners;
     this.groupBackends = groupBackends;
     this.accountActivationValidationListeners = accountActivationValidationListeners;
@@ -240,6 +244,10 @@
       return add(patchSetWebLinks, patchSetWebLink);
     }
 
+    public Registration add(EditWebLink editWebLink) {
+      return add(editWebLinks, editWebLink);
+    }
+
     public Registration add(RevisionCreatedListener revisionCreatedListener) {
       return add(revisionCreatedListeners, revisionCreatedListener);
     }
diff --git a/java/com/google/gerrit/extensions/common/DiffInfo.java b/java/com/google/gerrit/extensions/common/DiffInfo.java
index 2511e96..5a59613 100644
--- a/java/com/google/gerrit/extensions/common/DiffInfo.java
+++ b/java/com/google/gerrit/extensions/common/DiffInfo.java
@@ -52,6 +52,8 @@
     public Integer lines;
     // Links to the file in external sites
     public List<WebLinkInfo> webLinks;
+    // Links to edit the file in external sites
+    public List<WebLinkInfo> editWebLinks;
   }
 
   public static final class ContentEntry {
diff --git a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
index 0953bfe..d0212f3 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
@@ -64,4 +64,9 @@
     isNotNull();
     return check("webLinks").that(fileMeta.webLinks);
   }
+
+  public IterableSubject editWebLinks() {
+    isNotNull();
+    return check("editWebLinks").that(fileMeta.editWebLinks);
+  }
 }
diff --git a/java/com/google/gerrit/extensions/webui/EditWebLink.java b/java/com/google/gerrit/extensions/webui/EditWebLink.java
new file mode 100644
index 0000000..cd70feb
--- /dev/null
+++ b/java/com/google/gerrit/extensions/webui/EditWebLink.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+
+@ExtensionPoint
+public interface EditWebLink extends WebLink {
+
+  /**
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo} describing a link from a file to an
+   * external service for editing.
+   *
+   * <p>In order for the web link to be visible {@link WebLinkInfo#url} and {@link WebLinkInfo#name}
+   * must be set.
+   *
+   * @param projectName name of the project
+   * @param revision name of the revision (e.g. branch or commit ID)
+   * @param fileName name of the file
+   * @return WebLinkInfo that links to project in external service, null if there should be no link.
+   */
+  WebLinkInfo getEditWebLink(String projectName, String revision, String fileName);
+}
diff --git a/java/com/google/gerrit/pgm/util/ErrorLogJsonLayout.java b/java/com/google/gerrit/pgm/util/ErrorLogJsonLayout.java
index 2ea1f82..7d4abfc 100644
--- a/java/com/google/gerrit/pgm/util/ErrorLogJsonLayout.java
+++ b/java/com/google/gerrit/pgm/util/ErrorLogJsonLayout.java
@@ -97,7 +97,7 @@
 
     private String getSourceHost() {
       try {
-        return InetAddress.getLocalHost().getHostName();
+        return InetAddress.getLocalHost().getHostAddress();
       } catch (UnknownHostException e) {
         return "unknown-host";
       }
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index e66e7f5..3b626ea 100644
--- a/java/com/google/gerrit/server/WebLinks.java
+++ b/java/com/google/gerrit/server/WebLinks.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.BranchWebLink;
 import com.google.gerrit.extensions.webui.DiffWebLink;
+import com.google.gerrit.extensions.webui.EditWebLink;
 import com.google.gerrit.extensions.webui.FileHistoryWebLink;
 import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.extensions.webui.ParentWebLink;
@@ -56,6 +57,7 @@
 
   private final DynamicSet<PatchSetWebLink> patchSetLinks;
   private final DynamicSet<ParentWebLink> parentLinks;
+  private final DynamicSet<EditWebLink> editLinks;
   private final DynamicSet<FileWebLink> fileLinks;
   private final DynamicSet<FileHistoryWebLink> fileHistoryLinks;
   private final DynamicSet<DiffWebLink> diffLinks;
@@ -67,6 +69,7 @@
   public WebLinks(
       DynamicSet<PatchSetWebLink> patchSetLinks,
       DynamicSet<ParentWebLink> parentLinks,
+      DynamicSet<EditWebLink> editLinks,
       DynamicSet<FileWebLink> fileLinks,
       DynamicSet<FileHistoryWebLink> fileLogLinks,
       DynamicSet<DiffWebLink> diffLinks,
@@ -75,6 +78,7 @@
       DynamicSet<TagWebLink> tagLinks) {
     this.patchSetLinks = patchSetLinks;
     this.parentLinks = parentLinks;
+    this.editLinks = editLinks;
     this.fileLinks = fileLinks;
     this.fileHistoryLinks = fileLogLinks;
     this.diffLinks = diffLinks;
@@ -115,6 +119,18 @@
    * @param project Project name.
    * @param revision SHA1 of revision.
    * @param file File name.
+   * @return Links for editing.
+   */
+  public ImmutableList<WebLinkInfo> getEditLinks(String project, String revision, String file) {
+    return Patch.isMagic(file)
+        ? ImmutableList.of()
+        : filterLinks(editLinks, webLink -> webLink.getEditWebLink(project, revision, file));
+  }
+
+  /**
+   * @param project Project name.
+   * @param revision SHA1 of revision.
+   * @param file File name.
    * @return Links for files.
    */
   public ImmutableList<WebLinkInfo> getFileLinks(String project, String revision, String file) {
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index bb851e2..339b350 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -66,6 +66,7 @@
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.extensions.webui.BranchWebLink;
 import com.google.gerrit.extensions.webui.DiffWebLink;
+import com.google.gerrit.extensions.webui.EditWebLink;
 import com.google.gerrit.extensions.webui.FileHistoryWebLink;
 import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.extensions.webui.ParentWebLink;
@@ -395,6 +396,7 @@
     DynamicSet.setOf(binder(), FileWebLink.class);
     DynamicSet.setOf(binder(), FileHistoryWebLink.class);
     DynamicSet.setOf(binder(), DiffWebLink.class);
+    DynamicSet.setOf(binder(), EditWebLink.class);
     DynamicSet.setOf(binder(), ProjectWebLink.class);
     DynamicSet.setOf(binder(), BranchWebLink.class);
     DynamicSet.setOf(binder(), TagWebLink.class);
diff --git a/java/com/google/gerrit/server/config/GitwebConfig.java b/java/com/google/gerrit/server/config/GitwebConfig.java
index 8214f03..97cc830 100644
--- a/java/com/google/gerrit/server/config/GitwebConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.extensions.webui.BranchWebLink;
+import com.google.gerrit.extensions.webui.EditWebLink;
 import com.google.gerrit.extensions.webui.FileHistoryWebLink;
 import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.extensions.webui.ParentWebLink;
@@ -71,6 +72,7 @@
         }
 
         if (!isNullOrEmpty(type.getFile()) || !isNullOrEmpty(type.getRootTree())) {
+          DynamicSet.bind(binder(), EditWebLink.class).to(GitwebLinks.class);
           DynamicSet.bind(binder(), FileWebLink.class).to(GitwebLinks.class);
         }
 
@@ -253,6 +255,7 @@
   @Singleton
   static class GitwebLinks
       implements BranchWebLink,
+          EditWebLink,
           FileHistoryWebLink,
           FileWebLink,
           PatchSetWebLink,
@@ -327,6 +330,12 @@
     }
 
     @Override
+    public WebLinkInfo getEditWebLink(String projectName, String revision, String fileName) {
+      // For Gitweb treat edit links the same as file links
+      return getFileWebLink(projectName, revision, fileName);
+    }
+
+    @Override
     public WebLinkInfo getPatchSetWebLink(
         String projectName, String commit, String commitMessage, String branchName) {
       if (revision != null) {
diff --git a/java/com/google/gerrit/server/diff/DiffInfoCreator.java b/java/com/google/gerrit/server/diff/DiffInfoCreator.java
index c29ffc8..53f0019 100644
--- a/java/com/google/gerrit/server/diff/DiffInfoCreator.java
+++ b/java/com/google/gerrit/server/diff/DiffInfoCreator.java
@@ -156,8 +156,10 @@
         FileContentUtil.resolveContentType(
             state, side.fileName(), fileInfo.mode, fileInfo.mimeType);
     result.lines = fileInfo.content.getSize();
-    ImmutableList<WebLinkInfo> links = webLinksProvider.getFileWebLinks(side.type());
-    result.webLinks = links.isEmpty() ? null : links;
+    ImmutableList<WebLinkInfo> fileLinks = webLinksProvider.getFileWebLinks(side.type());
+    result.webLinks = fileLinks.isEmpty() ? null : fileLinks;
+    ImmutableList<WebLinkInfo> editLinks = webLinksProvider.getEditWebLinks(side.type());
+    result.editWebLinks = editLinks.isEmpty() ? null : editLinks;
     result.commitId = fileInfo.commitId;
     return Optional.of(result);
   }
diff --git a/java/com/google/gerrit/server/diff/DiffWebLinksProvider.java b/java/com/google/gerrit/server/diff/DiffWebLinksProvider.java
index 0f71b17..d4c7f5b 100644
--- a/java/com/google/gerrit/server/diff/DiffWebLinksProvider.java
+++ b/java/com/google/gerrit/server/diff/DiffWebLinksProvider.java
@@ -24,6 +24,9 @@
   /** Returns links associated with the diff view */
   ImmutableList<DiffWebLinkInfo> getDiffLinks();
 
-  /** Returns links associated with the diff side */
+  /** Returns file links associated with the diff side */
   ImmutableList<WebLinkInfo> getFileWebLinks(DiffSide.Type fileInfoType);
+
+  /** Returns edit links associated with the diff side */
+  ImmutableList<WebLinkInfo> getEditWebLinks(DiffSide.Type fileInfoType);
 }
diff --git a/java/com/google/gerrit/server/git/DelegateRepository.java b/java/com/google/gerrit/server/git/DelegateRepository.java
index 2816429..ddfc115 100644
--- a/java/com/google/gerrit/server/git/DelegateRepository.java
+++ b/java/com/google/gerrit/server/git/DelegateRepository.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.gerrit.common.UsedAt;
 import java.io.File;
 import java.io.IOException;
@@ -30,6 +32,7 @@
 import org.eclipse.jgit.errors.RevisionSyntaxException;
 import org.eclipse.jgit.events.ListenerList;
 import org.eclipse.jgit.events.RepositoryEvent;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.BaseRepositoryBuilder;
 import org.eclipse.jgit.lib.ObjectDatabase;
@@ -391,4 +394,19 @@
       throws IOException {
     delegate.writeRebaseTodoFile(path, steps, append);
   }
+
+  /**
+   * Converts between ref storage formats.
+   *
+   * @param format the format to convert to, either "reftable" or "refdir"
+   * @param writeLogs whether to write reflogs
+   * @param backup whether to make a backup of the old data
+   * @throws IOException on I/O problems.
+   */
+  public void convertRefStorage(String format, boolean writeLogs, boolean backup)
+      throws IOException {
+    checkState(
+        delegate instanceof FileRepository, "Repository is not an instance of FileRepository!");
+    ((FileRepository) delegate).convertRefStorage(format, writeLogs, backup);
+  }
 }
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index cbaa121..6b145ca 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -51,6 +51,7 @@
 import java.util.Objects;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 
 /**
@@ -104,7 +105,8 @@
             new PluginMergeValidationListener(mergeValidationListeners),
             projectConfigValidatorFactory.create(),
             accountValidatorFactory.create(),
-            groupValidatorFactory.create());
+            groupValidatorFactory.create(),
+            new DestBranchRefValidator());
 
     for (MergeValidationListener validator : validators) {
       validator.onPreMerge(repo, revWalk, commit, destProject, destBranch, patchSetId, caller);
@@ -198,7 +200,7 @@
                   throw new MergeValidationException(SET_BY_ADMIN, e);
                 } catch (PermissionBackendException e) {
                   logger.atWarning().withCause(e).log("Cannot check ADMINISTRATE_SERVER");
-                  throw new MergeValidationException("validation unavailable");
+                  throw new MergeValidationException("validation unavailable", e);
                 }
               } else {
                 try {
@@ -210,7 +212,7 @@
                   throw new MergeValidationException(SET_BY_OWNER, e);
                 } catch (PermissionBackendException e) {
                   logger.atWarning().withCause(e).log("Cannot check WRITE_CONFIG");
-                  throw new MergeValidationException("validation unavailable");
+                  throw new MergeValidationException("validation unavailable", e);
                 }
               }
               if (allUsersName.equals(destProject.getNameKey())
@@ -317,7 +319,7 @@
         }
       } catch (StorageException e) {
         logger.atSevere().withCause(e).log("Cannot validate account update");
-        throw new MergeValidationException("account validation unavailable");
+        throw new MergeValidationException("account validation unavailable", e);
       }
 
       try {
@@ -329,7 +331,7 @@
         }
       } catch (IOException e) {
         logger.atSevere().withCause(e).log("Cannot validate account update");
-        throw new MergeValidationException("account validation unavailable");
+        throw new MergeValidationException("account validation unavailable", e);
       }
     }
   }
@@ -366,4 +368,34 @@
       throw new MergeValidationException("group update not allowed");
     }
   }
+
+  /**
+   * Validator to ensure that destBranch is not a symbolic reference (an attempt to merge into a
+   * symbolic ref branch leads to LOCK_FAILURE exception).
+   */
+  private static class DestBranchRefValidator implements MergeValidationListener {
+    @Override
+    public void onPreMerge(
+        Repository repo,
+        CodeReviewRevWalk revWalk,
+        CodeReviewCommit commit,
+        ProjectState destProject,
+        BranchNameKey destBranch,
+        PatchSet.Id patchSetId,
+        IdentifiedUser caller)
+        throws MergeValidationException {
+      try {
+        Ref ref = repo.exactRef(destBranch.branch());
+        // Usually the target branch exists, but there is an exception for some branches (see
+        // {@link com.google.gerrit.server.git.receive.ReceiveCommits} for details).
+        // Such non-existing branches should be ignored.
+        if (ref != null && ref.isSymbolic()) {
+          throw new MergeValidationException("the target branch is a symbolic ref");
+        }
+      } catch (IOException e) {
+        logger.atSevere().withCause(e).log("Cannot validate destination branch");
+        throw new MergeValidationException("symref validation unavailable", e);
+      }
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetDiff.java b/java/com/google/gerrit/server/restapi/change/GetDiff.java
index b8902b7..d48d76a 100644
--- a/java/com/google/gerrit/server/restapi/change/GetDiff.java
+++ b/java/com/google/gerrit/server/restapi/change/GetDiff.java
@@ -226,18 +226,26 @@
     }
 
     @Override
+    public ImmutableList<WebLinkInfo> getEditWebLinks(DiffSide.Type type) {
+      String rev = getSideRev(type);
+      DiffSide side = getDiffSide(type);
+      return webLinks.getEditLinks(projectName.get(), rev, side.fileName());
+    }
+
+    @Override
     public ImmutableList<WebLinkInfo> getFileWebLinks(DiffSide.Type type) {
-      String rev;
-      DiffSide side;
-      if (type == DiffSide.Type.SIDE_A) {
-        rev = revA;
-        side = sideA;
-      } else {
-        rev = revB;
-        side = sideB;
-      }
+      String rev = getSideRev(type);
+      DiffSide side = getDiffSide(type);
       return webLinks.getFileLinks(projectName.get(), rev, side.fileName());
     }
+
+    private String getSideRev(DiffSide.Type sideType) {
+      return DiffSide.Type.SIDE_A == sideType ? revA : revB;
+    }
+
+    private DiffSide getDiffSide(DiffSide.Type sideType) {
+      return DiffSide.Type.SIDE_A == sideType ? sideA : sideB;
+    }
   }
 
   public GetDiff setBase(String base) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetFixPreview.java b/java/com/google/gerrit/server/restapi/change/GetFixPreview.java
index 6089778..5191fc8 100644
--- a/java/com/google/gerrit/server/restapi/change/GetFixPreview.java
+++ b/java/com/google/gerrit/server/restapi/change/GetFixPreview.java
@@ -140,5 +140,10 @@
     public ImmutableList<WebLinkInfo> getFileWebLinks(Type fileInfoType) {
       return ImmutableList.of();
     }
+
+    @Override
+    public ImmutableList<WebLinkInfo> getEditWebLinks(Type fileInfoType) {
+      return ImmutableList.of();
+    }
   }
 }
diff --git a/java/com/google/gerrit/sshd/commands/ConvertRefStorage.java b/java/com/google/gerrit/sshd/commands/ConvertRefStorage.java
new file mode 100644
index 0000000..21d90ed
--- /dev/null
+++ b/java/com/google/gerrit/sshd/commands/ConvertRefStorage.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.git.DelegateRepository;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.io.IOException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(
+    name = "convert-ref-storage",
+    description = "Convert ref storage to reftable (experimental)",
+    runsAt = MASTER_OR_SLAVE)
+public class ConvertRefStorage extends SshCommand {
+  @Inject private GitRepositoryManager repoManager;
+
+  private enum StorageFormatOption {
+    reftable,
+    refdir,
+  }
+
+  @Option(
+      name = "--format",
+      usage = "storage format to convert to (reftable or refdir) (default: reftable)")
+  private StorageFormatOption storageFormat = StorageFormatOption.reftable;
+
+  @Option(
+      name = "--backup",
+      aliases = {"-b"},
+      usage = "create backup of old ref storage format (default: true)")
+  private boolean backup = true;
+
+  @Option(
+      name = "--reflogs",
+      aliases = {"-r"},
+      usage = "write reflogs to reftable (default: true)")
+  private boolean writeLogs = true;
+
+  @Option(
+      name = "--project",
+      aliases = {"-p"},
+      metaVar = "PROJECT",
+      required = true,
+      usage = "project for which the storage format should be changed")
+  private ProjectState projectState;
+
+  @Override
+  public void run() throws Exception {
+    enableGracefulStop();
+    Project.NameKey projectName = projectState.getNameKey();
+    try (Repository repo = repoManager.openRepository(projectName)) {
+      if (repo instanceof DelegateRepository) {
+        ((DelegateRepository) repo).convertRefStorage(storageFormat.name(), writeLogs, backup);
+      } else {
+        checkState(
+            repo instanceof FileRepository, "Repository is not an instance of FileRepository!");
+        ((FileRepository) repo).convertRefStorage(storageFormat.name(), writeLogs, backup);
+      }
+    } catch (RepositoryNotFoundException e) {
+      throw die("'" + projectName + "': not a git archive", e);
+    } catch (IOException e) {
+      throw die("Error converting: '" + projectName + "': " + e.getMessage(), e);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index cfd17f4..8ee6a0d 100644
--- a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -47,6 +47,7 @@
     command(gerrit, AproposCommand.class);
     command(gerrit, BanCommitCommand.class);
     command(gerrit, CloseConnection.class);
+    command(gerrit, ConvertRefStorage.class);
     command(gerrit, FlushCaches.class);
     command(gerrit, ListProjectsCommand.class);
     command(gerrit, ListMembersCommand.class);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index a8aff81..e17f854 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -202,6 +202,8 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -3054,6 +3056,24 @@
   }
 
   @Test
+  public void submitToSymref() throws Exception {
+    // Create symref in the origin repository (testRepo references to a local repository)
+    try (Repository repo = repoManager.openRepository(project)) {
+      RefUpdate u = repo.updateRef("refs/heads/master_symref");
+      assertThat(u.link("refs/heads/master")).isEqualTo(Result.NEW);
+    }
+
+    PushOneCommit.Result r = createChange("refs/for/master_symref");
+    String id = r.getChangeId();
+
+    gApi.changes().id(id).current().review(ReviewInput.approve());
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(id).current().submit());
+    assertThat(thrown).hasMessageThat().contains("the target branch is a symbolic ref");
+  }
+
+  @Test
   public void check() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes().id(r.getChangeId()).get().problems).isNull();
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 0b18503..37b4a1c 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -30,6 +30,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
@@ -40,8 +42,11 @@
 import com.google.gerrit.extensions.common.ChangeType;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.webui.EditWebLink;
+import com.google.inject.Inject;
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -75,6 +80,8 @@
           .collect(joining());
   private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n";
 
+  @Inject private ExtensionRegistry extensionRegistry;
+
   private boolean intraline;
   private boolean useNewDiffCacheListFiles;
   private boolean useNewDiffCacheGetDiff;
@@ -142,6 +149,24 @@
   }
 
   @Test
+  public void editWebLinkIncludedInDiff() throws Exception {
+    try (Registration registration = newEditWebLink()) {
+      String fileName = "a_new_file.txt";
+      String fileContent = "First line\nSecond line\n";
+      PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
+      DiffInfo info =
+          gApi.changes()
+              .id(result.getChangeId())
+              .revision(result.getCommit().name())
+              .file(fileName)
+              .diff();
+      assertThat(info.metaB.editWebLinks).hasSize(1);
+      assertThat(info.metaB.editWebLinks.get(0).url)
+          .isEqualTo("http://edit/" + project + "/" + fileName);
+    }
+  }
+
+  @Test
   public void deletedFileIsIncludedInDiff() throws Exception {
     gApi.changes().id(changeId).edit().deleteFile(FILE_NAME);
     gApi.changes().id(changeId).edit().publish();
@@ -2875,6 +2900,18 @@
     assertThat(e).hasMessageThat().isEqualTo("edit not allowed as base");
   }
 
+  private Registration newEditWebLink() {
+    EditWebLink webLink =
+        new EditWebLink() {
+          @Override
+          public WebLinkInfo getEditWebLink(String projectName, String revision, String fileName) {
+            return new WebLinkInfo(
+                "name", "imageURL", "http://edit/" + projectName + "/" + fileName);
+          }
+        };
+    return extensionRegistry.newRegistration().add(webLink);
+  }
+
   private String updatedCommitMessage() {
     return "An unchanged patchset\n\nChange-Id: " + changeId;
   }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index bbe7b81..2b37cfd 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -45,6 +45,7 @@
       ImmutableList.of(
           "apropos",
           "close-connection",
+          "convert-ref-storage",
           "flush-caches",
           "gc",
           "logging",
diff --git a/plugins/replication b/plugins/replication
index 75c44d0..0022a34 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 75c44d0b1dec203859112ad42074eb16839ea353
+Subproject commit 0022a34428cf8bfe4feb0935cdd20b0257bfc8a3
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index f026a9d..ee4ea9e 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -15,11 +15,6 @@
  * limitations under the License.
  */
 
-// IMPORTANT !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-// The entire API is currently in DRAFT state.
-// Changes to all type and interfaces are expected.
-// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-
 export interface ChecksPluginApi {
   /**
    * Must only be called once. You cannot register twice. You cannot unregister.
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index a10cb14..360ff5c 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -53,6 +53,35 @@
 }
 
 /**
+ * Represents a syntax block in a code (e.g. method, function, class, if-else).
+ */
+export interface SyntaxBlock {
+  /** Name of the block (e.g. name of the method/class)*/
+  name: string;
+  /** Where does this block syntatically starts and ends (line number and column).*/
+  range: {
+    /** first line of the block (1-based inclusive). */
+    start_line: number;
+    /**
+     * column of the range start inside the first line (e.g. "{" character ending a function/method)
+     * (1-based inclusive).
+     */
+    start_column: number;
+    /**
+     * last line of the block (1-based inclusive).
+     */
+    end_line: number;
+    /**
+     * column of the block end inside the end line (e.g. "}" character ending a function/method)
+     * (1-based inclusive).
+     */
+    end_column: number;
+  };
+  /** Sub-blocks of the current syntax block (e.g. methods of a class) */
+  children: SyntaxBlock[];
+}
+
+/**
  * The DiffFileMetaInfo entity contains meta information about a file diff.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#diff-file-meta-info
  */
@@ -65,6 +94,12 @@
   lines: number;
   // TODO: Not documented.
   language?: string;
+  /**
+   * The first level of syntax blocks tree (outline) within the current file.
+   * It contains an hierarchical structure where each block contains its
+   * sub-blocks (children).
+   */
+  syntax_tree?: SyntaxBlock[];
 }
 
 export declare type ChangeType =
@@ -276,10 +311,11 @@
     }
   | {type: 'magnifier-clicked'}
   | {type: 'magnifier-dragged'}
-  | {type: 'version-switcher-clicked'; button: 'base' | 'revision'}
+  | {type: 'version-switcher-clicked'; button: 'base' | 'revision' | 'switch'}
   | {type: 'zoom-level-changed'; scale: number | 'fit'}
   | {type: 'follow-mouse-changed'; value: boolean}
-  | {type: 'background-color-changed'; value: string};
+  | {type: 'background-color-changed'; value: string}
+  | {type: 'automatic-blink-changed'; value: boolean};
 
 export enum GrDiffLineType {
   ADD = 'add',
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 827cbf1..b4d25a6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -122,6 +122,7 @@
   DraftInfo,
   isDraftThread,
   isRobot,
+  isUnresolved,
 } from '../../../utils/comment-util';
 import {
   PolymerDeepPropertyChange,
@@ -901,6 +902,13 @@
     return false;
   }
 
+  _computeShowUnresolved(threads?: CommentThread[]) {
+    // If all threads are resolved and the Comments Tab is opened then show
+    // all threads instead
+    if (!threads?.length) return true;
+    return threads.filter(thread => isUnresolved(thread)).length > 0;
+  }
+
   _robotCommentCountPerPatchSet(threads: CommentThread[]) {
     return threads.reduce((robotCommentCountMap, thread) => {
       const comments = thread.comments;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index ae4ad79..6025268 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -570,7 +570,7 @@
           logged-in="[[_loggedIn]]"
           comment-tab-state="[[_tabState.commentTab]]"
           only-show-robot-comments-with-human-reply=""
-          unresolved-only
+          unresolved-only="[[_computeShowUnresolved(_commentThreads)]]"
           show-comment-context
         ></gr-thread-list>
       </template>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index bb3c975..ca16ec0 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -618,52 +618,20 @@
     return changeComments.computeCommentsString(patchRange, file.__path, file);
   }
 
-  _computeDraftCount(
-    changeComments?: ChangeComments,
-    patchRange?: PatchRange,
-    path?: string
-  ) {
-    if (
-      changeComments === undefined ||
-      patchRange === undefined ||
-      path === undefined
-    ) {
-      return '';
-    }
-    return (
-      changeComments.computeDraftCount({
-        patchNum: patchRange.basePatchNum,
-        path,
-      }) +
-      changeComments.computeDraftCount({
-        patchNum: patchRange.patchNum,
-        path,
-      }) +
-      changeComments.computePortedDraftCount(
-        {
-          patchNum: patchRange.patchNum,
-          basePatchNum: patchRange.basePatchNum,
-        },
-        path
-      )
-    );
-  }
-
   /**
    * Computes a string with the number of drafts.
    */
   _computeDraftsString(
     changeComments?: ChangeComments,
     patchRange?: PatchRange,
-    path?: string
+    file?: NormalizedFileInfo
   ) {
-    const draftCount = this._computeDraftCount(
-      changeComments,
+    const draftCount = changeComments?.computeDraftCountForFile(
       patchRange,
-      path
+      file
     );
-    if (draftCount === '') return draftCount;
-    return pluralize(draftCount, 'draft');
+    if (draftCount === 0) return '';
+    return pluralize(Number(draftCount), 'draft');
   }
 
   /**
@@ -672,12 +640,11 @@
   _computeDraftsStringMobile(
     changeComments?: ChangeComments,
     patchRange?: PatchRange,
-    path?: string
+    file?: NormalizedFileInfo
   ) {
-    const draftCount = this._computeDraftCount(
-      changeComments,
+    const draftCount = changeComments?.computeDraftCountForFile(
       patchRange,
-      path
+      file
     );
     return draftCount === 0 ? '' : `${draftCount}d`;
   }
@@ -688,23 +655,23 @@
   _computeCommentsStringMobile(
     changeComments?: ChangeComments,
     patchRange?: PatchRange,
-    path?: string
+    file?: NormalizedFileInfo
   ) {
     if (
       changeComments === undefined ||
       patchRange === undefined ||
-      path === undefined
+      file === undefined
     ) {
       return '';
     }
     const commentThreadCount =
       changeComments.computeCommentThreadCount({
         patchNum: patchRange.basePatchNum,
-        path,
+        path: file.__path,
       }) +
       changeComments.computeCommentThreadCount({
         patchNum: patchRange.patchNum,
-        path,
+        path: file.__path,
       });
     return commentThreadCount === 0 ? '' : `${commentThreadCount}c`;
   }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
index 59338df..40bd5bc 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
@@ -423,8 +423,7 @@
               <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
+              -->[[_computeDraftsString(changeComments, patchRange, file)]]<!-- This comments ensure that span is empty when
                 the function returns empty string.
            --></span
               >
@@ -450,14 +449,14 @@
                 ><!-- 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
+                file)]]<!-- This comments ensure that span is empty when
                 the function returns empty string.
            --></span
               >
               <span
                 ><!--
              -->[[_computeCommentsStringMobile(changeComments, patchRange,
-                file.__path)]]<!--
+                file)]]<!--
            --></span
               >
               <span class="noCommentsScreenReaderText">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index b8ba86c..dcc2e46 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -360,103 +360,103 @@
 
       assert.equal(
           element._computeCommentsStringMobile(element.changeComments, parentTo1
-              , '/COMMIT_MSG'), '2c');
+              , {__path: '/COMMIT_MSG'}), '2c');
       assert.equal(
           element._computeCommentsStringMobile(element.changeComments, _1To2
-              , '/COMMIT_MSG'), '3c');
+              , {__path: '/COMMIT_MSG'}), '3c');
       assert.equal(
           element._computeDraftsString(element.changeComments, parentTo1,
-              'unresolved.file'), '1 draft');
+              {__path: 'unresolved.file'}), '1 draft');
       assert.equal(
           element._computeDraftsString(element.changeComments, _1To2,
-              'unresolved.file'), '1 draft');
+              {__path: 'unresolved.file'}), '1 draft');
       assert.equal(
           element._computeDraftsStringMobile(element.changeComments, parentTo1,
-              'unresolved.file'), '1d');
+              {__path: 'unresolved.file'}), '1d');
       assert.equal(
           element._computeDraftsStringMobile(element.changeComments, _1To2,
-              'unresolved.file'), '1d');
+              {__path: 'unresolved.file'}), '1d');
       assert.equal(
           element._computeCommentsStringMobile(
               element.changeComments,
               parentTo1,
-              'myfile.txt'
+              {__path: 'myfile.txt'}
           ), '1c');
       assert.equal(
           element._computeCommentsStringMobile(element.changeComments, _1To2,
-              'myfile.txt'), '3c');
+              {__path: 'myfile.txt'}), '3c');
       assert.equal(
           element._computeDraftsString(element.changeComments, parentTo1,
-              'myfile.txt'), '');
+              {__path: 'myfile.txt'}), '');
       assert.equal(
           element._computeDraftsString(element.changeComments, _1To2,
-              'myfile.txt'), '');
+              {__path: 'myfile.txt'}), '');
       assert.equal(
           element._computeDraftsStringMobile(element.changeComments, parentTo1,
-              'myfile.txt'), '');
+              {__path: 'myfile.txt'}), '');
       assert.equal(
           element._computeDraftsStringMobile(element.changeComments, _1To2,
-              'myfile.txt'), '');
+              {__path: 'myfile.txt'}), '');
       assert.equal(
           element._computeCommentsStringMobile(
               element.changeComments,
               parentTo1,
-              'file_added_in_rev2.txt'
+              {__path: 'file_added_in_rev2.txt'}
           ), '');
       assert.equal(
           element._computeCommentsStringMobile(element.changeComments, _1To2,
-              'file_added_in_rev2.txt'), '');
+              {__path: 'file_added_in_rev2.txt'}), '');
       assert.equal(
           element._computeDraftsString(element.changeComments, parentTo1,
-              'file_added_in_rev2.txt'), '');
+              {__path: 'file_added_in_rev2.txt'}), '');
       assert.equal(
           element._computeDraftsString(element.changeComments, _1To2,
-              'file_added_in_rev2.txt'), '');
+              {__path: 'file_added_in_rev2.txt'}), '');
       assert.equal(
           element._computeDraftsStringMobile(element.changeComments, parentTo1,
-              'file_added_in_rev2.txt'), '');
+              {__path: 'file_added_in_rev2.txt'}), '');
       assert.equal(
           element._computeDraftsStringMobile(element.changeComments, _1To2,
-              'file_added_in_rev2.txt'), '');
+              {__path: 'file_added_in_rev2.txt'}), '');
       assert.equal(
           element._computeCommentsStringMobile(
               element.changeComments,
               parentTo2,
-              '/COMMIT_MSG'
+              {__path: '/COMMIT_MSG'}
           ), '1c');
       assert.equal(
           element._computeCommentsStringMobile(element.changeComments, _1To2,
-              '/COMMIT_MSG'), '3c');
+              {__path: '/COMMIT_MSG'}), '3c');
       assert.equal(
           element._computeDraftsString(element.changeComments, parentTo1,
-              '/COMMIT_MSG'), '2 drafts');
+              {__path: '/COMMIT_MSG'}), '2 drafts');
       assert.equal(
           element._computeDraftsString(element.changeComments, _1To2,
-              '/COMMIT_MSG'), '2 drafts');
+              {__path: '/COMMIT_MSG'}), '2 drafts');
       assert.equal(
           element._computeDraftsStringMobile(
               element.changeComments,
               parentTo1,
-              '/COMMIT_MSG'
+              {__path: '/COMMIT_MSG'}
           ), '2d');
       assert.equal(
           element._computeDraftsStringMobile(element.changeComments, _1To2,
-              '/COMMIT_MSG'), '2d');
+              {__path: '/COMMIT_MSG'}), '2d');
       assert.equal(
           element._computeCommentsStringMobile(
               element.changeComments,
               parentTo2,
-              'myfile.txt'
+              {__path: 'myfile.txt'}
           ), '2c');
       assert.equal(
           element._computeCommentsStringMobile(element.changeComments, _1To2,
-              'myfile.txt'), '3c');
+              {__path: 'myfile.txt'}), '3c');
       assert.equal(
           element._computeDraftsStringMobile(element.changeComments, parentTo2,
-              'myfile.txt'), '');
+              {__path: 'myfile.txt'}), '');
       assert.equal(
           element._computeDraftsStringMobile(element.changeComments, _1To2,
-              'myfile.txt'), '');
+              {__path: 'myfile.txt'}), '');
     });
 
     test('_reviewedTitle', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index fed02a7..26af2a2 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -62,6 +62,7 @@
 const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.]?$/;
 const UPLOADED_NEW_PATCHSET_PATTERN = /Uploaded patch set (\d+)./;
 const MERGED_PATCHSET_PATTERN = /(\d+) is the latest approved patch-set/;
+const VOTE_RESET_TEXT = '0 (vote reset)';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -466,7 +467,7 @@
       )
       .map(ms => {
         const label = ms?.[2];
-        const value = ms?.[1] === '-' ? 'removed' : ms?.[3];
+        const value = ms?.[1] === '-' ? VOTE_RESET_TEXT : ms?.[3];
         return {label, value};
       });
   }
@@ -479,7 +480,7 @@
     if (!score.value) {
       return '';
     }
-    if (score.value === 'removed') {
+    if (score.value.includes(VOTE_RESET_TEXT)) {
       return 'removed';
     }
     const classes = [];
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index 8cc7a3f..73afde8 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -51,6 +51,7 @@
 import {appContext} from '../../../services/app-context';
 import {CommentSide, Side} from '../../../constants/constants';
 import {pluralize} from '../../../utils/string-util';
+import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
 
 export type CommentIdToCommentThreadMap = {
   [urlEncodedCommentId: string]: CommentThread;
@@ -522,6 +523,33 @@
       .length;
   }
 
+  computeDraftCountForFile(patchRange?: PatchRange, file?: NormalizedFileInfo) {
+    if (patchRange === undefined || file === undefined) {
+      return 0;
+    }
+    const getCommentForPath = (path?: string) => {
+      if (!path) return 0;
+      return (
+        this.computeDraftCount({
+          patchNum: patchRange.basePatchNum,
+          path,
+        }) +
+        this.computeDraftCount({
+          patchNum: patchRange.patchNum,
+          path,
+        }) +
+        this.computePortedDraftCount(
+          {
+            patchNum: patchRange.patchNum,
+            basePatchNum: patchRange.basePatchNum,
+          },
+          path
+        )
+      );
+    };
+    return getCommentForPath(file.__path) + getCommentForPath(file.old_path);
+  }
+
   /**
    * @param includeUnmodified Included unmodified status of the file in the
    * comment string or not. For files we opt of chip instead of a string.
@@ -537,6 +565,14 @@
     if (!patchRange) return '';
 
     const threads = this.getThreadsBySideForFile({path}, patchRange);
+    if (changeFileInfo?.old_path) {
+      threads.push(
+        ...this.getThreadsBySideForFile(
+          {path: changeFileInfo.old_path},
+          patchRange
+        )
+      );
+    }
     const commentThreadCount = threads.filter(thread => !isDraftThread(thread))
       .length;
     const unresolvedCount = threads.reduce((cnt, thread) => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
index 7c01a95..5ba3606 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
@@ -97,8 +97,13 @@
       return section;
     }
 
+    let diffInfo;
+    let renderPrefs;
+
     setup(() => {
-      builder = new GrDiffBuilder({content: []}, prefs, null, []);
+      diffInfo = {content: []};
+      renderPrefs = {};
+      builder = new GrDiffBuilder(diffInfo, prefs, null, [], renderPrefs);
     });
 
     test('no +10 buttons for 10 or less lines', () => {
@@ -149,6 +154,70 @@
       assert.include([...buttons[0].classList.values()], 'aboveButton');
       assert.include([...buttons[1].classList.values()], 'aboveButton');
     });
+
+    suite('with block expansion', () => {
+      setup(() => {
+        builder._numLinesLeft = 50;
+        renderPrefs.use_block_expansion = true;
+        diffInfo.meta_b = {
+          syntax_tree: [],
+        };
+      });
+
+      test('context control with block expansion at the top', () => {
+        const section = createContextSectionForGroups({offset: 0, count: 20});
+
+        const fullExpansionButtons = section
+            .querySelectorAll('.fullExpansion gr-button');
+        const partialExpansionButtons = section
+            .querySelectorAll('.partialExpansion gr-button');
+        const blockExpansionButtons = section
+            .querySelectorAll('.blockExpansion gr-button');
+        assert.equal(fullExpansionButtons.length, 1);
+        assert.equal(partialExpansionButtons.length, 1);
+        assert.equal(blockExpansionButtons.length, 1);
+        assert.equal(blockExpansionButtons[0].textContent, '+Block');
+        assert.include([...blockExpansionButtons[0].classList.values()],
+            'belowButton');
+      });
+
+      test('context control in the middle', () => {
+        const section = createContextSectionForGroups({offset: 10, count: 20});
+
+        const fullExpansionButtons = section
+            .querySelectorAll('.fullExpansion gr-button');
+        const partialExpansionButtons = section
+            .querySelectorAll('.partialExpansion gr-button');
+        const blockExpansionButtons = section
+            .querySelectorAll('.blockExpansion gr-button');
+        assert.equal(fullExpansionButtons.length, 1);
+        assert.equal(partialExpansionButtons.length, 2);
+        assert.equal(blockExpansionButtons.length, 2);
+        assert.equal(blockExpansionButtons[0].textContent, '+Block');
+        assert.equal(blockExpansionButtons[1].textContent, '+Block');
+        assert.include([...blockExpansionButtons[0].classList.values()],
+            'aboveButton');
+        assert.include([...blockExpansionButtons[1].classList.values()],
+            'belowButton');
+      });
+
+      test('context control at the bottom', () => {
+        const section = createContextSectionForGroups({offset: 30, count: 20});
+
+        const fullExpansionButtons = section
+            .querySelectorAll('.fullExpansion gr-button');
+        const partialExpansionButtons = section
+            .querySelectorAll('.partialExpansion gr-button');
+        const blockExpansionButtons = section
+            .querySelectorAll('.blockExpansion gr-button');
+        assert.equal(fullExpansionButtons.length, 1);
+        assert.equal(partialExpansionButtons.length, 1);
+        assert.equal(blockExpansionButtons.length, 1);
+        assert.equal(blockExpansionButtons[0].textContent, '+Block');
+        assert.include([...blockExpansionButtons[0].classList.values()],
+            'aboveButton');
+      });
+    });
   });
 
   test('newlines 1', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index c8b69f0..d5e6ecd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -20,6 +20,7 @@
   DiffContextExpandedExternalDetail,
   MovedLinkClickedEventDetail,
   RenderPreferences,
+  SyntaxBlock,
 } from '../../../api/diff';
 import {getBaseUrl} from '../../../utils/url-util';
 import {GrDiffLine, GrDiffLineType, LineNumber} from '../gr-diff/gr-diff-line';
@@ -73,6 +74,19 @@
   }
 }
 
+function findMostNestedContainingBlock(
+  lineNum: number,
+  blocks?: SyntaxBlock[]
+): SyntaxBlock | undefined {
+  const containingBlock = blocks?.find(
+    ({range}) => range.start_line < lineNum && range.end_line > lineNum
+  );
+  const containingChildBlock = containingBlock
+    ? findMostNestedContainingBlock(lineNum, containingBlock?.children)
+    : undefined;
+  return containingChildBlock || containingBlock;
+}
+
 export abstract class GrDiffBuilder {
   private readonly _diff: DiffInfo;
 
@@ -306,6 +320,7 @@
     );
   }
 
+  // TODO(renanoliveira): Move context controls to polymer component (or at least a separate class).
   _createContextControls(
     section: HTMLElement,
     contextGroups: GrDiffGroup[],
@@ -314,9 +329,9 @@
     const leftStart = contextGroups[0].lineRange.left.start_line;
     const leftEnd =
       contextGroups[contextGroups.length - 1].lineRange.left.end_line;
-    const numLines = leftEnd - leftStart + 1;
-
-    if (numLines === 0) console.error('context group without lines');
+    const rightStart = contextGroups[0].lineRange.right.start_line;
+    const rightEnd =
+      contextGroups[contextGroups.length - 1].lineRange.right.end_line;
 
     const firstGroupIsSkipped = !!contextGroups[0].skip;
     const lastGroupIsSkipped = !!contextGroups[contextGroups.length - 1].skip;
@@ -335,7 +350,8 @@
         contextGroups,
         showAbove,
         showBelow,
-        numLines
+        rightStart,
+        rightEnd
       )
     );
     if (showBelow) {
@@ -354,8 +370,12 @@
     contextGroups: GrDiffGroup[],
     showAbove: boolean,
     showBelow: boolean,
-    numLines: number
+    rightStart: number,
+    rightEnd: number
   ): HTMLElement {
+    const numLines = rightEnd - rightStart + 1;
+    if (numLines === 0) console.error('context group without lines');
+
     const row = this._createElement('tr', 'contextDivider');
     if (!(showAbove && showBelow)) {
       row.classList.add('collapsed');
@@ -364,13 +384,55 @@
     const element = this._createElement('td', 'dividerCell');
     row.appendChild(element);
 
-    const showAllContainer = this._createElement('div', 'aboveBelowButtons');
+    const showAllContainer = this._createExpandAllButtonContainer(
+      section,
+      contextGroups,
+      showAbove,
+      showBelow,
+      numLines
+    );
     element.appendChild(showAllContainer);
 
+    const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
+    if (showPartialLinks) {
+      const partialExpansionContainer = this._createPartialExpansionButtons(
+        section,
+        contextGroups,
+        showAbove,
+        showBelow,
+        numLines
+      );
+      if (partialExpansionContainer) {
+        element.appendChild(partialExpansionContainer);
+      }
+      const blockExpansionContainer = this._createBlockExpansionButtons(
+        section,
+        contextGroups,
+        showAbove,
+        showBelow,
+        rightStart,
+        rightEnd,
+        numLines
+      );
+      if (blockExpansionContainer) {
+        element.appendChild(blockExpansionContainer);
+      }
+    }
+    return row;
+  }
+
+  private _createExpandAllButtonContainer(
+    section: HTMLElement,
+    contextGroups: GrDiffGroup[],
+    showAbove: boolean,
+    showBelow: boolean,
+    numLines: number
+  ) {
     const showAllButton = this._createContextButton(
       ContextButtonType.ALL,
       section,
       contextGroups,
+      numLines,
       numLines
     );
     showAllButton.classList.add(
@@ -380,61 +442,131 @@
         ? 'aboveButton'
         : 'belowButton'
     );
+    const showAllContainer = this._createElement(
+      'div',
+      'aboveBelowButtons fullExpansion'
+    );
     showAllContainer.appendChild(showAllButton);
+    return showAllContainer;
+  }
 
-    const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
-    if (showPartialLinks) {
-      const container = this._createElement('div', 'aboveBelowButtons');
-      if (showAbove) {
-        container.appendChild(
-          this._createContextButton(
-            ContextButtonType.ABOVE,
-            section,
-            contextGroups,
-            numLines
-          )
-        );
-      }
-      if (showBelow) {
-        container.appendChild(
-          this._createContextButton(
-            ContextButtonType.BELOW,
-            section,
-            contextGroups,
-            numLines
-          )
-        );
-      }
-      element.appendChild(container);
-      if (this._renderPrefs?.use_block_expansion) {
-        const blockExpansionContainer = this._createElement(
-          'div',
-          'aboveBelowButtons'
-        );
-        if (showAbove) {
-          blockExpansionContainer.appendChild(
-            this._createContextButton(
-              ContextButtonType.BLOCK_ABOVE,
-              section,
-              contextGroups,
-              numLines
-            )
-          );
-        }
-        if (showBelow) {
-          blockExpansionContainer.appendChild(
-            this._createContextButton(
-              ContextButtonType.BLOCK_BELOW,
-              section,
-              contextGroups,
-              numLines
-            )
-          );
-        }
-        element.appendChild(blockExpansionContainer);
+  private _createPartialExpansionButtons(
+    section: HTMLElement,
+    contextGroups: GrDiffGroup[],
+    showAbove: boolean,
+    showBelow: boolean,
+    numLines: number
+  ) {
+    let aboveButton;
+    let belowButton;
+    if (showAbove) {
+      aboveButton = this._createContextButton(
+        ContextButtonType.ABOVE,
+        section,
+        contextGroups,
+        numLines,
+        PARTIAL_CONTEXT_AMOUNT
+      );
+    }
+    if (showBelow) {
+      belowButton = this._createContextButton(
+        ContextButtonType.BELOW,
+        section,
+        contextGroups,
+        numLines,
+        PARTIAL_CONTEXT_AMOUNT
+      );
+    }
+    if (aboveButton || belowButton) {
+      const partialExpansionContainer = this._createElement(
+        'div',
+        'aboveBelowButtons partialExpansion'
+      );
+      aboveButton && partialExpansionContainer.appendChild(aboveButton);
+      belowButton && partialExpansionContainer.appendChild(belowButton);
+      return partialExpansionContainer;
+    }
+    return undefined;
+  }
+
+  private _createBlockExpansionButtons(
+    section: HTMLElement,
+    contextGroups: GrDiffGroup[],
+    showAbove: boolean,
+    showBelow: boolean,
+    rightStart: number,
+    rightEnd: number,
+    numLines: number
+  ) {
+    if (!this._renderPrefs?.use_block_expansion) {
+      return undefined;
+    }
+    let aboveBlockButton;
+    let belowBlockButton;
+    const rightSyntaxTree = this._diff.meta_b.syntax_tree;
+    if (showAbove) {
+      aboveBlockButton = this._createBlockButton(
+        section,
+        contextGroups,
+        ContextButtonType.BLOCK_ABOVE,
+        numLines,
+        rightStart - 1,
+        rightSyntaxTree
+      );
+    }
+    if (showBelow) {
+      belowBlockButton = this._createBlockButton(
+        section,
+        contextGroups,
+        ContextButtonType.BLOCK_BELOW,
+        numLines,
+        rightEnd + 1,
+        rightSyntaxTree
+      );
+    }
+    if (aboveBlockButton || belowBlockButton) {
+      const blockExpansionContainer = this._createElement(
+        'div',
+        'blockExpansion aboveBelowButtons'
+      );
+      aboveBlockButton && blockExpansionContainer.appendChild(aboveBlockButton);
+      belowBlockButton && blockExpansionContainer.appendChild(belowBlockButton);
+      return blockExpansionContainer;
+    }
+    return undefined;
+  }
+
+  private _createBlockButton(
+    section: HTMLElement,
+    contextGroups: GrDiffGroup[],
+    buttonType: ContextButtonType,
+    numLines: number,
+    referenceLine: number,
+    syntaxTree?: SyntaxBlock[]
+  ) {
+    const containingBlock = findMostNestedContainingBlock(
+      referenceLine,
+      syntaxTree
+    );
+    let linesToExpand = numLines;
+    if (containingBlock) {
+      const {range} = containingBlock;
+      const targetLine =
+        buttonType === ContextButtonType.BLOCK_ABOVE
+          ? range.end_line
+          : range.start_line;
+      const distanceToTargetLine = Math.abs(targetLine - referenceLine);
+      if (distanceToTargetLine < numLines) {
+        linesToExpand = distanceToTargetLine;
       }
     }
-    return row;
+    return this._createContextButton(
+      buttonType,
+      section,
+      contextGroups,
+      numLines,
+      linesToExpand
+    );
   }
 
   /**
@@ -469,10 +601,9 @@
     type: ContextButtonType,
     section: HTMLElement,
     contextGroups: GrDiffGroup[],
-    numLines: number
+    numLines: number,
+    linesToExpand: number
   ) {
-    const linesToExpand =
-      type === ContextButtonType.ALL ? numLines : PARTIAL_CONTEXT_AMOUNT;
     const button = this._createElement('gr-button', 'showContext');
     button.classList.add('contextControlButton');
     button.setAttribute('link', 'true');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
index 1d6bc95..2a4b250 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -394,7 +394,7 @@
         >
           Base
         </paper-button>
-        <paper-fab mini icon="gr-icons:swapHoriz" @click="${this.toggleImage}">
+        <paper-fab mini icon="gr-icons:swapHoriz" @click="${this.manualBlink}">
         </paper-fab>
         <paper-button
           class="right"
@@ -634,7 +634,14 @@
     );
   }
 
-  toggleImage() {
+  manualBlink() {
+    this.toggleImage();
+    this.dispatchEvent(
+      createEvent({type: 'version-switcher-clicked', button: 'switch'})
+    );
+  }
+
+  private toggleImage() {
     if (this.baseUrl && this.revisionUrl) {
       this.baseSelected = !this.baseSelected;
     }
@@ -651,6 +658,9 @@
         this.automaticBlinkTimer = undefined;
       }
     }
+    this.dispatchEvent(
+      createEvent({type: 'automatic-blink-changed', value: this.automaticBlink})
+    );
   }
 
   private setBlinkInterval() {
diff --git a/polygerrit-ui/app/samples/add-from-favorite-reviewers.js b/polygerrit-ui/app/samples/add-from-favorite-reviewers.js
index 76b2787..1616ef3 100644
--- a/polygerrit-ui/app/samples/add-from-favorite-reviewers.js
+++ b/polygerrit-ui/app/samples/add-from-favorite-reviewers.js
@@ -14,6 +14,7 @@
  * 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.
@@ -142,4 +143,4 @@
       'reply-reviewers', ReviewerShortcut.is, {slot: 'right'});
   plugin.registerCustomComponent(
       'reply-reviewers', ReviewerShortcutContent.is, {slot: 'below'});
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/samples/bind-parameters.js b/polygerrit-ui/app/samples/bind-parameters.js
index 30c7c3d..4527a80 100644
--- a/polygerrit-ui/app/samples/bind-parameters.js
+++ b/polygerrit-ui/app/samples/bind-parameters.js
@@ -14,15 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-// Element class exists in all browsers:
-// https://developer.mozilla.org/en-US/docs/Web/API/Element
-// Rename it to PolymerElement to avoid conflicts. Also,
-// typescript reports the following error:
-// error TS2451: Cannot redeclare block-scoped variable 'Element'.
-const {html, Element: PolymerElement} = Polymer;
-
-class MyBindSample extends PolymerElement {
+class MyBindSample extends Polymer.Element {
   static get is() { return 'my-bind-sample'; }
 
   static get properties() {
@@ -39,7 +31,7 @@
   }
 
   static get template() {
-    return html`
+    return Polymer.html`
     Template example: Patchset number [[revision._number]]. <br/>
     Computed example: [[computedExample]].
     `;
diff --git a/polygerrit-ui/app/samples/coverage-plugin.js b/polygerrit-ui/app/samples/coverage-plugin.js
deleted file mode 100644
index 8d321c7..0000000
--- a/polygerrit-ui/app/samples/coverage-plugin.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * @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.
- */
-function populateWithDummyData(coverageData) {
-  coverageData['/COMMIT_MSG'] = {
-    linesMissingCoverage: [3, 4, 7, 14],
-    totalLines: 14,
-    changeNum: 94,
-    patchNum: 2,
-  };
-
-  // more coverage info on other files
-}
-
-/**
- * This plugin will add a toggler on file diff page to
- * display fake coverage data.
- *
- * As the fake coverage data only provided for COMMIT_MSG file,
- * so it will only work for COMMIT_MSG file diff.
- */
-Gerrit.install(plugin => {
-  const coverageData = {};
-  let displayCoverage = false;
-  const annotationApi = plugin.annotationApi();
-  const styleApi = plugin.styles();
-
-  const coverageStyle = styleApi.css('background-color: #EF9B9B !important');
-  const emptyStyle = styleApi.css('');
-
-  annotationApi.setLayer(context => {
-    if (Object.keys(coverageData).length === 0) {
-      // Coverage data is not ready yet.
-      return;
-    }
-    const path = context.path;
-    const line = context.line;
-    // Highlight lines missing coverage with this background color if
-    // coverage should be displayed, else do nothing.
-    const annotationStyle = displayCoverage
-      ? coverageStyle
-      : emptyStyle;
-
-    // ideally should check to make sure its the same patch for same change
-    // for demo purpose, this is only checking to make sure we have fake data
-    if (coverageData[path]) {
-      const linesMissingCoverage = coverageData[path].linesMissingCoverage;
-      if (linesMissingCoverage.includes(line.afterNumber)) {
-        context.annotateRange(0, line.text.length, annotationStyle, 'right');
-        context.annotateLineNumber(annotationStyle, 'right');
-      }
-    }
-  }).enableToggleCheckbox('Display Coverage', checkbox => {
-    populateWithDummyData(coverageData);
-    checkbox.disabled = false;
-    checkbox.onclick = e => {
-      displayCoverage = e.target.checked;
-      Object.keys(coverageData).forEach(file => {
-        annotationApi.notify(file, 0, coverageData[file].totalLines, 'right');
-      });
-    };
-  });
-});
diff --git a/polygerrit-ui/app/samples/extra-column-on-file-list.js b/polygerrit-ui/app/samples/extra-column-on-file-list.js
index 2e37c01..c64bcd4 100644
--- a/polygerrit-ui/app/samples/extra-column-on-file-list.js
+++ b/polygerrit-ui/app/samples/extra-column-on-file-list.js
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 /**
  * This plugin will an extra column to file list on change page to show
  * the first character of the path.
@@ -74,4 +75,4 @@
       'change-view-file-list-header-prepend', ColumnHeader.is);
   plugin.registerDynamicCustomComponent(
       'change-view-file-list-content-prepend', ColumnContent.is);
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/samples/lgtm-plugin.js b/polygerrit-ui/app/samples/lgtm-plugin.js
index 9de1496..537b1fa 100644
--- a/polygerrit-ui/app/samples/lgtm-plugin.js
+++ b/polygerrit-ui/app/samples/lgtm-plugin.js
@@ -14,8 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 /**
- * This plugin will +1 on Code-Review label if detect that you have
+ * This plugin will +1 on Code-Review label if it detects that you have
  * LGTM as start of your reply.
  */
 Gerrit.install(plugin => {
@@ -29,4 +30,4 @@
       replyApi.setLabelValue(label, '+1');
     }
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/samples/repo-command.js b/polygerrit-ui/app/samples/repo-command.js
index 4f64059..acecd7d 100644
--- a/polygerrit-ui/app/samples/repo-command.js
+++ b/polygerrit-ui/app/samples/repo-command.js
@@ -15,14 +15,7 @@
  * limitations under the License.
  */
 
-// Element class exists in all browsers:
-// https://developer.mozilla.org/en-US/docs/Web/API/Element
-// Rename it to PolymerElement to avoid conflicts. Also,
-// typescript reports the following error:
-// error TS2451: Cannot redeclare block-scoped variable 'Element'.
-const {html, Element: PolymerElement} = Polymer;
-
-class RepoCommandLow extends PolymerElement {
+class RepoCommandLow extends Polymer.Element {
   static get is() { return 'repo-command-low'; }
 
   static get properties() {
@@ -32,20 +25,16 @@
   }
 
   static get template() {
-    return html`
-    <style include="shared-styles">
-    :host {
-      display: block;
-      margin-bottom: var(--spacing-xxl);
-    }
-    </style>
-    <h3>Low-level bork</h3>
-    <gr-button
-      on-click="_handleCommandTap"
-    >
-      Low-level bork
-    </gr-button>
-   `;
+    return Polymer.html`
+      <style include="shared-styles">
+      :host {
+        display: block;
+        margin-bottom: var(--spacing-xxl);
+      }
+      </style>
+      <h3>Plugin Bork</h3>
+      <gr-button on-click="_handleCommandTap">Bork</gr-button>
+    `;
   }
 
   connectedCallback() {
@@ -56,32 +45,16 @@
   }
 
   _handleCommandTap() {
-    alert('(softly) bork, bork.');
+    alert('bork');
   }
 }
 
-// register the custom component
 customElements.define(RepoCommandLow.is, RepoCommandLow);
 
 /**
- * This plugin will add two new commands in command page for
- * All-Projects.
- *
- * The added commands will simply alert you when click.
+ * This plugin adds a new command to the command page of the repo All-Projects.
  */
 Gerrit.install(plugin => {
-  // High-level API
-  plugin.project()
-      .createCommand('Bork', (repoName, projectConfig) => {
-        if (repoName !== 'All-Projects') {
-          return false;
-        }
-      })
-      .onTap(() => {
-        alert('Bork, bork!');
-      });
-
-  // Low-level API
   plugin.registerCustomComponent(
       'repo-command', 'repo-command-low');
 });
diff --git a/polygerrit-ui/app/samples/some-screen.js b/polygerrit-ui/app/samples/some-screen.js
index c600fe4..8edaaa9 100644
--- a/polygerrit-ui/app/samples/some-screen.js
+++ b/polygerrit-ui/app/samples/some-screen.js
@@ -15,14 +15,7 @@
  * limitations under the License.
  */
 
-// Element class exists in all browsers:
-// https://developer.mozilla.org/en-US/docs/Web/API/Element
-// Rename it to PolymerElement to avoid conflicts. Also,
-// typescript reports the following error:
-// error TS2451: Cannot redeclare block-scoped variable 'Element'.
-const {html, Element: PolymerElement} = Polymer;
-
-class SomeScreenMain extends PolymerElement {
+class SomeScreenMain extends Polymer.Element {
   static get is() { return 'some-screen-main'; }
 
   static get properties() {
@@ -32,7 +25,7 @@
   }
 
   static get template() {
-    return html`
+    return Polymer.html`
       This is the <b>main</b> plugin screen at [[token]]
       <ul>
         <li><a href$="[[rootUrl]]/bar">without component</a></li>
@@ -46,7 +39,6 @@
   }
 }
 
-// register the custom component
 customElements.define(SomeScreenMain.is, SomeScreenMain);
 
 /**
diff --git a/polygerrit-ui/app/samples/suggest-vote.js b/polygerrit-ui/app/samples/suggest-vote.js
index b3c3046..10d0d4a 100644
--- a/polygerrit-ui/app/samples/suggest-vote.js
+++ b/polygerrit-ui/app/samples/suggest-vote.js
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 /**
  * This plugin will upgrade your +1 on Code-Review label
  * to +2 and show a message below the voting labels.
@@ -29,8 +30,7 @@
     if (wasSuggested && name === CODE_REVIEW) {
       replyApi.showMessage('');
       wasSuggested = false;
-    } else if (replyApi.getLabelValue(CODE_REVIEW) === '+1' &&
-    !wasSuggested) {
+    } else if (replyApi.getLabelValue(CODE_REVIEW) === '+1' && !wasSuggested) {
       replyApi.setLabelValue(CODE_REVIEW, '+2');
       replyApi.showMessage(`Suggested ${CODE_REVIEW} upgrade: +2`);
       wasSuggested = true;
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index c3b38c6..d30917a0 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -74,6 +74,11 @@
 export interface DiffFileMetaInfo extends DiffFileMetaInfoApi {
   /** Links to the file in external sites as a list of WebLinkInfo entries. */
   web_links?: WebLinkInfo[];
+  /**
+   * Links to edit the file in external sites as a list of WebLinkInfo
+   * entries.
+   */
+  edit_web_links?: WebLinkInfo[];
 }
 
 /**
diff --git a/polygerrit-ui/app/utils/path-list-util.ts b/polygerrit-ui/app/utils/path-list-util.ts
index dda6031..fd922fc 100644
--- a/polygerrit-ui/app/utils/path-list-util.ts
+++ b/polygerrit-ui/app/utils/path-list-util.ts
@@ -64,6 +64,8 @@
   return file === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
 }
 
+// In case there are files with comments on them but they are unchanged, then
+// we explicitly displays the file to render the comments with Unchanged status
 export function addUnmodifiedFiles(
   files: {[filename: string]: FileInfo},
   commentedPaths: {[fileName: string]: boolean}
@@ -73,6 +75,18 @@
     if (hasOwnProperty(files, commentedPath) || shouldHideFile(commentedPath)) {
       return;
     }
+
+    // if file is Renamed but has comments, then do not show the entry for the
+    // old file path name
+    if (
+      Object.values(files).some(
+        file =>
+          file.status === FileInfoStatus.RENAMED &&
+          file.old_path === commentedPath
+      )
+    ) {
+      return;
+    }
     // TODO(TS): either change FileInfo to mark delta and size optional
     // or fill in 0 here
     files[commentedPath] = {
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 7c63cc3..03a29da 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -83,13 +83,13 @@
         srcs = [plugin_name + ".js"],
     )
 
-def gerrit_js_bundle(name, srcs, entry_point):
+def gerrit_js_bundle(name, entry_point, srcs = []):
     """Produces a Gerrit JavaScript bundle archive.
 
     This rule bundles and minifies the javascript files of a frontend plugin and
     produces a file archive.
     Output of this rule is an archive with "${name}.jar" with specific layout for
-    Gerrit frontentd plugins. That archive should be provided to gerrit_plugin
+    Gerrit frontend plugins. That archive should be provided to gerrit_plugin
     rule as resource_jars attribute.
 
     Args:
@@ -97,8 +97,13 @@
       srcs: Plugin sources.
       entry_point: Plugin entry_point.
     """
+
+    bundle = name + "-bundle"
+    minified = name + ".min"
+    main = name + ".js"
+
     rollup_bundle(
-        name = name + "-bundle",
+        name = bundle,
         srcs = srcs,
         entry_point = entry_point,
         format = "iife",
@@ -110,22 +115,22 @@
     )
 
     terser_minified(
-        name = name + ".min",
+        name = minified,
         sourcemap = False,
-        src = name + "-bundle.js",
+        src = bundle,
     )
 
     native.genrule(
         name = name + "_rename_js",
-        srcs = [name + ".min"],
-        outs = [name + ".js"],
+        srcs = [minified],
+        outs = [main],
         cmd = "cp $< $@",
         output_to_bindir = True,
     )
 
     genrule2(
         name = name,
-        srcs = [name + ".js"],
+        srcs = [main],
         outs = [name + ".jar"],
         cmd = " && ".join([
             "mkdir $$TMP/static",