Merge "Fix cherrypick message empty error"
diff --git a/.buckversion b/.buckversion
index 560aff2..7eb591f 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-d6949e1440ef2048d697c637a4adae1b509bf72d
+e27df656657f93f8d57a7aaac69a5ae0e298a292
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 6575018..54a5fb3 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -824,6 +824,15 @@
 the caller needs to have the Submit permission on `refs/for/<ref>`
 (e.g. on `refs/for/refs/heads/master`).
 
+Submitting to the `refs/meta/config` branch is only allowed to project
+owners. Any explicit submit permissions for non-project-owners on this
+branch are ignored. By submitting to the `refs/meta/config` branch the
+configuration of the project is changed, which can include changes to
+the access rights of the project. Allowing this to be done by a
+non-project-owner would open a security hole enabling editing of access
+rights, and thus granting of powers beyond submitting to the
+configuration.
+
 [[category_submit_on_behalf_of]]
 === Submit (On Behalf Of)
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index c30d8bd..fb1a49f 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -4029,7 +4029,7 @@
   }
 ----
 
-[[list-comments]]
+[[list-robot-comments]]
 === List Robot Comments
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/'
@@ -4829,14 +4829,19 @@
 
 [options="header",cols="1,^1,5"]
 |===========================
-|Field Name    ||Description
-|`value`       |optional|
+|Field Name               ||Description
+|`value`                  |optional|
 The vote that the user has given for the label. If present and zero, the
 user is permitted to vote on the label. If absent, the user is not
 permitted to vote on that label.
-|`date`        |optional|
+|`permitted_voting_range` |optional|
+The link:#voting-range-info[VotingRangeInfo] the user is authorized to vote
+on that label. If present, the user is permitted to vote on the label
+regarding the range values. If absent, the user is not permitted to vote
+on that label.
+|`date`                   |optional|
 The time and date describing when the approval was made.
-|`tag`                 |optional|
+|`tag`                    |optional|
 Value of the `tag` field from link:#review-input[ReviewInput] set
 while posting the review.
 NOTE: To apply different tags on on different votes/comments multiple
@@ -6118,6 +6123,18 @@
 The topic will be deleted if not set.
 |===========================
 
+[[voting-range-info]]
+=== VotingRangeInfo
+The `VotingRangeInfo` entity describes the continuous voting range from min
+to max values.
+
+[options="header",cols="1,6"]
+|======================
+|Field Name|Description
+|`min`     |The minimum voting value.
+|`max`     |The maximum voting value.
+|======================
+
 [[web-link-info]]
 === WebLinkInfo
 The `WebLinkInfo` entity describes a link to an external site.
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 23d4c5b..51bf173 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -1207,11 +1207,6 @@
 [[ids]]
 == IDs
 
-[[account-id]]
-=== link:rest-api-accounts.html#account-id[\{account-id\}]
---
---
-
 [[group-id]]
 === \{group-id\}
 Identifier for a group.
@@ -1319,7 +1314,7 @@
 name. +
 If not set, the new group will be self-owned.
 |`members`       |optional|The initial members in a list of +
-link:#account-id[account ids].
+link:rest-api-accounts.html#account-id[account ids].
 |===========================
 
 [[group-options-info]]
@@ -1369,11 +1364,11 @@
 |==========================
 |Field Name   ||Description
 |`_one_member`|optional|
-The link:#account-id[id] of one account that should be added or
-deleted.
-|`members`    |optional|
-A list of link:#account-id[account ids] that identify the accounts that
+The link:rest-api-accounts.html#account-id[id] of one account that
 should be added or deleted.
+|`members`    |optional|
+A list of link:rest-api-accounts.html#account-id[account ids] that
+identify the accounts that should be added or deleted.
 |==========================
 
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index c2618f5..c52989d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -1823,6 +1823,7 @@
           @Override
           public String onSubmit(String newCommitMessage, RevCommit original,
               RevCommit mergeTip, Branch.NameKey destination) {
+            assertThat(original.getName()).isNotEqualTo(mergeTip.getName());
             return newCommitMessage + "Custom: " + destination.get();
           }
         });
@@ -2549,6 +2550,68 @@
     assertThat(change.permittedLabels).isEmpty();
   }
 
