Merge "Add optional file link to gr-comment-thread"
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/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/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 f1667d3..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);
       }
@@ -136,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>
@@ -183,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-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 bcc16ad..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
@@ -1952,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/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: