Merge "Make sure threads have commentSide attribute"
diff --git a/Documentation/dev-build-plugins.txt b/Documentation/dev-build-plugins.txt
index 5dacd71..072c22c 100644
--- a/Documentation/dev-build-plugins.txt
+++ b/Documentation/dev-build-plugins.txt
@@ -121,11 +121,24 @@
 ]
 ----
 
+If the plugin(s) being bundled in the release have external dependencies, include them
+in `plugins/external_plugin_deps`. You should alias `external_plugin_deps()` so it
+can be imported for multiple plugins. For example:
+
+----
+load(":my-plugin/external_plugin_deps.bzl", my_plugin="external_plugin_deps")
+load(":my-other-plugin/external_plugin_deps.bzl", my_other_plugin="external_plugin_deps")
+
+def external_plugin_deps():
+  my_plugin()
+  my_other_plugin()
+----
+
 [NOTE]
-Since `tools/bzl/plugins.bzl` is part of Gerrit's source code and the version
-of the war is based on the state of the git repository that is built; you should
-commit this change before building, otherwise the version will be marked as
-'dirty'.
+Since `tools/bzl/plugins.bzl` and `plugins/external_plugin_deps.bzl` are part of
+Gerrit's source code and the version of the war is based on the state of the git
+repository that is built; you should commit this change before building, otherwise
+the version will be marked as 'dirty'.
 
 == Bazel standalone driven
 
diff --git a/WORKSPACE b/WORKSPACE
index 7852958..ac0ffd2 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1099,6 +1099,13 @@
 )
 
 bower_archive(
+    name = "paper-tabs",
+    package = "polymerelements/paper-tabs",
+    sha1 = "b6dd2fbd7ee887534334057a29eb545b940fc5cf",
+    version = "2.0.0",
+)
+
+bower_archive(
     name = "iron-icon",
     package = "polymerelements/iron-icon",
     sha1 = "7da49a0d33cd56017740e0dbcf41d2b71532023f",
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index a68266a..15a330e 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -84,7 +84,6 @@
     dbInjector = createDbInjector(MULTI_USER);
     globalConfig = dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
     threads = ThreadLimiter.limitThreads(dbInjector, threads);
-    checkNotSlaveMode();
     overrideConfig();
     LifecycleManager dbManager = new LifecycleManager();
     dbManager.add(dbInjector);
@@ -141,26 +140,21 @@
         "invalid index name(s): " + new TreeSet<>(invalid) + " available indices are: " + valid);
   }
 