+  @Test
+  public void maxPermittedValueAllowed() throws Exception {
+    final int minPermittedValue = -2;
+    final int maxPermittedValue = +2;
+    String heads = "refs/heads/*";
+
+    PushOneCommit.Result r = createChange();
+    String triplet = project.get() + "~master~" + r.getChangeId();
+
+    gApi.changes().id(triplet).addReviewer(user.username);
+
+    ChangeInfo c = gApi.changes()
+      .id(triplet)
+      .get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    LabelInfo codeReview = c.labels.get("Code-Review");
+    assertThat(codeReview.all).hasSize(1);
+    ApprovalInfo approval = codeReview.all.get(0);
+    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval.permittedVotingRange).isNotNull();
+    // default values
+    assertThat(approval.permittedVotingRange.min).isEqualTo(-1);
+    assertThat(approval.permittedVotingRange.max).isEqualTo(1);
+
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.allow(cfg,
+      Permission.forLabel("Code-Review"), minPermittedValue, maxPermittedValue,
+      REGISTERED_USERS, heads);
+    saveProjectConfig(project, cfg);
+
+    c = gApi.changes()
+      .id(triplet)
+      .get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    codeReview = c.labels.get("Code-Review");
+    assertThat(codeReview.all).hasSize(1);
+    approval = codeReview.all.get(0);
+    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval.permittedVotingRange).isNotNull();
+    assertThat(approval.permittedVotingRange.min).isEqualTo(minPermittedValue);
+    assertThat(approval.permittedVotingRange.max).isEqualTo(maxPermittedValue);
+  }
+
+  @Test
+  public void maxPermittedValueBlocked() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    blockLabel(cfg, "Code-Review", REGISTERED_USERS, "refs/heads/*");
+    saveProjectConfig(project, cfg);
+
+    PushOneCommit.Result r = createChange();
+    String triplet = project.get() + "~master~" + r.getChangeId();
+
+    gApi.changes().id(triplet).addReviewer(user.username);
+
+    ChangeInfo c = gApi.changes()
+      .id(triplet)
+      .get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    LabelInfo codeReview = c.labels.get("Code-Review");
+    assertThat(codeReview.all).hasSize(1);
+    ApprovalInfo approval = codeReview.all.get(0);
+    assertThat(approval._accountId).isEqualTo(user.id.get());
+    assertThat(approval.permittedVotingRange).isNull();
+  }
+
   private static Iterable<Account.Id> getReviewers(
       Collection<AccountInfo> r) {
     return Iterables.transform(r, a -> new Account.Id(a._accountId));
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
index d53e69a..2d3e2da 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
@@ -4,16 +4,16 @@
   group = 'rest_project',
   srcs = glob(['*IT.java']),
   deps = [
-    ':branch',
     ':project',
+    ':refassert',
   ],
   labels = ['rest'],
 )
 
 java_library(
-  name = 'branch',
+  name = 'refassert',
   srcs = [
-    'BranchAssert.java',
+    'RefAssert.java',
   ],
   deps = [
     '//lib:truth',
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD
index 579171f..fbb6bde 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD
@@ -4,16 +4,16 @@
   group = 'rest_project',
   srcs = glob(['*IT.java']),
   deps = [
-    ':branch',
     ':project',
+    ':refassert',
   ],
   labels = ['rest'],
 )
 
 java_library(
-  name = 'branch',
+  name = 'refassert',
   srcs = [
-    'BranchAssert.java',
+    'RefAssert.java',
   ],
   deps = [
     '//lib:truth',
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index af1383b..3f0c43e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance.rest.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertRefNames;
+import static com.google.gerrit.acceptance.rest.project.RefAssert.assertRefNames;
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
 import com.google.gerrit.extensions.api.projects.ProjectApi;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.RefNames;
 
@@ -107,6 +108,31 @@
     assertBranchesDeleted();
   }
 
+  @Test
+  public void missingInput() throws Exception {
+    DeleteBranchesInput input = null;
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("branches must be specified");
+    project().deleteBranches(input);
+  }
+
+  @Test
+  public void missingBranchList() throws Exception {
+    DeleteBranchesInput input = new DeleteBranchesInput();
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("branches must be specified");
+    project().deleteBranches(input);
+  }
+
+  @Test
+  public void emptyBranchList() throws Exception {
+    DeleteBranchesInput input = new DeleteBranchesInput();
+    input.branches = Lists.newArrayList();
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("branches must be specified");
+    project().deleteBranches(input);
+  }
+
   private String errorMessageForBranches(List<String> branches) {
     StringBuilder message = new StringBuilder();
     for (String branch : branches) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index 7c98188..1305d2c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertBranches;
-import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertRefNames;
+import static com.google.gerrit.acceptance.rest.project.RefAssert.assertRefs;
+import static com.google.gerrit.acceptance.rest.project.RefAssert.assertRefNames;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -47,7 +47,7 @@
   @Test
   @TestProjectInput(createEmptyCommit = false)
   public void listBranchesOfEmptyProject() throws Exception {
-    assertBranches(ImmutableList.of(
+    assertRefs(ImmutableList.of(
           branch("HEAD", null, false),
           branch(RefNames.REFS_CONFIG,  null, false)),
         list().get());
@@ -57,7 +57,7 @@
   public void listBranches() throws Exception {
     String master = pushTo("refs/heads/master").getCommit().name();
     String dev = pushTo("refs/heads/dev").getCommit().name();
-    assertBranches(ImmutableList.of(
+    assertRefs(ImmutableList.of(
           branch("HEAD", "master", false),
           branch(RefNames.REFS_CONFIG,  null, false),
           branch("refs/heads/dev", dev, true),
@@ -72,7 +72,7 @@
     pushTo("refs/heads/dev");
     setApiUser(user);
     // refs/meta/config is hidden since user is no project owner
-    assertBranches(ImmutableList.of(
+    assertRefs(ImmutableList.of(
           branch("HEAD", "master", false),
           branch("refs/heads/master", master, false)),
         list().get());
@@ -85,7 +85,7 @@
     String dev = pushTo("refs/heads/dev").getCommit().name();
     setApiUser(user);
     // refs/meta/config is hidden since user is no project owner
-    assertBranches(ImmutableList.of(branch("refs/heads/dev", dev, false)),
+    assertRefs(ImmutableList.of(branch("refs/heads/dev", dev, false)),
         list().get());
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/RefAssert.java
similarity index 69%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/RefAssert.java
index 522836d..0cbf79a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/RefAssert.java
@@ -17,26 +17,26 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Iterables;
-import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.RefInfo;
 
 import java.util.List;
 
-public class BranchAssert {
-  public static void assertBranches(List<BranchInfo> expectedBranches,
-      List<BranchInfo> actualBranches) {
-    assertRefNames(refs(expectedBranches), actualBranches);
-    for (int i = 0; i < expectedBranches.size(); i++) {
-      assertBranchInfo(expectedBranches.get(i), actualBranches.get(i));
+public class RefAssert {
+  public static void assertRefs(List<? extends RefInfo> expectedRefs,
+      List<? extends RefInfo> actualRefs) {
+    assertRefNames(refs(expectedRefs), actualRefs);
+    for (int i = 0; i < expectedRefs.size(); i++) {
+      assertRefInfo(expectedRefs.get(i), actualRefs.get(i));
     }
   }
 
   public static void assertRefNames(Iterable<String> expectedRefs,
-      Iterable<BranchInfo> actualBranches) {
-    Iterable<String> actualNames = refs(actualBranches);
+      Iterable<? extends RefInfo> actualRefs) {
+    Iterable<String> actualNames = refs(actualRefs);
     assertThat(actualNames).containsExactlyElementsIn(expectedRefs).inOrder();
   }
 
-  public static void assertBranchInfo(BranchInfo expected, BranchInfo actual) {
+  public static void assertRefInfo(RefInfo expected, RefInfo actual) {
     assertThat(actual.ref).isEqualTo(expected.ref);
     if (expected.revision != null) {
       assertThat(actual.revision).named("revision of " + actual.ref)
@@ -46,7 +46,7 @@
         .isEqualTo(toBoolean(expected.canDelete));
   }
 
-  private static Iterable<String> refs(Iterable<BranchInfo> infos) {
+  private static Iterable<String> refs(Iterable<? extends RefInfo> infos) {
     return Iterables.transform(infos, b -> b.ref);
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
index 77513a2..8ef1b8e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
@@ -21,7 +21,6 @@
 import java.util.Map;
 
 public class BranchInfo extends RefInfo {
-  public Boolean canDelete;
   public Map<String, ActionInfo> actions;
   public List<WebLinkInfo> webLinks;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
index 1844a76..c573600 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
@@ -17,4 +17,5 @@
 public class RefInfo {
   public String ref;
   public String revision;
+  public Boolean canDelete;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index d59e813..9125bfd 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -21,6 +21,7 @@
   public Integer value;
   public Timestamp date;
   public Boolean postSubmit;
+  public VotingRangeInfo permittedVotingRange;
 
   public ApprovalInfo(Integer id) {
     super(id);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/VotingRangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/VotingRangeInfo.java
new file mode 100644
index 0000000..5c35a49
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/VotingRangeInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class VotingRangeInfo {
+  public int min;
+  public int max;
+
+  public VotingRangeInfo(int min, int max) {
+    this.min = min;
+    this.max = max;
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index ff060b8..054bfdd 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -305,10 +305,20 @@
     public final native boolean hasValue() /*-{ return this.hasOwnProperty('value'); }-*/;
     public final native short value() /*-{ return this.value || 0; }-*/;
 
+    public final native VotingRangeInfo permittedVotingRange() /*-{ return this.permitted_voting_range; }-*/;
+
     protected ApprovalInfo() {
     }
   }
 
+  public static class VotingRangeInfo extends AccountInfo {
+    public final native short min() /*-{ return this.min || 0; }-*/;
+    public final native short max() /*-{ return this.max || 0; }-*/;
+
+    protected VotingRangeInfo() {
+    }
+  }
+
   public static class EditInfo extends JavaScriptObject {
     public final native String name() /*-{ return this.name; }-*/;
     public final native String setName(String n) /*-{ this.name = n; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index 36107ee..779c32b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -36,8 +36,20 @@
 
 class Actions extends Composite {
   private static final String[] CORE = {
-    "abandon", "cherrypick", "followup", "hashtags", "publish",
-    "rebase", "restore", "revert", "submit", "topic", "/",};
+    "abandon",
+    "assignee",
+    "cherrypick",
+    "description",
+    "followup",
+    "hashtags",
+    "publish",
+    "rebase",
+    "restore",
+    "revert",
+    "submit",
+    "topic",
+    "/",
+  };
 
   interface Binder extends UiBinder<FlowPanel, Actions> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index e0c252c..6b4cf36 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -255,6 +255,7 @@
     Map<Integer, VotableInfo> d = new HashMap<>();
     for (String name : change.labels()) {
       LabelInfo label = change.label(name);
+      short labelMaxValue = LabelInfo.parseValue(label.maxValue());
       if (label.all() != null) {
         for (ApprovalInfo ai : Natives.asList(label.all())) {
           int id = ai._accountId();
@@ -263,7 +264,10 @@
             ad = new VotableInfo();
             d.put(id, ad);
           }
-          if (ai.hasValue()) {
+          if (ai.permittedVotingRange() != null
+              && ai.permittedVotingRange().max() == labelMaxValue) {
+            ad.votable(name + " (" + label.maxValue() + ") ");
+          } else if (ai.hasValue()) {
             ad.votable(name);
           }
         }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
index 3f471bf..b39e2a2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -76,13 +75,14 @@
   }
 
   @Override
-  protected ProjectAccess updateProjectConfig(CurrentUser user,
+  protected ProjectAccess updateProjectConfig(ProjectControl projectControl,
       ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
       throws IOException, NoSuchProjectException, ConfigInvalidException {
     RevCommit commit = config.commit(md);
 
     gitRefUpdated.fire(config.getProject().getNameKey(), RefNames.REFS_CONFIG,
-        base, commit.getId(), user.asIdentifiedUser().getAccount());
+        base, commit.getId(),
+        projectControl.getUser().asIdentifiedUser().getAccount());
 
     projectCache.evict(config.getProject());
     return projectAccessFactory.create(projectName).call();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index bd88e6a..69ec6d9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -206,8 +206,8 @@
 
     detail.setLocal(local);
     detail.setOwnerOf(ownerOf);
-    detail.setCanUpload(pc.isOwner()
-        || (metaConfigControl.isVisible() && metaConfigControl.canUpload()));
+    detail.setCanUpload(metaConfigControl.isVisible()
+        && (pc.isOwner() || metaConfigControl.canUpload()));
     detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible());
     detail.setGroupInfo(buildGroupInfo(local));
     detail.setLabelTypes(pc.getLabelTypes());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index 111dfc9..4c7d257 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -163,17 +162,17 @@
         md.setMessage("Modify access rules\n");
       }
 
-      return updateProjectConfig(projectControl.getUser(), config, md,
+      return updateProjectConfig(projectControl, config, md,
           parentProjectUpdate);
     } catch (RepositoryNotFoundException notFound) {
       throw new NoSuchProjectException(projectName);
     }
   }
 
-  protected abstract T updateProjectConfig(CurrentUser user,
+  protected abstract T updateProjectConfig(ProjectControl projectControl,
       ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
       throws IOException, NoSuchProjectException, ConfigInvalidException,
-      OrmException;
+      OrmException, PermissionDeniedException;
 
   private void replace(ProjectConfig config, Set<String> toDelete,
       AccessSection section) throws NoSuchGroupException {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 9260e01..966cd88 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -27,7 +28,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.change.ChangeInserter;
@@ -43,6 +43,7 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.project.SetParent;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -106,9 +107,20 @@
   }
 
   @Override
-  protected Change.Id updateProjectConfig(CurrentUser user,
+  protected Change.Id updateProjectConfig(ProjectControl projectControl,
       ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
-      throws IOException, OrmException {
+          throws IOException, OrmException, PermissionDeniedException {
+    RefControl refsMetaConfigControl =
+        projectControl.controlForRef(RefNames.REFS_CONFIG);
+    if (!refsMetaConfigControl.isVisible()) {
+      throw new PermissionDeniedException(
+          RefNames.REFS_CONFIG + " not visible");
+    }
+    if (!projectControl.isOwner() && !refsMetaConfigControl.canUpload()) {
+      throw new PermissionDeniedException(
+          "cannot upload to " + RefNames.REFS_CONFIG);
+    }
+
     md.setInsertChangeId(true);
     Change.Id changeId = new Change.Id(seq.nextChangeId());
     RevCommit commit =
@@ -120,9 +132,9 @@
 
     try (RevWalk rw = new RevWalk(md.getRepository());
         ObjectInserter objInserter = md.getRepository().newObjectInserter();
-        BatchUpdate bu = updateFactory.create(
-          db, config.getProject().getNameKey(), user,
-          TimeUtil.nowTs())) {
+        BatchUpdate bu =
+            updateFactory.create(db, config.getProject().getNameKey(),
+                projectControl.getUser(), TimeUtil.nowTs())) {
       bu.setRepository(md.getRepository(), rw, objInserter);
       bu.insertChange(
           changeInserterFactory.create(changeId, commit, RefNames.REFS_CONFIG)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
index e65879b..d664de9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -100,6 +100,8 @@
               .compare(a.patchSet, b.patchSet, NULLS_FIRST)
               .compare(side(a), side(b))
               .compare(a.line, b.line, NULLS_FIRST)
+              .compare(a.inReplyTo, b.inReplyTo, NULLS_FIRST)
+              .compare(a.message, b.message)
               .compare(a.id, b.id)
               .result();
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 680234c..d924b33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -45,6 +45,7 @@
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.LinkedHashMultimap;
@@ -54,6 +55,7 @@
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Table;
+import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -75,6 +77,7 @@
 import com.google.gerrit.extensions.common.PushCertificateInfo;
 import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.common.VotingRangeInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
@@ -602,7 +605,7 @@
     LabelTypes labelTypes = ctl.getLabelTypes();
     Map<String, LabelWithStatus> withStatus = cd.change().getStatus().isOpen()
       ? labelsForOpenChange(ctl, cd, labelTypes, standard, detailed)
-      : labelsForClosedChange(cd, labelTypes, standard, detailed);
+      : labelsForClosedChange(ctl, cd, labelTypes, standard, detailed);
     return ImmutableMap.copyOf(
         Maps.transformValues(withStatus, LabelWithStatus::label));
   }
@@ -722,6 +725,8 @@
     for (Account.Id accountId : allUsers) {
       IdentifiedUser user = userFactory.create(accountId);
       ChangeControl ctl = baseCtrl.forUser(user);
+      Map<String, VotingRangeInfo> pvr =
+        getPermittedVotingRanges(permittedLabels(ctl, cd));
       for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
         LabelType lt = ctl.getLabelTypes().byLabel(e.getKey());
         if (lt == null) {
@@ -730,6 +735,8 @@
           continue;
         }
         Integer value;
+        VotingRangeInfo permittedVotingRange =
+          pvr.getOrDefault(lt.getName(), null);
         String tag = null;
         Timestamp date = null;
         PatchSetApproval psa = current.get(accountId, lt.getName());
@@ -753,19 +760,53 @@
           value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
         }
         addApproval(e.getValue().label(),
-            approvalInfo(accountId, value, tag, date));
+            approvalInfo(accountId, value, permittedVotingRange, tag, date));
       }
     }
   }
 
+  private Map<String, VotingRangeInfo> getPermittedVotingRanges(
+      Map<String, Collection<String>> permittedLabels) {
+    Map<String, VotingRangeInfo> permittedVotingRanges =
+      Maps.newHashMapWithExpectedSize(permittedLabels.size());
+    for (String label : permittedLabels.keySet()) {
+      List<Integer> permittedVotingRange = permittedLabels.get(label)
+        .stream()
+        .map(this::parseRangeValue)
+        .filter(java.util.Objects::nonNull)
+        .sorted()
+        .collect(toList());
+
+      if (permittedVotingRange.isEmpty()) {
+        permittedVotingRanges.put(label, null);
+      } else {
+        int minPermittedValue = permittedVotingRange.get(0);
+        int maxPermittedValue = Iterables.getLast(permittedVotingRange);
+        permittedVotingRanges.put(label,
+          new VotingRangeInfo(minPermittedValue, maxPermittedValue));
+      }
+    }
+    return permittedVotingRanges;
+  }
+
+  private Integer parseRangeValue(String value) {
+    if (value.startsWith("+")) {
+      value = value.substring(1);
+    } else if (value.startsWith(" ")) {
+      value = value.trim();
+    }
+    return Ints.tryParse(value);
+  }
+
   private Timestamp getSubmittedOn(ChangeData cd)
       throws OrmException {
     Optional<PatchSetApproval> s = cd.getSubmitApproval();
     return s.isPresent() ? s.get().getGranted() : null;
   }
 
-  private Map<String, LabelWithStatus> labelsForClosedChange(ChangeData cd,
-      LabelTypes labelTypes, boolean standard, boolean detailed)
+  private Map<String, LabelWithStatus> labelsForClosedChange(
+      ChangeControl baseCtrl, ChangeData cd, LabelTypes labelTypes,
+      boolean standard, boolean detailed)
       throws OrmException {
     Set<Account.Id> allUsers = new HashSet<>();
     if (detailed) {
@@ -831,10 +872,12 @@
     for (Account.Id accountId : allUsers) {
       Map<String, ApprovalInfo> byLabel =
           Maps.newHashMapWithExpectedSize(labels.size());
-
+      Map<String, VotingRangeInfo> pvr = Collections.emptyMap();
       if (detailed) {
+        ChangeControl ctl = baseCtrl.forUser(userFactory.create(accountId));
+        pvr = getPermittedVotingRanges(permittedLabels(ctl, cd));
         for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
-          ApprovalInfo ai = approvalInfo(accountId, 0, null, null);
+          ApprovalInfo ai = approvalInfo(accountId, 0, null, null, null);
           byLabel.put(entry.getKey(), ai);
           addApproval(entry.getValue().label(), ai);
         }
@@ -849,6 +892,7 @@
         ApprovalInfo info = byLabel.get(type.getName());
         if (info != null) {
           info.value = Integer.valueOf(val);
+          info.permittedVotingRange = pvr.getOrDefault(type.getName(), null);
           info.date = psa.getGranted();
           info.tag = psa.getTag();
           if (psa.isPostSubmit()) {
@@ -865,17 +909,18 @@
     return labels;
   }
 
-  private ApprovalInfo approvalInfo(Account.Id id, Integer value, String tag,
-      Timestamp date) {
-    ApprovalInfo ai = getApprovalInfo(id, value, tag, date);
+  private ApprovalInfo approvalInfo(Account.Id id, Integer value,
+      VotingRangeInfo permittedVotingRange, String tag, Timestamp date) {
+    ApprovalInfo ai = getApprovalInfo(id, value, permittedVotingRange, tag, date);
     accountLoader.put(ai);
     return ai;
   }
 
-  public static ApprovalInfo getApprovalInfo(
-      Account.Id id, Integer value, String tag, Timestamp date) {
+  public static ApprovalInfo getApprovalInfo(Account.Id id, Integer value,
+      VotingRangeInfo permittedVotingRange, String tag, Timestamp date) {
     ApprovalInfo ai = new ApprovalInfo(id.get());
     ai.value = value;
+    ai.permittedVotingRange = permittedVotingRange;
     ai.date = date;
     ai.tag = tag;
     return ai;
@@ -1112,7 +1157,7 @@
           out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS), fillCommit);
         }
         if (addFooters) {
-          Ref ref = repo.exactRef(in.getRefName());
+          Ref ref = repo.exactRef(ctl.getChange().getDest().get());
           RevCommit mergeTip = null;
           if (ref != null){
             mergeTip = rw.parseCommit(ref.getObjectId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 9c51425..1e2bb4b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -68,7 +68,7 @@
   private CommitValidators.Policy validate;
   private boolean forceContentMerge;
   private boolean copyApprovals = true;
-  private boolean detailedCommitMessage = false;
+  private boolean detailedCommitMessage;
   private boolean postMessage = true;
 
   private RevCommit rebasedCommit;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
index 5e4eb6f..162a6b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -93,7 +93,7 @@
     for (Map.Entry<String, Short> e : approvals.entrySet()) {
       Integer value = e.getValue() != null ? Integer.valueOf(e.getValue()) : null;
       result.put(e.getKey(),
-          ChangeJson.getApprovalInfo(a.getId(), value, null, ts));
+          ChangeJson.getApprovalInfo(a.getId(), value, null, null, ts));
     }
     return result;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 0a4a430..b97dbc6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -107,7 +107,7 @@
     private final DynamicSet<ChangeMessageModifier> changeMessageModifiers;
 
     @Inject
-    public PluggableCommitMessageGenerator(
+    PluggableCommitMessageGenerator(
         DynamicSet<ChangeMessageModifier> changeMessageModifiers) {
       this.changeMessageModifiers = changeMessageModifiers;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
index 7c841c5..ab7eb0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -84,7 +84,7 @@
     this.db = db;
   }
 
-  boolean isStale(Change.Id id) throws IOException, OrmException {
+  public boolean isStale(Change.Id id) throws IOException, OrmException {
     ChangeIndex i = indexes.getSearchIndex();
     if (i == null) {
       return false; // No index; caller couldn't do anything if it is stale.
@@ -100,17 +100,24 @@
       return true; // Not in index, but caller wants it to be.
     }
     ChangeData cd = result.get();
-    if (reviewDbChangeIsStale(
-        cd.change(),
-        ChangeNotes.readOneReviewDbChange(db.get(), cd.getId()))) {
-      return true;
-    }
+    return isStale(repoManager, id, cd.change(),
+        ChangeNotes.readOneReviewDbChange(db.get(), id),
+        parseStates(cd), parsePatterns(cd));
+  }
 
-    return isStale(repoManager, id, parseStates(cd), parsePatterns(cd));
+  public static boolean isStale(
+      GitRepositoryManager repoManager,
+      Change.Id id,
+      Change indexChange,
+      @Nullable Change reviewDbChange,
+      SetMultimap<Project.NameKey, RefState> states,
+      Multimap<Project.NameKey, RefStatePattern> patterns) {
+    return reviewDbChangeIsStale(indexChange, reviewDbChange)
+        || refsAreStale(repoManager, id, states, patterns);
   }
 
   @VisibleForTesting
-  static boolean isStale(GitRepositoryManager repoManager,
+  static boolean refsAreStale(GitRepositoryManager repoManager,
       Change.Id id,
       SetMultimap<Project.NameKey, RefState> states,
       Multimap<Project.NameKey, RefStatePattern> patterns) {
@@ -118,7 +125,7 @@
         Sets.union(states.keySet(), patterns.keySet());
 
     for (Project.NameKey p : projects) {
-      if (isStale(repoManager, id, p, states, patterns)) {
+      if (refsAreStale(repoManager, id, p, states, patterns)) {
         return true;
       }
     }
@@ -145,8 +152,7 @@
     return parseStates(cd.getRefStates());
   }
 
-  @VisibleForTesting
-  static SetMultimap<Project.NameKey, RefState> parseStates(
+  public static SetMultimap<Project.NameKey, RefState> parseStates(
       Iterable<byte[]> states) {
     RefState.check(states != null, null);
     SetMultimap<Project.NameKey, RefState> result = HashMultimap.create();
@@ -188,7 +194,7 @@
     return result;
   }
 
-  private static boolean isStale(GitRepositoryManager repoManager,
+  private static boolean refsAreStale(GitRepositoryManager repoManager,
       Change.Id id, Project.NameKey project,
       SetMultimap<Project.NameKey, RefState> allStates,
       Multimap<Project.NameKey, RefStatePattern> allPatterns) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index b30eb81..83ffab6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2016 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.
@@ -467,12 +467,20 @@
     patchSetData.put("refName", patchSet.getRefName());
     soyContext.put("patchSet", patchSetData);
 
-    soyContext.put("reviewerEmails",
-        getEmailsByState(ReviewerStateInternal.REVIEWER));
-    soyContext.put("ccEmails",
-        getEmailsByState(ReviewerStateInternal.CC));
-
     // TODO(wyatta): patchSetInfo
+
+    footers.add("Gerrit-MessageType: " + messageClass);
+    footers.add("Gerrit-Change-Id: " + change.getKey().get());
+    footers.add("Gerrit-Change-Number: " +
+        Integer.toString(change.getChangeId()));
+    footers.add("Gerrit-PatchSet: " + patchSet.getPatchSetId());
+    footers.add("Gerrit-Owner: " + getNameEmailFor(change.getOwner()));
+    for (String reviewer : getEmailsByState(ReviewerStateInternal.REVIEWER)) {
+      footers.add("Gerrit-Reviewer: " + reviewer);
+    }
+    for (String reviewer : getEmailsByState(ReviewerStateInternal.CC)) {
+      footers.add("Gerrit-CC: " + reviewer);
+    }
   }
 
   private Set<String> getEmailsByState(ReviewerStateInternal state) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentFormatter.java
index 9363722..722fe1f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentFormatter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentFormatter.java
@@ -33,7 +33,8 @@
   public static class Block {
     public BlockType type;
     public String text;
-    public List<String> items;
+    public List<String> items; // For the items of list blocks.
+    public List<Block> quotedBlocks; // For the contents of quote blocks.
   }
 
   /**
@@ -146,15 +147,16 @@
   }
 
   private static Block makeQuote(String p) {
-    if (p.startsWith("> ")) {
-      p = p.substring(2);
-    } else if (p.startsWith(" > ")) {
-      p = p.substring(3);
+    String quote = p.replaceAll("\n\\s?>\\s?", "\n");
+    if (quote.startsWith("> ")) {
+      quote = quote.substring(2);
+    } else if (quote.startsWith(" > ")) {
+      quote = quote.substring(3);
     }
 
     Block block = new Block();
     block.type = BlockType.QUOTE;
-    block.text = p.replaceAll("\n\\s?>\\s", "\n").trim();
+    block.quotedBlocks = CommentFormatter.parse(quote);
     return block;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
index 5910aed..0cc4152 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -481,7 +481,9 @@
         Map<String, Object> commentData = new HashMap<>();
         commentData.put("lines", getLinesOfComment(comment, group.fileData));
         commentData.put("message", comment.message.trim());
-        commentData.put("messageBlocks", formatComment(comment.message));
+        List<CommentFormatter.Block> blocks =
+            CommentFormatter.parse(comment.message);
+        commentData.put("messageBlocks", commentBlocksToSoyData(blocks));
 
         // Set the prefix.
         String prefix = getCommentLinePrefix(comment);
@@ -519,11 +521,14 @@
           commentData.put("isRobotComment", false);
         }
 
-        // Set parent comment info.
-        Optional<Comment> parent = getParent(comment);
-        if (parent.isPresent()) {
-          commentData.put("parentMessage",
-              getShortenedCommentMessage(parent.get()));
+        // If the comment has a quote, don't bother loading the parent message.
+        if (!hasQuote(blocks)) {
+          // Set parent comment info.
+          Optional<Comment> parent = getParent(comment);
+          if (parent.isPresent()) {
+            commentData.put("parentMessage",
+                getShortenedCommentMessage(parent.get()));
+          }
         }
 
         commentsList.add(commentData);
@@ -535,9 +540,9 @@
     return commentGroups;
   }
 
-  private List<Map<String, Object>> formatComment(String comment) {
-    return CommentFormatter.parse(comment)
-        .stream()
+  private List<Map<String, Object>> commentBlocksToSoyData(
+      List<CommentFormatter.Block> blocks) {
+    return blocks.stream()
         .map(b -> {
           Map<String, Object> map = new HashMap<>();
           switch (b.type) {
@@ -551,7 +556,7 @@
               break;
             case QUOTE:
               map.put("type", "quote");
-              map.put("text", b.text);
+              map.put("quotedBlocks", commentBlocksToSoyData(b.quotedBlocks));
               break;
             case LIST:
               map.put("type", "list");
@@ -563,6 +568,15 @@
         .collect(Collectors.toList());
   }
 
+  private boolean hasQuote(List<CommentFormatter.Block> blocks) {
+    for (CommentFormatter.Block block : blocks) {
+      if (block.type == CommentFormatter.BlockType.QUOTE) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   private Repository getRepository() {
     try {
       return args.server.openRepository(projectState.getProject().getNameKey());
@@ -574,11 +588,19 @@
   @Override
   protected void setupSoyContext() {
     super.setupSoyContext();
+    boolean hasComments = false;
     try (Repository repo = getRepository()) {
-      soyContext.put("commentFiles", getCommentGroupsTemplateData(repo));
+      List<Map<String, Object>> files = getCommentGroupsTemplateData(repo);
+      soyContext.put("commentFiles", files);
+      hasComments = !files.isEmpty();
     }
+
     soyContext.put("commentTimestamp", getCommentTimestamp());
-    soyContext.put("coverLetterBlocks", formatComment(getCoverLetter()));
+    soyContext.put("coverLetterBlocks",
+        commentBlocksToSoyData(CommentFormatter.parse(getCoverLetter())));
+
+    footers.add("Gerrit-Comment-Date: " + getCommentTimestamp());
+    footers.add("Gerrit-HasComments: " + (hasComments ? "Yes" : "No"));
   }
 
   private String getLine(PatchFile fileInfo, short side, int lineNbr) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index baf0d93..0ded9d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -123,5 +123,8 @@
     Map<String, String> branchData = new HashMap<>();
     branchData.put("shortName", branch.getShortName());
     soyContext.put("branch", branchData);
+
+    footers.add("Gerrit-Project: " + branch.getParentKey().get());
+    footers.add("Gerrit-Branch: " + branch.getShortName());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 1051b9d..fcbd790 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -48,11 +48,13 @@
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
@@ -74,6 +76,7 @@
   protected VelocityContext velocityContext;
   protected Map<String, Object> soyContext;
   protected Map<String, Object> soyContextEmailData;
+  protected List<String> footers;
   protected final EmailArguments args;
   protected Account.Id fromId;
   protected NotifyHandling notify = NotifyHandling.ALL;
@@ -462,8 +465,10 @@
 
   protected void setupSoyContext() {
     soyContext = new HashMap<>();
+    footers = new ArrayList<>();
 
     soyContext.put("messageClass", messageClass);
+    soyContext.put("footers", footers);
 
     soyContextEmailData = new HashMap<>();
     soyContextEmailData.put("settingsUrl", getSettingsUrl());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
index f4fa446..bcaf79a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -16,10 +16,11 @@
 
 import static java.lang.String.format;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.IdentifiedUser;
@@ -72,13 +73,10 @@
 
   @Override
   public Response<?> apply(ProjectResource project, DeleteBranchesInput input)
-      throws OrmException, IOException, ResourceConflictException {
+      throws OrmException, IOException, RestApiException {
 
-    if (input == null) {
-      input = new DeleteBranchesInput();
-    }
-    if (input.branches == null) {
-      input.branches = Lists.newArrayListWithCapacity(1);
+    if (input == null || input.branches == null || input.branches.isEmpty()) {
+      throw new BadRequestException("branches must be specified");
     }
 
     try (Repository r = repoManager.openRepository(project.getNameKey())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 01b4bc1..085f34c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -765,11 +765,12 @@
   }
 
   public Change reloadChange() throws OrmException {
-    notes = notesFactory.create(db, project, legacyId);
-    change = notes.getChange();
-    if (change == null) {
-      throw new OrmException("Unable to load change " + legacyId);
+    try {
+      notes = notesFactory.createChecked(db, project, legacyId);
+    } catch (NoSuchChangeException e) {
+      throw new OrmException("Unable to load change " + legacyId, e);
     }
+    change = notes.getChange();
     setPatchSets(null);
     return change;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 13fa437..73951c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -248,10 +248,12 @@
         AccountCache accountCache,
         @GerritServerConfig Config cfg) {
       this(db, queryProvider, rewriter, opFactories, hasOperands, userFactory,
-          self, capabilityControlFactory, changeControlGenericFactory, notesFactory, changeDataFactory, fillArgs, commentsUtil,
-          accountResolver, groupBackend, allProjectsName, allUsersName, patchListCache, repoManager, projectCache, listChildProjects, submitDryRun, conflictsCache,
-          trackingFooters, indexes != null ? indexes.getSearchIndex() : null,
-          indexConfig, listMembers, starredChangesUtil, accountCache,
+          self, capabilityControlFactory, changeControlGenericFactory, notesFactory,
+          changeDataFactory, fillArgs, commentsUtil, accountResolver, groupBackend,
+          allProjectsName, allUsersName, patchListCache, repoManager, projectCache,
+          listChildProjects, submitDryRun, conflictsCache, trackingFooters,
+          indexes != null ? indexes.getSearchIndex() : null, indexConfig, listMembers,
+          starredChangesUtil, accountCache,
           cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true));
     }
 
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy
index 4958bde..a034872 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy
@@ -19,15 +19,7 @@
 /**
  * The .ChangeFooter template will determine the contents of the footer text
  * that will be appended to ALL emails related to changes.
- * @param branch
- * @param ccEmails
- * @param change
- * @param changeId
  * @param email
- * @param messageClass
- * @param patchSet
- * @param projectName
- * @param reviewerEmails
  */
 {template .ChangeFooter autoescape="strict" kind="text"}
   --{sp}
@@ -44,18 +36,4 @@
   {if $email.changeUrl or $email.settingsUrl}
     {\n}
   {/if}
-
-  Gerrit-MessageType: {$messageClass}{\n}
-  Gerrit-Change-Id: {$changeId}{\n}
-  Gerrit-Change-Number: {$change.changeNumber}{\n}
-  Gerrit-PatchSet: {$patchSet.patchSetId}{\n}
-  Gerrit-Project: {$projectName}{\n}
-  Gerrit-Branch: {$branch.shortName}{\n}
-  Gerrit-Owner: {$change.ownerEmail}{\n}
-  {foreach $reviewer in $reviewerEmails}
-    Gerrit-Reviewer: {$reviewer}{\n}
-  {/foreach}
-  {foreach $reviewer in $ccEmails}
-    Gerrit-CC: {$reviewer}{\n}
-  {/foreach}
 {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
index 28b2c28..5091cfe 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
@@ -17,21 +17,9 @@
 {namespace com.google.gerrit.server.mail.template}
 
 /**
- * @param branch
- * @param ccEmails
- * @param change
- * @param changeId
  * @param email
- * @param messageClass
- * @param patchSet
- * @param projectName
- * @param reviewerEmails
  */
 {template .ChangeFooterHtml autoescape="strict" kind="html"}
-  {let $footerStyle kind="css"}
-    display: none;
-  {/let}
-
   {if $email.changeUrl or $email.settingsUrl}
     <p>
       {if $email.changeUrl}
@@ -44,22 +32,6 @@
     </p>
   {/if}
 
-  <p style="{$footerStyle}">
-    Gerrit-MessageType: {$messageClass}<br/>
-    Gerrit-Change-Id: {$changeId}<br/>
-    Gerrit-Change-Number: {$change.changeNumber}<br/>
-    Gerrit-PatchSet: {$patchSet.patchSetId}<br/>
-    Gerrit-Project: {$projectName}<br/>
-    Gerrit-Branch: {$branch.shortName}<br/>
-    Gerrit-Owner: {$change.ownerEmail}
-    {foreach $reviewer in $reviewerEmails}
-      Gerrit-Reviewer: {$reviewer}</br>
-    {/foreach}
-    {foreach $reviewer in $ccEmails}
-      Gerrit-CC: {$reviewer}</br>
-    {/foreach}
-  </p>
-
   {if $email.changeUrl}
     <div itemscope itemtype="http://schema.org/EmailMessage">
       <div itemscope itemprop="action" itemtype="http://schema.org/ViewAction">
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy
index cce313b..73fdfba 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy
@@ -20,14 +20,6 @@
  * The .CommentFooter template will determine the contents of the footer text
  * that will be appended to emails related to a user submitting comments on
  * changes.
- * @param commentFiles
- * @param commentTimestamp
  */
 {template .CommentFooter autoescape="strict" kind="text"}
-  Gerrit-Comment-Date: {$commentTimestamp}
-  {if length($commentFiles) > 0}
-    Gerrit-HasComments: Yes
-  {else}
-    Gerrit-HasComments: No
-  {/if}
 {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy
index 84af859..7bf28e7 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy
@@ -16,21 +16,5 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
-/**
- * @param commentFiles
- * @param commentTimestamp
- */
 {template .CommentFooterHtml autoescape="strict" kind="html"}
-  {let $footerStyle kind="css"}
-    display: none;
-  {/let}
-
-  <p style="{$footerStyle}">
-    Gerrit-Comment-Date: {$commentTimestamp}
-    {if length($commentFiles) > 0}
-      Gerrit-HasComments: Yes
-    {else}
-      Gerrit-HasComments: No
-    {/if}
-  </p>
 {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy
index 6467e95..24db2fd 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy
@@ -20,6 +20,10 @@
  * The .Footer template will determine the contents of the footer text
  * appended to the end of all outgoing emails after the ChangeFooter and
  * CommentFooter.
+ * @param footers
  */
-{template .Footer}
+{template .Footer autoescape="strict" kind="text"}
+  {foreach $footer in $footers}
+    {$footer}{\n}
+  {/foreach}
 {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy
index 9befa51..9f9c503 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy
@@ -16,5 +16,14 @@
 
 {namespace com.google.gerrit.server.mail.template}
 
+/**
+ * @param footers
+ */
 {template .FooterHtml autoescape="strict" kind="html"}
+  {\n}
+  {\n}
+  {foreach $footer in $footers}
+    <div style="display:none">{sp}{$footer}{sp}</div>{\n}
+  {/foreach}
+  {\n}
 {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy
index a256a06..7c12325 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy
@@ -45,10 +45,11 @@
  * Take a list of unescaped comment blocks and emit safely escaped HTML to
  * render it nicely with wiki-like format.
  *
- * Each block is a map with a type key. When the type is 'paragraph', 'quote',
- * or 'pre', it also has a 'text' key that maps to the unescaped text content
- * for the block. If the type is 'list', the map will have a 'items' key which
- * maps to list of unescaped list item strings.
+ * Each block is a map with a type key. When the type is 'paragraph', or 'pre',
+ * it also has a 'text' key that maps to the unescaped text content for the
+ * block. If the type is 'list', the map will have a 'items' key which maps to
+ * list of unescaped list item strings. If the type is quote, the map will have
+ * a 'quotedBlocks' key which maps to the blocks contained within the quote.
  *
  * This mechanism encodes as little structure as possible in order to depend on
  * the Soy autoescape mechanism for all of the content.
@@ -66,7 +67,9 @@
     {if $block.type == 'paragraph'}
       <p>{$block.text}</p>
     {elseif $block.type == 'quote'}
-      <blockquote style="{$blockquoteStyle}">{$block.text}</blockquote>
+      <blockquote style="{$blockquoteStyle}">
+        {call .WikiFormat}{param content: $block.quotedBlocks /}{/call}
+      </blockquote>
     {elseif $block.type == 'pre'}
       {call .Pre}{param content: $block.text /}{/call}
     {elseif $block.type == 'list'}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
index cdd1e07..913ce93 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
-import static com.google.gerrit.server.index.change.StalenessChecker.isStale;
+import static com.google.gerrit.server.index.change.StalenessChecker.refsAreStale;
 import static com.google.gerrit.testutil.TestChanges.newChange;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
@@ -189,7 +189,7 @@
 
     // Not stale.
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id1.name()),
@@ -199,7 +199,7 @@
 
     // Wrong ref value.
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, SHA1),
@@ -209,7 +209,7 @@
 
     // Swapped repos.
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id2.name()),
@@ -222,7 +222,7 @@
     ObjectId id3 = tr1.update(ref3, tr1.commit().message("commit 3"));
     tr1.update(ref3, id3);
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id1.name()),
@@ -232,7 +232,7 @@
 
     // Ignore ref not mentioned.
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id1.name())),
@@ -241,7 +241,7 @@
 
     // One ref wrong.
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id1.name()),
@@ -257,7 +257,7 @@
 
     // ref1 is only ref matching pattern.
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id1.name())),
@@ -269,7 +269,7 @@
     String ref2 = "refs/heads/bar";
     ObjectId id2 = tr1.update(ref2, tr1.commit().message("commit 2"));
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id1.name())),
@@ -277,7 +277,7 @@
                     P1, RefStatePattern.create("refs/heads/*"))))
         .isTrue();
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id1.name()),
@@ -295,7 +295,7 @@
 
     // ref1 is only ref matching pattern.
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id1.name())),
@@ -307,7 +307,7 @@
     String ref3 = "refs/other/foo";
     ObjectId id3 = tr1.update(ref3, tr1.commit().message("commit 3"));
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id1.name())),
@@ -315,7 +315,7 @@
                     P1, RefStatePattern.create("refs/*/foo"))))
         .isTrue();
     assertThat(
-            isStale(
+            refsAreStale(
                 repoManager, C,
                 ImmutableSetMultimap.of(
                     P1, RefState.create(ref1, id1.name()),
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/CommentFormatterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/CommentFormatterTest.java
index 8a51c94..2944038 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/CommentFormatterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/CommentFormatterTest.java
@@ -31,6 +31,7 @@
     assertThat(block.type).isEqualTo(type);
     assertThat(block.text).isEqualTo(text);
     assertThat(block.items).isNull();
+    assertThat(block.quotedBlocks).isNull();
   }
 
   private void assertListBlock(List<CommentFormatter.Block> list, int index,
@@ -39,6 +40,16 @@
     assertThat(block.type).isEqualTo(LIST);
     assertThat(block.items.get(itemIndex)).isEqualTo(text);
     assertThat(block.text).isNull();
+    assertThat(block.quotedBlocks).isNull();
+  }
+
+  private void assertQuoteBlock(List<CommentFormatter.Block> list, int index,
+      int size) {
+    CommentFormatter.Block block = list.get(index);
+    assertThat(block.type).isEqualTo(QUOTE);
+    assertThat(block.items).isNull();
+    assertThat(block.text).isNull();
+    assertThat(block.quotedBlocks).hasSize(size);
   }
 
   @Test
@@ -86,7 +97,8 @@
     List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
 
     assertThat(result).hasSize(1);
-    assertBlock(result, 0, QUOTE, "Quote text");
+    assertQuoteBlock(result, 0, 1);
+    assertBlock(result.get(0).quotedBlocks, 0, PARAGRAPH, "Quote text");
   }
 
   @Test
@@ -105,7 +117,8 @@
     List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
 
     assertThat(result).hasSize(1);
-    assertBlock(result, 0, QUOTE, "Quote text");
+    assertQuoteBlock(result, 0, 1);
+    assertBlock(result.get(0).quotedBlocks, 0, PARAGRAPH, "Quote text");
   }
 
   @Test
@@ -114,7 +127,9 @@
     List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
 
     assertThat(result).hasSize(1);
-    assertBlock(result, 0, QUOTE, "Quote line 1\nQuote line 2\nQuote line 3");
+    assertQuoteBlock(result, 0, 1);
+    assertBlock(result.get(0).quotedBlocks, 0, PARAGRAPH,
+        "Quote line 1\nQuote line 2\nQuote line 3\n");
   }
 
   @Test
@@ -206,7 +221,9 @@
 
     assertThat(result).hasSize(7);
     assertBlock(result, 0, PARAGRAPH, "Paragraph\nacross\na\nfew\nlines.");
-    assertBlock(result, 1, QUOTE, "Quote\nacross\nnot many lines.");
+    assertQuoteBlock(result, 1, 1);
+    assertBlock(result.get(1).quotedBlocks, 0, PARAGRAPH,
+        "Quote\nacross\nnot many lines.");
     assertBlock(result, 2, PARAGRAPH, "Another paragraph");
     assertListBlock(result, 3, 0, "Series");
     assertListBlock(result, 3, 1, "of");
@@ -360,7 +377,9 @@
     List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
 
     assertThat(result).hasSize(2);
-    assertBlock(result, 0, QUOTE, "I'm happy\nwith quotes!");
+    assertQuoteBlock(result, 0, 1);
+    assertBlock(result.get(0).quotedBlocks, 0, PARAGRAPH,
+        "I'm happy\nwith quotes!");
     assertBlock(result, 1, PARAGRAPH, "See above.");
   }
 
@@ -371,7 +390,9 @@
 
     assertThat(result).hasSize(3);
     assertBlock(result, 0, PARAGRAPH, "See this said:");
-    assertBlock(result, 1, QUOTE, "a quoted\nstring block");
+    assertQuoteBlock(result, 1, 1);
+    assertBlock(result.get(1).quotedBlocks, 0, PARAGRAPH,
+        "a quoted\nstring block");
     assertBlock(result, 2, PARAGRAPH, "OK?");
   }
 
@@ -381,8 +402,53 @@
     List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
 
     assertThat(result).hasSize(1);
+    assertQuoteBlock(result, 0, 2);
+    assertQuoteBlock(result.get(0).quotedBlocks, 0, 1);
+    assertBlock(result.get(0).quotedBlocks.get(0).quotedBlocks, 0, PARAGRAPH,
+        "prior");
+    assertBlock(result.get(0).quotedBlocks, 1, PARAGRAPH, "next\n");
+  }
 
-    // Note: block does not encode nesting.
-    assertBlock(result, 0, QUOTE, "> prior\n\nnext");
+  @Test
+  public void largeMixedQuote() {
+    String comment =
+        "> > Paragraph 1.\n" +
+        "> > \n" +
+        "> > > Paragraph 2.\n" +
+        "> > \n" +
+        "> > Paragraph 3.\n" +
+        "> > \n" +
+        "> >    pre line 1;\n" +
+        "> >    pre line 2;\n" +
+        "> > \n" +
+        "> > Paragraph 4.\n" +
+        "> > \n" +
+        "> > * List item 1.\n" +
+        "> > * List item 2.\n" +
+        "> > \n" +
+        "> > Paragraph 5.\n" +
+        "> \n" +
+        "> Paragraph 6.\n" +
+        "\n" +
+        "Paragraph 7.\n";
+    List<CommentFormatter.Block> result = CommentFormatter.parse(comment);
+
+    assertThat(result).hasSize(2);
+    assertQuoteBlock(result, 0, 2);
+
+    assertQuoteBlock(result.get(0).quotedBlocks, 0, 7);
+    List<CommentFormatter.Block> bigQuote =
+        result.get(0).quotedBlocks.get(0).quotedBlocks;
+    assertBlock(bigQuote, 0, PARAGRAPH, "Paragraph 1.");
+    assertQuoteBlock(bigQuote, 1, 1);
+    assertBlock(bigQuote.get(1).quotedBlocks, 0, PARAGRAPH, "Paragraph 2.");
+    assertBlock(bigQuote, 2, PARAGRAPH, "Paragraph 3.");
+    assertBlock(bigQuote, 3, PRE_FORMATTED, "   pre line 1;\n   pre line 2;");
+    assertBlock(bigQuote, 4, PARAGRAPH, "Paragraph 4.");
+    assertListBlock(bigQuote, 5, 0, "List item 1.");
+    assertListBlock(bigQuote, 5, 1, "List item 2.");
+    assertBlock(bigQuote, 6, PARAGRAPH, "Paragraph 5.");
+    assertBlock(result.get(0).quotedBlocks, 1, PARAGRAPH, "Paragraph 6.");
+    assertBlock(result, 1, PARAGRAPH, "Paragraph 7.\n");
   }
 }
diff --git a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
index 34f2672..4eee495 100644
--- a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
+++ b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
@@ -25,12 +25,12 @@
 import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.restapi.Url;
 
-import org.apache.http.client.utils.DateUtils;
-
 import java.io.BufferedReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.security.Principal;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Enumeration;
@@ -55,6 +55,8 @@
 /** Simple fake implementation of {@link HttpServletRequest}. */
 public class FakeHttpServletRequest implements HttpServletRequest {
   public static final String SERVLET_PATH = "/b";
+  public static final DateTimeFormatter rfcDateformatter =
+      DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss ZZZ");
 
   private final Map<String, Object> attributes;
   private final ListMultimap<String, String> headers;
@@ -263,7 +265,8 @@
   @Override
   public long getDateHeader(String name) {
     String v = getHeader(name);
-    return v != null ? DateUtils.parseDate(v).getTime() : 0;
+    return v == null ? 0 :
+        rfcDateformatter.parse(v, Instant::from).getEpochSecond();
   }
 
   @Override
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 d6b1505..ccfb124 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
@@ -52,10 +52,10 @@
   var QUICK_APPROVE_ACTION = {
     __key: 'review',
     __type: 'change',
+    enabled: true,
     key: 'review',
     label: 'Quick Approve',
     method: 'POST',
-    title: 'Set maximal score to all labels you can.',
   };
 
   Polymer({
@@ -256,12 +256,84 @@
       var actions = this._getActionValues(
         actionsChangeRecord, primariesChangeRecord,
         additionalActionsChangeRecord, ActionType.CHANGE, change);
-      if (actions.length && this._canQuickApprove(change)) {
-        actions.unshift(QUICK_APPROVE_ACTION);
+      var quickApprove = this._getQuickApproveAction();
+      if (quickApprove) {
+        actions.unshift(quickApprove);
       }
       return actions;
     },
 
+    _getMaxScoreTextForLabel: function(label) {
+      if (!this.change ||
+          !this.change.permitted_labels ||
+          !this.change.permitted_labels[label] ||
+          !this.change.permitted_labels[label].length) {
+        return null;
+      }
+      return this.change.permitted_labels[label].slice(-1)[0];
+    },
+
+    _getMaxScoreForLabel: function(label) {
+      return parseInt(this._getMaxScoreTextForLabel(label), 10);
+    },
+
+    /**
+     * Get highest score for missing permitted label for current change.
+     *
+     * @return {{label: string, score: string}}
+     */
+    _getTopMissingApproval: function() {
+      var change = this.change;
+      if (!change || !change.labels || !change.permitted_labels) {
+        return null;
+      }
+
+      // Use only labels that satisfy all of following:
+      // - label scoring is permitted.
+      // - label is not approved yet.
+      // - label score is less than max permitted.
+      var missingApprovals = Object.keys(change.labels)
+          .filter(function(label) {
+            return label in change.permitted_labels &&
+                !change.labels[label].approved &&
+                (change.labels[label].value == null ||
+                  change.labels[label].value <
+                    this._getMaxScoreForLabel(label));
+          }.bind(this))
+          .sort(function(a, b) {
+            // Sort descending by max permitted score.
+            return this._getMaxScoreForLabel(b) - this._getMaxScoreForLabel(a);
+          }.bind(this));
+      if (!missingApprovals.length) {
+        return null;
+      }
+      var score = this._getMaxScoreForLabel(missingApprovals[0]);
+      // Guard against votes that fail to parse as integers. (Shouldn't happen.)
+      if (isNaN(score) || score <= 0) {
+        return null;
+      }
+      return {
+        label: missingApprovals[0],
+        score: this._getMaxScoreTextForLabel(missingApprovals[0]),
+      };
+    },
+
+    _getQuickApproveAction: function() {
+      var approval = this._getTopMissingApproval();
+      if (!approval) {
+        return null;
+      }
+      var action = Object.assign({}, QUICK_APPROVE_ACTION);
+      action.label = approval.label + approval.score;
+      var review = {
+        drafts: 'PUBLISH_ALL_REVISIONS',
+        labels: {},
+      };
+      review.labels[approval.label] = approval.score;
+      action.payload = review;
+      return action;
+    },
+
     _getActionValues: function(actionsChangeRecord, primariesChangeRecord,
         additionalActionsChangeRecord, type) {
       if (!actionsChangeRecord || !primariesChangeRecord) { return []; }
@@ -303,17 +375,6 @@
       return result.concat(additionalActions);
     },
 
-    _canQuickApprove: function(change) {
-      if (!change || !change.labels || !change.permitted_labels) {
-        return false;
-      }
-      var missingApprovals = Object.keys(change.labels).filter(function(label) {
-        return !change.labels[label].approved;
-      });
-      return missingApprovals.some(
-          function(label) { return label in change.permitted_labels; });
-    },
-
     _computeLoadingLabel: function(action) {
       return ActionLoadingLabels[action] || 'Working...';
     },
@@ -370,17 +431,11 @@
       } else if (key === ChangeActions.ABANDON) {
         this._showActionDialog(this.$.confirmAbandonDialog);
       } else if (key === QUICK_APPROVE_ACTION.key) {
-        var review = {
-          drafts: 'PUBLISH_ALL_REVISIONS',
-          labels: {},
-        };
-        var permittedLabels = this.change.permitted_labels;
-        Object.keys(permittedLabels).forEach(function(label) {
-          // Set label to maximal score permitted for it.
-          review.labels[label] = permittedLabels[label].slice(-1)[0];
+        var action = this._changeActionValues.find(function(o) {
+          return o.key === key;
         });
         this._fireAction(
-            this._prependSlash(key), QUICK_APPROVE_ACTION, true, review);
+          this._prependSlash(key), action, true, action.payload);
       } else {
         this._fireAction(this._prependSlash(key), this.actions[key], false);
       }
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 8ba8a5e..2720b29 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
@@ -505,10 +505,10 @@
         element.change = {
           current_revision: 'abc1234',
           labels: {
-            'foo': {},
+            foo: {},
           },
           permitted_labels: {
-            'foo': ['-1', '0', '+1'],
+            foo: ['-1', '0', '+1'],
           },
         };
         flushAsynchronousOperations();
@@ -519,23 +519,21 @@
         assert.isNotNull(approveButton);
       });
 
-      test('not added when no actions available', function() {
-        element.actions = [];
-        flushAsynchronousOperations();
-        var approveButton = element.$$('gr-button[data-action-key=\'review\']');
-        assert.isNull(approveButton);
+      test('is first in list of actions', function() {
+        var approveButton = element.$$('gr-button');
+        assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
       });
 
       test('not added when already approved', function() {
         element.change = {
           current_revision: 'abc1234',
           labels: {
-            'foo': {
+            foo: {
               approved: {},
             },
           },
           permitted_labels: {
-            'foo': [],
+            foo: [],
           },
         };
         flushAsynchronousOperations();
@@ -543,14 +541,14 @@
         assert.isNull(approveButton);
       });
 
-      test('not added when can not approve', function() {
+      test('not added when label not permitted', function() {
         element.change = {
           current_revision: 'abc1234',
           labels: {
-            'foo': {},
+            foo: {},
           },
           permitted_labels: {
-            'bar': [],
+            bar: [],
           },
         };
         flushAsynchronousOperations();
@@ -569,6 +567,70 @@
         assert.deepEqual(payload.labels, {foo: '+1'});
         fireActionStub.restore();
       });
+
+      test('higher permitted score has priority', function() {
+        element.change = {
+          current_revision: 'abc1234',
+          labels: {
+            foo: {},
+            bar: {},
+          },
+          permitted_labels: {
+            foo: [' 0', '+1'],
+            bar: [' 0', '+1', '+2'],
+          },
+        };
+        flushAsynchronousOperations();
+        var approveButton = element.$$('gr-button[data-action-key=\'review\']');
+        assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
+      });
+
+      test('button label for missing approval', function() {
+        element.change = {
+          current_revision: 'abc1234',
+          labels: {
+            foo: {},
+            bar: {approved: {}},
+          },
+          permitted_labels: {
+            foo: [' 0', '+1'],
+            bar: [' 0', '+1', '+2'],
+          },
+        };
+        flushAsynchronousOperations();
+        var approveButton = element.$$('gr-button[data-action-key=\'review\']');
+        assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
+      });
+
+      test('non-approving score', function() {
+        element.change = {
+          current_revision: 'abc1234',
+          labels: {
+            bar: {value: 1},
+          },
+          permitted_labels: {
+            bar: [' 0', '+1'],
+          },
+        };
+        flushAsynchronousOperations();
+        var approveButton = element.$$('gr-button[data-action-key=\'review\']');
+        assert.isNull(approveButton);
+      });
+
+      test('approving label with a non-max score', function() {
+        element.change = {
+          current_revision: 'abc1234',
+          labels: {
+            bar: {value: 1},
+          },
+          permitted_labels: {
+            bar: [' 0', '+1', '+2'],
+          },
+        };
+        flushAsynchronousOperations();
+        var approveButton = element.$$('gr-button[data-action-key=\'review\']');
+        assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 83e182a..ce17b3a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -20,6 +20,7 @@
 <link rel="import" href="../../shared/gr-label/gr-label.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
+<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
 
@@ -145,11 +146,20 @@
     <section>
       <span class="title">Topic</span>
       <span class="value">
-        <gr-editable-label
-            value="{{change.topic}}"
-            placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
-            read-only="[[_topicReadOnly]]"
-            on-changed="_handleTopicChanged"></gr-editable-label>
+        <template is="dom-if" if="[[change.topic]]">
+          <gr-linked-chip
+              text="[[change.topic]]"
+              href="[[_computeTopicHref(change.topic)]]"
+              removable
+              on-remove="_handleTopicRemoved"></gr-linked-chip>
+        </template>
+        <template is="dom-if" if="[[!change.topic]]">
+          <gr-editable-label
+              value="{{change.topic}}"
+              placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
+              read-only="[[_topicReadOnly]]"
+              on-changed="_handleTopicChanged"></gr-editable-label>
+        </template>
       </span>
     </section>
     <section class="strategy" hidden$="[[_computeHideStrategy(change)]]" hidden>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 01ef473..e458068 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -172,5 +172,14 @@
       }
       return output;
     },
+
+    _computeTopicHref: function(topic) {
+      return '/q/topic:' + encodeURIComponent(encodeURIComponent(topic)) +
+          '+(status:open OR status:merged)';
+    },
+
+    _handleTopicRemoved: function() {
+      this.set(['change', 'topic'], '');
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 7fa4744..04cc212 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -173,6 +173,14 @@
         element._handleTopicChanged({}, 'the new topic');
         assert.isTrue(topicStub.calledWith('the id', 'the new topic'));
       });
+
+      test('clicking x on topic chip removes topic', function() {
+        sandbox.stub(element, '_handleTopicChanged');
+        flushAsynchronousOperations();
+        var remove = element.$$('gr-linked-chip').$.remove;
+        MockInteractions.tap(remove);
+        assert.equal(element.change.topic, '');
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 1d31c12..ceaf02d 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -17,21 +17,12 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-account-dropdown">
   <template>
     <style>
-      :host {
-        display: inline-block;
-      }
-      .dropdown-trigger {
-        text-decoration: none;
-      }
-      .dropdown-content {
-        background-color: #fff;
-        box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
-      }
       button {
         background: none;
         border: none;
@@ -43,51 +34,15 @@
         width: 2em;
         vertical-align: middle;
       }
-      ul {
-        list-style: none;
-      }
-      ul .accountName {
-        font-weight: bold;
-      }
-      li .accountInfo,
-      li a {
-        display: block;
-        padding: .85em 1em;
-      }
-      li a:link,
-      li a:visited {
-        color: #00e;
-        text-decoration: none;
-      }
-      li a:hover {
-        background-color: #6B82D6;
-        color: #fff;
-      }
     </style>
-    <gr-button link class="dropdown-trigger" id="trigger"
-        on-tap="_showDropdownTapHandler">
-      <span hidden$="[[_hasAvatars]]" hidden>[[account.name]]</span>
-      <gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden
-          image-size="56"></gr-avatar>
-    </gr-button>
-    <iron-dropdown id="dropdown"
-        vertical-align="top"
-        vertical-offset="25"
+    <gr-dropdown items=[[links]] top-content=[[topContent]]
         horizontal-align="right">
-      <div class="dropdown-content">
-        <ul>
-          <li>
-            <div class="accountInfo">
-              <div class="accountName">[[account.name]]</div>
-              <div>[[account.email]]</div>
-            </div>
-          </li>
-          <li><a href$="[[_computeRelativeURL('/settings')]]">Settings</a></li>
-          <li><a href$="[[_computeRelativeURL('/switch-account')]]">Switch account</a></li>
-          <li><a href$="[[_computeRelativeURL('/logout')]]">Sign out</a></li>
-        </ul>
-      </div>
-    </iron-dropdown>
+      <gr-button link class="dropdown-trigger" id="trigger">
+        <span hidden$="[[_hasAvatars]]" hidden>[[account.name]]</span>
+        <gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden
+            image-size="56"></gr-avatar>
+      </gr-button>
+    </gr-dropdown>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-account-dropdown.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index ad944dc..4011135 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -20,26 +20,31 @@
     properties: {
       account: Object,
       _hasAvatars: Boolean,
+      links: {
+        type: Array,
+        value: [
+          {name: 'Settings', url: '/settings'},
+          {name: 'Switch account', url: '/switch-account'},
+          {name: 'Sign out', url: '/logout'},
+        ],
+      },
+      topContent: {
+        type: Array,
+        computed: '_getTopContent(account)',
+      },
     },
 
     attached: function() {
       this.$.restAPI.getConfig().then(function(cfg) {
         this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
       }.bind(this));
-
-      this.listen(this.$.dropdown, 'tap', '_handleDropdownTap');
     },
 
-    _handleDropdownTap: function(e) {
-      this.$.dropdown.close();
-    },
-
-    _showDropdownTapHandler: function(e) {
-      this.$.dropdown.open();
-    },
-
-    _computeRelativeURL: function(path) {
-      return '//' + window.location.host + path;
+    _getTopContent: function(account) {
+      return [
+        {text: account.name, bold: true},
+        {text: account.email},
+      ];
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
index 8c39da8..ec9141f 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -41,11 +41,10 @@
       element = fixture('basic');
     });
 
-    test('tap on trigger opens menu', function() {
-      assert.isFalse(element.$.dropdown.opened);
-      MockInteractions.tap(element.$.trigger);
-      assert.isTrue(element.$.dropdown.opened);
+    test('account information', function() {
+      element.account = {name: 'John Doe', email: 'john@doe.com'};
+      assert.deepEqual(element.topContent,
+          [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]);
     });
-
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 3e30810..76a1c1f 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -17,8 +17,8 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
 <link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html">
+<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
 <link rel="import" href="../gr-search-bar/gr-search-bar.html">
 
 <dom-module id="gr-main-header">
@@ -42,12 +42,6 @@
       ul {
         list-style: none;
       }
-      .links {
-        margin-left: 1em;
-      }
-      .links .menuContainer {
-        display: none;
-      }
       .links > li {
         cursor: default;
         display: inline-block;
@@ -55,31 +49,8 @@
         padding: .5em 0;
         position: relative;
       }
-      .links li:hover .menuContainer,
-      .links li:active .menuContainer {
-        background-color: #fff;
-        border-radius: 3px;
-        box-shadow: 0 1px 1px rgba(0, 0, 0, .3);
-        display: block;
-        left: -.5em;
-        padding: .5em 0;
-        position: absolute;
-        top: 2.4em;
-        z-index: 1000;
-      }
-      .links li ul li a:link,
-      .links li ul li a:visited {
-        color: #00e;
-        display: block;
-        padding: .3em 1em;
-        text-decoration: none;
-        white-space: nowrap;
-      }
-      .links li ul li:hover a,
-      .links li ul li:active a {
-        background-color: var(--selection-background-color);
-      }
       .linksTitle {
+        color: black;
         display: inline-block;
         padding-right: 1em;
         position: relative;
@@ -121,6 +92,13 @@
         overflow: hidden;
         text-overflow: ellipsis;
       }
+      .dropdown-trigger {
+        text-decoration: none;
+      }
+      .dropdown-content {
+        background-color: #fff;
+        box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
+      }
       @media screen and (max-width: 50em) {
         .bigTitle {
           font-size: 14px;
@@ -139,16 +117,13 @@
       <ul class="links">
         <template is="dom-repeat" items="[[_links]]" as="linkGroup">
           <li>
-            <span class="linksTitle">
+          <gr-dropdown
+              items = [[linkGroup.links]]
+              horizontal-align="left">
+            <span class="linksTitle" id="[[linkGroup.title]]">
               [[linkGroup.title]] <i class="downArrow"></i>
             </span>
-            <div class="menuContainer">
-              <ul>
-                <template is="dom-repeat" items="[[linkGroup.links]]" as="link">
-                  <li><a href$="[[link.url]]">[[link.name]]</a></li>
-                </template>
-              </ul>
-            </div>
+          </gr-dropdown>
           </li>
         </template>
       </ul>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 2f017de..ebeb9af 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -36,7 +36,7 @@
     is: 'gr-main-header',
 
     hostAttributes: {
-      role: 'banner'
+      role: 'banner',
     },
 
     properties: {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 8905854..72138ff 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -125,12 +125,7 @@
       this.comment.message = this._messageText;
       this.disabled = true;
 
-      this.$.storage.eraseDraftComment({
-        changeNum: this.changeNum,
-        patchNum: this.patchNum,
-        path: this.comment.path,
-        line: this.comment.line,
-      });
+      this._eraseDraftComment();
 
       this._xhrPromise = this._saveDraft(this.comment).then(function(response) {
         this.disabled = false;
@@ -155,6 +150,15 @@
       }.bind(this));
     },
 
+    _eraseDraftComment: function() {
+      this.$.storage.eraseDraftComment({
+        changeNum: this.changeNum,
+        patchNum: this.patchNum,
+        path: this.comment.path,
+        line: this.comment.line,
+      });
+    },
+
     _commentChanged: function(comment) {
       this.editing = !!comment.__editing;
       if (this.editing) { // It's a new draft/reply, notify.
@@ -338,6 +342,8 @@
       }
       this.editing = false;
       this.disabled = true;
+      this._eraseDraftComment();
+
       if (!this.comment.id) {
         this.disabled = false;
         this._fireDiscard();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index 96caebf..f562d3a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -182,6 +182,7 @@
 
   suite('gr-diff-comment draft tests', function() {
     var element;
+    var sandbox;
 
     setup(function() {
       stub('gr-rest-api-interface', {
@@ -219,6 +220,11 @@
         path: '/path/to/file',
         line: 5,
       };
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(function() {
+      sandbox.restore();
     });
 
     test('button visibility states', function() {
@@ -332,6 +338,8 @@
       assert.isTrue(element.editing);
 
       element._messageText = '';
+      var eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+
       // Save should be disabled on an empty message.
       var disabled = element.$$('.save').hasAttribute('disabled');
       assert.isTrue(disabled, 'save button should be disabled.');
@@ -345,18 +353,30 @@
       var numDiscardEvents = 0;
       element.addEventListener('comment-discard', function(e) {
         numDiscardEvents++;
-        if (numDiscardEvents == 3) {
+        assert.isFalse(eraseMessageDraftSpy.called);
+        if (numDiscardEvents === 2) {
           assert.isFalse(updateStub.called);
           done();
         }
       });
       MockInteractions.tap(element.$$('.cancel'));
-      MockInteractions.tap(element.$$('.discard'));
       element.flushDebouncer('fire-update');
       element._messageText = '';
       MockInteractions.pressAndReleaseKeyOn(element.$.editTextarea, 27); // esc
     });
 
+    test('draft discard removes message from storage', function(done) {
+      element._messageText = '';
+      var eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+
+      var numDiscardEvents = 0;
+      element.addEventListener('comment-discard', function(e) {
+        assert.isTrue(eraseMessageDraftSpy.called);
+        done();
+      });
+      MockInteractions.tap(element.$$('.discard'));
+    });
+
     test('ctrl+s saves comment', function(done) {
       var stub = sinon.stub(element, 'save', function() {
         assert.isTrue(stub.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index 65331d6..dfff56d 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -72,7 +72,8 @@
       assert.equal(element._computeShowEmail(
           false, undefined), false);
 
-      assert.equal(element._computeEmailStr({name: 'test', email: 'test'}), '(test)');
+      assert.equal(
+          element._computeEmailStr({name: 'test', email: 'test'}), '(test)');
       assert.equal(element._computeEmailStr({email: 'test'}, ''), 'test');
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
new file mode 100644
index 0000000..00d50e9
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -0,0 +1,112 @@
+<!--
+Copyright (C) 2016 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-dropdown/iron-dropdown.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<dom-module id="gr-dropdown">
+  <template>
+    <style>
+      :host {
+        display: inline-block;
+      }
+      .dropdown-trigger {
+        text-decoration: none;
+      }
+      .dropdown-content {
+        background-color: #fff;
+        box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
+      }
+      button {
+        background: none;
+        border: none;
+        font: inherit;
+        padding: .3em 0;
+      }
+      gr-avatar {
+        height: 2em;
+        width: 2em;
+        vertical-align: middle;
+      }
+      ul {
+        list-style: none;
+      }
+      ul .accountName {
+        font-weight: bold;
+      }
+      li .accountInfo,
+      li a {
+        display: block;
+        padding: .85em 1em;
+      }
+      li a:link,
+      li a:visited {
+        color: #00e;
+        text-decoration: none;
+      }
+      li a:hover {
+        background-color: #6B82D6;
+        color: #fff;
+      }
+      .topContent {
+        display: block;
+        padding: .85em 1em;
+      }
+      .bold-text {
+        font-weight: bold;
+      }
+    </style>
+    <gr-button link class="dropdown-trigger" id="trigger"
+        on-tap="_showDropdownTapHandler">
+      <content></content>
+    </gr-button>
+    <iron-dropdown id="dropdown"
+        vertical-align="top"
+        vertical-offset="25"
+        horizontal-align="[[horizontalAlign]]"
+        on-tap="_handleDropdownTap">
+      <div class="dropdown-content">
+        <ul>
+          <template is="dom-if" if="[[topContent]]">
+            <div class="topContent">
+              <template
+                  is="dom-repeat"
+                  items="[[topContent]]"
+                  as="item"
+                  initial-count="75">
+                <div class$="[[_getClassIfBold(item.bold)]] top-item">
+                  [[item.text]]
+                </div>
+              </template>
+            </div>
+          </template>
+          <template
+              is="dom-repeat"
+              items="[[items]]"
+              as="link"
+              initial-count="75">
+            <li><a href$="[[_computeRelativeURL(link.url)]]">[[link.name]]</a>
+            </li>
+          </template>
+        </ul>
+      </div>
+    </iron-dropdown>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-dropdown.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
new file mode 100644
index 0000000..d10d219
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -0,0 +1,57 @@
+// Copyright (C) 2016 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-dropdown',
+
+    properties: {
+      items: Array,
+      topContent: Object,
+      horizontalAlign: {
+        type: String,
+        value: 'left',
+      },
+      _hasAvatars: String,
+    },
+
+    attached: function() {
+      this.$.restAPI.getConfig().then(function(cfg) {
+        this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+      }.bind(this));
+    },
+
+    _handleDropdownTap: function(e) {
+      this.$.dropdown.close();
+    },
+
+    _showDropdownTapHandler: function(e) {
+      this.$.dropdown.open();
+    },
+
+    _getClassIfBold: function(bold) {
+      return bold ? 'bold-text' : '';
+    },
+
+    _computeURLHelper: function(host, path) {
+      return '//' + host + path;
+    },
+
+    _computeRelativeURL: function(path) {
+      var host = window.location.host;
+      return this._computeURLHelper(host, path);
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
new file mode 100644
index 0000000..b2f2d21
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-dropdown</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-dropdown.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-dropdown></gr-dropdown>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-dropdown tests', function() {
+    var element;
+
+    setup(function() {
+      stub('gr-rest-api-interface', {
+        getConfig: function() { return Promise.resolve({}); },
+      });
+      element = fixture('basic');
+    });
+
+    test('tap on trigger opens menu', function() {
+      assert.isFalse(element.$.dropdown.opened);
+      MockInteractions.tap(element.$.trigger);
+      assert.isTrue(element.$.dropdown.opened);
+    });
+
+    test('_computeRelativeURL', function() {
+      var path = '/test';
+      var host = 'http://www.testsite.com';
+      var computedPath = element._computeURLHelper(host, path);
+      assert.equal(computedPath, '//http://www.testsite.com/test');
+    });
+
+    test('_getClassIfBold', function() {
+      var bold = true;
+      assert.equal(element._getClassIfBold(bold), 'bold-text');
+
+      bold = false;
+      assert.equal(element._getClassIfBold(bold), '');
+    });
+
+    test('Top text exists and is bolded correctly', function() {
+      element.topContent = [{text: 'User', bold: true}, {text: 'email'}];
+      flushAsynchronousOperations();
+      var topItems = Polymer.dom(element.root).querySelectorAll('.top-item');
+      assert.equal(topItems.length, 2);
+      assert.isTrue(topItems[0].classList.contains('bold-text'));
+      assert.isFalse(topItems[1].classList.contains('bold-text'));
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
index e7080c0..1477d43 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
@@ -189,7 +189,7 @@
       assert.equal(result[1].type, 'quote');
       assert.lengthOf(result[1].blocks, 1);
       assertBlock(result[1].blocks, 0, 'paragraph',
-          'Quote\nacross\nnot many lines.')
+          'Quote\nacross\nnot many lines.');
 
       assertBlock(result, 2, 'paragraph', 'Another paragraph');
       assertListBlock(result, 3, 0, 'Series');
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
new file mode 100644
index 0000000..a3d2769
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -0,0 +1,66 @@
+<!--
+Copyright (C) 2016 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="../gr-button/gr-button.html">
+<dom-module id="gr-linked-chip">
+  <template>
+    <style>
+      :host {
+        display: block;
+        overflow: hidden;
+      }
+      .container {
+        align-items: center;
+        background: #eee;
+        border-radius: .75em;
+        display: inline-flex;
+        padding: 0 .5em;
+      }
+      gr-button.remove,
+      gr-button.remove:hover,
+      gr-button.remove:focus {
+        border-color: transparent;
+        color: #333;
+      }
+      gr-button.remove {
+        background: #eee;
+        color: #666;
+        font-size: 1.7em;
+        font-weight: normal;
+        height: .6em;
+        line-height: .6em;
+        margin-left: .15em;
+        padding: 0;
+        text-decoration: none;
+      }
+      .transparentBackground,
+      gr-button.transparentBackground {
+        background-color: transparent;
+      }
+    </style>
+    <div class$="container [[_getBackgroundClass(transparentBackground)]]">
+      <a href$="[[href]]">[[text]]</a>
+      <gr-button
+          id="remove"
+          hidden$="[[!removable]]"
+          hidden
+          class$="remove [[_getBackgroundClass(transparentBackground)]]"
+          on-tap="_handleRemoveTap">×</gr-button>
+    </div>
+  </template>
+  <script src="gr-linked-chip.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
new file mode 100644
index 0000000..c6a5e4e
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -0,0 +1,42 @@
+// Copyright (C) 2016 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-linked-chip',
+
+    properties: {
+      href: String,
+      removable: {
+        type: Boolean,
+        value: false,
+      },
+      text: String,
+      transparentBackground: {
+        type: Boolean,
+        value: false,
+      },
+    },
+
+    _getBackgroundClass: function(transparent) {
+      return transparent ? 'transparentBackground' : '';
+    },
+
+    _handleRemoveTap: function(e) {
+      e.preventDefault();
+      this.fire('remove');
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
new file mode 100644
index 0000000..5e2cac5
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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-linked-chip</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-linked-chip.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-linked-chip></gr-linked-chip>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-linked-chip tests', function() {
+    var element;
+    var sandbox;
+
+    setup(function() {
+      element = fixture('basic');
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(function() {
+      sandbox.restore();
+    });
+
+    test('remove fired', function() {
+      var spy = sandbox.spy();
+      element.addEventListener('remove', spy);
+      flushAsynchronousOperations();
+      MockInteractions.tap(element.$.remove);
+      assert.isTrue(spy.called);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index 7feaa51..90b09ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -175,5 +175,28 @@
       assert.equal(element.$.output.innerHTML, 'foo:baz');
     });
 
+    test('overlapping links', function() {
+      element.config = {
+        b1: {
+          match: '(B:\\s*)(\\d+)',
+          html: '$1<a href="ftp://foo/$2">$2</a>',
+        },
+        b2: {
+          match: '(B:\\s*\\d+\\s*,\\s*)(\\d+)',
+          html: '$1<a href="ftp://foo/$2">$2</a>',
+        },
+      };
+      element.content = '- B: 123, 45';
+      var links = Polymer.dom(element.root).querySelectorAll('a');
+
+      assert.equal(links.length, 2);
+      assert.equal(element.$$('span').textContent, '- B: 123, 45');
+
+      assert.equal(links[0].href, 'ftp://foo/123');
+      assert.equal(links[0].textContent, '123');
+
+      assert.equal(links[1].href, 'ftp://foo/45');
+      assert.equal(links[1].textContent, '45');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
index 303a9cc..b28097a 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -165,12 +165,27 @@
       var result = match[0].replace(pattern,
           patterns[p].html || patterns[p].link);
 
+      // Skip portion of replacement string that is equal to original.
+      for (var i = 0; i < result.length; i++) {
+        if (result[i] !== match[0][i]) {
+          break;
+        }
+      }
+      result = result.slice(i);
+
       if (patterns[p].html) {
         this.addHTML(
-            result, susbtrIndex + match.index, match[0].length, outputArray);
+          result,
+          susbtrIndex + match.index + i,
+          match[0].length - i,
+          outputArray);
       } else if (patterns[p].link) {
-        this.addLink(match[0], result,
-            susbtrIndex + match.index, match[0].length, outputArray);
+        this.addLink(
+          match[0],
+          result,
+          susbtrIndex + match.index + i,
+          match[0].length - i,
+          outputArray);
       } else {
         throw Error('linkconfig entry ' + p +
             ' doesn’t contain a link or html attribute.');
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 c187cf2..8a66533 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
@@ -68,7 +68,7 @@
     REVIEWER_UPDATES: 19,
 
     // Set the submittable boolean.
-    SUBMITTABLE: 20
+    SUBMITTABLE: 20,
   };
 
   Polymer({
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 7574d18..48ddaf2 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -94,6 +94,7 @@
     'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
     'shared/gr-js-api-interface/gr-change-reply-js-api_test.html',
     'shared/gr-js-api-interface/gr-js-api-interface_test.html',
+    'shared/gr-linked-chip/gr-linked-chip_test.html',
     'shared/gr-linked-text/gr-linked-text_test.html',
     'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
     'shared/gr-select/gr-select_test.html',
diff --git a/tools/eclipse/project_bzl.py b/tools/eclipse/project_bzl.py
index 6bc7398..a7ddf6f 100755
--- a/tools/eclipse/project_bzl.py
+++ b/tools/eclipse/project_bzl.py
@@ -170,10 +170,6 @@
     m = java_library.match(p)
     if m:
       gwt_src.add(m.group(1))
-      # Exception: we need source here for GWT SDM mode to work
-      if p.endswith('libEdit.jar'):
-        p = p[:-4] + '-src.jar'
-        lib.add(p)
 
   for s in sorted(src):
     out = None
@@ -213,10 +209,18 @@
         p = path.join(prefix, "jar", "%s-src.jar" % suffix)
         if path.exists(p):
           s = p
-      # TODO(davido): make plugins actually work
       if args.plugins:
         classpathentry('lib', j, s, exported=True)
       else:
+        # Filter out the source JARs that we pull through transitive closure of
+        # GWT plugin API (we add source directories themself).  Exception is
+        # libEdit-src.jar, that is needed for GWT SDM to work.
+        m = java_library.match(j)
+        if m:
+          if m.group(1).startswith("gerrit-") and \
+              j.endswith("-src.jar") and \
+              not j.endswith("libEdit-src.jar"):
+            continue
         classpathentry('lib', j, s)
 
   for s in sorted(gwt_src):