-  private void checkNotSlaveMode() throws Die {
-    if (globalConfig.getBoolean("container", "slave", false)) {
-      throw die("Cannot run reindex in slave mode");
-    }
-  }
-
   private Injector createSysInjector() {
     Map<String, Integer> versions = new HashMap<>();
     if (changesVersion != null) {
       versions.put(ChangeSchemaDefinitions.INSTANCE.getName(), changesVersion);
     }
+    boolean slave = globalConfig.getBoolean("container", "slave", false);
     List<Module> modules = new ArrayList<>();
     Module indexModule;
     switch (IndexModule.getIndexType(dbInjector)) {
       case LUCENE:
-        indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(versions, threads, false);
+        indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
         break;
       case ELASTICSEARCH:
         indexModule =
-            ElasticIndexModule.singleVersionWithExplicitVersions(versions, threads, false);
+            ElasticIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
         break;
       default:
         throw new IllegalStateException("unsupported index.type");
diff --git a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
index 1485ee3..5143dc7 100644
--- a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
+++ b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.mail.MailHeader;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import org.apache.james.mime4j.dom.field.FieldName;
 
 /** Send an email to inform users that parsing their inbound email failed. */
 public class InboundEmailRejectionSender extends OutgoingEmail {
@@ -55,11 +56,12 @@
   protected void init() throws EmailException {
     super.init();
     setListIdHeader();
+    setHeader(FieldName.SUBJECT, "[Gerrit Code Review] Unable to process your email");
 
     add(RecipientType.TO, to);
 
     if (!threadId.isEmpty()) {
-      setHeader(MailHeader.REFERENCES.fieldName(), "<" + threadId + ">");
+      setHeader(MailHeader.REFERENCES.fieldName(), threadId);
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
index f2a5d2f..4b6f8b2 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.collect.ImmutableMap.toImmutableMap;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.extensions.client.ListGroupsOption.MEMBERS;
 
@@ -27,6 +28,7 @@
 import com.google.gerrit.acceptance.pgm.IndexUpgradeController.UpgradeAttempt;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.index.GerritIndexStatus;
@@ -36,6 +38,8 @@
 import com.google.inject.Provider;
 import java.nio.file.Files;
 import java.util.Set;
+import java.util.function.Consumer;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
 import org.junit.Test;
@@ -80,6 +84,91 @@
   }
 
   @Test
+  public void offlineReindexForChangesIsNotPossibleInSlaveMode() throws Exception {
+    enableSlaveMode();
+
+    int exitCode =
+        runGerritAndReturnExitCode(
+            "reindex",
+            "--index",
+            "changes",
+            "-d",
+            sitePaths.site_path.toString(),
+            "--show-stack-trace");
+
+    assertWithMessage("Slave hosts shouldn't allow to offline reindex changes")
+        .that(exitCode)
+        .isGreaterThan(0);
+  }
+
+  @Test
+  public void offlineReindexForAccountsIsNotPossibleInSlaveMode() throws Exception {
+    enableSlaveMode();
+
+    int exitCode =
+        runGerritAndReturnExitCode(
+            "reindex",
+            "--index",
+            "accounts",
+            "-d",
+            sitePaths.site_path.toString(),
+            "--show-stack-trace");
+
+    assertWithMessage("Slave hosts shouldn't allow to offline reindex accounts")
+        .that(exitCode)
+        .isGreaterThan(0);
+  }
+
+  @Test
+  public void offlineReindexForProjectsIsNotPossibleInSlaveMode() throws Exception {
+    enableSlaveMode();
+
+    int exitCode =
+        runGerritAndReturnExitCode(
+            "reindex",
+            "--index",
+            "projects",
+            "-d",
+            sitePaths.site_path.toString(),
+            "--show-stack-trace");
+
+    assertWithMessage("Slave hosts shouldn't allow to offline reindex projects")
+        .that(exitCode)
+        .isGreaterThan(0);
+  }
+
+  @Test
+  public void offlineReindexForGroupsIsPossibleInSlaveMode() throws Exception {
+    enableSlaveMode();
+
+    int exitCode =
+        runGerritAndReturnExitCode(
+            "reindex",
+            "--index",
+            "groups",
+            "-d",
+            sitePaths.site_path.toString(),
+            "--show-stack-trace");
+
+    assertWithMessage("Slave hosts should allow to offline reindex groups")
+        .that(exitCode)
+        .isEqualTo(0);
+  }
+
+  @Test
+  public void offlineReindexForAllAvailableIndicesIsPossibleInSlaveMode() throws Exception {
+    enableSlaveMode();
+
+    int exitCode =
+        runGerritAndReturnExitCode(
+            "reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+
+    assertWithMessage("Slave hosts should allow to perform a general offline reindex")
+        .that(exitCode)
+        .isEqualTo(0);
+  }
+
+  @Test
   public void onlineUpgradeChanges() throws Exception {
     int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
     int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
@@ -143,12 +232,24 @@
   }
 
   private void setOnlineUpgradeConfig(boolean enable) throws Exception {
+    updateConfig(cfg -> cfg.setBoolean("index", null, "onlineUpgrade", enable));
+  }
+
+  private void enableSlaveMode() throws Exception {
+    updateConfig(config -> config.setBoolean("container", null, "slave", true));
+  }
+
+  private void updateConfig(Consumer<Config> configConsumer) throws Exception {
     FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
     cfg.load();
-    cfg.setBoolean("index", null, "onlineUpgrade", enable);
+    configConsumer.accept(cfg);
     cfg.save();
   }
 
+  private static int runGerritAndReturnExitCode(String... args) throws Exception {
+    return GerritLauncher.mainImpl(args);
+  }
+
   private void assertSearchVersion(ServerContext ctx, int expected) {
     assertThat(
             ctx.getInjector()
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index 822841c..2958888 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -14,7 +14,21 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.testing.Util;
+import java.util.List;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.junit.Test;
 
 public class IndexChangeIT extends AbstractDaemonTest {
@@ -30,4 +44,62 @@
     blockRead("refs/heads/master");
     userRestSession.post("/changes/" + changeId + "/index/").assertNotFound();
   }
+
+  @Test
+  public void indexChangeAfterOwnerLosesVisibility() throws Exception {
+    // Create a test group with 2 users as members
+    TestAccount user2 = accountCreator.user2();
+    String group = createGroup("test");
+    gApi.groups().id(group).addMembers("admin", "user", user2.username);
+
+    // Create a project and restrict its visibility to the group
+    Project.NameKey p = createProject("p");
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+    Util.allow(
+        cfg,
+        Permission.READ,
+        groupCache.get(new AccountGroup.NameKey(group)).get().getGroupUUID(),
+        "refs/*");
+    Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(p, cfg);
+
+    // Clone it and push a change as a regular user
+    TestRepository<InMemoryRepository> repo = cloneProject(p, user);
+    PushOneCommit push = pushFactory.create(db, user.getIdent(), repo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+    assertThat(result.getChange().change().getOwner()).isEqualTo(user.id);
+    String changeId = result.getChangeId();
+
+    // User can see the change and it is mergeable
+    setApiUser(user);
+    List<ChangeInfo> changes = gApi.changes().query(changeId).get();
+    assertThat(changes).hasSize(1);
+    assertThat(changes.get(0).mergeable).isNotNull();
+
+    // Other user can see the change and it is mergeable
+    setApiUser(user2);
+    changes = gApi.changes().query(changeId).get();
+    assertThat(changes).hasSize(1);
+    assertThat(changes.get(0).mergeable).isTrue();
+
+    // Remove the user from the group so they can no longer see the project
+    setApiUser(admin);
+    gApi.groups().id(group).removeMembers("user");
+
+    // User can no longer see the change
+    setApiUser(user);
+    changes = gApi.changes().query(changeId).get();
+    assertThat(changes).isEmpty();
+
+    // Reindex the change
+    setApiUser(admin);
+    gApi.changes().id(changeId).index();
+
+    // Other user can still see the change and it is still mergeable
+    setApiUser(user2);
+    changes = gApi.changes().query(changeId).get();
+    assertThat(changes).hasSize(1);
+    assertThat(changes.get(0).mergeable).isTrue();
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 71976f5..f34fe33 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -255,5 +255,6 @@
     assertNotifyTo(user);
     Message message = sender.nextMessage();
     assertThat(message.body()).contains("was unable to parse your email");
+    assertThat(message.headers()).containsKey("Subject");
   }
 }
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index c035793..5ee3535 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -25,8 +25,8 @@
   bower_archive(
     name = "font-roboto",
     package = "PolymerElements/font-roboto",
-    version = "1.0.3",
-    sha1 = "edf478d20ae2fc0704d7c155e20162caaabdd5ae")
+    version = "1.1.0",
+    sha1 = "ab4218d87b9ce569d6282b01f7642e551879c3d5")
   bower_archive(
     name = "iron-a11y-announcer",
     package = "PolymerElements/iron-a11y-announcer",
@@ -39,7 +39,7 @@
     sha1 = "f58358ee652c67e6e721364ba50fb77a2ece1465")
   bower_archive(
     name = "iron-behaviors",
-    package = "polymerelements/iron-behaviors",
+    package = "PolymerElements/iron-behaviors",
     version = "1.0.18",
     sha1 = "e231a1a02b090f5183db917639fdb96cdd0dca18")
   bower_archive(
@@ -55,8 +55,8 @@
   bower_archive(
     name = "iron-flex-layout",
     package = "PolymerElements/iron-flex-layout",
-    version = "1.3.7",
-    sha1 = "4d4cf3232cf750a17a7df0a37476117f831ac633")
+    version = "1.3.9",
+    sha1 = "d987b924cf29fcfe4b393833e81fdc9f1e268796")
   bower_archive(
     name = "iron-form-element-behavior",
     package = "PolymerElements/iron-form-element-behavior",
@@ -103,6 +103,11 @@
     version = "1.0.13",
     sha1 = "a81eab28a952e124c208430e17508d9a1aae4ee7")
   bower_archive(
+    name = "paper-icon-button",
+    package = "PolymerElements/paper-icon-button",
+    version = "2.1.0",
+    sha1 = "caead6a276877888d128ace809376980c3f3fe42")
+  bower_archive(
     name = "paper-ripple",
     package = "PolymerElements/paper-ripple",
     version = "1.0.10",
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
index fb40855..dc16ccf 100644
--- a/lib/js/bower_components.bzl
+++ b/lib/js/bower_components.bzl
@@ -229,6 +229,16 @@
     seed = True,
   )
   bower_component(
+    name = "paper-icon-button",
+    license = "//lib:LICENSE-polymer",
+    deps = [
+      ":iron-icon",
+      ":paper-behaviors",
+      ":paper-styles",
+      ":polymer",
+    ],
+  )
+  bower_component(
     name = "paper-input",
     license = "//lib:LICENSE-polymer",
     deps = [
@@ -282,6 +292,23 @@
     ],
   )
   bower_component(
+    name = "paper-tabs",
+    license = "//lib:LICENSE-polymer",
+    deps = [
+      ":iron-behaviors",
+      ":iron-flex-layout",
+      ":iron-icon",
+      ":iron-iconset-svg",
+      ":iron-menu-behavior",
+      ":iron-resizable-behavior",
+      ":paper-behaviors",
+      ":paper-icon-button",
+      ":paper-styles",
+      ":polymer",
+    ],
+    seed = True,
+  )
+  bower_component(
     name = "paper-toggle-button",
     license = "//lib:LICENSE-polymer",
     deps = [
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 0c2cd5e..7487ad5 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -26,6 +26,7 @@
         "//lib/js:paper-input",
         "//lib/js:paper-item",
         "//lib/js:paper-listbox",
+        "//lib/js:paper-tabs",
         "//lib/js:paper-toggle-button",
         "//lib/js:polymer",
         "//lib/js:polymer-resin",
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index 1ce05b1..5257e69 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -15,6 +15,8 @@
   'use strict';
 
   const SUGGESTIONS_LIMIT = 15;
+  const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
+      'permission to add it';
 
   const URL_REGEX = '^(?:[a-z]+:)?//';
 
@@ -186,7 +188,16 @@
 
     _handleSavingIncludedGroups() {
       return this.$.restAPI.saveIncludedGroup(this._groupName,
-          this._includedGroupSearch)
+          this._includedGroupSearch, err => {
+            if (err.status === 404) {
+              this.dispatchEvent(new CustomEvent('show-alert', {
+                detail: {message: SAVING_ERROR_TEXT},
+                bubbles: true,
+              }));
+              return err;
+            }
+            throw Error(err.statusText);
+          })
           .then(config => {
             if (!config) {
               return;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index 194750e..d670d4d 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -183,6 +183,24 @@
       });
     });
 
+    test('add included group 404 shows helpful error text', () => {
+      element._groupOwner = true;
+
+      const memberName = 'bad-name';
+      const alertStub = sandbox.stub();
+      element.addEventListener('show-alert', alertStub);
+
+      sandbox.stub(element.$.restAPI, 'saveGroupMembers',
+          () => Promise.reject({status: 404}));
+
+      element.$.groupMemberSearchInput.text = memberName;
+      element.$.groupMemberSearchInput.value = 1234;
+
+      return element._handleSavingIncludedGroups().then(() => {
+        assert.isTrue(alertStub.called);
+      });
+    });
+
     test('_getAccountSuggestions empty', () => {
       return element._getAccountSuggestions('nonexistent').then(accounts => {
         assert.equal(accounts.length, 0);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index e5e077f..45109cd 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -23,6 +23,7 @@
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
 <link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
+<link rel="import" href="../../shared/gr-change-status/gr-change-status.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -69,6 +70,17 @@
         height: 0;
         overflow: hidden;
       }
+      .status {
+        align-items: center;
+        display: inline-flex;
+      }
+      .status .comma {
+        padding-right: .2rem;
+      }
+      /* Used to hide the leading separator comma for statuses. */
+      .status .comma:first-of-type {
+        display: none;
+      }
       a {
         color: var(--default-text-color);
         cursor: pointer;
@@ -81,6 +93,9 @@
       .positionIndicator {
         visibility: hidden;
       }
+      .size {
+        text-align: center;
+      }
       :host([selected]) .positionIndicator {
         visibility: visible;
       }
@@ -100,6 +115,7 @@
       .u-gray-background {
         background-color: #F5F5F5;
       }
+      .comma,
       .placeholder {
         color: rgba(0, 0, 0, .87);
       }
@@ -107,6 +123,9 @@
         :host {
           display: flex;
         }
+        :host([selected]) {
+          border-left: none;
+        }
       }
     </style>
     <style include="gr-change-list-styles"></style>
@@ -133,21 +152,26 @@
     </td>
     <td class="cell status"
         hidden$="[[isColumnHidden('Status', visibleChangeTableColumns)]]">
-      <template is="dom-if" if="[[status]]">
-        [[status]]
+      <template is="dom-repeat" items="[[statuses]]" as="status">
+        <div class="comma">,</div>
+        <gr-change-status flat status="[[status]]"></gr-change-status>
       </template>
-      <template is="dom-if" if="[[!status]]">
+      <template is="dom-if" if="[[!statuses.length]]">
         <span class="placeholder">--</span>
       </template>
     </td>
     <td class="cell owner"
         hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]">
-      <gr-account-link account="[[change.owner]]"></gr-account-link>
+      <gr-account-link
+          account="[[change.owner]]"
+          additional-text="[[_computeAccountStatusString(change.owner)]]"></gr-account-link>
     </td>
     <td class="cell assignee"
         hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]">
       <template is="dom-if" if="[[change.assignee]]">
-        <gr-account-link account="[[change.assignee]]"></gr-account-link>
+        <gr-account-link
+            account="[[change.assignee]]"
+            additional-text="[[_computeAccountStatusString(change.owner)]]"></gr-account-link>
       </template>
       <template is="dom-if" if="[[!change.assignee]]">
         <span class="placeholder">--</span>
@@ -180,10 +204,10 @@
           has-tooltip
           date-str="[[change.updated]]"></gr-date-formatter>
     </td>
-    <td class="cell size u-monospace"
+    <td class="cell size"
+        title$="[[_computeSizeTooltip(change)]]"
         hidden$="[[isColumnHidden('Size', visibleChangeTableColumns)]]">
-      <span class="u-green"><span>+</span>[[change.insertions]]</span>,
-      <span class="u-red"><span>-</span>[[change.deletions]]</span>
+      <span>[[_computeChangeSize(change)]]</span>
     </td>
     <template is="dom-repeat" items="[[labelNames]]" as="labelName">
       <td title$="[[_computeLabelTitle(change, labelName)]]"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 0735e2c..5d7121a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -14,6 +14,13 @@
 (function() {
   'use strict';
 
+  const CHANGE_SIZE = {
+    XS: 10,
+    SMALL: 50,
+    MEDIUM: 250,
+    LARGE: 1000,
+  };
+
   Polymer({
     is: 'gr-change-list-item',
 
@@ -29,9 +36,9 @@
         type: String,
         computed: '_computeChangeURL(change)',
       },
-      status: {
-        type: String,
-        computed: 'changeStatusString(change)',
+      statuses: {
+        type: Array,
+        computed: 'changeStatuses(change)',
       },
       showStar: {
         type: Boolean,
@@ -125,5 +132,40 @@
       if (!project) { return ''; }
       return this.truncatePath(project, 2);
     },
+
+    _computeAccountStatusString(account) {
+      return account && account.status ? `(${account.status})` : '';
+    },
+
+    _computeSizeTooltip(change) {
+      if (change.insertions + change.deletions === 0 ||
+          isNaN(change.insertions + change.deletions)) {
+        return 'Size unknown';
+      } else {
+        return `+${change.insertions}, -${change.deletions}`;
+      }
+    },
+
+    /**
+     * TShirt sizing is based on the following paper:
+     * http://dirkriehle.com/wp-content/uploads/2008/09/hicss-42-csdistr-final-web.pdf
+     */
+    _computeChangeSize(change) {
+      const delta = change.insertions + change.deletions;
+      if (isNaN(delta) || delta === 0) {
+        return '🤷'; // Unknown
+      }
+      if (delta < CHANGE_SIZE.XS) {
+        return 'XS';
+      } else if (delta < CHANGE_SIZE.SMALL) {
+        return 'S';
+      } else if (delta < CHANGE_SIZE.MEDIUM) {
+        return 'M';
+      } else if (delta < CHANGE_SIZE.LARGE) {
+        return 'L';
+      } else {
+        return 'XL';
+      }
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 6f2e6da..4b001c3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -200,5 +200,53 @@
       flushAsynchronousOperations();
       assert.isOk(element.$$('.assignee gr-account-link'));
     });
+
+    test('_computeAccountStatusString', () => {
+      assert.equal(element._computeAccountStatusString({}), '');
+      assert.equal(element._computeAccountStatusString({status: 'Working'}),
+          '(Working)');
+    });
+
+    test('TShirt sizing tooltip', () => {
+      assert.equal(element._computeSizeTooltip({
+        insertions: 'foo',
+        deletions: 'bar',
+      }), 'Size unknown');
+      assert.equal(element._computeSizeTooltip({
+        insertions: 0,
+        deletions: 0,
+      }), 'Size unknown');
+      assert.equal(element._computeSizeTooltip({
+        insertions: 1,
+        deletions: 2,
+      }), '+1, -2');
+    });
+
+    test('TShirt sizing', () => {
+      assert.equal(element._computeChangeSize({
+        insertions: 'foo',
+        deletions: 'bar',
+      }), '🤷');
+      assert.equal(element._computeChangeSize({
+        insertions: 1,
+        deletions: 1,
+      }), 'XS');
+      assert.equal(element._computeChangeSize({
+        insertions: 9,
+        deletions: 1,
+      }), 'S');
+      assert.equal(element._computeChangeSize({
+        insertions: 10,
+        deletions: 200,
+      }), 'M');
+      assert.equal(element._computeChangeSize({
+        insertions: 99,
+        deletions: 900,
+      }), 'L');
+      assert.equal(element._computeChangeSize({
+        insertions: 99,
+        deletions: 999,
+      }), 'XL');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index a954559..adab1c8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -211,6 +211,10 @@
         type: Boolean,
         value: false,
       },
+      _hideQuickApproveAction: {
+        type: Boolean,
+        value: false,
+      },
       changeNum: String,
       changeStatus: String,
       commitNum: String,
@@ -653,7 +657,18 @@
       return null;
     },
 
+    hideQuickApproveAction() {
+      this._topLevelSecondaryActions =
+        this._topLevelSecondaryActions.filter(sa => {
+          return sa.key !== QUICK_APPROVE_ACTION.key;
+        });
+      this._hideQuickApproveAction = true;
+    },
+
     _getQuickApproveAction() {
+      if (this._hideQuickApproveAction) {
+        return null;
+      }
       const approval = this._getTopMissingApproval();
       if (!approval) {
         return null;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 8986816..835d560 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -1091,6 +1091,21 @@
         assert.isNotNull(approveButton);
       });
 
+      test('hide quick approve', () => {
+        const approveButton =
+            element.$$('gr-button[data-action-key=\'review\']');
+        assert.isNotNull(approveButton);
+        assert.isFalse(element._hideQuickApproveAction);
+
+        // Assert approve button gets removed from list of buttons.
+        element.hideQuickApproveAction();
+        flushAsynchronousOperations();
+        const approveButtonUpdated =
+            element.$$('gr-button[data-action-key=\'review\']');
+        assert.isNull(approveButtonUpdated);
+        assert.isTrue(element._hideQuickApproveAction);
+      });
+
       test('is first in list of secondary actions', () => {
         const approveButton = element.$.secondaryActions
             .querySelector('gr-button');
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 6ce6a1e..464e1bb 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -42,7 +42,7 @@
       }
       .container {
         display: flex;
-        margin: 5px 0;
+        margin: .5em 0;
       }
       .lineNum {
         margin-right: .5em;
@@ -53,6 +53,16 @@
         flex: 1;
         --gr-formatted-text-prose-max-width: 80ch;
       }
+      @media screen and (max-width: 50em) {
+        .container {
+          flex-direction: column;
+          margin: 0 0 .5em .5em;
+        }
+        .lineNum {
+          min-width: initial;
+          text-align: left;
+        }
+      }
     </style>
     <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
       <div class="file">[[computeDisplayPath(file)]]:</div>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 4370d7e..ea70de9 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -95,7 +95,7 @@
               type="radio"
               on-tap="_handleRebaseOnOther">
           <label id="rebaseOnOtherLabel" for="rebaseOnOtherInput">
-            Rebase on a specific change or ref <span hidden$="[[!hasParent]]">
+            Rebase on a specific change, ref, or commit <span hidden$="[[!hasParent]]">
               (breaks relation chain)
             </span>
           </label>
@@ -107,7 +107,8 @@
               text="{{_inputText}}"
               on-tap="_handleEnterChangeNumberTap"
               on-commit="_handleBaseSelected"
-              placeholder="Change number">
+              allow-non-suggested-values
+              placeholder="Change number, ref, or commit hash">
           </gr-autocomplete>
         </div>
       </div>
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 a326f54..0e7a709 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
@@ -14,8 +14,6 @@
 (function() {
   'use strict';
 
-  const ERR_EDIT_LOADED = 'You cannot change the review status of an edit.';
-
   // Maximum length for patch set descriptions.
   const PATCH_DESC_MAX_LENGTH = 500;
   const WARN_SHOW_ALL_THRESHOLD = 1000;
@@ -397,10 +395,7 @@
     },
 
     _reviewFile(path) {
-      if (this.editMode) {
-        this.fire('show-alert', {message: ERR_EDIT_LOADED});
-        return;
-      }
+      if (this.editMode) { return; }
       const index = this._reviewed.indexOf(path);
       const reviewed = index !== -1;
       if (reviewed) {
@@ -896,7 +891,7 @@
           diffElem.comments = this.changeComments.getCommentsBySideForPath(
               path, this.patchRange, this.projectConfig);
           const promises = [diffElem.reload()];
-          if (this._isLoggedIn) {
+          if (this._loggedIn && !this.diffPrefs.manual_review) {
             promises.push(this._reviewFile(path));
           }
           return Promise.all(promises);
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 38ab31b..72a9629 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
@@ -85,7 +85,7 @@
             .returns({meta: {}, left: [], right: []});
         done();
       });
-
+      element.diffPrefs = {};
       element.numFilesShown = 200;
       saveStub = sandbox.stub(element, '_saveReviewedState',
           () => { return Promise.resolve(); });
@@ -927,7 +927,7 @@
     });
 
     test('_renderInOrder logged in', done => {
-      element._isLoggedIn = true;
+      element._loggedIn = true;
       const reviewStub = sandbox.stub(element, '_reviewFile');
       let callCount = 0;
       const diffs = [{
@@ -959,6 +959,24 @@
           });
     });
 
+    test('_renderInOrder respects diffPrefs.manual_review', () => {
+      element._loggedIn = true;
+      element.diffPrefs = {manual_review: true};
+      const reviewStub = sandbox.stub(element, '_reviewFile');
+      const diffs = [{
+        path: 'p',
+        reload() { return Promise.resolve(); },
+      }];
+
+      return element._renderInOrder(['p'], diffs, 1).then(() => {
+        assert.isFalse(reviewStub.called);
+        delete element.diffPrefs.manual_review;
+        return element._renderInOrder(['p'], diffs, 1).then(() => {
+          assert.isTrue(reviewStub.called);
+        });
+      });
+    });
+
     test('_loadingChanged fired from reload in debouncer', done => {
       element.changeNum = 123;
       element.patchRange = {patchNum: 12};
@@ -1102,6 +1120,8 @@
       commentApiWrapper = fixture('basic');
       element = commentApiWrapper.$.fileList;
       loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+      element.diffPrefs = {};
+      sandbox.stub(element, '_reviewFile');
 
       // Stub methods on the changeComments object after changeComments has
       // been initalized.
@@ -1323,20 +1343,17 @@
 
     suite('editMode behavior', () => {
       test('reviewed checkbox', () => {
-        const alertStub = sandbox.stub();
+        element._reviewFile.restore();
         const saveReviewStub = sandbox.stub(element, '_saveReviewedState');
 
-        element.addEventListener('show-alert', alertStub);
         element.editMode = false;
         MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
-        assert.isFalse(alertStub.called);
         assert.isTrue(saveReviewStub.calledOnce);
 
         element.editMode = true;
         flushAsynchronousOperations();
 
         MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
-        assert.isTrue(alertStub.called);
         assert.isTrue(saveReviewStub.calledOnce);
       });
 
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index b1e6a5a..348eabb 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -130,13 +130,13 @@
     },
 
     _computeReviewerTooltip(reviewer, change) {
-      if (!change || !change.permitted_labels) return '';
+      if (!change || !change.labels) { return ''; }
       const maxScores = [];
       const maxPermitted = this._getMaxPermittedScores(change);
-      for (const label of Object.keys(change.permitted_labels)) {
+      for (const label of Object.keys(change.labels)) {
         const maxScore =
               this._getReviewerPermittedScore(reviewer, change, label);
-        if (isNaN(maxScore) || maxScore < 0) continue;
+        if (isNaN(maxScore) || maxScore < 0) { continue; }
         if (maxScore > 0 && maxScore === maxPermitted[label]) {
           maxScores.push(`${label}: +${maxScore}`);
         } else {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 985e4bb..24fc4d1 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -307,7 +307,6 @@
         },
         permitted_labels: {
           Foo: ['-1', ' 0', '+1', '+2'],
-          Bar: ['-1', ' 0', '+1', '+2'],
           FooBar: ['-1', ' 0'],
         },
       };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
index f300992..cb2eebe 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -15,20 +15,16 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-storage/gr-storage.html">
 <link rel="import" href="../gr-diff-comment/gr-diff-comment.html">
-<link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-diff-comment-thread">
   <template>
     <style include="shared-styles">
-      :host {
-        border: 1px solid #bbb;
-        display: block;
-        margin-bottom: 1px;
-        white-space: normal;
-      }
       gr-button {
         margin-left: .5em;
         --gr-button-color: #212121;
@@ -39,6 +35,10 @@
       }
       #container {
         background-color: #fcfad6;
+        border: 1px solid #bbb;
+        display: block;
+        margin-bottom: 1px;
+        white-space: normal;
       }
       #container.unresolved {
         background-color: #fcfaa6;
@@ -52,7 +52,22 @@
         margin: auto 0;
         padding: .5em .7em;
       }
+      .pathInfo {
+        display: flex;
+        align-items: baseline;
+      }
+      .descriptionText {
+        margin-left: .5rem;
+        font-size: var(--font-size-small);
+        font-style: italic;
+      }
     </style>
+    <template is="dom-if" if="[[showFilePath]]">
+      <div class="pathInfo">
+        <a href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a>
+        <span class="descriptionText">Patchset [[patchNum]]</span>
+      </div>
+    </template>
     <div id="container" class$="[[_computeHostClass(unresolved)]]">
       <template id="commentList" is="dom-repeat" items="[[_orderedComments]]"
           as="comment">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index 4de073e..e14370c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -58,6 +58,16 @@
         value: null,
       },
       rootId: String,
+      /**
+       * If this is true, the comment thread also needs to have the change and
+       * line properties property set
+       */
+      showFilePath: {
+        type: Boolean,
+        value: false,
+      },
+      /** Necessary only if showFilePath is true */
+      lineNum: Number,
       unresolved: {
         type: Boolean,
         notify: true,
@@ -71,6 +81,7 @@
 
     behaviors: [
       Gerrit.KeyboardShortcutBehavior,
+      Gerrit.PathListBehavior,
     ],
 
     listeners: {
@@ -117,6 +128,17 @@
       this.push('comments', draft);
     },
 
+    _getDiffUrlForComment(projectName, changeNum, path, patchNum) {
+      return Gerrit.Nav.getUrlForDiffById(changeNum,
+          projectName, path, patchNum,
+          null, this.lineNum);
+    },
+
+    _computeDisplayPath(path) {
+      const lineString = this.lineNum ? `#${this.lineNum}` : '';
+      return this.computeDisplayPath(path) + lineString;
+    },
+
     _getLoggedIn() {
       return this.$.restAPI.getLoggedIn();
     },
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index c84c260..de6c3b01 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -170,6 +170,35 @@
         done();
       });
     });
+
+    test('optionally show file path', () => {
+      // Path info doesn't exist when showFilePath is false. Because it's in a
+      // dom-if it is not yet in the dom.
+      assert.isNotOk(element.$$('.pathInfo'));
+
+      sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+      element.changeNum = 123;
+      element.projectName = 'test project';
+      element.path = 'path/to/file';
+      element.patchNum = 3;
+      element.lineNum = 5;
+      element.showFilePath = true;
+      flushAsynchronousOperations();
+      assert.isOk(element.$$('.pathInfo'));
+      assert.notEqual(getComputedStyle(element.$$('.pathInfo')).display,
+          'none');
+      assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly(
+          element.changeNum, element.projectName, element.path,
+          element.patchNum, null, element.lineNum));
+    });
+
+    test('_computeDisplayPath', () => {
+      const path = 'path/to/file';
+      assert.equal(element._computeDisplayPath(path), 'path/to/file');
+
+      element.lineNum = 5;
+      assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
+    });
   });
 
   suite('comment action tests', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
index 1dcfc68..ccc5361 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -120,11 +120,6 @@
       this.$.prefsOverlay.close();
     },
 
-    _handlePrefsTap(e) {
-      e.preventDefault();
-      this._openPrefs();
-    },
-
     open() {
       this.$.prefsOverlay.open().then(() => {
         const focusStops = this.getFocusStops();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 794e1fb..36ae32a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -287,6 +287,12 @@
                   on-tap="_handlePrefsTap">Preferences</gr-button>
             </span>
           </span>
+          <gr-endpoint-decorator name="annotation-toggler">
+            <span hidden id="annotation-span">
+              <label for="annotation-checkbox" id="annotation-label"></label>
+              <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
+            </span>
+          </gr-endpoint-decorator>
           <span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _isBlameSupported)]]">
             <span class="separator"></span>
             <gr-button
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index 4fedf73..bd07375 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -135,7 +135,8 @@
 
     _viewEditInChangeView() {
       const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum;
-      Gerrit.Nav.navigateToChange(this._change, patch);
+      Gerrit.Nav.navigateToChange(this._change, patch, null,
+          patch !== this.EDIT_NAME);
     },
 
     _getFileData(changeNum, path, patchNum) {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
new file mode 100644
index 0000000..b38509d
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
@@ -0,0 +1,137 @@
+<!--
+Copyright (C) 2017 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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-gpg-editor">
+  <template>
+    <style include="shared-styles"></style>
+    <style include="gr-form-styles">
+      .statusHeader {
+        width: 4em;
+      }
+      .keyHeader {
+        width: 9em;
+      }
+      .userIdHeader {
+        width: 15em;
+      }
+      #viewKeyOverlay {
+        padding: 2em;
+        width: 50em;
+      }
+      .publicKey {
+        font-family: var(--monospace-font-family);
+        overflow-x: scroll;
+        overflow-wrap: break-word;
+        width: 30em;
+      }
+      .closeButton {
+        bottom: 2em;
+        position: absolute;
+        right: 2em;
+      }
+      #existing {
+        margin-bottom: 1em;
+      }
+      #existing .commentColumn {
+        min-width: 27em;
+        width: auto;
+      }
+    </style>
+    <div class="gr-form-styles">
+      <fieldset id="existing">
+        <table>
+          <thead>
+            <tr>
+              <th class="idColumn">ID</th>
+              <th class="fingerPrintColumn">Fingerprint</th>
+              <th class="userIdHeader">User IDs</th>
+              <th class="keyHeader">Public Key</th>
+              <th></th>
+            </tr>
+          </thead>
+          <tbody>
+            <template is="dom-repeat" items="[[_keys]]" as="key">
+              <tr>
+                <td class="idColumn">[[key.id]]</td>
+                <td class="fingerPrintColumn">[[key.fingerprint]]</td>
+                <td class="userIdHeader">
+                  <template is="dom-repeat" items="[[key.user_ids]]">
+                    [[item]]
+                  </template>
+                </td>
+                <td class="keyHeader">
+                  <gr-button
+                      on-tap="_showKey"
+                      data-index$="[[index]]"
+                      link>Click to View</gr-button>
+                </td>
+                <td>
+                  <gr-button
+                      data-index$="[[index]]"
+                      on-tap="_handleDeleteKey">Delete</gr-button>
+                </td>
+              </tr>
+            </template>
+          </tbody>
+        </table>
+        <gr-overlay id="viewKeyOverlay" with-backdrop>
+          <fieldset>
+            <section>
+              <span class="title">Status</span>
+              <span class="value">[[_keyToView.status]]</span>
+            </section>
+            <section>
+              <span class="title">Key</span>
+              <span class="value">[[_keyToView.key]]</span>
+            </section>
+          </fieldset>
+          <gr-button
+              class="closeButton"
+              on-tap="_closeOverlay">Close</gr-button>
+        </gr-overlay>
+        <gr-button
+            on-tap="save"
+            disabled$="[[!hasUnsavedChanges]]">Save changes</gr-button>
+      </fieldset>
+      <fieldset>
+        <section>
+          <span class="title">New GPG key</span>
+          <span class="value">
+            <iron-autogrow-textarea
+                id="newKey"
+                autocomplete="on"
+                bind-value="{{_newKey}}"
+                placeholder="New GPG Key"></iron-autogrow-textarea>
+          </span>
+        </section>
+        <gr-button
+            id="addButton"
+            disabled$="[[_computeAddButtonDisabled(_newKey)]]"
+            on-tap="_handleAddKey">Add new GPG key</gr-button>
+      </fieldset>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-gpg-editor.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
new file mode 100644
index 0000000..f5bf8bc
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -0,0 +1,102 @@
+// Copyright (C) 2017 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() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-gpg-editor',
+
+    properties: {
+      hasUnsavedChanges: {
+        type: Boolean,
+        value: false,
+        notify: true,
+      },
+      _keys: Array,
+      /** @type {?} */
+      _keyToView: Object,
+      _newKey: {
+        type: String,
+        value: '',
+      },
+      _keysToRemove: {
+        type: Array,
+        value() { return []; },
+      },
+    },
+
+    loadData() {
+      this._keys = [];
+      return this.$.restAPI.getAccountGPGKeys().then(keys => {
+        if (!keys) {
+          return;
+        }
+        this._keys = Object.keys(keys)
+         .map(key => {
+           const gpgKey = keys[key];
+           gpgKey.id = key;
+           return gpgKey;
+         });
+      });
+    },
+
+    save() {
+      const promises = this._keysToRemove.map(key => {
+        this.$.restAPI.deleteAccountGPGKey(key.id);
+      });
+
+      return Promise.all(promises).then(() => {
+        this._keysToRemove = [];
+        this.hasUnsavedChanges = false;
+      });
+    },
+
+    _showKey(e) {
+      const el = Polymer.dom(e).localTarget;
+      const index = parseInt(el.getAttribute('data-index'), 10);
+      this._keyToView = this._keys[index];
+      this.$.viewKeyOverlay.open();
+    },
+
+    _closeOverlay() {
+      this.$.viewKeyOverlay.close();
+    },
+
+    _handleDeleteKey(e) {
+      const el = Polymer.dom(e).localTarget;
+      const index = parseInt(el.getAttribute('data-index'), 10);
+      this.push('_keysToRemove', this._keys[index]);
+      this.splice('_keys', index, 1);
+      this.hasUnsavedChanges = true;
+    },
+
+    _handleAddKey() {
+      this.$.addButton.disabled = true;
+      this.$.newKey.disabled = true;
+      return this.$.restAPI.addAccountGPGKey({add: [this._newKey.trim()]})
+          .then(key => {
+            this.$.newKey.disabled = false;
+            this._newKey = '';
+            this.loadData();
+          }).catch(() => {
+            this.$.addButton.disabled = false;
+            this.$.newKey.disabled = false;
+          });
+    },
+
+    _computeAddButtonDisabled(newKey) {
+      return !newKey.length;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
new file mode 100644
index 0000000..f749130
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
@@ -0,0 +1,192 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-gpg-editor</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-gpg-editor.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-gpg-editor></gr-gpg-editor>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-gpg-editor tests', () => {
+    let element;
+    let keys;
+
+    setup(done => {
+      const fingerprint1 = '0192 723D 42D1 0C5B 32A6  E1E0 9350 9E4B AFC8 A49B';
+      const fingerprint2 = '0196 723D 42D1 0C5B 32A6  E1E0 9350 9E4B AFC8 A49B';
+      keys = {
+        AFC8A49B: {
+          fingerprint: fingerprint1,
+          user_ids: [
+            'John Doe john.doe@example.com',
+          ],
+          key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+               '\nVersion: BCPG v1.52\n\t<key 1>',
+          status: 'TRUSTED',
+          problems: [],
+        },
+        AED9B59C: {
+          fingerprint: fingerprint2,
+          user_ids: [
+            'Gerrit gerrit@example.com',
+          ],
+          key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+               '\nVersion: BCPG v1.52\n\t<key 2>',
+          status: 'TRUSTED',
+          problems: [],
+        },
+      };
+
+      stub('gr-rest-api-interface', {
+        getAccountGPGKeys() { return Promise.resolve(keys); },
+      });
+
+      element = fixture('basic');
+
+      element.loadData().then(() => { flush(done); });
+    });
+
+    test('renders', () => {
+      const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+
+      assert.equal(rows.length, 2);
+
+      let cells = rows[0].querySelectorAll('td');
+      assert.equal(cells[0].textContent, 'AFC8A49B');
+
+      cells = rows[1].querySelectorAll('td');
+      assert.equal(cells[0].textContent, 'AED9B59C');
+    });
+
+    test('remove key', done => {
+      const lastKey = keys[Object.keys(keys)[1]];
+
+      const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey',
+          () => { return Promise.resolve(); });
+
+      assert.equal(element._keysToRemove.length, 0);
+      assert.isFalse(element.hasUnsavedChanges);
+
+      // Get the delete button for the last row.
+      const button = Polymer.dom(element.root).querySelector(
+          'tbody tr:last-of-type td:nth-child(5) gr-button');
+
+      MockInteractions.tap(button);
+
+      assert.equal(element._keys.length, 1);
+      assert.equal(element._keysToRemove.length, 1);
+      assert.equal(element._keysToRemove[0], lastKey);
+      assert.isTrue(element.hasUnsavedChanges);
+      assert.isFalse(saveStub.called);
+
+      element.save().then(() => {
+        assert.isTrue(saveStub.called);
+        assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]);
+        assert.equal(element._keysToRemove.length, 0);
+        assert.isFalse(element.hasUnsavedChanges);
+        done();
+      });
+    });
+
+    test('show key', () => {
+      const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
+
+      // Get the show button for the last row.
+      const button = Polymer.dom(element.root).querySelector(
+          'tbody tr:last-of-type td:nth-child(4) gr-button');
+
+      MockInteractions.tap(button);
+
+      assert.equal(element._keyToView, keys[Object.keys(keys)[1]]);
+      assert.isTrue(openSpy.called);
+    });
+
+    test('add key', done => {
+      const newKeyString =
+          '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+          '\nVersion: BCPG v1.52\n\t<key 3>';
+      const newKeyObject = {
+        ADE8A59B: {
+          fingerprint: '0194 723D 42D1 0C5B 32A6  E1E0 9350 9E4B AFC8 A49B',
+          user_ids: [
+            'John john@example.com',
+          ],
+          key: newKeyString,
+          status: 'TRUSTED',
+          problems: [],
+        },
+      };
+
+      const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+          () => { return Promise.resolve(newKeyObject); });
+
+      element._newKey = newKeyString;
+
+      assert.isFalse(element.$.addButton.disabled);
+      assert.isFalse(element.$.newKey.disabled);
+
+      element._handleAddKey().then(() => {
+        assert.isTrue(element.$.addButton.disabled);
+        assert.isFalse(element.$.newKey.disabled);
+        assert.equal(element._keys.length, 2);
+        done();
+      });
+
+      assert.isTrue(element.$.addButton.disabled);
+      assert.isTrue(element.$.newKey.disabled);
+
+      assert.isTrue(addStub.called);
+      assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+    });
+
+    test('add invalid key', done => {
+      const newKeyString = 'not even close to valid';
+
+      const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+          () => { return Promise.reject(); });
+
+      element._newKey = newKeyString;
+
+      assert.isFalse(element.$.addButton.disabled);
+      assert.isFalse(element.$.newKey.disabled);
+
+      element._handleAddKey().then(() => {
+        assert.isFalse(element.$.addButton.disabled);
+        assert.isFalse(element.$.newKey.disabled);
+        assert.equal(element._keys.length, 2);
+        done();
+      });
+
+      assert.isTrue(element.$.addButton.disabled);
+      assert.isTrue(element.$.newKey.disabled);
+
+      assert.isTrue(addStub.called);
+      assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index a1de1b2..416c426 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -31,6 +31,7 @@
 <link rel="import" href="../gr-agreements-list/gr-agreements-list.html">
 <link rel="import" href="../gr-edit-preferences/gr-edit-preferences.html">
 <link rel="import" href="../gr-email-editor/gr-email-editor.html">
+<link rel="import" href="../gr-gpg-editor/gr-gpg-editor.html">
 <link rel="import" href="../gr-group-list/gr-group-list.html">
 <link rel="import" href="../gr-http-password/gr-http-password.html">
 <link rel="import" href="../gr-identities/gr-identities.html">
@@ -73,6 +74,9 @@
           <li hidden$="[[!_serverConfig.sshd]]"><a href="#SSHKeys">
             SSH Keys
           </a></li>
+          <li hidden$="[[!_serverConfig.receive.enable_signed_push]]"><a href="#GPGKeys">
+            GPG Keys
+          </a></li>
           <li><a href="#Groups">Groups</a></li>
           <li><a href="#Identities">Identities</a></li>
           <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
@@ -414,6 +418,14 @@
               id="sshEditor"
               has-unsaved-changes="{{_keysChanged}}"></gr-ssh-editor>
         </div>
+        <div hidden$="[[!_serverConfig.receive.enable_signed_push]]">
+          <h2
+              id="GPGKeys"
+              class$="[[_computeHeaderClass(_gpgKeysChanged)]]">GPG keys</h2>
+          <gr-gpg-editor
+              id="gpgEditor"
+              has-unsaved-changes="{{_gpgKeysChanged}}"></gr-gpg-editor>
+        </div>
         <h2 id="Groups">Groups</h2>
         <fieldset>
           <gr-group-list id="groupList"></gr-group-list>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 8e14018..912712c 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -104,6 +104,10 @@
         type: Boolean,
         value: false,
       },
+      _gpgKeysChanged: {
+        type: Boolean,
+        value: false,
+      },
       _newEmail: String,
       _addingEmail: {
         type: Boolean,
@@ -167,10 +171,16 @@
         this._serverConfig = config;
         const configPromises = [];
 
-        if (this._serverConfig.sshd) {
+        if (this._serverConfig && this._serverConfig.sshd) {
           configPromises.push(this.$.sshEditor.loadData());
         }
 
+        if (this._serverConfig &&
+            this._serverConfig.receive &&
+            this._serverConfig.receive.enable_signed_push) {
+          configPromises.push(this.$.gpgEditor.loadData());
+        }
+
         configPromises.push(
             this.getDocsBaseUrl(config, this.$.restAPI)
                 .then(baseUrl => { this._docsBaseUrl = baseUrl; }));
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
index cdd5414..eab0173 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.html
@@ -26,7 +26,6 @@
       .chip {
         border-radius: 4px;
         background-color: var(--chip-background-color);
-        color: #fff;
         font-family: var(--font-family);
         font-size: var(--font-size-normal);
         padding: .1em .5em;
@@ -34,27 +33,42 @@
       }
       :host(.merged) .chip {
         background-color: #5b9d52;
+        color: #5b9d52;
       }
       :host(.abandoned) .chip {
         background-color: #afafaf;
+        color: #afafaf;
       }
       :host(.wip) .chip {
         background-color: #8f756c;
+        color: #8f756c;
       }
       :host(.private) .chip {
         background-color: #c17ccf;
+        color: #c17ccf;
       }
       :host(.merge-conflict) .chip {
         background-color: #dc5c60;
+        color: #dc5c60;
       }
       :host(.active) .chip {
         background-color: #29b6f6;
+        color: #29b6f6;
       }
       :host(.ready-to-submit) .chip {
         background-color: #e10ca3;
+        color: #e10ca3;
       }
       :host(.custom) .chip {
         background-color: #825cc2;
+        color: #825cc2;
+      }
+      :host([flat]) .chip {
+        background-color: transparent;
+        padding: .1em;
+      }
+      :host:not([flat]) .chip {
+        color: white;
       }
     </style>
     <gr-tooltip-content
@@ -62,8 +76,11 @@
         position-below
         title="[[tooltipText]]"
         max-width="40em">
-      <div class="chip" aria-label$="Label: [[status]]">
-          [[_computeStatusString(status)]]</div>
+      <div
+          class="chip"
+          aria-label$="Label: [[status]]">
+        [[_computeStatusString(status)]]
+      </div>
     </gr-tooltip-content>
   </template>
   <script src="gr-change-status.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 991fea3..cd27a28 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -33,6 +33,11 @@
     is: 'gr-change-status',
 
     properties: {
+      flat: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
       status: {
         type: String,
         observer: '_updateChipDetails',
@@ -44,7 +49,7 @@
     },
 
     _computeStatusString(status) {
-      if (status === ChangeStates.WIP) {
+      if (status === ChangeStates.WIP && !this.flat) {
         return 'Work in Progress';
       }
       return status;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
index 801249d..212296f 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
@@ -52,6 +52,15 @@
       assert.isTrue(element.classList.contains('wip'));
     });
 
+    test('WIP flat', () => {
+      element.flat = true;
+      element.status = 'WIP';
+      assert.equal(element.$$('.chip').innerText, 'WIP');
+      assert.isDefined(element.tooltipText);
+      assert.isTrue(element.classList.contains('wip'));
+      assert.isTrue(element.hasAttribute('flat'));
+    });
+
     test('merged', () => {
       element.status = 'Merged';
       assert.equal(element.$$('.chip').innerText, element.status);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js
index 94bae45..6350c54 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.js
@@ -52,6 +52,45 @@
   };
 
   /**
+   * Returns a checkbox HTMLElement that can be used to toggle annotations
+   * on/off. The checkbox will be initially disabled. Plugins should enable it
+   * when data is ready and should add a click handler to toggle CSS on/off.
+   *
+   * Note1: Calling this method from multiple plugins will only work for the
+   *        1st call. It will print an error message for all subsequent calls
+   *        and will not invoke their onAttached functions.
+   * Note2: This method will be deprecated and eventually removed when
+   *        https://bugs.chromium.org/p/gerrit/issues/detail?id=8077 is
+   *        implemented.
+   *
+   * @param {String} checkboxLabel Will be used as the label for the checkbox.
+   *     Optional. "Enable" is used if this is not specified.
+   * @param {Function<HTMLElement>} onAttached The function that will be called
+   *     when the checkbox is attached to the page.
+   */
+  GrAnnotationActionsInterface.prototype.enableToggleCheckbox = function(
+      checkboxLabel, onAttached) {
+    this.plugin.hook('annotation-toggler').onAttached(element => {
+      if (!element.content.hidden) {
+        console.error(
+            element.content.id + ' is already enabled. Cannot re-enable.');
+        return;
+      }
+      element.content.removeAttribute('hidden');
+
+      const label = element.content.querySelector('#annotation-label');
+      if (checkboxLabel) {
+        label.textContent = checkboxLabel;
+      } else {
+        label.textContent = 'Enable';
+      }
+      const checkbox = element.content.querySelector('#annotation-checkbox');
+      onAttached(checkbox);
+    });
+    return this;
+  };
+
+  /**
    * The notify function will call the listeners of all required annotation
    * layers. Intended to be called by the plugin when all required data for
    * annotation is available.
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
index 39623ed..a19df85 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
@@ -23,14 +23,23 @@
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="../../change/gr-change-actions/gr-change-actions.html">
 
+<test-fixture id="basic">
+  <template>
+    <span hidden id="annotation-span">
+      <label for="annotation-checkbox" id="annotation-label"></label>
+      <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
+    </span>
+  </template>
+</test-fixture>
+
 <script>
   suite('gr-annotation-actions-js-api tests', () => {
     let annotationActions;
     let sandbox;
+    let plugin;
 
     setup(() => {
       sandbox = sinon.sandbox.create();
-      let plugin;
       Gerrit.install(p => { plugin = p; }, '0.1',
           'http://test.com/plugins/testplugin/static/test.js');
       annotationActions = plugin.annotationApi();
@@ -101,6 +110,45 @@
       assert.isTrue(layer2Spy.called);
     });
 
+    test('toggle checkbox', () => {
+      fakeEl = {content: fixture('basic')};
+      const hookStub = {onAttached: sandbox.stub()};
+      sandbox.stub(plugin, 'hook').returns(hookStub);
+
+      let checkbox;
+      let onAttachedFuncCalled = false;
+      const onAttachedFunc = c => {
+        checkbox = c;
+        onAttachedFuncCalled = true;
+      };
+      annotationActions.enableToggleCheckbox('test label', onAttachedFunc);
+      emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
+      emulateAttached();
+
+      // Assert that onAttachedFunc is called and HTML elements have the
+      // expected state.
+      assert.isTrue(onAttachedFuncCalled);
+      assert.equal(checkbox.id, 'annotation-checkbox');
+      assert.isTrue(checkbox.disabled);
+      assert.equal(document.getElementById('annotation-label').textContent,
+          'test label');
+      assert.isFalse(document.getElementById('annotation-span').hidden);
+
+      // Assert that error is shown if we try to enable checkbox again.
+      onAttachedFuncCalled = false;
+      annotationActions.enableToggleCheckbox('test label2', onAttachedFunc);
+      const errorStub = sandbox.stub(
+          console, 'error', (msg, err) => undefined);
+      emulateAttached();
+      assert.isTrue(
+          errorStub.calledWith(
+              'annotation-span is already enabled. Cannot re-enable.'));
+      // Assert that onAttachedFunc is not called and the label has not changed.
+      assert.isFalse(onAttachedFuncCalled);
+      assert.equal(document.getElementById('annotation-label').textContent,
+          'test label');
+    });
+
     test('layer notify listeners', () => {
       const annotationLayer = annotationActions.getLayer(
           '/dummy/path', 1, 2);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
index fe74906..7be007f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -62,6 +62,11 @@
     });
   };
 
+  GrChangeActionsInterface.prototype.hideQuickApproveAction = function() {
+    ensureEl(this);
+    this._el.hideQuickApproveAction();
+  };
+
   GrChangeActionsInterface.prototype.setActionOverflow = function(type, key,
       overflow) {
     ensureEl(this);
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 6749437..27f330e 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
@@ -437,12 +437,16 @@
           .then(response => this.getResponseObject(response));
     },
 
-    saveIncludedGroup(groupName, includedGroup) {
+    saveIncludedGroup(groupName, includedGroup, opt_errFn) {
       const encodeName = encodeURIComponent(groupName);
       const encodeIncludedGroup = encodeURIComponent(includedGroup);
       return this.send('PUT',
-          `/groups/${encodeName}/groups/${encodeIncludedGroup}`)
-          .then(response => this.getResponseObject(response));
+          `/groups/${encodeName}/groups/${encodeIncludedGroup}`, null,
+          opt_errFn).then(response => {
+            if (response.ok) {
+              return this.getResponseObject(response);
+            }
+          });
     },
 
     deleteGroupMembers(groupName, groupMembers) {
@@ -1948,6 +1952,28 @@
       return this.send('DELETE', '/accounts/self/sshkeys/' + id);
     },
 
+    getAccountGPGKeys() {
+      return this.fetchJSON('/accounts/self/gpgkeys');
+    },
+
+    addAccountGPGKey(key) {
+      return this.send('POST', '/accounts/self/gpgkeys', key)
+          .then(response => {
+            if (response.status < 200 && response.status >= 300) {
+              return Promise.reject();
+            }
+            return this.getResponseObject(response);
+          })
+          .then(obj => {
+            if (!obj) { return Promise.reject(); }
+            return obj;
+          });
+    },
+
+    deleteAccountGPGKey(id) {
+      return this.send('DELETE', '/accounts/self/gpgkeys/' + id);
+    },
+
     deleteVote(changeNum, account, label) {
       const e = `/reviewers/${account}/votes/${encodeURIComponent(label)}`;
       return this.getChangeURLAndSend(changeNum, 'DELETE', null, e);
diff --git a/polygerrit-ui/app/samples/coverage-plugin.html b/polygerrit-ui/app/samples/coverage-plugin.html
index 6f76dc4..9bec658 100644
--- a/polygerrit-ui/app/samples/coverage-plugin.html
+++ b/polygerrit-ui/app/samples/coverage-plugin.html
@@ -20,35 +20,53 @@
         changeNum: 77001,
         patchNum: 1,
       };
+      coverageData['go/sklog/sklog.go'] = {
+        linesMissingCoverage: [3, 322, 323, 324],
+        totalLines: 350,
+        changeNum: 85963,
+        patchNum: 13,
+      };
     }
 
     Gerrit.install(plugin => {
       const coverageData = {};
-      plugin.annotationApi().addNotifier(notifyFunc => {
-        new Promise(resolve => setTimeout(resolve, 3000)).then(
-            () => {
-              populateWithDummyData(coverageData);
-              Object.keys(coverageData).forEach(file => {
-                notifyFunc(file, 0, coverageData[file].totalLines, 'right');
-              });
-            });
-      }).addLayer(context => {
+      let displayCoverage = false;
+      const annotationApi = plugin.annotationApi();
+      annotationApi.addLayer(context => {
         if (Object.keys(coverageData).length === 0) {
-          // Coverage data is not ready yet.
+           // Coverage data is not ready yet.
           return;
         }
         const path = context.path;
         const line = context.line;
-        // Highlight lines missing coverage with this background color.
-        const cssClass = Gerrit.css('background-color: #EF9B9B');
+          // Highlight lines missing coverage with this background color if
+          // coverage should be displayed, else do nothing.
+        const cssClass = displayCoverage
+                         ? Gerrit.css('background-color: #EF9B9B')
+                         : Gerrit.css('');
         if (coverageData[path] &&
-            coverageData[path].changeNum === context.changeNum &&
-            coverageData[path].patchNum === context.patchNum) {
+              coverageData[path].changeNum === context.changeNum &&
+              coverageData[path].patchNum === context.patchNum) {
           const linesMissingCoverage = coverageData[path].linesMissingCoverage;
           if (linesMissingCoverage.includes(line.afterNumber)) {
             context.annotateRange(0, line.text.length, cssClass, 'right');
           }
         }
+      }).enableToggleCheckbox('Display Coverage', checkbox => {
+        // Checkbox is attached so now add the notifier that will be controlled
+        // by the checkbox.
+        annotationApi.addNotifier(notifyFunc => {
+          new Promise(resolve => setTimeout(resolve, 3000)).then(() => {
+            populateWithDummyData(coverageData);
+            checkbox.disabled = false;
+            checkbox.onclick = e => {
+              displayCoverage = e.target.checked;
+              Object.keys(coverageData).forEach(file => {
+                notifyFunc(file, 0, coverageData[file].totalLines, 'right');
+              });
+            };
+          });
+        });
       });
     });
   </script>
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index c109381..7ba4fc6 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -171,7 +171,7 @@
         }
         .owner,
         .size {
-          width: auto;
+          max-width: none;
         }
       }
       @media only screen and (min-width: 1450px) {
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index c3d856e..2bb8b2e 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -127,6 +127,7 @@
     'settings/gr-cla-view/gr-cla-view_test.html',
     'settings/gr-edit-preferences/gr-edit-preferences_test.html',
     'settings/gr-email-editor/gr-email-editor_test.html',
+    'settings/gr-gpg-editor/gr-gpg-editor_test.html',
     'settings/gr-group-list/gr-group-list_test.html',
     'settings/gr-http-password/gr-http-password_test.html',
     'settings/gr-identities/gr-identities_test.html',
diff --git a/resources/com/google/gerrit/server/mail/Merged.soy b/resources/com/google/gerrit/server/mail/Merged.soy
index 1d6ae9c..40924e6 100644
--- a/resources/com/google/gerrit/server/mail/Merged.soy
+++ b/resources/com/google/gerrit/server/mail/Merged.soy
@@ -1,3 +1,4 @@
+
 /**
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -21,16 +22,10 @@
  * a change successfully merged to the head.
  * @param change
  * @param email
- * @param fromEmail
  * @param fromName
- * @param patchSetInfo
  */
 {template .Merged kind="text"}
-  {$fromName} merged this change
-  {if $patchSetInfo.authorEmail != $fromEmail}
-    {sp}by {$patchSetInfo.authorName}
-  {/if}.
-
+  {$fromName} has submitted this change and it was merged.
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
   Change subject: {$change.subject}{\n}
diff --git a/resources/com/google/gerrit/server/mail/MergedHtml.soy b/resources/com/google/gerrit/server/mail/MergedHtml.soy
index 414479e..b11c5e5 100644
--- a/resources/com/google/gerrit/server/mail/MergedHtml.soy
+++ b/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -19,16 +19,11 @@
 /**
  * @param diffLines
  * @param email
- * @param fromEmail
  * @param fromName
- * @param patchSetInfo
  */
 {template .MergedHtml}
   <p>
-    {$fromName} <strong>merged</strong> this change
-    {if $patchSetInfo.authorEmail != $fromEmail}
-      {sp}by {$patchSetInfo.authorName}
-    {/if}.
+    {$fromName} <strong>merged</strong> this change.
   </p>
 
   {if $email.changeUrl}
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py
index 7479021..ccdf2df 100755
--- a/tools/js/bower2bazel.py
+++ b/tools/js/bower2bazel.py
@@ -58,11 +58,13 @@
   "neon-animation": "polymer",
   "page": "page.js",
   "paper-button": "polymer",
+  "paper-icon-button": "polymer",
   "paper-input": "polymer",
   "paper-item": "polymer",
   "paper-listbox": "polymer",
   "paper-toggle-button": "polymer",
   "paper-styles": "polymer",
+  "paper-tabs": "polymer",
   "polymer": "polymer",
   "polymer-resin": "polymer",
   "promise-polyfill": "promise-polyfill",
diff --git a/tools/release-announcement.py b/tools/release-announcement.py
index 83a78fe..f700185 100755
--- a/tools/release-announcement.py
+++ b/tools/release-announcement.py
@@ -142,7 +142,10 @@
     if not os.path.isdir(gpghome):
         print("Skipping signing due to missing gnupg home folder")
     else:
-        gpg = GPG(homedir=gpghome)
+        try:
+            gpg = GPG(homedir=gpghome)
+        except TypeError:
+            gpg = GPG(gnupghome=gpghome)
         signed = gpg.sign(output)
         filename = filename + ".asc"
         with open(filename, "w") as f: