Merge changes Ib3f0849f,I9d12ed46,I38520cff

* changes:
  Factor out Contributor Agreements from ProjectControl
  Add ProjectPermissions for upload and receive pack, migrate callers
  Add ProjectPermission.READ_NO_CONFIG
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 6e1f394..93910d9 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -628,6 +628,23 @@
 +
 By default, true.
 
+[[auth.autoUpdateAccountActiveStatus]]auth.autoUpdateAccountActiveStatus::
++
+Whether to allow automatic synchronization of an account's inactive flag upon login.
+If set to true, upon login, if the authentication back-end reports the account as active,
+the account's inactive flag in the internal Gerrit database will be updated to be active.
+If the authentication back-end reports the account as inactive, the account's flag will be
+updated to be inactive and the login attempt will be blocked. Users enabling this feature
+should ensure that their authentication back-end is supported. Currently, only
+strict 'LDAP' authentication is supported.
++
+In addition, if this parameter is not set, or false, the corresponding scheduled
+task to deactivate inactive Gerrit accounts will also be disabled. If this
+parameter is set to true, users should also consider configuring the
+link:#accountDeactivation[accountDeactivation] section appropriately.
++
+By default, false.
+
 [[cache]]
 === Section cache
 
@@ -4538,6 +4555,44 @@
 If no groups are added, any user will be allowed to execute
 'upload-pack' on the server.
 
+[[accountDeactivation]]
+=== Section accountDeactivation
+
+Configures the parameters for the scheduled task to sweep and deactivate Gerrit
+accounts according to their status reported by the auth backend. Currently only
+supported for LDAP backends.
+
+[[accountDeactivation.startTime]]accountDeactivation.startTime::
++
+Start time to define the first execution of account deactivations.
+If the configured `'accountDeactivation.interval'` is shorter than `'accountDeactivation.startTime - now'`
+the start time will be preponed by the maximum integral multiple of
+`'accountDeactivation.interval'` so that the start time is still in the future.
++
+----
+<day of week> <hours>:<minutes>
+or
+<hours>:<minutes>
+
+<day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
+<hours>       : 00-23
+<minutes>     : 0-59
+----
+
+[[accountDeactivation.interval]]accountDeactivation.interval::
++
+Interval for periodic repetition of triggering account deactivation sweeps.
+The interval must be larger than zero. The following suffixes are supported
+to define the time unit for the interval:
++
+* `s, sec, second, seconds`
+* `m, min, minute, minutes`
+* `h, hr, hour, hours`
+* `d, day, days`
+* `w, week, weeks` (`1 week` is treated as `7 days`)
+* `mon, month, months` (`1 month` is treated as `30 days`)
+* `y, year, years` (`1 year` is treated as `365 days`)
+
 [[urlAlias]]
 === Section urlAlias
 
diff --git a/Documentation/dev-stars.txt b/Documentation/dev-stars.txt
index 1fb871a..a83ad44 100644
--- a/Documentation/dev-stars.txt
+++ b/Documentation/dev-stars.txt
@@ -61,18 +61,24 @@
 
 The ignore star is represented by the special star label 'ignore'.
 
-[[mute-star]]
-== Mute Star
+[[reviewed-star]]
+== Reviewed Star
 
-If the "mute/<patchset_id>"-star is set by a user, and <patchset_id>
+If the "reviewed/<patchset_id>"-star is set by a user, and <patchset_id>
 matches the current patch set, the change is always reported as "reviewed"
 in the ChangeInfo.
 
 This allows users to "de-highlight" changes in a dashboard until a new
 patchset has been uploaded.
 
-The ChangeInfo muted-field will show if the change is currently in a
-mute state.
+[[unreviewed-star]]
+== Unreviewed Star
+
+If the "unreviewed/<patchset_id>"-star is set by a user, and <patchset_id>
+matches the current patch set, the change is always reported as "unreviewed"
+in the ChangeInfo.
+
+This allows users to "highlight" changes in a dashboard.
 
 [[query-stars]]
 == Query Stars
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt
index da81dff..6b9374f 100644
--- a/Documentation/linux-quickstart.txt
+++ b/Documentation/linux-quickstart.txt
@@ -73,7 +73,7 @@
 == Update the listen URL
 
 Another recommended task is to change the URL that Gerrit listens to from `*`
-to `localhost`. This changes helps prevent outside connections from contacting
+to `localhost`. This change helps prevent outside connections from contacting
 the instance.
 
 ....
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index fa91c2d..50afe40 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2329,13 +2329,13 @@
   PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unignore HTTP/1.0
 ----
 
-[[mute]]
-=== Mute
+[[mark-as-reviewed]]
+=== Mark as Reviewed
 --
-'PUT /changes/link:#change-id[\{change-id\}]/mute'
+'PUT /changes/link:#change-id[\{change-id\}]/reviewed'
 --
 
-Marks a change as muted.
+Marks a change as reviewed.
 
 This allows users to "de-highlight" changes in their dashboard until a new
 patch set is uploaded.
@@ -2347,20 +2347,22 @@
 
 .Request
 ----
-  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/mute HTTP/1.0
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewed HTTP/1.0
 ----
 
-[[unmute]]
-=== Unmute
+[[mark-as-unreviewed]]
+=== Mark as Unreviewed
 --
-'PUT /changes/link:#change-id[\{change-id\}]/unmute'
+'PUT /changes/link:#change-id[\{change-id\}]/unreviewed'
 --
 
-Unmutes a change.
+Marks a change as unreviewed.
+
+This allows users to "highlight" changes in their dashboard
 
 .Request
 ----
-  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unmute HTTP/1.0
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unreviewed HTTP/1.0
 ----
 
 [[get-hashtags]]
@@ -5641,8 +5643,6 @@
 change. The labels are lexicographically sorted.
 |`reviewed`           |not set if `false`|
 Whether the change was reviewed by the calling user.
-|`muted`              |not set if `false`|
-Whether the change has been link:#mute[muted] by the calling user.
 Only set if link:#reviewed[reviewed] is requested.
 |`submit_type`        |optional|
 The link:project-configuration.html#submit_type[submit type] of the change. +
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 65e5909..c2d3184 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
@@ -3374,7 +3374,7 @@
   }
 
   @Test
-  public void mute() throws Exception {
+  public void markAsReviewed() throws Exception {
     TestAccount user2 = accountCreator.user2();
 
     PushOneCommit.Result r = createChange();
@@ -3384,16 +3384,16 @@
     gApi.changes().id(r.getChangeId()).addReviewer(in);
 
     setApiUser(user);
-    assertThat(gApi.changes().id(r.getChangeId()).muted()).isFalse();
-    gApi.changes().id(r.getChangeId()).mute(true);
-    assertThat(gApi.changes().id(r.getChangeId()).muted()).isTrue();
+    assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
+    gApi.changes().id(r.getChangeId()).markAsReviewed(true);
+    assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isTrue();
 
     setApiUser(user2);
     sender.clear();
     amendChange(r.getChangeId());
 
     setApiUser(user);
-    assertThat(gApi.changes().id(r.getChangeId()).muted()).isFalse();
+    assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
 
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
@@ -3401,12 +3401,73 @@
   }
 
   @Test
-  public void cannotMuteOwnChange() throws Exception {
+  public void cannotSetUnreviewedLabelForPatchSetThatAlreadyHasReviewedLabel() throws Exception {
     String changeId = createChange().getChangeId();
 
+    setApiUser(user);
+    gApi.changes().id(changeId).markAsReviewed(true);
+    assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
+
     exception.expect(BadRequestException.class);
-    exception.expectMessage("cannot mute own change");
-    gApi.changes().id(changeId).mute(true);
+    exception.expectMessage(
+        "The labels "
+            + StarredChangesUtil.REVIEWED_LABEL
+            + "/"
+            + 1
+            + " and "
+            + StarredChangesUtil.UNREVIEWED_LABEL
+            + "/"
+            + 1
+            + " are mutually exclusive. Only one of them can be set.");
+    gApi.accounts()
+        .self()
+        .setStars(
+            changeId, new StarsInput(ImmutableSet.of(StarredChangesUtil.UNREVIEWED_LABEL + "/1")));
+  }
+
+  @Test
+  public void cannotSetReviewedLabelForPatchSetThatAlreadyHasUnreviewedLabel() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    setApiUser(user);
+    gApi.changes().id(changeId).markAsReviewed(false);
+    assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage(
+        "The labels "
+            + StarredChangesUtil.REVIEWED_LABEL
+            + "/"
+            + 1
+            + " and "
+            + StarredChangesUtil.UNREVIEWED_LABEL
+            + "/"
+            + 1
+            + " are mutually exclusive. Only one of them can be set.");
+    gApi.accounts()
+        .self()
+        .setStars(
+            changeId, new StarsInput(ImmutableSet.of(StarredChangesUtil.REVIEWED_LABEL + "/1")));
+  }
+
+  @Test
+  public void setReviewedAndUnreviewedLabelsForDifferentPatchSets() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    setApiUser(user);
+    gApi.changes().id(changeId).markAsReviewed(true);
+    assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
+
+    amendChange(changeId);
+    assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
+
+    gApi.changes().id(changeId).markAsReviewed(false);
+    assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
+
+    assertThat(gApi.accounts().self().getStars(changeId))
+        .containsExactly(
+            StarredChangesUtil.REVIEWED_LABEL + "/" + 1,
+            StarredChangesUtil.UNREVIEWED_LABEL + "/" + 2);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java
index bfe6b8b..5cdb583 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java
@@ -14,13 +14,32 @@
 
 package com.google.gerrit.acceptance.api.project;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.DashboardInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.project.DashboardsCollection;
+import java.util.List;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
 public class DashboardIT extends AbstractDaemonTest {
+  @Before
+  public void setup() throws Exception {
+    allow("refs/meta/dashboards/*", Permission.CREATE, REGISTERED_USERS);
+  }
+
   @Test
   public void defaultDashboardDoesNotExist() throws Exception {
     exception.expect(ResourceNotFoundException.class);
@@ -32,4 +51,81 @@
     exception.expect(ResourceNotFoundException.class);
     gApi.projects().name(project.get()).dashboard("my:dashboard").get();
   }
+
+  @Test
+  public void getDashboard() throws Exception {
+    assertThat(dashboards()).isEmpty();
+    DashboardInfo info = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test");
+    DashboardInfo result = gApi.projects().name(project.get()).dashboard(info.id).get();
+    assertThat(result.id).isEqualTo(info.id);
+    assertThat(result.path).isEqualTo(info.path);
+    assertThat(result.ref).isEqualTo(info.ref);
+    assertThat(result.project).isEqualTo(project.get());
+    assertThat(result.definingProject).isEqualTo(project.get());
+    assertThat(dashboards()).hasSize(1);
+  }
+
+  @Test
+  public void setDefaultDashboard() throws Exception {
+    DashboardInfo info = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test");
+    assertThat(info.isDefault).isNull();
+    gApi.projects().name(project.get()).dashboard(info.id).setDefault();
+    assertThat(gApi.projects().name(project.get()).dashboard(info.id).get().isDefault).isTrue();
+    assertThat(gApi.projects().name(project.get()).defaultDashboard().get().id).isEqualTo(info.id);
+  }
+
+  @Test
+  public void replaceDefaultDashboard() throws Exception {
+    DashboardInfo d1 = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test1");
+    DashboardInfo d2 = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test2");
+    assertThat(d1.isDefault).isNull();
+    assertThat(d2.isDefault).isNull();
+    gApi.projects().name(project.get()).dashboard(d1.id).setDefault();
+    assertThat(gApi.projects().name(project.get()).dashboard(d1.id).get().isDefault).isTrue();
+    assertThat(gApi.projects().name(project.get()).dashboard(d2.id).get().isDefault).isNull();
+    assertThat(gApi.projects().name(project.get()).defaultDashboard().get().id).isEqualTo(d1.id);
+    gApi.projects().name(project.get()).dashboard(d2.id).setDefault();
+    assertThat(gApi.projects().name(project.get()).defaultDashboard().get().id).isEqualTo(d2.id);
+    assertThat(gApi.projects().name(project.get()).dashboard(d1.id).get().isDefault).isNull();
+    assertThat(gApi.projects().name(project.get()).dashboard(d2.id).get().isDefault).isTrue();
+  }
+
+  @Test
+  public void cannotGetDashboardWithInheritedForNonDefault() throws Exception {
+    DashboardInfo info = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test");
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("inherited flag can only be used with default");
+    gApi.projects().name(project.get()).dashboard(info.id).get(true);
+  }
+
+  private List<DashboardInfo> dashboards() throws Exception {
+    return gApi.projects().name(project.get()).dashboards().get();
+  }
+
+  private DashboardInfo createDashboard(String ref, String path) throws Exception {
+    DashboardInfo info = DashboardsCollection.newDashboardInfo(ref, path);
+    String canonicalRef = DashboardsCollection.normalizeDashboardRef(info.ref);
+    try {
+      gApi.projects().name(project.get()).branch(canonicalRef).create(new BranchInput());
+    } catch (ResourceConflictException e) {
+      // The branch already exists if this method has already been called once.
+      if (!e.getMessage().contains("already exists")) {
+        throw e;
+      }
+    }
+    try (Repository r = repoManager.openRepository(project)) {
+      TestRepository<Repository>.CommitBuilder cb =
+          new TestRepository<>(r).branch(canonicalRef).commit();
+      String content =
+          "[dashboard]\n"
+              + "Description = Test\n"
+              + "foreach = owner:self\n"
+              + "[section \"Mine\"]\n"
+              + "query = is:open";
+      cb.add(info.path, content);
+      RevCommit c = cb.create();
+      gApi.projects().name(project.get()).commit(c.name());
+    }
+    return info;
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 1fc04b4..481681e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -119,18 +119,12 @@
   boolean ignored() throws RestApiException;
 
   /**
-   * Mute or un-mute this change.
+   * Mark this change as reviewed/unreviewed.
    *
-   * @param mute mute the change if true
+   * @param reviewed flag to decide if this change should be marked as reviewed ({@code true}) or
+   *     unreviewed ({@code false})
    */
-  void mute(boolean mute) throws RestApiException;
-
-  /**
-   * Check if this change is muted.
-   *
-   * @return true if the change is muted.
-   */
-  boolean muted() throws RestApiException;
+  void markAsReviewed(boolean reviewed) throws RestApiException;
 
   /**
    * Create a new change that reverts this change.
@@ -583,12 +577,7 @@
     }
 
     @Override
-    public void mute(boolean mute) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public boolean muted() throws RestApiException {
+    public void markAsReviewed(boolean reviewed) throws RestApiException {
       throw new NotImplementedException();
     }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java
index a411e0e..3cde570 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java
@@ -23,6 +23,8 @@
 
   DashboardInfo get(boolean inherited) throws RestApiException;
 
+  void setDefault() throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -37,5 +39,10 @@
     public DashboardInfo get(boolean inherited) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void setDefault() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index a61b68a..3379edc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -154,6 +154,12 @@
    */
   DashboardApi defaultDashboard() throws RestApiException;
 
+  abstract class ListDashboardsRequest {
+    public abstract List<DashboardInfo> get() throws RestApiException;
+  }
+
+  ListDashboardsRequest dashboards() throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -273,5 +279,10 @@
     public DashboardApi defaultDashboard() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public ListDashboardsRequest dashboards() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 706482f..f802049 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -37,7 +37,6 @@
   public Timestamp submitted;
   public AccountInfo submitter;
   public Boolean starred;
-  public Boolean muted;
   public Collection<String> stars;
   public Boolean reviewed;
   public SubmitType submitType;
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 5ef3dce..866d74f 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
@@ -138,8 +138,6 @@
 
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
 
-  public final native boolean muted() /*-{ return this.muted ? true : false; }-*/;
-
   public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
 
   public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 0ee720a..9940cd9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.config.AuthConfig;
@@ -30,7 +29,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.ServletModule;
 import java.io.IOException;
@@ -59,7 +57,6 @@
     }
   }
 
-  private final Provider<ReviewDb> db;
   private final boolean enabled;
   private final DynamicItem<WebSession> session;
   private final PermissionBackend permissionBackend;
@@ -67,12 +64,10 @@
 
   @Inject
   RunAsFilter(
-      Provider<ReviewDb> db,
       AuthConfig config,
       DynamicItem<WebSession> session,
       PermissionBackend permissionBackend,
       AccountResolver accountResolver) {
-    this.db = db;
     this.enabled = config.isRunAsEnabled();
     this.session = session;
     this.permissionBackend = permissionBackend;
@@ -111,7 +106,7 @@
 
       Account target;
       try {
-        target = accountResolver.find(db.get(), runas);
+        target = accountResolver.find(runas);
       } catch (OrmException | IOException | ConfigInvalidException e) {
         log.warn("cannot resolve account for " + RUN_AS, e);
         replyError(req, res, SC_INTERNAL_SERVER_ERROR, "cannot resolve " + RUN_AS, e);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java
index 2f3d32f..c508b2d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java
@@ -15,11 +15,17 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.common.cache.Cache;
+import com.google.gerrit.common.TimeUtil;
+import java.io.IOException;
+import java.nio.file.FileSystems;
 import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
 
 class PolyGerritUiServlet extends ResourceServlet {
   private static final long serialVersionUID = 1L;
 
+  private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
+
   private final Path ui;
 
   PolyGerritUiServlet(Cache<Path, Resource> cache, Path ui) {
@@ -31,4 +37,16 @@
   protected Path getResourcePath(String pathInfo) {
     return ui.resolve(pathInfo);
   }
+
+  @Override
+  protected FileTime getLastModifiedTime(Path p) throws IOException {
+    if (ui.getFileSystem().equals(FileSystems.getDefault())) {
+      // Assets are being served from disk, so we can trust the mtime.
+      return super.getLastModifiedTime(p);
+    }
+    // Assume this FileSystem is serving from a WAR. All WAR outputs from the build process have
+    // mtimes of 1980/1/1, so we can't trust it, and return the initialization time of this class
+    // instead.
+    return NOW;
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 150acc6..94ee221 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -31,7 +31,6 @@
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.hash.Hashing;
-import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gwtexpui.server.CacheHeaders;
@@ -252,7 +251,7 @@
       return true;
     }
 
-    long lastModified = FileUtil.lastModified(p);
+    long lastModified = getLastModifiedTime(p).toMillis();
     if (req.getDateHeader(IF_MODIFIED_SINCE) >= lastModified) {
       rsp.setStatus(SC_NOT_MODIFIED);
       return true;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java
index 93bd5ae..3f6ff25 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java
@@ -15,12 +15,16 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.common.cache.Cache;
+import com.google.gerrit.common.TimeUtil;
 import java.nio.file.FileSystem;
 import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
 
 class WarDocServlet extends ResourceServlet {
   private static final long serialVersionUID = 1L;
 
+  private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
+
   private final FileSystem warFs;
 
   WarDocServlet(Cache<Path, Resource> cache, FileSystem warFs) {
@@ -32,4 +36,11 @@
   protected Path getResourcePath(String pathInfo) {
     return warFs.getPath("/Documentation/" + pathInfo);
   }
+
+  @Override
+  protected FileTime getLastModifiedTime(Path p) {
+    // Return initialization time of this class, since the WAR outputs from the build process all
+    // have mtimes of 1980/1/1.
+    return NOW;
+  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index a71a7fae..6b5c157 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -51,6 +51,7 @@
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.server.LibModuleLoader;
 import com.google.gerrit.server.StartupChecks;
+import com.google.gerrit.server.account.AccountDeactivator;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.change.ChangeCleanupRunner;
@@ -461,6 +462,7 @@
         });
     modules.add(new GarbageCollectionModule());
     if (!slave) {
+      modules.add(new AccountDeactivator.Module());
       modules.add(new ChangeCleanupRunner.Module());
     }
     modules.addAll(LibModuleLoader.loadModules(cfgInjector));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
index 13c24e0..a8cc0f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toSet;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.CharMatcher;
@@ -26,6 +27,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
@@ -49,6 +51,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
@@ -154,7 +157,8 @@
 
   public static final String DEFAULT_LABEL = "star";
   public static final String IGNORE_LABEL = "ignore";
-  public static final String MUTE_LABEL = "mute";
+  public static final String REVIEWED_LABEL = "reviewed";
+  public static final String UNREVIEWED_LABEL = "unreviewed";
   public static final ImmutableSortedSet<String> DEFAULT_LABELS =
       ImmutableSortedSet.of(DEFAULT_LABEL);
 
@@ -330,37 +334,41 @@
     return isIgnoredBy(rsrc.getChange().getId(), rsrc.getUser().asIdentifiedUser().getAccountId());
   }
 
-  private static String getMuteLabel(Change change) {
-    return MUTE_LABEL + "/" + change.currentPatchSetId().get();
+  private static String getReviewedLabel(Change change) {
+    return getReviewedLabel(change.currentPatchSetId().get());
   }
 
-  public void mute(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+  private static String getReviewedLabel(int ps) {
+    return REVIEWED_LABEL + "/" + ps;
+  }
+
+  private static String getUnreviewedLabel(Change change) {
+    return getUnreviewedLabel(change.currentPatchSetId().get());
+  }
+
+  private static String getUnreviewedLabel(int ps) {
+    return UNREVIEWED_LABEL + "/" + ps;
+  }
+
+  public void markAsReviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException {
     star(
         rsrc.getUser().asIdentifiedUser().getAccountId(),
         rsrc.getProject(),
         rsrc.getChange().getId(),
-        ImmutableSet.of(getMuteLabel(rsrc.getChange())),
-        ImmutableSet.of());
+        ImmutableSet.of(getReviewedLabel(rsrc.getChange())),
+        ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())));
   }
 
-  public void unmute(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+  public void markAsUnreviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException {
     star(
         rsrc.getUser().asIdentifiedUser().getAccountId(),
         rsrc.getProject(),
         rsrc.getChange().getId(),
-        ImmutableSet.of(),
-        ImmutableSet.of(getMuteLabel(rsrc.getChange())));
+        ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())),
+        ImmutableSet.of(getReviewedLabel(rsrc.getChange())));
   }
 
-  public boolean isMutedBy(Change change, Account.Id accountId) throws OrmException {
-    return getLabels(accountId, change.getId()).contains(getMuteLabel(change));
-  }
-
-  public boolean isMuted(ChangeResource rsrc) throws OrmException {
-    return isMutedBy(rsrc.getChange(), rsrc.getUser().asIdentifiedUser().getAccountId());
-  }
-
-  private static StarRef readLabels(Repository repo, String refName) throws IOException {
+  public static StarRef readLabels(Repository repo, String refName) throws IOException {
     Ref ref = repo.exactRef(refName);
     if (ref == null) {
       return StarRef.MISSING;
@@ -394,6 +402,25 @@
     if (labels.containsAll(ImmutableSet.of(DEFAULT_LABEL, IGNORE_LABEL))) {
       throw new MutuallyExclusiveLabelsException(DEFAULT_LABEL, IGNORE_LABEL);
     }
+
+    Set<Integer> reviewedPatchSets =
+        labels
+            .stream()
+            .filter(l -> l.startsWith(REVIEWED_LABEL))
+            .map(l -> Integer.valueOf(l.substring(REVIEWED_LABEL.length() + 1)))
+            .collect(toSet());
+    Set<Integer> unreviewedPatchSets =
+        labels
+            .stream()
+            .filter(l -> l.startsWith(UNREVIEWED_LABEL))
+            .map(l -> Integer.valueOf(l.substring(UNREVIEWED_LABEL.length() + 1)))
+            .collect(toSet());
+    Optional<Integer> ps =
+        Sets.intersection(reviewedPatchSets, unreviewedPatchSets).stream().findFirst();
+    if (ps.isPresent()) {
+      throw new MutuallyExclusiveLabelsException(
+          getReviewedLabel(ps.get()), getUnreviewedLabel(ps.get()));
+    }
   }
 
   private static void validateLabels(Collection<String> labels) throws InvalidLabelsException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java
new file mode 100644
index 0000000..c222756
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ScheduleConfig;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.query.account.AccountPredicates;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Runnable to enable scheduling account deactivations to run periodically */
+public class AccountDeactivator implements Runnable {
+  private static final Logger log = LoggerFactory.getLogger(AccountDeactivator.class);
+
+  public static class Module extends LifecycleModule {
+    @Override
+    protected void configure() {
+      listener().to(Lifecycle.class);
+    }
+  }
+
+  static class Lifecycle implements LifecycleListener {
+    private final WorkQueue queue;
+    private final AccountDeactivator deactivator;
+    private final boolean supportAutomaticAccountActivityUpdate;
+    private final ScheduleConfig scheduleConfig;
+
+    @Inject
+    Lifecycle(WorkQueue queue, AccountDeactivator deactivator, @GerritServerConfig Config cfg) {
+      this.queue = queue;
+      this.deactivator = deactivator;
+      scheduleConfig = new ScheduleConfig(cfg, "accountDeactivation");
+      supportAutomaticAccountActivityUpdate =
+          cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false);
+    }
+
+    @Override
+    public void start() {
+      if (!supportAutomaticAccountActivityUpdate) {
+        return;
+      }
+      long interval = scheduleConfig.getInterval();
+      long delay = scheduleConfig.getInitialDelay();
+      if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) {
+        log.info("Ignoring missing accountDeactivator schedule configuration");
+      } else if (delay < 0 || interval <= 0) {
+        log.warn(
+            String.format(
+                "Ignoring invalid accountDeactivator schedule configuration: %s", scheduleConfig));
+      } else {
+        queue
+            .getDefaultQueue()
+            .scheduleAtFixedRate(deactivator, delay, interval, TimeUnit.MILLISECONDS);
+      }
+    }
+
+    @Override
+    public void stop() {
+      // handled by WorkQueue.stop() already
+    }
+  }
+
+  private final Provider<InternalAccountQuery> accountQueryProvider;
+  private final Realm realm;
+  private final SetInactiveFlag sif;
+
+  @Inject
+  AccountDeactivator(
+      Provider<InternalAccountQuery> accountQueryProvider, SetInactiveFlag sif, Realm realm) {
+    this.accountQueryProvider = accountQueryProvider;
+    this.sif = sif;
+    this.realm = realm;
+  }
+
+  @Override
+  public void run() {
+    log.debug("Running account deactivations");
+    try {
+      int numberOfAccountsDeactivated = 0;
+      for (AccountState acc : accountQueryProvider.get().query(AccountPredicates.isActive())) {
+        log.debug("processing account " + acc.getUserName());
+        if (acc.getUserName() != null && !realm.isActive(acc.getUserName())) {
+          sif.deactivate(acc.getAccount().getId());
+          log.debug("deactivated accout " + acc.getUserName());
+          numberOfAccountsDeactivated++;
+        }
+      }
+      log.info(
+          "Deactivations complete, {} account(s) were deactivated", numberOfAccountsDeactivated);
+    } catch (Exception e) {
+      log.error("Failed to deactivate inactive accounts " + e.getMessage(), e);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "account deactivator";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index ec756bc..046b60a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -22,6 +22,8 @@
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -70,6 +72,8 @@
   private final ExternalIds externalIds;
   private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
   private final GroupsUpdate.Factory groupsUpdateFactory;
+  private final boolean autoUpdateAccountActiveStatus;
+  private final SetInactiveFlag setInactiveFlag;
 
   @Inject
   AccountManager(
@@ -86,7 +90,8 @@
       Provider<InternalAccountQuery> accountQueryProvider,
       ExternalIds externalIds,
       ExternalIdsUpdate.Server externalIdsUpdateFactory,
-      GroupsUpdate.Factory groupsUpdateFactory) {
+      GroupsUpdate.Factory groupsUpdateFactory,
+      SetInactiveFlag setInactiveFlag) {
     this.schema = schema;
     this.sequences = sequences;
     this.accounts = accounts;
@@ -102,6 +107,9 @@
     this.externalIds = externalIds;
     this.externalIdsUpdateFactory = externalIdsUpdateFactory;
     this.groupsUpdateFactory = groupsUpdateFactory;
+    this.autoUpdateAccountActiveStatus =
+        cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false);
+    this.setInactiveFlag = setInactiveFlag;
   }
 
   /** @return user identified by this external identity string */
@@ -122,8 +130,8 @@
    * @param who identity of the user, with any details we received about them.
    * @return the result of authenticating the user.
    * @throws AccountException the account does not exist, and cannot be created, or exists, but
-   *     cannot be located, or is inactive, or cannot be added to the admin group (only for the
-   *     first account).
+   *     cannot be located, is unable to be activated or deactivated, or is inactive, or cannot be
+   *     added to the admin group (only for the first account).
    */
   public AuthResult authenticate(AuthRequest who) throws AccountException, IOException {
     who = realm.authenticate(who);
@@ -138,6 +146,24 @@
 
         // Account exists
         Account act = byIdCache.get(id.accountId()).getAccount();
+        if (autoUpdateAccountActiveStatus && who.authProvidesAccountActiveStatus()) {
+          if (who.isActive() && !act.isActive()) {
+            try {
+              setInactiveFlag.activate(act.getId());
+              act = byIdCache.get(id.accountId()).getAccount();
+            } catch (ResourceNotFoundException e) {
+              throw new AccountException("Unable to activate account " + act.getId(), e);
+            }
+          } else if (!who.isActive() && act.isActive()) {
+            try {
+              setInactiveFlag.deactivate(act.getId());
+              act = byIdCache.get(id.accountId()).getAccount();
+            } catch (RestApiException e) {
+              throw new AccountException("Unable to deactivate account " + act.getId(), e);
+            }
+          }
+        }
+
         if (!act.isActive()) {
           throw new AccountException("Authentication error, account inactive");
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 94f63a7..a1cca06 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -17,7 +17,6 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -63,9 +62,8 @@
    * @return the single account that matches; null if no account matches or there are multiple
    *     candidates.
    */
-  public Account find(ReviewDb db, String nameOrEmail)
-      throws OrmException, IOException, ConfigInvalidException {
-    Set<Account.Id> r = findAll(db, nameOrEmail);
+  public Account find(String nameOrEmail) throws OrmException, IOException, ConfigInvalidException {
+    Set<Account.Id> r = findAll(nameOrEmail);
     if (r.size() == 1) {
       return byId.get(r.iterator().next()).getAccount();
     }
@@ -87,13 +85,12 @@
   /**
    * Find all accounts matching the name or name/email string.
    *
-   * @param db open database handle.
    * @param nameOrEmail a string of the format "Full Name &lt;email@example&gt;", just the email
    *     address ("email@example"), a full name ("Full Name"), an account id ("18419") or an user
    *     name ("username").
    * @return the accounts that match, empty collection if none. Never null.
    */
-  public Set<Account.Id> findAll(ReviewDb db, String nameOrEmail)
+  public Set<Account.Id> findAll(String nameOrEmail)
       throws OrmException, IOException, ConfigInvalidException {
     Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail);
     if (m.matches()) {
@@ -119,34 +116,30 @@
       }
     }
 
-    return findAllByNameOrEmail(db, nameOrEmail);
+    return findAllByNameOrEmail(nameOrEmail);
   }
 
   /**
    * Locate exactly one account matching the name or name/email string.
    *
-   * @param db open database handle.
    * @param nameOrEmail a string of the format "Full Name &lt;email@example&gt;", just the email
    *     address ("email@example"), a full name ("Full Name").
    * @return the single account that matches; null if no account matches or there are multiple
    *     candidates.
    */
-  public Account findByNameOrEmail(ReviewDb db, String nameOrEmail)
-      throws OrmException, IOException {
-    Set<Account.Id> r = findAllByNameOrEmail(db, nameOrEmail);
+  public Account findByNameOrEmail(String nameOrEmail) throws OrmException, IOException {
+    Set<Account.Id> r = findAllByNameOrEmail(nameOrEmail);
     return r.size() == 1 ? byId.get(r.iterator().next()).getAccount() : null;
   }
 
   /**
    * Locate exactly one account matching the name or name/email string.
    *
-   * @param db open database handle.
    * @param nameOrEmail a string of the format "Full Name &lt;email@example&gt;", just the email
    *     address ("email@example"), a full name ("Full Name").
    * @return the accounts that match, empty collection if none. Never null.
    */
-  public Set<Account.Id> findAllByNameOrEmail(ReviewDb db, String nameOrEmail)
-      throws OrmException, IOException {
+  public Set<Account.Id> findAllByNameOrEmail(String nameOrEmail) throws OrmException, IOException {
     int lt = nameOrEmail.indexOf('<');
     int gt = nameOrEmail.indexOf('>');
     if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index dcda816..19a8259 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -39,7 +38,6 @@
 @Singleton
 public class AccountsCollection
     implements RestCollection<TopLevelResource, AccountResource>, AcceptsCreate<TopLevelResource> {
-  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> self;
   private final AccountResolver resolver;
   private final AccountControl.Factory accountControlFactory;
@@ -50,7 +48,6 @@
 
   @Inject
   AccountsCollection(
-      Provider<ReviewDb> db,
       Provider<CurrentUser> self,
       AccountResolver resolver,
       AccountControl.Factory accountControlFactory,
@@ -58,7 +55,6 @@
       Provider<QueryAccounts> list,
       DynamicMap<RestView<AccountResource>> views,
       CreateAccount.Factory createAccountFactory) {
-    this.db = db;
     this.self = self;
     this.resolver = resolver;
     this.accountControlFactory = accountControlFactory;
@@ -144,7 +140,7 @@
       }
     }
 
-    Account match = resolver.find(db.get(), id);
+    Account match = resolver.find(id);
     if (match == null) {
       return null;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
index e654b8d..6647ca4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
@@ -63,6 +63,8 @@
   private boolean skipAuthentication;
   private String authPlugin;
   private String authProvider;
+  private boolean authProvidesAccountActiveStatus;
+  private boolean active;
 
   public AuthRequest(ExternalId.Key externalId) {
     this.externalId = externalId;
@@ -140,4 +142,20 @@
   public void setAuthProvider(String authProvider) {
     this.authProvider = authProvider;
   }
+
+  public boolean authProvidesAccountActiveStatus() {
+    return authProvidesAccountActiveStatus;
+  }
+
+  public void setAuthProvidesAccountActiveStatus(boolean authProvidesAccountActiveStatus) {
+    this.authProvidesAccountActiveStatus = authProvidesAccountActiveStatus;
+  }
+
+  public boolean isActive() {
+    return active;
+  }
+
+  public void setActive(Boolean isActive) {
+    this.active = isActive;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
index 0ff5342..43669c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -49,6 +49,6 @@
     if (self.get() == rsrc.getUser()) {
       throw new ResourceConflictException("cannot deactivate own account");
     }
-    return setInactiveFlag.deactivate(rsrc.getUser());
+    return setInactiveFlag.deactivate(rsrc.getUser().getAccountId());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
index 825ef10..7ce2ea8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -41,6 +41,6 @@
   @Override
   public Response<String> apply(AccountResource rsrc, Input input)
       throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
-    return setInactiveFlag.activate(rsrc.getUser());
+    return setInactiveFlag.activate(rsrc.getUser().getAccountId());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index b5e4cba..c375dd6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -19,6 +19,8 @@
 import com.google.gerrit.server.IdentifiedUser;
 import java.io.IOException;
 import java.util.Set;
+import javax.naming.NamingException;
+import javax.security.auth.login.LoginException;
 
 public interface Realm {
   /** Can the end-user modify this field of their own account? */
@@ -45,4 +47,15 @@
    * into an email address, and then locate the user by that email address.
    */
   Account.Id lookup(String accountName) throws IOException;
+
+  /**
+   * @return true if the account is active.
+   * @throws NamingException
+   * @throws LoginException
+   * @throws AccountException
+   */
+  default boolean isActive(@SuppressWarnings("unused") String username)
+      throws LoginException, NamingException, AccountException {
+    return true;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java
index 1698387..6e12c3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -36,14 +35,14 @@
     this.accountsUpdate = accountsUpdate;
   }
 
-  public Response<?> deactivate(IdentifiedUser user)
+  public Response<?> deactivate(Account.Id accountId)
       throws RestApiException, IOException, ConfigInvalidException {
     AtomicBoolean alreadyInactive = new AtomicBoolean(false);
     Account account =
         accountsUpdate
             .create()
             .update(
-                user.getAccountId(),
+                accountId,
                 a -> {
                   if (!a.isActive()) {
                     alreadyInactive.set(true);
@@ -60,14 +59,14 @@
     return Response.none();
   }
 
-  public Response<String> activate(IdentifiedUser user)
+  public Response<String> activate(Account.Id accountId)
       throws ResourceNotFoundException, IOException, ConfigInvalidException {
     AtomicBoolean alreadyActive = new AtomicBoolean(false);
     Account account =
         accountsUpdate
             .create()
             .update(
-                user.getAccountId(),
+                accountId,
                 a -> {
                   if (a.isActive()) {
                     alreadyActive.set(true);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index d43327f..0fba74a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -69,8 +69,9 @@
 import com.google.gerrit.server.change.ListChangeComments;
 import com.google.gerrit.server.change.ListChangeDrafts;
 import com.google.gerrit.server.change.ListChangeRobotComments;
+import com.google.gerrit.server.change.MarkAsReviewed;
+import com.google.gerrit.server.change.MarkAsUnreviewed;
 import com.google.gerrit.server.change.Move;
-import com.google.gerrit.server.change.Mute;
 import com.google.gerrit.server.change.PostHashtags;
 import com.google.gerrit.server.change.PostPrivate;
 import com.google.gerrit.server.change.PostReviewers;
@@ -88,7 +89,6 @@
 import com.google.gerrit.server.change.SubmittedTogether;
 import com.google.gerrit.server.change.SuggestChangeReviewers;
 import com.google.gerrit.server.change.Unignore;
-import com.google.gerrit.server.change.Unmute;
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -140,8 +140,8 @@
   private final DeletePrivate deletePrivate;
   private final Ignore ignore;
   private final Unignore unignore;
-  private final Mute mute;
-  private final Unmute unmute;
+  private final MarkAsReviewed markAsReviewed;
+  private final MarkAsUnreviewed markAsUnreviewed;
   private final SetWorkInProgress setWip;
   private final SetReadyForReview setReady;
   private final PutMessage putMessage;
@@ -185,8 +185,8 @@
       DeletePrivate deletePrivate,
       Ignore ignore,
       Unignore unignore,
-      Mute mute,
-      Unmute unmute,
+      MarkAsReviewed markAsReviewed,
+      MarkAsUnreviewed markAsUnreviewed,
       SetWorkInProgress setWip,
       SetReadyForReview setReady,
       PutMessage putMessage,
@@ -228,8 +228,8 @@
     this.deletePrivate = deletePrivate;
     this.ignore = ignore;
     this.unignore = unignore;
-    this.mute = mute;
-    this.unmute = unmute;
+    this.markAsReviewed = markAsReviewed;
+    this.markAsUnreviewed = markAsUnreviewed;
     this.setWip = setWip;
     this.setReady = setReady;
     this.putMessage = putMessage;
@@ -677,26 +677,18 @@
   }
 
   @Override
-  public void mute(boolean mute) throws RestApiException {
+  public void markAsReviewed(boolean reviewed) throws RestApiException {
     // TODO(dborowitz): Convert to RetryingRestModifyView. Needs to plumb BatchUpdate.Factory into
     // StarredChangesUtil.
     try {
-      if (mute) {
-        this.mute.apply(change, new Mute.Input());
+      if (reviewed) {
+        markAsReviewed.apply(change, new MarkAsReviewed.Input());
       } else {
-        unmute.apply(change, new Unmute.Input());
+        markAsUnreviewed.apply(change, new MarkAsUnreviewed.Input());
       }
     } catch (OrmException | IllegalLabelException e) {
-      throw asRestApiException("Cannot mute change", e);
-    }
-  }
-
-  @Override
-  public boolean muted() throws RestApiException {
-    try {
-      return stars.isMuted(change);
-    } catch (OrmException e) {
-      throw asRestApiException("Cannot check if muted", e);
+      throw asRestApiException(
+          "Cannot mark change as " + (reviewed ? "reviewed" : "unreviewed"), e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
index 58cb59e..f0a6009 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.extensions.api.projects.DashboardApi;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
+import com.google.gerrit.extensions.common.SetDashboardInput;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -26,6 +27,7 @@
 import com.google.gerrit.server.project.DashboardsCollection;
 import com.google.gerrit.server.project.GetDashboard;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.SetDashboard;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -39,6 +41,7 @@
 
   private final DashboardsCollection dashboards;
   private final Provider<GetDashboard> get;
+  private final SetDashboard set;
   private final ProjectResource project;
   private final String id;
 
@@ -46,10 +49,12 @@
   DashboardApiImpl(
       DashboardsCollection dashboards,
       Provider<GetDashboard> get,
+      SetDashboard set,
       @Assisted ProjectResource project,
       @Assisted String id) {
     this.dashboards = dashboards;
     this.get = get;
+    this.set = set;
     this.project = project;
     this.id = id;
   }
@@ -68,6 +73,17 @@
     }
   }
 
+  @Override
+  public void setDefault() throws RestApiException {
+    SetDashboardInput input = new SetDashboardInput();
+    input.id = id;
+    try {
+      set.apply(DashboardResource.projectDefault(project.getControl()), input);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set default dashboard", e);
+    }
+  }
+
   private DashboardResource resource()
       throws ResourceNotFoundException, IOException, ConfigInvalidException,
           PermissionBackendException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 5012280..89c92d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 import static com.google.gerrit.server.project.DashboardsCollection.DEFAULT_DASHBOARD_NAME;
+import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
@@ -28,6 +29,7 @@
 import com.google.gerrit.extensions.api.projects.ConfigInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.DashboardApi;
+import com.google.gerrit.extensions.api.projects.DashboardInfo;
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
 import com.google.gerrit.extensions.api.projects.DeleteTagsInput;
 import com.google.gerrit.extensions.api.projects.DescriptionInput;
@@ -39,6 +41,7 @@
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -58,6 +61,7 @@
 import com.google.gerrit.server.project.GetDescription;
 import com.google.gerrit.server.project.ListBranches;
 import com.google.gerrit.server.project.ListChildProjects;
+import com.google.gerrit.server.project.ListDashboards;
 import com.google.gerrit.server.project.ListTags;
 import com.google.gerrit.server.project.ProjectJson;
 import com.google.gerrit.server.project.ProjectResource;
@@ -68,6 +72,7 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
+import java.util.Collections;
 import java.util.List;
 
 public class ProjectApiImpl implements ProjectApi {
@@ -104,6 +109,7 @@
   private final CommitApiImpl.Factory commitApi;
   private final DashboardApiImpl.Factory dashboardApi;
   private final CheckAccess checkAccess;
+  private final Provider<ListDashboards> listDashboards;
 
   @AssistedInject
   ProjectApiImpl(
@@ -132,6 +138,7 @@
       CommitApiImpl.Factory commitApi,
       DashboardApiImpl.Factory dashboardApi,
       CheckAccess checkAccess,
+      Provider<ListDashboards> listDashboards,
       @Assisted ProjectResource project) {
     this(
         user,
@@ -160,6 +167,7 @@
         commitApi,
         dashboardApi,
         checkAccess,
+        listDashboards,
         null);
   }
 
@@ -190,6 +198,7 @@
       CommitApiImpl.Factory commitApi,
       DashboardApiImpl.Factory dashboardApi,
       CheckAccess checkAccess,
+      Provider<ListDashboards> listDashboards,
       @Assisted String name) {
     this(
         user,
@@ -218,6 +227,7 @@
         commitApi,
         dashboardApi,
         checkAccess,
+        listDashboards,
         name);
   }
 
@@ -248,6 +258,7 @@
       CommitApiImpl.Factory commitApi,
       DashboardApiImpl.Factory dashboardApi,
       CheckAccess checkAccess,
+      Provider<ListDashboards> listDashboards,
       String name) {
     this.user = user;
     this.permissionBackend = permissionBackend;
@@ -275,6 +286,7 @@
     this.createAccessChange = createAccessChange;
     this.dashboardApi = dashboardApi;
     this.checkAccess = checkAccess;
+    this.listDashboards = listDashboards;
     this.name = name;
   }
 
@@ -473,6 +485,27 @@
     return dashboard(DEFAULT_DASHBOARD_NAME);
   }
 
+  @Override
+  public ListDashboardsRequest dashboards() throws RestApiException {
+    return new ListDashboardsRequest() {
+      @Override
+      public List<DashboardInfo> get() throws RestApiException {
+        try {
+          List<?> r = listDashboards.get().apply(checkExists());
+          if (r.isEmpty()) {
+            return Collections.emptyList();
+          }
+          if (r.get(0) instanceof DashboardInfo) {
+            return r.stream().map(i -> (DashboardInfo) i).collect(toList());
+          }
+          throw new NotImplementedException("list with inheritance");
+        } catch (Exception e) {
+          throw asRestApiException("Cannot list dashboards", e);
+        }
+      }
+    };
+  }
+
   private ProjectResource checkExists() throws ResourceNotFoundException {
     if (project == null) {
       throw new ResourceNotFoundException(name);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index ce31cac..988b9df7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResolver;
@@ -24,7 +23,6 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -36,14 +34,12 @@
 import org.kohsuke.args4j.spi.Setter;
 
 public class AccountIdHandler extends OptionHandler<Account.Id> {
-  private final Provider<ReviewDb> db;
   private final AccountResolver accountResolver;
   private final AccountManager accountManager;
   private final AuthType authType;
 
   @Inject
   public AccountIdHandler(
-      Provider<ReviewDb> db,
       AccountResolver accountResolver,
       AccountManager accountManager,
       AuthConfig authConfig,
@@ -51,7 +47,6 @@
       @Assisted OptionDef option,
       @Assisted Setter<Account.Id> setter) {
     super(parser, option, setter);
-    this.db = db;
     this.accountResolver = accountResolver;
     this.accountManager = accountManager;
     this.authType = authConfig.getAuthType();
@@ -62,7 +57,7 @@
     String token = params.getParameter(0);
     Account.Id accountId;
     try {
-      Account a = accountResolver.find(db.get(), token);
+      Account a = accountResolver.find(token);
       if (a != null) {
         accountId = a.getId();
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index a34e3fc..ec803e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
+import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -232,7 +233,15 @@
       }
       try {
         final Helper.LdapSchema schema = helper.getSchema(ctx);
-        final LdapQuery.Result m = helper.findAccount(schema, ctx, username, fetchMemberOfEagerly);
+        LdapQuery.Result m;
+        who.setAuthProvidesAccountActiveStatus(true);
+        try {
+          m = helper.findAccount(schema, ctx, username, fetchMemberOfEagerly);
+          who.setActive(true);
+        } catch (NoSuchUserException e) {
+          who.setActive(false);
+          return who;
+        }
 
         if (authConfig.getAuthType() == AuthType.LDAP && !who.isSkipAuthentication()) {
           // We found the user account, but we need to verify
@@ -314,6 +323,19 @@
     }
   }
 
+  @Override
+  public boolean isActive(String username)
+      throws LoginException, NamingException, AccountException {
+    try {
+      DirContext ctx = helper.open();
+      Helper.LdapSchema schema = helper.getSchema(ctx);
+      helper.findAccount(schema, ctx, username, false);
+    } catch (NoSuchUserException e) {
+      return false;
+    }
+    return true;
+  }
+
   static class UserLoader extends CacheLoader<String, Optional<Account.Id>> {
     private final ExternalIds externalIds;
 
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 e2237f3..8dc53bc 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
@@ -551,22 +551,13 @@
     if (user.isIdentifiedUser()) {
       Collection<String> stars = cd.stars(user.getAccountId());
       out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null;
-      out.muted =
-          stars.contains(StarredChangesUtil.MUTE_LABEL + "/" + cd.currentPatchSet().getPatchSetId())
-              ? true
-              : null;
       if (!stars.isEmpty()) {
         out.stars = stars;
       }
     }
 
     if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) {
-      Account.Id accountId = user.getAccountId();
-      if (out.muted != null) {
-        out.reviewed = true;
-      } else {
-        out.reviewed = cd.reviewedBy().contains(accountId) ? true : null;
-      }
+      out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
     }
 
     out.labels = labelsFor(perm, cd, has(LABELS), has(DETAILED_LABELS));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsReviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsReviewed.java
new file mode 100644
index 0000000..265b2b0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsReviewed.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+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.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class MarkAsReviewed
+    implements RestModifyView<ChangeResource, MarkAsReviewed.Input>, UiAction<ChangeResource> {
+  private static final Logger log = LoggerFactory.getLogger(MarkAsReviewed.class);
+
+  public static class Input {}
+
+  private final Provider<ReviewDb> dbProvider;
+  private final ChangeData.Factory changeDataFactory;
+  private final StarredChangesUtil stars;
+
+  @Inject
+  MarkAsReviewed(
+      Provider<ReviewDb> dbProvider,
+      ChangeData.Factory changeDataFactory,
+      StarredChangesUtil stars) {
+    this.dbProvider = dbProvider;
+    this.changeDataFactory = changeDataFactory;
+    this.stars = stars;
+  }
+
+  @Override
+  public Description getDescription(ChangeResource rsrc) {
+    return new UiAction.Description()
+        .setLabel("Mark Reviewed")
+        .setTitle("Mark the change as reviewed to unhighlight it in the dashboard")
+        .setVisible(!isReviewed(rsrc));
+  }
+
+  @Override
+  public Response<String> apply(ChangeResource rsrc, Input input)
+      throws RestApiException, OrmException, IllegalLabelException {
+    stars.markAsReviewed(rsrc);
+    return Response.ok("");
+  }
+
+  private boolean isReviewed(ChangeResource rsrc) {
+    try {
+      return changeDataFactory
+          .create(dbProvider.get(), rsrc.getNotes())
+          .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
+    } catch (OrmException e) {
+      log.error("failed to check if change is reviewed", e);
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsUnreviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsUnreviewed.java
new file mode 100644
index 0000000..6de84ee
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsUnreviewed.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class MarkAsUnreviewed
+    implements RestModifyView<ChangeResource, MarkAsUnreviewed.Input>, UiAction<ChangeResource> {
+  private static final Logger log = LoggerFactory.getLogger(MarkAsUnreviewed.class);
+
+  public static class Input {}
+
+  private final Provider<ReviewDb> dbProvider;
+  private final ChangeData.Factory changeDataFactory;
+  private final StarredChangesUtil stars;
+
+  @Inject
+  MarkAsUnreviewed(
+      Provider<ReviewDb> dbProvider,
+      ChangeData.Factory changeDataFactory,
+      StarredChangesUtil stars) {
+    this.dbProvider = dbProvider;
+    this.changeDataFactory = changeDataFactory;
+    this.stars = stars;
+  }
+
+  @Override
+  public Description getDescription(ChangeResource rsrc) {
+    return new UiAction.Description()
+        .setLabel("Mark Unreviewed")
+        .setTitle("Mark the change as unreviewed to highlight it in the dashboard")
+        .setVisible(isReviewed(rsrc));
+  }
+
+  @Override
+  public Response<String> apply(ChangeResource rsrc, Input input)
+      throws OrmException, IllegalLabelException {
+    stars.markAsUnreviewed(rsrc);
+    return Response.ok("");
+  }
+
+  private boolean isReviewed(ChangeResource rsrc) {
+    try {
+      return changeDataFactory
+          .create(dbProvider.get(), rsrc.getNotes())
+          .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
+    } catch (OrmException e) {
+      log.error("failed to check if change is reviewed", e);
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index f3a6c66..f648d5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -90,8 +90,8 @@
     delete(CHANGE_KIND, "private").to(DeletePrivate.class);
     put(CHANGE_KIND, "ignore").to(Ignore.class);
     put(CHANGE_KIND, "unignore").to(Unignore.class);
-    put(CHANGE_KIND, "mute").to(Mute.class);
-    put(CHANGE_KIND, "unmute").to(Unmute.class);
+    put(CHANGE_KIND, "reviewed").to(MarkAsReviewed.class);
+    put(CHANGE_KIND, "unreviewed").to(MarkAsUnreviewed.class);
     post(CHANGE_KIND, "wip").to(SetWorkInProgress.class);
     post(CHANGE_KIND, "ready").to(SetReadyForReview.class);
     put(CHANGE_KIND, "message").to(PutMessage.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
deleted file mode 100644
index 9da993b..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.extensions.restapi.BadRequestException;
-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.extensions.webui.UiAction;
-import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Singleton
-public class Mute implements RestModifyView<ChangeResource, Mute.Input>, UiAction<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(Mute.class);
-
-  public static class Input {}
-
-  private final StarredChangesUtil stars;
-
-  @Inject
-  Mute(StarredChangesUtil stars) {
-    this.stars = stars;
-  }
-
-  @Override
-  public Description getDescription(ChangeResource rsrc) {
-    return new UiAction.Description()
-        .setLabel("Mute")
-        .setTitle("Mute the change to unhighlight it in the dashboard")
-        .setVisible(isMuteable(rsrc));
-  }
-
-  @Override
-  public Response<String> apply(ChangeResource rsrc, Input input)
-      throws RestApiException, OrmException, IllegalLabelException {
-    if (rsrc.isUserOwner()) {
-      throw new BadRequestException("cannot mute own change");
-    }
-    if (!isMuted(rsrc)) {
-      stars.mute(rsrc);
-    }
-    return Response.ok("");
-  }
-
-  private boolean isMuted(ChangeResource rsrc) {
-    try {
-      return stars.isMuted(rsrc);
-    } catch (OrmException e) {
-      log.error("failed to check muted star", e);
-    }
-    return false;
-  }
-
-  private boolean isMuteable(ChangeResource rsrc) {
-    try {
-      return !rsrc.isUserOwner() && !isMuted(rsrc) && !stars.isIgnored(rsrc);
-    } catch (OrmException e) {
-      log.error("failed to check ignored star", e);
-    }
-    return false;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
index ccc7587..c29faee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
@@ -25,11 +25,9 @@
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -40,12 +38,10 @@
 
 @Singleton
 public class NotifyUtil {
-  private final Provider<ReviewDb> dbProvider;
   private final AccountResolver accountResolver;
 
   @Inject
-  NotifyUtil(Provider<ReviewDb> dbProvider, AccountResolver accountResolver) {
-    this.dbProvider = dbProvider;
+  NotifyUtil(AccountResolver accountResolver) {
     this.accountResolver = accountResolver;
   }
 
@@ -90,19 +86,19 @@
         if (m == null) {
           m = MultimapBuilder.hashKeys().arrayListValues().build();
         }
-        m.putAll(e.getKey(), find(dbProvider.get(), accounts));
+        m.putAll(e.getKey(), find(accounts));
       }
     }
 
     return m != null ? m : ImmutableListMultimap.of();
   }
 
-  private List<Account.Id> find(ReviewDb db, List<String> nameOrEmails)
+  private List<Account.Id> find(List<String> nameOrEmails)
       throws OrmException, BadRequestException, IOException, ConfigInvalidException {
     List<String> missing = new ArrayList<>(nameOrEmails.size());
     List<Account.Id> r = new ArrayList<>(nameOrEmails.size());
     for (String nameOrEmail : nameOrEmails) {
-      Account a = accountResolver.find(db, nameOrEmail);
+      Account a = accountResolver.find(nameOrEmail);
       if (a != null) {
         r.add(a.getId());
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
deleted file mode 100644
index 16d6d88..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Singleton
-public class Unmute
-    implements RestModifyView<ChangeResource, Unmute.Input>, UiAction<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(Unmute.class);
-
-  public static class Input {}
-
-  private final StarredChangesUtil stars;
-
-  @Inject
-  Unmute(StarredChangesUtil stars) {
-    this.stars = stars;
-  }
-
-  @Override
-  public Description getDescription(ChangeResource rsrc) {
-    return new UiAction.Description()
-        .setLabel("Unmute")
-        .setTitle("Unmute the change")
-        .setVisible(isMuted(rsrc));
-  }
-
-  @Override
-  public Response<String> apply(ChangeResource rsrc, Input input)
-      throws OrmException, IllegalLabelException {
-    if (isMuted(rsrc)) {
-      stars.unmute(rsrc);
-    }
-    return Response.ok("");
-  }
-
-  private boolean isMuted(ChangeResource rsrc) {
-    try {
-      return stars.isMuted(rsrc);
-    } catch (OrmException e) {
-      log.error("failed to check muted star", e);
-    }
-    return false;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 901084e..0e4e8b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -81,6 +81,7 @@
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.account.AccountDeactivator;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
@@ -279,6 +280,7 @@
 
     bind(GcConfig.class);
     bind(ChangeCleanupConfig.class);
+    bind(AccountDeactivator.class);
 
     bind(ApprovalsUtil.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 3c8bcea..b7aa416 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -2126,7 +2126,7 @@
         checkNotNull(magicBranch);
         recipients.add(magicBranch.getMailRecipients());
         approvals = magicBranch.labels;
-        recipients.add(getRecipientsFromFooters(db, accountResolver, footerLines));
+        recipients.add(getRecipientsFromFooters(accountResolver, footerLines));
         recipients.remove(me);
         StringBuilder msg =
             new StringBuilder(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index bcb8564..4455aed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -291,7 +291,7 @@
             psDescription);
 
     update.setPsDescription(psDescription);
-    recipients.add(getRecipientsFromFooters(ctx.getDb(), accountResolver, commit.getFooterLines()));
+    recipients.add(getRecipientsFromFooters(accountResolver, commit.getFooterLines()));
     recipients.remove(ctx.getAccountId());
     ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getNotes());
     MailRecipients oldRecipients = getRecipientsFromReviewers(cd.reviewers());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index 96024d2..b6bcb3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -152,7 +152,7 @@
         case HTTP_LDAP:
         case CLIENT_SSL_CERT_LDAP:
         case LDAP:
-          if (accountResolver.find(db.get(), nameOrEmailOrId) == null) {
+          if (accountResolver.find(nameOrEmailOrId) == null) {
             // account does not exist, try to create it
             Account a = createAccountByLdap(nameOrEmailOrId);
             if (a != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index c103c89..95bdaab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -91,7 +91,10 @@
   @Deprecated static final Schema<ChangeData> V46 = schema(V45);
 
   // Removal of draft change workflow requires reindexing
-  static final Schema<ChangeData> V47 = schema(V46);
+  @Deprecated static final Schema<ChangeData> V47 = schema(V46);
+
+  // Rename of star label 'mute' to 'reviewed' requires reindexing
+  static final Schema<ChangeData> V48 = schema(V47);
 
   public static final String NAME = "changes";
   public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
index be1c9f5..0487cc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gwtorm.server.OrmException;
@@ -39,15 +38,15 @@
       DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss ZZZ");
 
   public static MailRecipients getRecipientsFromFooters(
-      ReviewDb db, AccountResolver accountResolver, List<FooterLine> footerLines)
+      AccountResolver accountResolver, List<FooterLine> footerLines)
       throws OrmException, IOException {
     MailRecipients recipients = new MailRecipients();
     for (FooterLine footerLine : footerLines) {
       try {
         if (isReviewer(footerLine)) {
-          recipients.reviewers.add(toAccountId(db, accountResolver, footerLine.getValue().trim()));
+          recipients.reviewers.add(toAccountId(accountResolver, footerLine.getValue().trim()));
         } else if (footerLine.matches(FooterKey.CC)) {
-          recipients.cc.add(toAccountId(db, accountResolver, footerLine.getValue().trim()));
+          recipients.cc.add(toAccountId(accountResolver, footerLine.getValue().trim()));
         }
       } catch (NoSuchAccountException e) {
         continue;
@@ -63,10 +62,9 @@
     return recipients;
   }
 
-  private static Account.Id toAccountId(
-      ReviewDb db, AccountResolver accountResolver, String nameOrEmail)
+  private static Account.Id toAccountId(AccountResolver accountResolver, String nameOrEmail)
       throws OrmException, NoSuchAccountException, IOException {
-    Account a = accountResolver.findByNameOrEmail(db, nameOrEmail);
+    Account a = accountResolver.findByNameOrEmail(nameOrEmail);
     if (a == null) {
       throw new NoSuchAccountException("\"" + nameOrEmail + "\" is not registered");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java
index 281e37e..b8d3fbc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -34,7 +33,6 @@
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import javax.servlet.http.HttpServletResponse;
@@ -43,18 +41,15 @@
 @Singleton
 public class CheckAccess implements RestModifyView<ProjectResource, AccessCheckInput> {
   private final AccountResolver accountResolver;
-  private final Provider<ReviewDb> db;
   private final IdentifiedUser.GenericFactory userFactory;
   private final PermissionBackend permissionBackend;
 
   @Inject
   CheckAccess(
       AccountResolver resolver,
-      Provider<ReviewDb> db,
       IdentifiedUser.GenericFactory userFactory,
       PermissionBackend permissionBackend) {
     this.accountResolver = resolver;
-    this.db = db;
     this.userFactory = userFactory;
     this.permissionBackend = permissionBackend;
   }
@@ -72,7 +67,7 @@
       throw new BadRequestException("input requires 'account'");
     }
 
-    Account match = accountResolver.find(db.get(), input.account);
+    Account match = accountResolver.find(input.account);
     if (match == null) {
       throw new UnprocessableEntityException(
           String.format("cannot find account %s", input.account));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
index ea2935d..d43a066 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -173,7 +173,7 @@
     return views;
   }
 
-  static DashboardInfo newDashboardInfo(String ref, String path) {
+  public static DashboardInfo newDashboardInfo(String ref, String path) {
     DashboardInfo info = new DashboardInfo();
     info.ref = ref;
     info.path = path;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
index 7296311..958de55 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
@@ -16,12 +16,9 @@
 
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
 import com.google.gerrit.extensions.common.SetDashboardInput;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 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.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
@@ -40,9 +37,7 @@
 
   @Override
   public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-          ResourceNotFoundException, MethodNotAllowedException, IOException,
-          PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     if (resource.isProjectDefault()) {
       SetDashboardInput in = new SetDashboardInput();
       in.commitMessage = input != null ? input.commitMessage : null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
index adca214..cdf23bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
@@ -21,9 +21,11 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -51,11 +53,9 @@
 
   @Override
   public DashboardInfo apply(DashboardResource resource)
-      throws ResourceNotFoundException, ResourceConflictException, IOException,
-          PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     if (inherited && !resource.isProjectDefault()) {
-      // inherited flag can only be used with default.
-      throw new ResourceNotFoundException("inherited");
+      throw new BadRequestException("inherited flag can only be used with default");
     }
 
     String project = resource.getControl().getProject().getName();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
index 1d3c58c..6960b47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -46,7 +46,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-class ListDashboards implements RestReadView<ProjectResource> {
+public class ListDashboards implements RestReadView<ProjectResource> {
   private static final Logger log = LoggerFactory.getLogger(ListDashboards.class);
 
   private final GitRepositoryManager gitManager;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
index 9222322..21ec077 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
@@ -16,12 +16,9 @@
 
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
 import com.google.gerrit.extensions.common.SetDashboardInput;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 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.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
@@ -30,7 +27,7 @@
 import java.io.IOException;
 
 @Singleton
-class SetDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
+public class SetDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
   private final Provider<SetDefaultDashboard> defaultSetter;
 
   @Inject
@@ -40,9 +37,7 @@
 
   @Override
   public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-          MethodNotAllowedException, ResourceNotFoundException, IOException,
-          PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     if (resource.isProjectDefault()) {
       return defaultSetter.get().apply(resource, input);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
index 256b6f2..9aa9ae7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 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.Project;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -59,8 +60,7 @@
 
   @Override
   public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-          ResourceNotFoundException, IOException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     if (input == null) {
       input = new SetDashboardInput(); // Delete would set input to null.
     }
@@ -132,8 +132,7 @@
 
     @Override
     public Response<DashboardInfo> apply(ProjectResource resource, SetDashboardInput input)
-        throws AuthException, BadRequestException, ResourceConflictException,
-            ResourceNotFoundException, IOException, PermissionBackendException {
+        throws RestApiException, IOException, PermissionBackendException {
       SetDefaultDashboard set = setDefault.get();
       set.inherited = inherited;
       return set.apply(DashboardResource.projectDefault(resource.getControl()), input);
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 40b384d..2a71258 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
@@ -1084,6 +1084,22 @@
     return draftsByUser;
   }
 
+  public boolean isReviewedBy(Account.Id accountId) throws OrmException {
+    Collection<String> stars = stars(accountId);
+
+    if (stars.contains(
+        StarredChangesUtil.REVIEWED_LABEL + "/" + currentPatchSet().getPatchSetId())) {
+      return true;
+    }
+
+    if (stars.contains(
+        StarredChangesUtil.UNREVIEWED_LABEL + "/" + currentPatchSet().getPatchSetId())) {
+      return false;
+    }
+
+    return reviewedBy().contains(accountId);
+  }
+
   public Set<Account.Id> reviewedBy() throws OrmException {
     if (reviewedBy == null) {
       if (!lazyLoad) {
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 beff5f2..1ae579e 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
@@ -901,7 +901,7 @@
     if (isSelf(who)) {
       return is_visible();
     }
-    Set<Account.Id> m = args.accountResolver.findAll(args.db.get(), who);
+    Set<Account.Id> m = args.accountResolver.findAll(who);
     if (!m.isEmpty()) {
       List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
       for (Account.Id id : m) {
@@ -1258,7 +1258,7 @@
     if (isSelf(who)) {
       return Collections.singleton(self());
     }
-    Set<Account.Id> matches = args.accountResolver.findAll(args.db.get(), who);
+    Set<Account.Id> matches = args.accountResolver.findAll(who);
     if (matches.isEmpty()) {
       throw error("User " + who + " not found");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 6bd6e24..057cc44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
@@ -38,7 +37,6 @@
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
@@ -58,7 +56,6 @@
       new QueryBuilder.Definition<>(GroupQueryBuilder.class);
 
   public static class Arguments {
-    final Provider<ReviewDb> db;
     final GroupIndex groupIndex;
     final GroupCache groupCache;
     final GroupBackend groupBackend;
@@ -66,12 +63,10 @@
 
     @Inject
     Arguments(
-        Provider<ReviewDb> db,
         GroupIndexCollection groupIndexCollection,
         GroupCache groupCache,
         GroupBackend groupBackend,
         AccountResolver accountResolver) {
-      this.db = db;
       this.groupIndex = groupIndexCollection.getSearchIndex();
       this.groupCache = groupCache;
       this.groupBackend = groupBackend;
@@ -189,7 +184,7 @@
 
   private Set<Account.Id> parseAccount(String nameOrEmail)
       throws QueryParseException, OrmException, IOException, ConfigInvalidException {
-    Set<Account.Id> foundAccounts = args.accountResolver.findAll(args.db.get(), nameOrEmail);
+    Set<Account.Id> foundAccounts = args.accountResolver.findAll(nameOrEmail);
     if (foundAccounts.isEmpty()) {
       throw error("User " + nameOrEmail + " not found");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index e92b003..d1cbad6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_160> C = Schema_160.class;
+  public static final Class<Schema_161> C = Schema_161.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_161.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_161.java
new file mode 100644
index 0000000..407492d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_161.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
+import com.google.gerrit.server.StarredChangesUtil.StarRef;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+public class Schema_161 extends SchemaVersion {
+  private static final String MUTE_LABEL = "mute";
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+
+  @Inject
+  Schema_161(
+      Provider<Schema_160> prior, GitRepositoryManager repoManager, AllUsersName allUsersName) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    try (Repository git = repoManager.openRepository(allUsersName);
+        RevWalk rw = new RevWalk(git)) {
+      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
+      for (Ref ref : git.getRefDatabase().getRefs(RefNames.REFS_STARRED_CHANGES).values()) {
+        StarRef starRef = StarredChangesUtil.readLabels(git, ref.getName());
+        if (starRef.labels().contains(MUTE_LABEL)) {
+          ObjectId id =
+              StarredChangesUtil.writeLabels(
+                  git,
+                  starRef
+                      .labels()
+                      .stream()
+                      .map(l -> l.equals(MUTE_LABEL) ? StarredChangesUtil.REVIEWED_LABEL : l)
+                      .collect(toList()));
+          bru.addCommand(new ReceiveCommand(ObjectId.zeroId(), id, ref.getName()));
+        }
+      }
+      bru.execute(rw, new TextProgressMonitor());
+    } catch (IOException | IllegalLabelException ex) {
+      throw new OrmException(ex);
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index bbe736d..275da7c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 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.account.AccountResolver;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.VisibleRefFilter;
@@ -51,7 +50,6 @@
   @Inject private AccountResolver accountResolver;
   @Inject private OneOffRequestContext requestContext;
   @Inject private VisibleRefFilter.Factory refFilterFactory;
-  @Inject private ReviewDb db;
   @Inject private GitRepositoryManager repoManager;
 
   @Option(
@@ -79,7 +77,7 @@
   protected void run() throws Failure {
     Account userAccount;
     try {
-      userAccount = accountResolver.find(db, userName);
+      userAccount = accountResolver.find(userName);
     } catch (OrmException | IOException | ConfigInvalidException e) {
       throw die(e);
     }
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 2d4c1d1..9a02fcd 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.pgm.util.LogFileCompressor;
 import com.google.gerrit.server.LibModuleLoader;
 import com.google.gerrit.server.StartupChecks;
+import com.google.gerrit.server.account.AccountDeactivator;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.change.ChangeCleanupRunner;
@@ -365,6 +366,7 @@
         });
     modules.add(new GarbageCollectionModule());
     modules.add(new ChangeCleanupRunner.Module());
+    modules.add(new AccountDeactivator.Module());
     modules.addAll(LibModuleLoader.loadModules(cfgInjector));
     return cfgInjector.createChildInjector(modules);
   }
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
new file mode 100644
index 0000000..0148377
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
@@ -0,0 +1,37 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<script>
+(function(window) {
+  'use strict';
+
+  window.Gerrit = window.Gerrit || {};
+
+  /** @polymerBehavior Gerrit.AsyncForeachBehavior */
+  Gerrit.AsyncForeachBehavior = {
+    /**
+     * @template T
+     * @param {!Array<T>} array
+     * @param {!Function} fn
+     * @return {!Promise<undefined>}
+     */
+    asyncForeach(array, fn) {
+      if (!array.length) { return Promise.resolve(); }
+      return fn(array[0]).then(() => this.asyncForeach(array.slice(1), fn));
+    },
+  };
+})(window);
+</script>
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
new file mode 100644
index 0000000..ba15ad7
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>async-foreach-behavior</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../test/common-test-setup.html"/>
+<link rel="import" href="async-foreach-behavior.html">
+
+<script>
+  suite('async-foreach-behavior tests', () => {
+    test('loops over each item', () => {
+      const fn = sinon.stub().returns(Promise.resolve());
+      return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+          .then(() => {
+            assert.isTrue(fn.calledThrice);
+            assert.equal(fn.getCall(0).args[0], 1);
+            assert.equal(fn.getCall(1).args[0], 2);
+            assert.equal(fn.getCall(2).args[0], 3);
+          });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index 9936730..13c232e 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -156,7 +156,6 @@
                 };
               });
       }
-      patchNums.sort((a, b) => { return a.num - b.num; });
       return Gerrit.PatchSetBehavior._computeWipForPatchSets(change, patchNums);
     },
 
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
index d63b961..f3db039 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
@@ -13,6 +13,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
+<script src="../../scripts/util.js"></script>
 <script>
 (function(window) {
   'use strict';
@@ -20,6 +21,10 @@
   window.Gerrit = window.Gerrit || {};
   /** @polymerBehavior Gerrit.PathListBehavior */
   Gerrit.PathListBehavior = {
+
+    COMMIT_MESSAGE_PATH: '/COMMIT_MSG',
+    MERGE_LIST_PATH: '/MERGE_LIST',
+
     /**
      * @param {string} a
      * @param {string} b
@@ -27,20 +32,18 @@
      */
     specialFilePathCompare(a, b) {
       // The commit message always goes first.
-      const COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
-      if (a === COMMIT_MESSAGE_PATH) {
+      if (a === Gerrit.PathListBehavior.COMMIT_MESSAGE_PATH) {
         return -1;
       }
-      if (b === COMMIT_MESSAGE_PATH) {
+      if (b === Gerrit.PathListBehavior.COMMIT_MESSAGE_PATH) {
         return 1;
       }
 
       // The merge list always comes next.
-      const MERGE_LIST_PATH = '/MERGE_LIST';
-      if (a === MERGE_LIST_PATH) {
+      if (a === Gerrit.PathListBehavior.MERGE_LIST_PATH) {
         return -1;
       }
-      if (b === MERGE_LIST_PATH) {
+      if (b === Gerrit.PathListBehavior.MERGE_LIST_PATH) {
         return 1;
       }
 
@@ -67,6 +70,40 @@
       }
       return aFile.localeCompare(bFile) || a.localeCompare(b);
     },
+
+    computeDisplayPath(path) {
+      if (path === Gerrit.PathListBehavior.COMMIT_MESSAGE_PATH) {
+        return 'Commit message';
+      } else if (path === Gerrit.PathListBehavior.MERGE_LIST_PATH) {
+        return 'Merge list';
+      }
+      return path;
+    },
+
+    computeTruncatedPath(path) {
+      return Gerrit.PathListBehavior.truncatePath(
+          Gerrit.PathListBehavior.computeDisplayPath(path));
+    },
+
+    /**
+     * Truncates URLs to display filename only
+     * Example
+     * // returns '.../text.html'
+     * util.truncatePath.('dir/text.html');
+     * Example
+     * // returns 'text.html'
+     * util.truncatePath.('text.html');
+     * @return {string} Returns the truncated value of a URL.
+     */
+    truncatePath(path) {
+      const pathPieces = path.split('/');
+
+      if (pathPieces.length < 2) {
+        return path;
+      }
+      // Character is an ellipsis.
+      return '\u2026/' + pathPieces[pathPieces.length - 1];
+    },
   };
 })(window);
 </script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index 37772d1..e0b1b7e 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -44,5 +44,34 @@
             '/mrPeanutbutter.py',
           ]);
     });
+
+    test('file display name', () => {
+      const name = Gerrit.PathListBehavior.computeDisplayPath;
+      assert.equal(name('/foo/bar/baz'), '/foo/bar/baz');
+      assert.equal(name('/foobarbaz'), '/foobarbaz');
+      assert.equal(name('/COMMIT_MSG'), 'Commit message');
+      assert.equal(name('/MERGE_LIST'), 'Merge list');
+    });
+
+    test('truncatePath with long path should add ellipsis', () => {
+      const truncatePath = Gerrit.PathListBehavior.truncatePath;
+      let path = 'level1/level2/level3/level4/file.js';
+      let shortenedPath = truncatePath(path);
+      // The expected path is truncated with an ellipsis.
+      const expectedPath = '\u2026/file.js';
+      assert.equal(shortenedPath, expectedPath);
+
+      path = 'level2/file.js';
+      shortenedPath = truncatePath(path);
+      assert.equal(shortenedPath, expectedPath);
+    });
+
+    test('truncatePath with short path should not add ellipsis', () => {
+      const truncatePath = Gerrit.PathListBehavior.truncatePath;
+      const path = 'file.js';
+      const expectedPath = 'file.js';
+      const shortenedPath = truncatePath(path);
+      assert.equal(shortenedPath, expectedPath);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index b86d0f7..d79dc69 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -20,6 +20,7 @@
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-change-list/gr-change-list.html">
+<link rel="import" href="../gr-user-header/gr-user-header.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-change-list-view">
@@ -46,6 +47,9 @@
       nav a:first-of-type {
         margin-right: .5em;
       }
+      .hide {
+        display: none;
+      }
       @media only screen and (max-width: 50em) {
         .loading,
         .error {
@@ -55,6 +59,9 @@
     </style>
     <div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div>
     <div hidden$="[[_loading]]" hidden>
+      <gr-user-header
+          user-id="[[_userId]]"
+          class$="[[_computeUserHeaderClass(_userId)]]"></gr-user-header>
       <gr-change-list
           changes="{{_changes}}"
           selected-index="{{viewState.selectedChangeIndex}}"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index 30fc679..cc35ff8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -19,6 +19,8 @@
     CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g,
   };
 
+  const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/;
+
   const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
 
   Polymer({
@@ -84,7 +86,10 @@
       /**
        * Change objects loaded from the server.
        */
-      _changes: Array,
+      _changes: {
+        type: Array,
+        observer: '_changesChanged',
+      },
 
       /**
        * For showing a "loading..." string during ajax requests.
@@ -93,6 +98,12 @@
         type: Boolean,
         value: true,
       },
+
+      /** @type {?String} */
+      _userId: {
+        type: String,
+        value: null,
+      },
     },
 
     listeners: {
@@ -188,5 +199,18 @@
       page.show(this._computeNavLink(
           this._query, this._offset, -1, this._changesPerPage));
     },
+
+    _changesChanged(changes) {
+      if (!changes || !changes.length ||
+          !USER_QUERY_PATTERN.test(this._query)) {
+        this._userId = null;
+        return;
+      }
+      this._userId = changes[0].owner.email;
+    },
+
+    _computeUserHeaderClass(userId) {
+      return userId ? '' : 'hide';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
index d70f0c9..5a28565 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -167,6 +167,21 @@
       assert.isTrue(showStub.called);
     });
 
+    test('_userId query', done => {
+      assert.isNull(element._userId);
+      element._query = 'owner: foo@bar';
+      element._changes = [{owner: {email: 'foo@bar'}}];
+      flush(() => {
+        assert.equal(element._userId, 'foo@bar');
+
+        element._query = 'foo bar baz';
+        element._changes = [{owner: {email: 'foo@bar'}}];
+        assert.isNull(element._userId);
+
+        done();
+      });
+    });
+
     suite('query based navigation', () => {
       test('Searching for a change ID redirects to change', done => {
         sandbox.stub(element, '_getChanges')
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
new file mode 100644
index 0000000..9a7ca33
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
@@ -0,0 +1,88 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
+<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-user-header">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: block;
+        height: 9em;
+        position: relative;
+        width: 100%;
+      }
+      gr-avatar {
+        height: 7em;
+        left: 1em;
+        position: absolute;
+        top: 1em;
+        width: 7em;
+      }
+      .info {
+        left: 9em;
+        position: absolute;
+        top: 1em;
+      }
+      .info > div > span {
+        display: inline-block;
+        font-weight: bold;
+        text-align: right;
+        width: 6em;
+      }
+      .name {
+        margin-bottom: .25em;
+      }
+      .name hr {
+        width: 100%;
+      }
+      .status.hide,
+      .name.hide {
+        display: none;
+      }
+    </style>
+    <gr-avatar
+        account="[[_accountDetails]]"
+        image-size="100"
+        aria-label="Account avatar"></gr-avatar>
+    <div class="info">
+      <h1 class$="name">
+        [[_computeDetail(_accountDetails, 'name')]]
+        <hr/>
+      </h1>
+      <div class$="status [[_computeStatusClass(_accountDetails)]]">
+        <span>Status:</span> [[_status]]
+      </div>
+      <div>
+        <span>Email:</span>
+        <a href="mailto:[[_computeDetail(_accountDetails, 'email')]]"><!--
+          -->[[_computeDetail(_accountDetails, 'email')]]</a>
+      </div>
+      <div>
+        <span>Joined:</span>
+        <gr-date-formatter
+            date-str="[[_computeDetail(_accountDetails, 'registered_on')]]">
+        </gr-date-formatter>
+      </div>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-user-header.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
new file mode 100644
index 0000000..dd3512a
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -0,0 +1,68 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-user-header',
+    properties: {
+      /** @type {?String} */
+      userId: {
+        type: String,
+        observer: '_accountChanged',
+      },
+
+      /**
+       * @type {?{name: ?, email: ?, registered_on: ?}}
+       */
+      _accountDetails: {
+        type: Object,
+        value: null,
+      },
+
+      /** @type {?String} */
+      _status: {
+        type: String,
+        value: null,
+      },
+    },
+
+    _accountChanged(userId) {
+      if (!userId) {
+        this._accountDetails = null;
+        this._status = null;
+        return;
+      }
+
+      this.$.restAPI.getAccountDetails(userId).then(details => {
+        this._accountDetails = details;
+      });
+      this.$.restAPI.getAccountStatus(userId).then(status => {
+        this._status = status;
+      });
+    },
+
+    _computeDisplayClass(status) {
+      return status ? ' ' : 'hide';
+    },
+
+    _computeDetail(accountDetails, name) {
+      return accountDetails ? accountDetails[name] : '';
+    },
+
+    _computeStatusClass(accountDetails) {
+      return this._computeDetail(accountDetails, 'status') ? '' : 'hide';
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
new file mode 100644
index 0000000..ab3b249
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-user-header</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-user-header.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-user-header></gr-user-header>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-user-header tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => { sandbox.restore(); });
+
+    test('loads and clears account info', done => {
+      sandbox.stub(element.$.restAPI, 'getAccountDetails')
+          .returns(Promise.resolve({
+            name: 'foo',
+            email: 'bar',
+            registered_on: '2015-03-12 18:32:08.000000000',
+          }));
+      sandbox.stub(element.$.restAPI, 'getAccountStatus')
+          .returns(Promise.resolve('baz'));
+
+      element.userId = 'foo.bar@baz';
+      flush(() => {
+        assert.isOk(element._accountDetails);
+        assert.isOk(element._status);
+
+        element.userId = null;
+        flush(() => {
+          flushAsynchronousOperations();
+          assert.isNull(element._accountDetails);
+          assert.isNull(element._status);
+
+          done();
+        });
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index f69e393..8f2468b 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
@@ -54,15 +54,15 @@
     DELETE_EDIT: 'deleteEdit',
     IGNORE: 'ignore',
     MOVE: 'move',
-    MUTE: 'mute',
     PRIVATE: 'private',
     PRIVATE_DELETE: 'private.delete',
     PUBLISH_EDIT: 'publishEdit',
     REBASE_EDIT: 'rebaseEdit',
     RESTORE: 'restore',
     REVERT: 'revert',
+    REVIEWED: 'reviewed',
     UNIGNORE: 'unignore',
-    UNMUTE: 'unmute',
+    UNREVIEWED: 'unreviewed',
     WIP: 'wip',
   };
 
@@ -267,11 +267,11 @@
             },
             {
               type: ActionType.CHANGE,
-              key: ChangeActions.MUTE,
+              key: ChangeActions.REVIEWED,
             },
             {
               type: ActionType.CHANGE,
-              key: ChangeActions.UNMUTE,
+              key: ChangeActions.UNREVIEWED,
             },
             {
               type: ActionType.CHANGE,
@@ -666,7 +666,7 @@
 
     _handleActionTap(e) {
       e.preventDefault();
-      const el = Polymer.dom(e).rootTarget;
+      const el = e.currentTarget;
       const key = el.getAttribute('data-action-key');
       if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX)) {
         this.fire(`${key}-tap`, {node: el});
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 e099170..70d26bf 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
@@ -866,22 +866,22 @@
       });
     });
 
-    suite('mute change', () => {
+    suite('reviewed change', () => {
       setup(done => {
         sandbox.stub(element, '_fireAction');
 
-        const MuteAction = {
-          __key: 'mute',
+        const ReviewedAction = {
+          __key: 'reviewed',
           __type: 'change',
           __primary: false,
           method: 'PUT',
-          label: 'Mute',
+          label: 'Mark reviewed',
           title: 'Working...',
           enabled: true,
         };
 
         element.actions = {
-          mute: MuteAction,
+          reviewed: ReviewedAction,
         };
 
         element.changeNum = '2';
@@ -890,37 +890,38 @@
         element.reload().then(() => { flush(done); });
       });
 
-      test('make sure the mute button is not outside of the overflow menu',
+      test('make sure the reviewed button is not outside of the overflow menu',
           () => {
-            assert.isNotOk(element.$$('[data-action-key="mute"]'));
+            assert.isNotOk(element.$$('[data-action-key="reviewed"]'));
           });
 
-      test('muting change', () => {
-        assert.isOk(element.$.moreActions.$$('span[data-id="mute-change"]'));
-        element.setActionOverflow('change', 'mute', false);
+      test('reviewing change', () => {
+        assert.isOk(
+            element.$.moreActions.$$('span[data-id="reviewed-change"]'));
+        element.setActionOverflow('change', 'reviewed', false);
         flushAsynchronousOperations();
-        assert.isOk(element.$$('[data-action-key="mute"]'));
+        assert.isOk(element.$$('[data-action-key="reviewed"]'));
         assert.isNotOk(
-            element.$.moreActions.$$('span[data-id="mute-change"]'));
+            element.$.moreActions.$$('span[data-id="reviewed-change"]'));
       });
     });
 
-    suite('unmute change', () => {
+    suite('unreviewed change', () => {
       setup(done => {
         sandbox.stub(element, '_fireAction');
 
-        const UnmuteAction = {
-          __key: 'unmute',
+        const UnreviewedAction = {
+          __key: 'unreviewed',
           __type: 'change',
           __primary: false,
           method: 'PUT',
-          label: 'Unmute',
+          label: 'Mark unreviewed',
           title: 'Working...',
           enabled: true,
         };
 
         element.actions = {
-          unmute: UnmuteAction,
+          unreviewed: UnreviewedAction,
         };
 
         element.changeNum = '2';
@@ -930,18 +931,18 @@
       });
 
 
-      test('unmute button not outside of the overflow menu', () => {
-        assert.isNotOk(element.$$('[data-action-key="unmute"]'));
+      test('unreviewed button not outside of the overflow menu', () => {
+        assert.isNotOk(element.$$('[data-action-key="unreviewed"]'));
       });
 
-      test('unmuting change', () => {
+      test('unreviewed change', () => {
         assert.isOk(
-            element.$.moreActions.$$('span[data-id="unmute-change"]'));
-        element.setActionOverflow('change', 'unmute', false);
+            element.$.moreActions.$$('span[data-id="unreviewed-change"]'));
+        element.setActionOverflow('change', 'unreviewed', false);
         flushAsynchronousOperations();
-        assert.isOk(element.$$('[data-action-key="unmute"]'));
+        assert.isOk(element.$$('[data-action-key="unreviewed"]'));
         assert.isNotOk(
-            element.$.moreActions.$$('span[data-id="unmute-change"]'));
+            element.$.moreActions.$$('span[data-id="unreviewed-change"]'));
       });
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
index 6129e9b..90776c0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -93,7 +93,8 @@
           plugin: {
             js_resource_paths: [],
             html_resource_paths: [
-              new URL('test/plugin.html', window.location.href).toString(),
+              new URL('test/plugin.html?' + Math.random(),
+                      window.location.href).toString(),
             ],
           },
         };
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 cbaf605..559157d 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
@@ -107,6 +107,9 @@
         opacity: 0;
         pointer-events: none;
       }
+      .hashtagChip {
+        margin-bottom: .5em;
+      }
       #externalStyle {
         display: block;
       }
@@ -222,6 +225,7 @@
           </template>
           <template is="dom-if" if="[[!change.topic]]">
             <gr-editable-label
+                uppercase
                 label-text="Add a topic"
                 value="[[change.topic]]"
                 placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
@@ -240,6 +244,7 @@
           <span class="value">
             <template is="dom-repeat" items="[[change.hashtags]]">
               <gr-linked-chip
+                  class="hashtagChip"
                   text="[[item]]"
                   href="[[_computeHashtagURL(item)]]"
                   removable="[[!_hashtagReadOnly]]"
@@ -247,6 +252,7 @@
               </gr-linked-chip>
             </template>
             <gr-editable-label
+                uppercase
                 label-text="Add a hashtag"
                 value="{{_newHashtag}}"
                 placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]"
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 b5b909a..1fb1c15 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
@@ -14,7 +14,7 @@
 (function() {
   'use strict';
 
-  const HASHTAG_ADD_MESSAGE = 'Click to add';
+  const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
 
   const SubmitTypeLabel = {
     FAST_FORWARD_ONLY: 'Fast Forward Only',
@@ -209,7 +209,7 @@
     },
 
     _computeTopicPlaceholder(_topicReadOnly) {
-      return _topicReadOnly ? 'No Topic' : 'Click to add topic';
+      return _topicReadOnly ? 'No Topic' : 'Add Topic';
     },
 
     _computeHashtagPlaceholder(_hashtagReadOnly) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 031acda..f6966683 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -205,7 +205,7 @@
         margin: 1em var(--default-horizontal-margin);
       }
       #fileList {
-        padding: .5em calc(var(--default-horizontal-margin) / 2);
+        padding: 0 calc(var(--default-horizontal-margin) / 2) .5em;
       }
       .scrollable {
         overflow: auto;
@@ -438,7 +438,8 @@
             shown-file-count="[[_shownFileCount]]"
             diff-prefs="[[_diffPrefs]]"
             diff-view-mode="{{viewState.diffMode}}"
-            patch-range="{{_patchRange}}"
+            patch-num="{{_patchNum}}"
+            base-patch-num="{{_basePatchNum}}"
             revisions="[[_sortedRevisions]]"
             on-open-diff-prefs="_handleOpenDiffPrefs"
             on-open-download-dialog="_handleOpenDownloadDialog"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 31faa54..e143cc1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -137,6 +137,16 @@
       _patchRange: {
         type: Object,
       },
+      // These are kept as separate properties from the patchRange so that the
+      // observer can be aware of the previous value. In order to view sub
+      // property changes for _patchRange, a complex observer must be used, and
+      // that only displays the new value.
+      //
+      // If a previous value did not exist, the change is not reloaded with the
+      // new patches. This is just the initial setting from the change view vs.
+      // an update coming from the two way data binding.
+      _patchNum: String,
+      _basePatchNum: String,
       _relatedChangesLoading: {
         type: Boolean,
         value: true,
@@ -211,6 +221,7 @@
       '_labelsChanged(_change.labels.*)',
       '_paramsAndChangeChanged(params, _change)',
       '_updateSortedRevisions(_change.revisions.*)',
+      '_patchRangeChanged(_patchRange.*)',
     ],
 
     keyBindings: {
@@ -311,6 +322,15 @@
       window.location.reload();
     },
 
+    /**
+     * Called when the patch range changes. does not detect sub property
+     * updates.
+     */
+    _patchRangeChanged() {
+      this._basePatchNum = this._patchRange.basePatchNum;
+      this._patchNum = this._patchRange.patchNum;
+    },
+
     _handleCommitMessageCancel(e) {
       this._editingCommitMessage = false;
     },
@@ -501,8 +521,8 @@
       if (this._initialLoadComplete && patchChanged) {
         if (patchRange.patchNum == null) {
           patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
+          this._patchRange = patchRange;
         }
-        this._patchRange = patchRange;
         this._reloadPatchNumDependentResources().then(() => {
           this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
             change: this._change,
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index c1b1a90..ec32991 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -778,6 +778,26 @@
       assert.isNull(element._getUrlParameter('test'));
     });
 
+    test('navigateToChange called when range select changes', () => {
+      element._change = {
+        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+        revisions: {
+          rev2: {_number: 2},
+          rev1: {_number: 1},
+          rev13: {_number: 13},
+          rev3: {_number: 3},
+        },
+        status: 'NEW',
+        labels: {},
+      };
+      element._basePatchNum = 1;
+      element._patchNum = 2;
+      element._patchNum = 3;
+      assert.equal(navigateToChangeStub.callCount, 1);
+      assert.isTrue(navigateToChangeStub.lastCall
+          .calledWithExactly(element._change, 3, 1));
+    });
+
     test('revert dialog opened with revert param', done => {
       sandbox.stub(element.$.restAPI, 'getLoggedIn', () => {
         return Promise.resolve(true);
@@ -794,6 +814,7 @@
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
           rev1: {_number: 1},
+          rev2: {_number: 2},
         },
         current_revision: 'rev1',
         status: element.ChangeStatus.MERGED,
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 705cf52..e6f37cf 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -57,7 +57,7 @@
     <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
       <div class="file">
         <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">
-          [[_computeFileDisplayName(file)]]
+          [[computeDisplayPath(file)]]
         </a>:
       </div>
       <template is="dom-repeat"
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index 72f1bfe..7e7a0ec 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -13,10 +13,6 @@
 // limitations under the License.
 (function() {
   'use strict';
-
-  const COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
-  const MERGE_LIST_PATH = '/MERGE_LIST';
-
   Polymer({
     is: 'gr-comment-list',
 
@@ -46,15 +42,6 @@
           file, patchNum);
     },
 
-    _computeFileDisplayName(path) {
-      if (path === COMMIT_MESSAGE_PATH) {
-        return 'Commit message';
-      } else if (path === MERGE_LIST_PATH) {
-        return 'Merge list';
-      }
-      return path;
-    },
-
     _isOnParent(comment) {
       return comment.side === 'PARENT';
     },
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index ed1ece6..0e47e30 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -60,15 +60,6 @@
       assert.deepEqual(element._computeFilesFromComments(null), []);
     });
 
-    test('_computeFileDisplayName', () => {
-      assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
-          'Commit message');
-      assert.equal(element._computeFileDisplayName('/MERGE_LIST'),
-          'Merge list');
-      assert.equal(element._computeFileDisplayName('/foo/bar/baz'),
-          '/foo/bar/baz');
-    });
-
     test('_computePatchDisplayName', () => {
       const comment = {line: 123, side: 'REVISION', patch_set: 10};
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index 252ea71..b9c1328 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -47,11 +47,13 @@
         background-color: #f6f6f6;
         border-bottom: 1px solid #ebebeb;
         display: flex;
-        padding: .5em calc(var(--default-horizontal-margin) / 2);
+        height: 3.2em;
+        padding: 0 calc(var(--default-horizontal-margin) / 2);
       }
       .patchInfo-header-wrapper {
         align-items: center;
         display: flex;
+        margin: 0 .25em;
         width: 100%;
       }
       .latestPatchContainer {
@@ -81,7 +83,9 @@
         align-items: center;
         display: flex;
         font-weight: bold;
-        margin: .5em calc(var(--default-horizontal-margin) / 2) 0;
+        height: 2.25em;
+        margin: 0 calc(var(--default-horizontal-margin) / 2);
+        padding: 0 .25em;
       }
       .rightControls {
         align-items: center;
@@ -102,20 +106,26 @@
       .editLoaded .showOnEdit {
         display: initial;
       }
+      .label {
+        font-family: var(--font-family-bold);
+        margin-right: 1em;
+      }
       @media screen and (max-width: 50em) {
         .desktop {
           display: none;
         }
       }
     </style>
-    <div class$="patchInfo-header [[_computeEditLoadedClass(editLoaded)]] [[_computePatchInfoClass(patchRange.patchNum, allPatchSets)]]">
+    <div class$="patchInfo-header [[_computeEditLoadedClass(editLoaded)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]">
       <div class="patchInfo-header-wrapper">
         <div>
+          <span class="label">Files</span>
           <gr-patch-range-select
               id="rangeSelect"
               comments="[[comments]]"
               change-num="[[changeNum]]"
-              patch-range="[[patchRange]]"
+              patch-num="{{patchNum}}"
+              base-patch-num="{{basePatchNum}}"
               available-patches="[[allPatchSets]]"
               revisions="[[change.revisions]]"
               on-patch-range-change="_handlePatchChange">
@@ -141,7 +151,7 @@
                 id="descriptionLabel"
                 class="descriptionLabel"
                 label-text="Add patchset description"
-                value="[[_computePatchSetDescription(change, patchRange.patchNum)]]"
+                value="[[_computePatchSetDescription(change, patchNum)]]"
                 placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
                 read-only="[[_descriptionReadOnly]]"
                 on-changed="_handleDescriptionChanged"></gr-editable-label>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index e1c7977..4b6d59c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -38,8 +38,16 @@
         type: String,
         notify: true,
       },
-      /** @type {?} */
-      patchRange: Object,
+      patchNum: {
+        type: String,
+        notify: true,
+        observer: '_patchOrBaseChanged',
+      },
+      basePatchNum: {
+        type: String,
+        notify: true,
+        observer: '_patchOrBaseChanged',
+      },
       revisions: Array,
       // Caps the number of files that can be shown and have the 'show diffs' /
       // 'hide diffs' buttons still be functional.
@@ -67,7 +75,7 @@
     },
 
     _computeDescriptionPlaceholder(readOnly) {
-      return (readOnly ? 'No' : 'Add a') + ' patch set description';
+      return (readOnly ? 'No' : 'Add') + ' patchset description';
     },
 
     _computeDescriptionReadOnly(loggedIn, change, account) {
@@ -80,7 +88,6 @@
           rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
     },
 
-
     /**
      * @param {!Object} revisions The revisions object keyed by revision hashes
      * @param {?Object} patchSet A revision already fetched from {revisions}
@@ -98,10 +105,10 @@
     _handleDescriptionChanged(e) {
       const desc = e.detail.trim();
       const rev = this.getRevisionByPatchNum(this.change.revisions,
-          this.patchRange.patchNum);
+          this.patchNum);
       const sha = this._getPatchsetHash(this.change.revisions, rev);
       this.$.restAPI.setDescription(this.changeNum,
-          this.patchRange.patchNum, desc)
+          this.patchNum, desc)
           .then(res => {
             if (res.ok) {
               this.set(['_change', 'revisions', sha, 'description'], desc);
@@ -137,35 +144,18 @@
           this.findSortedIndex(basePatchNum, this.revisions);
     },
 
-        /**
-     * Change active patch to the provided patch num.
-     * @param {number|string} basePatchNum the base patch to be viewed.
-     * @param {number|string} patchNum the patch number to be viewed.
-     * @param {boolean} opt_forceParams When set to true, the resulting URL will
-     *     always include the patch range, even if the requested patchNum is
-     *     known to be the latest.
+    /*
+     * Triggered by _patchNum and _basePatchNum observer, in order to detect if
+     * the patch has been previously set or not. The new patch number is not
+     * explicitly used, because this could be called by either _patchNum or
+     * _basePatchNum's observer. Since the behavior is the same, they are
+     * combined.
      */
-    _changePatchNum(basePatchNum, patchNum, opt_forceParams) {
-      if (!opt_forceParams) {
-        let currentPatchNum;
-        if (this.change.current_revision) {
-          currentPatchNum =
-              this.change.revisions[this.change.current_revision]._number;
-        } else {
-          currentPatchNum = this.computeLatestPatchNum(this.allPatchSets);
-        }
-        if (this.patchNumEquals(patchNum, currentPatchNum) &&
-            basePatchNum === 'PARENT') {
-          Gerrit.Nav.navigateToChange(this.change);
-          return;
-        }
-      }
-      Gerrit.Nav.navigateToChange(this.change, patchNum,
-          basePatchNum);
-    },
+    _patchOrBaseChanged(patchNew, patchOld) {
+      if (!patchOld) { return; }
 
-    _handlePatchChange(e) {
-      this._changePatchNum(e.detail.leftPatch, e.detail.rightPatch, true);
+      Gerrit.Nav.navigateToChange(this.change, this.patchNum,
+          this.basePatchNum);
     },
 
     _handlePrefsTap(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index 62ae5c7..f2bf809 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -43,11 +43,9 @@
   suite('gr-file-list-header tests', () => {
     let element;
     let sandbox;
-    let navigateToChangeStub;
 
     setup(() => {
       sandbox = sinon.sandbox.create();
-      navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
       stub('gr-rest-api-interface', {
         getConfig() { return Promise.resolve({test: 'config'}); },
         getAccount() { return Promise.resolve(null); },
@@ -93,9 +91,9 @@
 
     test('_computeDescriptionPlaceholder', () => {
       assert.equal(element._computeDescriptionPlaceholder(true),
-          'No patch set description');
+          'No patchset description');
       assert.equal(element._computeDescriptionPlaceholder(false),
-          'Add a patch set description');
+          'Add patchset description');
     });
 
     test('_computePatchSetDisabled', () => {
@@ -129,10 +127,9 @@
       sandbox.stub(element, '_computeDescriptionReadOnly');
 
       element.changeNum = '42';
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: 1,
-      };
+      element.basePatchNum = 'PARENT';
+      element.patchNum = 1;
+
       element.change = {
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
@@ -180,10 +177,8 @@
       const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
       element._files = [];
       element.changeNum = '42';
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: '2',
-      };
+      element.basePatchNum = 'PARENT';
+      element.patchNum = '2';
       element.shownFileCount = 1;
       flush(() => {
         assert.isTrue(computeSpy.lastCall.returnValue);
@@ -206,21 +201,8 @@
       assert.equal(select.nativeSelect.value, 'UNIFIED_DIFF');
     });
 
-    test('_changePatchNum called when range select changes', () => {
-      const leftPatch = 1;
-      const rightPatch = 2;
-      sandbox.stub(element, '_changePatchNum');
-      element.$.rangeSelect.fire('patch-range-change', {leftPatch, rightPatch});
-      assert.isTrue(element._changePatchNum.lastCall
-          .calledWithExactly(1, 2, true));
-    });
-
-    test('include base patch when not parent', () => {
-      element.changeNum = '42';
-      element.patchRange = {
-        basePatchNum: '2',
-        patchNum: '3',
-      };
+    test('navigateToChange called when range select changes', () => {
+      const navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
       element.change = {
         change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
         revisions: {
@@ -232,16 +214,12 @@
         status: 'NEW',
         labels: {},
       };
-
-      element._changePatchNum(2, 13);
-      assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
-          element.change, 13, 2));
-
-      element.patchRange.basePatchNum = 'PARENT';
-
-      element._changePatchNum('PARENT', 3);
-      assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
-          element.change, 3, 'PARENT'));
+      element.basePatchNum = 1;
+      element.patchNum = 2;
+      element.patchNum = 3;
+      assert.equal(navigateToChangeStub.callCount, 1);
+      assert.isTrue(navigateToChangeStub.lastCall
+          .calledWithExactly(element.change, 3, 1));
     });
 
     test('class is applied to file list on old patch set', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 10f4b27..c450fee 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -14,9 +14,10 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/async-foreach-behavior/async-foreach-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
@@ -40,7 +41,8 @@
         align-items: center;
         border-top: 1px solid #eee;
         display: flex;
-        padding: .1em .25em;
+        height: 2.25em;
+        padding: 0 .25em;
       }
       :host(.loading) .row {
         opacity: .5;
@@ -127,7 +129,7 @@
         display: none;
       }
       label.show-hide {
-        color: #00f;
+        color: var(--color-link);
         cursor: pointer;
         display: block;
         font-size: .8em;
@@ -240,13 +242,13 @@
               data-url="[[_computeDiffURL(change, patchRange.patchNum, patchRange.basePatchNum, file.__path)]]"
               class$="[[_computePathClass(file.__path, _expandedFilePaths.*)]]">
             <a href$="[[_computeDiffURL(change, patchRange.patchNum, patchRange.basePatchNum, file.__path)]]">
-              <span title$="[[_computeFileDisplayName(file.__path)]]"
+              <span title$="[[computeDisplayPath(file.__path)]]"
                   class="fullFileName">
-                [[_computeFileDisplayName(file.__path)]]
+                [[computeDisplayPath(file.__path)]]
               </span>
-              <span title$="[[_computeFileDisplayName(file.__path)]]"
+              <span title$="[[computeDisplayPath(file.__path)]]"
                   class="truncatedFileName">
-                [[_computeTruncatedFileDisplayName(file.__path)]]
+                [[computeTruncatedPath(file.__path)]]
               </span>
             </a>
             <div class="oldPath" hidden$="[[!file.old_path]]" hidden
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 7807f5e..68761fa 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -19,8 +19,6 @@
   // Maximum length for patch set descriptions.
   const PATCH_DESC_MAX_LENGTH = 500;
   const WARN_SHOW_ALL_THRESHOLD = 1000;
-  const COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
-  const MERGE_LIST_PATH = '/MERGE_LIST';
   const LOADING_DEBOUNCE_INTERVAL = 100;
 
   const FileStatus = {
@@ -120,8 +118,10 @@
     },
 
     behaviors: [
+      Gerrit.AsyncForeachBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
+      Gerrit.PathListBehavior,
     ],
 
     observers: [
@@ -665,19 +665,6 @@
       return Gerrit.Nav.getUrlForDiff(change, path, patchNum, basePatchNum);
     },
 
-    _computeFileDisplayName(path) {
-      if (path === COMMIT_MESSAGE_PATH) {
-        return 'Commit message';
-      } else if (path === MERGE_LIST_PATH) {
-        return 'Merge list';
-      }
-      return path;
-    },
-
-    _computeTruncatedFileDisplayName(path) {
-      return util.truncatePath(this._computeFileDisplayName(path));
-    },
-
     _formatBytes(bytes) {
       if (bytes == 0) return '+/-0 B';
       const bits = 1024;
@@ -706,7 +693,7 @@
 
     _computeClass(baseClass, path) {
       const classes = [baseClass];
-      if (path === COMMIT_MESSAGE_PATH || path === MERGE_LIST_PATH) {
+      if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
         classes.push('invisible');
       }
       return classes.join(' ');
@@ -858,22 +845,20 @@
      * @return {!Promise}
      */
     _renderInOrder(paths, diffElements, initialCount) {
-      if (!paths.length) {
+      let iter = 0;
+      return this.asyncForeach(paths, path => {
+        iter++;
+        console.log('Expanding diff', iter, 'of', initialCount, ':', path);
+        const diffElem = this._findDiffByPath(path, diffElements);
+        diffElem.comments = this.$.commentAPI.getCommentsForPath(path,
+            this.patchRange, this.projectConfig);
+        const promises = [diffElem.reload()];
+        if (this._isLoggedIn) {
+          promises.push(this._reviewFile(path));
+        }
+        return Promise.all(promises);
+      }).then(() => {
         console.log('Finished expanding', initialCount, 'diff(s)');
-        return Promise.resolve();
-      }
-      console.log('Expanding diff', 1 + initialCount - paths.length, 'of',
-          initialCount, ':', paths[0]);
-      const diffElem = this._findDiffByPath(paths[0], diffElements);
-      diffElem.comments = this.$.commentAPI.getCommentsForPath(paths[0],
-          this.patchRange, this.projectConfig);
-
-      const promises = [diffElem.reload()];
-      if (this._isLoggedIn) {
-        promises.push(this._reviewFile(paths[0]));
-      }
-      return Promise.all(promises).then(() => {
-        return this._renderInOrder(paths.slice(1), diffElements, initialCount);
       });
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index e77ad2c..b80a20f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -577,11 +577,6 @@
       assert.equal(element._computeFileStatus(undefined), 'M');
       assert.equal(element._computeFileStatus(null), 'M');
 
-      assert.equal(element._computeFileDisplayName('/foo/bar/baz'),
-          '/foo/bar/baz');
-      assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
-          'Commit message');
-
       assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz');
       assert.equal(element._computeClass('clazz', '/COMMIT_MSG'),
           'clazz invisible');
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
index 36e2bc7..6496091 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -51,19 +51,24 @@
       .selectedValueText.hidden {
         display: none;
       }
-      iron-selector > gr-button:first-of-type {
-        border-bottom-left-radius: 2px;
-        border-top-left-radius: 2px;
-      }
-      iron-selector > gr-button:last-of-type {
-        border-bottom-right-radius: 2px;
-        border-top-right-radius: 2px;
-      }
-      iron-selector > gr-button.iron-selected {
-        background-color: #ddd;
-      }
       gr-button {
         min-width: 40px;
+        --gr-button: {
+          border: 1px solid #d1d2d3;
+          border-radius: 12px;
+          box-shadow: none;
+          padding: .2em .85em;
+        }
+        --gr-button-background: #f5f5f5;
+        --gr-button-color: black;
+        --gr-button-hover-color: black;
+
+      }
+      iron-selector > gr-button.iron-selected {
+        --gr-button-background:#ddd;
+        --gr-button-color: black;
+        --gr-button-hover-background-color: #ddd;
+        --gr-button-hover-color: black;
       }
       .placeholder {
         display: inline-block;
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index 812f0bd..dd0bccc 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -106,6 +106,8 @@
       // nothing and then to the new item.
       if (!e.target.selectedItem) { return; }
       this._selectedValueText = e.target.selectedItem.getAttribute('title');
+      // Needed to update the style of the selected button.
+      this.updateStyles();
       this.fire('labels-changed');
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index c47d63f..15964a7 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -86,14 +86,14 @@
 
       assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
       assert.equal(getMessages().length, 20);
-      assert.equal(element.$.incrementMessagesBtn.innerText.trim(),
-          'Show 5 more');
+      assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
+          .trim(), 'SHOW 5 MORE');
       MockInteractions.tap(element.$.incrementMessagesBtn);
       flushAsynchronousOperations();
 
       assert.equal(getMessages().length, 25);
-      assert.equal(element.$.incrementMessagesBtn.innerText.trim(),
-          'Show 1 more');
+      assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
+          .trim(), 'SHOW 1 MORE');
       MockInteractions.tap(element.$.incrementMessagesBtn);
       flushAsynchronousOperations();
 
@@ -108,7 +108,8 @@
 
       assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
       assert.equal(getMessages().length, 20);
-      assert.equal(element.$.oldMessagesBtn.innerText, 'Show all 6 messages');
+      assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+          'SHOW ALL 6 MESSAGES');
       MockInteractions.tap(element.$.oldMessagesBtn);
       flushAsynchronousOperations();
 
@@ -121,7 +122,8 @@
           .concat(_.times(11, randomMessage));
       flushAsynchronousOperations();
 
-      assert.equal(element.$.oldMessagesBtn.innerText, 'Show 1 message');
+      assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+          'SHOW 1 MESSAGE');
       assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
       MockInteractions.tap(element.$.automatedMessageToggle);
       flushAsynchronousOperations();
@@ -134,12 +136,14 @@
           .concat(_.times(11, randomAutomated));
       flushAsynchronousOperations();
 
-      assert.equal(element.$.oldMessagesBtn.innerText, 'Show 1 message');
+      assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+          'SHOW 1 MESSAGE');
       assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
       MockInteractions.tap(element.$.automatedMessageToggle);
       flushAsynchronousOperations();
 
-      assert.equal(element.$.oldMessagesBtn.innerText, 'Show 1 message');
+      assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+          'SHOW 1 MESSAGE');
       assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 161dfe7..4b02c3d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -117,10 +117,6 @@
       .draftsContainer h3 {
         margin-top: .25em;
       }
-      .action:link,
-      .action:visited {
-        color: #00e;
-      }
       #checkingStatusLabel,
       #notLatestLabel {
         margin-left: 1em;
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 09f3029..9f5d39d 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
@@ -67,7 +67,9 @@
       .linksTitle {
         color: var(--primary-text-color);
         display: inline-block;
+        font-family: var(--font-family-bold);
         position: relative;
+        text-transform: uppercase;
       }
       .linksTitle:hover {
         opacity: .75;
@@ -157,7 +159,9 @@
           </li>
         </template>
         <li>
-          <a class="browse linksTitle" href$="[[_computeRelativeURL('/admin/projects')]]">
+          <a
+              class="browse linksTitle"
+              href$="[[_computeRelativeURL('/admin/projects')]]">
             Browse</a>
         </li>
       </ul>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 008852e..203d34f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -16,6 +16,7 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
+<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
@@ -85,7 +86,6 @@
         vertical-align: middle;
       }
       .dropdown-trigger {
-        color: #00e;
         cursor: pointer;
         padding: 0;
       }
@@ -111,7 +111,7 @@
         width: .3em;
       }
       .dropdown-content a:hover {
-        background-color: #00e;
+        background-color: var(--color-link);
         color: #fff;
       }
       .dropdown-content a[selected] {
@@ -125,7 +125,6 @@
         color: #000;
       }
       gr-button {
-        font: inherit;
         padding: .3em 0;
         text-decoration: none;
       }
@@ -229,7 +228,7 @@
               hidden$="[[!_loggedIn]]" hidden>
           <div class="jumpToFileContainer desktop">
             <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
-              <span>[[_computeFileDisplayName(_path)]]</span>
+              <span>[[computeDisplayPath(_path)]]</span>
               <span class="downArrow">&#9660;</span>
             </gr-button>
             <!-- *-align="" to disable iron-dropdown's element positioning. -->
@@ -246,7 +245,7 @@
                   <a href$="[[_computeDiffURL(_change, _patchRange.*, path)]]"
                     selected$="[[_computeFileSelected(path, _path)]]"
                     data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
-                    on-tap="_handleFileTap">[[_computeFileDisplayName(path)]]</a>
+                    on-tap="_handleFileTap">[[computeDisplayPath(path)]]</a>
                 </template>
               </div>
             </iron-dropdown>
@@ -257,7 +256,7 @@
                 <option
                     value$="[[path]]"
                     selected$="[[_computeFileSelected(path, _path)]]">
-                  [[_computeTruncatedFileDisplayName(path)]]
+                  [[computeTruncatedPath(path)]]
                 </option>
               </template>
             </select>
@@ -282,11 +281,11 @@
           <gr-patch-range-select
               id="rangeSelect"
               change-num="[[_changeNum]]"
-              patch-range="[[_patchRange]]"
+              patch-num="{{_patchNum}}"
+              base-patch-num="{{_basePatchNum}}"
               files-weblinks="[[_filesWeblinks]]"
-              available-patches="[[_computeAvailablePatches(_change.revisions, _change.revisions.*)]]"
-              revisions="[[_change.revisions]]"
-              on-patch-range-change="_handlePatchChange">
+              available-patches="[[_allPatchSets]]"
+              revisions="[[_change.revisions]]">
           </gr-patch-range-select>
           <span class="download desktop">
             <span class="separator">/</span>
@@ -331,7 +330,7 @@
         <a class="mobileNavLink"
           href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
           &lt;</a>
-        <div class="fullFileName mobile">[[_computeFileDisplayName(_path)]]
+        <div class="fullFileName mobile">[[computeDisplayPath(_path)]]
         </div>
         <a class="mobileNavLink"
             href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index fbd845a..c4746f2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -14,9 +14,6 @@
 (function() {
   'use strict';
 
-  const COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
-  const MERGE_LIST_PATH = '/MERGE_LIST';
-
   const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
   const MSG_LOADING_BLAME = 'Loading blame...';
   const MSG_LOADED_BLAME = 'Blame loaded';
@@ -66,6 +63,22 @@
       },
 
       _patchRange: Object,
+      // These are kept as separate properties from the patchRange so that the
+      // observer can be aware of the previous value. In order to view sub
+      // property changes for _patchRange, a complex observer must be used, and
+      // that only displays the new value.
+      //
+      // If a previous value did not exist, the change is not reloaded with the
+      // new patches. This is just the initial setting from the change view vs.
+      // an update coming from the two way data binding.
+      _patchNum: {
+        type: String,
+        observer: '_patchOrBaseChanged',
+      },
+      _basePatchNum: {
+        type: String,
+        observer: '_patchOrBaseChanged',
+      },
       /**
        * @type {{
        *  subject: string,
@@ -127,7 +140,6 @@
         type: Boolean,
         computed: '_computeEditLoaded(_patchRange.*)',
       },
-
       _isBlameSupported: {
         type: Boolean,
         value: false,
@@ -137,11 +149,16 @@
         type: Boolean,
         value: false,
       },
+      _allPatchSets: {
+        type: Array,
+        computed: 'computeAllPatchSets(_change, _change.revisions.*)',
+      },
     },
 
     behaviors: [
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
+      Gerrit.PathListBehavior,
       Gerrit.RESTClientBehavior,
     ],
 
@@ -149,6 +166,7 @@
       '_getProjectConfig(_change.project)',
       '_getFiles(_changeNum, _patchRange.*)',
       '_setReviewedObserver(_loggedIn, params.*)',
+      '_patchRangeChanged(_patchRange.*)',
     ],
 
     keyBindings: {
@@ -495,7 +513,7 @@
       // has been queued, the event can bubble up to the handler in gr-app.
       this.async(() => {
         this.fire('title-change',
-            {title: this._computeTruncatedFileDisplayName(this._path)});
+            {title: this.computeTruncatedPath(this._path)});
       });
 
       // When navigating away from the page, there is a possibility that the
@@ -565,10 +583,21 @@
       this.$.cursor.initialLineNumber = params.lineNum;
     },
 
+    _patchRangeChanged() {
+      this._basePatchNum = this._patchRange.basePatchNum;
+      this._patchNum = this._patchRange.patchNum;
+    },
+
+    _patchOrBaseChanged(patchNew, patchOld) {
+      if (!patchOld) { return; }
+
+      this._handlePatchChange(this._basePatchNum, this._patchNum);
+    },
+
     _pathChanged(path) {
       if (path) {
         this.fire('title-change',
-            {title: this._computeTruncatedFileDisplayName(path)});
+            {title: this.computeTruncatedPath(path)});
       }
 
       if (this._fileList.length == 0) { return; }
@@ -595,12 +624,6 @@
       return patchStr;
     },
 
-    _computeAvailablePatches(revs) {
-      return this.sortRevisions(Object.values(revs)).map(e => {
-        return {num: e._number};
-      });
-    },
-
     /**
      * When the latest patch of the change is selected (and there is no base
      * patch) then the patch range need not appear in the URL. Return a patch
@@ -640,19 +663,6 @@
       return this._getChangePath(change, patchRangeRecord.base, revisions);
     },
 
-    _computeFileDisplayName(path) {
-      if (path === COMMIT_MESSAGE_PATH) {
-        return 'Commit message';
-      } else if (path === MERGE_LIST_PATH) {
-        return 'Merge list';
-      }
-      return path;
-    },
-
-    _computeTruncatedFileDisplayName(path) {
-      return util.truncatePath(this._computeFileDisplayName(path));
-    },
-
     _computeFileSelected(path, currentPath) {
       return path == currentPath;
     },
@@ -690,11 +700,9 @@
       this.$.dropdown.open();
     },
 
-    _handlePatchChange(e) {
-      const rightPatch = e.detail.rightPatch;
-      const leftPatch = e.detail.leftPatch;
+    _handlePatchChange(basePatchNum, patchNum) {
       Gerrit.Nav.navigateToDiff(
-          this._change, this._path, rightPatch, leftPatch);
+          this._change, this._path, patchNum, basePatchNum);
     },
 
     _handlePrefsTap(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 68c2e52..8587fe5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -163,6 +163,7 @@
         _number: 42,
         revisions: {
           a: {_number: 10},
+          b: {_number: 5},
         },
       };
       element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
@@ -336,15 +337,6 @@
             '42-glados.txt-10-PARENT');
         assert.equal(linkEls[2].getAttribute('href'),
             '42-wheatley.md-10-PARENT');
-
-        assert.equal(element._computeFileDisplayName('/foo/bar/baz'),
-            '/foo/bar/baz');
-        assert.equal(element._computeFileDisplayName('/foobarbaz'),
-            '/foobarbaz');
-        assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
-            'Commit message');
-        assert.equal(element._computeFileDisplayName('/MERGE_LIST'),
-            'Merge list');
       });
 
       test('jump to file dropdown with patch range', () => {
@@ -447,15 +439,23 @@
     });
 
     test('_handlePatchChange calls navigateToDiff correctly', () => {
-      const leftPatch = 'PARENT';
-      const rightPatch = '3';
       const navigateStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
       element._change = {_number: 321, project: 'foo/bar'};
       element._path = 'path/to/file.txt';
-      element.$.rangeSelect.fire('patch-range-change', {leftPatch, rightPatch});
+
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '3',
+      };
+
+      assert.equal(element._basePatchNum, element._patchRange.basePatchNum);
+      assert.equal(element._patchNum, element._patchRange.patchNum);
+
+      element._patchNum = '1';
+
 
       assert(navigateStub.lastCall.calledWithExactly(element._change,
-          element._path, rightPatch, leftPatch));
+          element._path, '1', 'PARENT'));
     });
 
     test('download link', () => {
@@ -614,25 +614,6 @@
       assert.equal(element.$.cursor.side, 'right');
     });
 
-    test('_shortenPath with long path should add ellipsis', () => {
-      let path = 'level1/level2/level3/level4/file.js';
-      let shortenedPath = util.truncatePath(path);
-      // The expected path is truncated with an ellipsis.
-      const expectedPath = '\u2026/file.js';
-      assert.equal(shortenedPath, expectedPath);
-
-      path = 'level2/file.js';
-      shortenedPath = util.truncatePath(path);
-      assert.equal(shortenedPath, expectedPath);
-    });
-
-    test('_shortenPath with short path should not add ellipsis', () => {
-      const path = 'file.js';
-      const expectedPath = 'file.js';
-      const shortenedPath = util.truncatePath(path);
-      assert.equal(shortenedPath, expectedPath);
-    });
-
     test('_onLineSelected', () => {
       const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
       const replaceStateStub = sandbox.stub(history, 'replaceState');
@@ -787,6 +768,7 @@
       };
 
       test('reviewed checkbox', () => {
+        sandbox.stub(element, '_handlePatchChange');
         element._patchRange = {patchNum: '1'};
         // Reviewed checkbox should be shown.
         assert.isTrue(isVisible(element.$.reviewed));
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
index a228a83..4f90502 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -13,11 +13,13 @@
 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="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
+
+<link rel="import" href="../../shared/gr-select/gr-select.html">
 
 <dom-module id="gr-patch-range-select">
   <template>
@@ -32,27 +34,14 @@
         .filesWeblinks {
           display: none;
         }
-        select {
-          max-width: 5.25em;
-        }
       }
     </style>
-    Patch set:
     <span class="patchRange">
-      <gr-select id="leftPatchSelect" bind-value="{{_leftSelected}}"
-          on-change="_handlePatchChange">
-        <select>
-          <option value="PARENT">Base</option>
-          <template is="dom-repeat" items="{{availablePatches}}" as="basePatchNum">
-            <option value$="[[basePatchNum.num]]"
-                disabled$="[[_computeLeftDisabled(basePatchNum.num, patchRange.patchNum, _sortedRevisions)]]">
-              [[basePatchNum.num]]
-              [[_computePatchSetCommentsString(comments, basePatchNum.num)]]
-              [[_computePatchSetDescription(revisions, basePatchNum.num)]]
-            </option>
-          </template>
-        </select>
-      </gr-select>
+      <gr-dropdown-list
+          id="basePatchDropdown"
+          value="{{basePatchNum}}"
+          items="[[_baseDropdownContent]]">
+      </gr-dropdown-list>
     </span>
     <span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks">
       <template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink">
@@ -62,19 +51,11 @@
     </span>
     &rarr;
     <span class="patchRange">
-      <gr-select id="rightPatchSelect" bind-value="{{_rightSelected}}"
-          on-change="_handlePatchChange">
-        <select>
-          <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
-            <option value$="[[patchNum.num]]"
-                disabled$="[[_computeRightDisabled(patchNum.num, patchRange.basePatchNum, _sortedRevisions)]]">
-              [[patchNum.num]]
-              [[_computePatchSetCommentsString(comments, patchNum.num)]]
-              [[_computePatchSetDescription(revisions, patchNum.num)]]
-            </option>
-          </template>
-        </select>
-      </gr-select>
+      <gr-dropdown-list
+          id="patchNumDropdown"
+          value="{{patchNum}}"
+          items="[[_patchDropdownContent]]">
+      </gr-dropdown-list>
       <span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks">
         <template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink">
           <a target="_blank"
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index f6b759e..4ca9fb9 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -31,28 +31,89 @@
 
     properties: {
       availablePatches: Array,
+      _baseDropdownContent: {
+        type: Object,
+        computed: '_computeBaseDropdownContent(availablePatches, patchNum,' +
+            '_sortedRevisions, revisions)',
+      },
+      _patchDropdownContent: {
+        type: Object,
+        computed: '_computePatchDropdownContent(availablePatches,' +
+            'basePatchNum, _sortedRevisions, revisions)',
+      },
       changeNum: String,
       comments: Array,
       /** @type {{ meta_a: !Array, meta_b: !Array}} */
       filesWeblinks: Object,
-      /** @type {?} */
-      patchRange: Object,
+      patchNum: {
+        type: String,
+        notify: true,
+      },
+      basePatchNum: {
+        type: String,
+        notify: true,
+      },
       revisions: Object,
       _sortedRevisions: Array,
-      _rightSelected: String,
-      _leftSelected: String,
     },
 
     observers: [
       '_updateSortedRevisions(revisions.*)',
-      '_updateSelected(patchRange.*)',
     ],
 
     behaviors: [Gerrit.PatchSetBehavior],
 
-    _updateSelected() {
-      this._rightSelected = this.patchRange.patchNum;
-      this._leftSelected = this.patchRange.basePatchNum;
+    _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions,
+        revisions) {
+      const dropdownContent = [];
+      dropdownContent.push({
+        text: 'Base',
+        value: 'PARENT',
+      });
+      for (const basePatch of availablePatches) {
+        const basePatchNum = basePatch.num;
+        dropdownContent.push({
+          disabled: this._computeLeftDisabled(
+              basePatch.num, patchNum, _sortedRevisions),
+          triggerText: `Patchset ${basePatchNum}`,
+          text: `Patchset ${basePatchNum}` +
+              this._computePatchSetCommentsString(this.comments, basePatchNum),
+          mobileText: this._computeMobileText(basePatchNum, this.comments,
+              revisions),
+          bottomText: `${this._computePatchSetDescription(
+              revisions, basePatchNum)}`,
+          value: basePatch.num,
+        });
+      }
+      return dropdownContent;
+    },
+
+    _computeMobileText(patchNum, comments, revisions) {
+      return `${patchNum}` +
+          `${this._computePatchSetCommentsString(this.comments, patchNum)}` +
+          `${this._computePatchSetDescription(revisions, patchNum, true)}`;
+    },
+
+    _computePatchDropdownContent(availablePatches, basePatchNum,
+        _sortedRevisions, revisions) {
+      const dropdownContent = [];
+      for (const patch of availablePatches) {
+        const patchNum = patch.num;
+        dropdownContent.push({
+          disabled: this._computeRightDisabled(patchNum, basePatchNum,
+              _sortedRevisions),
+          triggerText: `Patchset ${patchNum}`,
+          text: `Patchset ${patchNum}` +
+              `${this._computePatchSetCommentsString(
+                  this.comments, patchNum)}`,
+          mobileText: this._computeMobileText(patchNum, this.comments,
+              revisions),
+          bottomText: `${this._computePatchSetDescription(
+              revisions, patchNum)}`,
+          value: patchNum,
+        });
+      }
+      return dropdownContent;
     },
 
     _updateSortedRevisions(revisionsRecord) {
@@ -60,13 +121,6 @@
       this._sortedRevisions = this.sortRevisions(Object.values(revisions));
     },
 
-    _handlePatchChange(e) {
-      const leftPatch = this._leftSelected;
-      const rightPatch = this._rightSelected;
-      this.fire('patch-range-change', {rightPatch, leftPatch});
-      e.target.blur();
-    },
-
     _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) {
       return this.findSortedIndex(basePatchNum, sortedRevisions) >=
           this.findSortedIndex(patchNum, sortedRevisions);
@@ -85,11 +139,11 @@
     // debounce these, but because they are detecting two different
     // events, sometimes the timing was off and one ended up missing.
     _synchronizeSelectionRight() {
-      this.$.rightPatchSelect.value = this._rightSelected;
+      this.$.rightPatchSelect.value = this.patchNum;
     },
 
     _synchronizeSelectionLeft() {
-      this.$.leftPatchSelect.value = this._leftSelected;
+      this.$.leftPatchSelect.value = this.basePatchNum;
     },
 
     // Copied from gr-file-list
@@ -145,7 +199,7 @@
       }
       let commentsStr = '';
       if (numComments > 0) {
-        commentsStr = '(' + numComments + ' comments';
+        commentsStr = ' (' + numComments + ' comments';
         if (numUnresolved > 0) {
           commentsStr += ', ' + numUnresolved + ' unresolved';
         }
@@ -154,9 +208,15 @@
       return commentsStr;
     },
 
-    _computePatchSetDescription(revisions, patchNum) {
+    /**
+     * @param {!Array} revisions
+     * @param {number|string} patchNum
+     * @param {boolean=} opt_addFrontSpace
+     */
+    _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) {
       const rev = this.getRevisionByPatchNum(revisions, patchNum);
       return (rev && rev.description) ?
+          (opt_addFrontSpace ? ' ' : '') +
           rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
     },
   });
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 92553a0..d49974b 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -79,19 +79,74 @@
           patchRange.basePatchNum, sortedRevisions));
     });
 
-    test('_updateSelected called with subproperty changes', () => {
-      sandbox.stub(element, '_updateSelected');
-      element.patchRange = {patchNum: 1, basePatchNum: 'PARENT'};
-      assert.equal(element._updateSelected.callCount, 1);
-
-      element.set('patchRange.patchNum', 2);
-      assert.equal(element._updateSelected.callCount, 2);
-
-      element.set('patchRange.basePatchNum', 1);
-      assert.equal(element._updateSelected.callCount, 3);
+    test('_computeBaseDropdownContent', () => {
+      element.comments = {};
+      const availablePatches = [
+        {num: 1},
+        {num: 2},
+        {num: 3},
+        {num: 'edit'},
+      ];
+      const revisions = [
+        {
+          commit: {},
+          _number: 2,
+          description: 'description',
+        },
+        {commit: {}},
+        {commit: {}},
+        {commit: {}},
+      ];
+      const patchNum = 1;
+      const sortedRevisions = [
+        {_number: 1},
+        {_number: 2},
+        {_number: element.EDIT_NAME, basePatchNum: 2},
+        {_number: 3},
+      ];
+      const expectedResult = [
+        {
+          text: 'Base',
+          value: 'PARENT',
+        },
+        {
+          disabled: true,
+          triggerText: 'Patchset 1',
+          text: 'Patchset 1',
+          mobileText: '1',
+          bottomText: '',
+          value: 1,
+        },
+        {
+          disabled: true,
+          triggerText: 'Patchset 2',
+          text: 'Patchset 2',
+          mobileText: '2 description',
+          bottomText: 'description',
+          value: 2,
+        },
+        {
+          disabled: true,
+          triggerText: 'Patchset 3',
+          text: 'Patchset 3',
+          mobileText: '3',
+          bottomText: '',
+          value: 3,
+        },
+        {
+          disabled: true,
+          triggerText: 'Patchset edit',
+          text: 'Patchset edit',
+          mobileText: 'edit',
+          bottomText: '',
+          value: 'edit',
+        },
+      ];
+      assert.deepEqual(element._computeBaseDropdownContent(availablePatches,
+          patchNum, sortedRevisions, revisions), expectedResult);
     });
 
-    test('_computeLeftDisabled called when patchNum updates', () => {
+    test('_computeBaseDropdownContent called when patchNum updates', () => {
       element.revisions = [
         {commit: {}},
         {commit: {}},
@@ -104,118 +159,103 @@
         {num: 3},
         {num: 'edit'},
       ];
-      element.patchRange = {patchNum: 2, basePatchNum: 'PARENT'};
+      element.patchNum = 2;
+      element.basePatchNum = 'PARENT';
+      flushAsynchronousOperations();
+
+      sandbox.stub(element, '_computeBaseDropdownContent');
+
+      // Should be recomputed for each available patch
+      element.set('patchNum', 1);
+      assert.equal(element._computeBaseDropdownContent.callCount, 1);
+    });
+
+    test('_computePatchDropdownContent called when basePatchNum updates', () => {
+      element.revisions = [
+        {commit: {}},
+        {commit: {}},
+        {commit: {}},
+        {commit: {}},
+      ];
+      element.availablePatches = [
+        {num: 1},
+        {num: 2},
+        {num: 3},
+        {num: 'edit'},
+      ];
+      element.patchNum = 2;
+      element.basePatchNum = 'PARENT';
       flushAsynchronousOperations();
 
       // Should be recomputed for each available patch
-      sandbox.stub(element, '_computeLeftDisabled');
-      element.set('patchRange.patchNum', '1');
-      assert.equal(element._computeLeftDisabled.callCount, 4);
+      sandbox.stub(element, '_computePatchDropdownContent');
+      element.set('basePatchNum', 1);
+      assert.equal(element._computePatchDropdownContent.callCount, 1);
     });
 
-    test('_computeRightDisabled called when basePatchNum updates', () => {
-      element.revisions = [
-        {commit: {}},
-        {commit: {}},
-        {commit: {}},
-        {commit: {}},
-      ];
-      element.availablePatches = [
+    test('_computePatchDropdownContent', () => {
+      element.comments = {};
+      const availablePatches = [
         {num: 1},
         {num: 2},
         {num: 3},
         {num: 'edit'},
       ];
-      element.patchRange = {patchNum: 2, basePatchNum: 'PARENT'};
-      flushAsynchronousOperations();
-
-      // Should be recomputed for each available patch
-      sandbox.stub(element, '_computeRightDisabled');
-      element.set('patchRange.basePatchNum', '1');
-      assert.equal(element._computeRightDisabled.callCount, 4);
-    });
-
-
-    test('changes in patch range fire event', done => {
-      sandbox.stub(element, '_computeLeftDisabled').returns(false);
-      sandbox.stub(element, '_computeRightDisabled').returns(false);
-      const patchRangeChangedStub = sandbox.stub();
-      element.addEventListener('patch-range-change', patchRangeChangedStub);
-
-      const leftSelectEl = element.$.leftPatchSelect;
-      const rightSelectEl = element.$.rightPatchSelect;
-      const blurSpy = sandbox.spy(leftSelectEl, 'blur');
-      element.changeNum = '42';
-      element.path = 'path/to/file.txt';
-      element.availablePatches =
-          [{num: '1'}, {num: '2'}, {num: '3'}, {num: 'edit'}];
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: '3',
-      };
-      flushAsynchronousOperations();
-
-      let numEvents = 0;
-      leftSelectEl.addEventListener('change', e => {
-        numEvents++;
-        if (numEvents === 1) {
-          assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
-              {rightPatch: '3', leftPatch: 'PARENT'});
-          leftSelectEl.nativeSelect.value = 'edit';
-          element.fire('change', {}, {node: leftSelectEl});
-          assert(blurSpy.called, 'Dropdown should be blurred after selection');
-        } else if (numEvents === 2) {
-          assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
-              {rightPatch: '3', leftPatch: 'edit'});
-          rightSelectEl.nativeSelect.value = '1';
-          element.fire('change', {}, {node: rightSelectEl});
-        }
-      });
-      rightSelectEl.addEventListener('change', e => {
-        assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
-            {rightPatch: '1', leftPatch: 'edit'});
-        done();
-      });
-      leftSelectEl.nativeSelect.value = 'PARENT';
-      rightSelectEl.nativeSelect.value = '3';
-      element.fire('change', {}, {node: leftSelectEl});
-    });
-
-    test('diff against dropdown', done => {
-      element.revisions = [
-        {commit: {}},
+      const revisions = [
+        {
+          commit: {},
+          _number: 2,
+          description: 'description',
+        },
         {commit: {}},
         {commit: {}},
         {commit: {}},
       ];
-      element.availablePatches = [
-        {num: 1},
-        {num: 2},
-        {num: 3},
-        {num: 'edit'},
+      const basePatchNum = 1;
+      const sortedRevisions = [
+        {_number: 1},
+        {_number: 2},
+        {_number: element.EDIT_NAME, basePatchNum: 2},
+        {_number: 3},
       ];
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: '3',
-      };
 
-      const patchRangeChangedStub = sandbox.stub();
-      element.addEventListener('patch-range-change', patchRangeChangedStub);
+      const expectedResult = [
+        {
+          disabled: true,
+          triggerText: 'Patchset 1',
+          text: 'Patchset 1',
+          mobileText: '1',
+          bottomText: '',
+          value: 1,
+        },
+        {
+          disabled: false,
+          triggerText: 'Patchset 2',
+          text: 'Patchset 2',
+          mobileText: '2 description',
+          bottomText: 'description',
+          value: 2,
+        },
+        {
+          disabled: false,
+          triggerText: 'Patchset 3',
+          text: 'Patchset 3',
+          mobileText: '3',
+          bottomText: '',
+          value: 3,
+        },
+        {
+          disabled: false,
+          triggerText: 'Patchset edit',
+          text: 'Patchset edit',
+          mobileText: 'edit',
+          bottomText: '',
+          value: 'edit',
+        },
+      ];
 
-      flush(() => {
-        const selectEl = element.$.leftPatchSelect;
-        assert.equal(selectEl.nativeSelect.value, 'PARENT');
-        assert.isTrue(element.$$('#leftPatchSelect option[value="3"]')
-            .hasAttribute('disabled'));
-        selectEl.addEventListener('change', () => {
-          assert.equal(selectEl.nativeSelect.value, 'edit');
-          assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
-              {leftPatch: 'edit', rightPatch: '3'});
-          done();
-        });
-        selectEl.nativeSelect.value = 'edit';
-        element.fire('change', {}, {node: selectEl.nativeSelect});
-      });
+      assert.deepEqual(element._computePatchDropdownContent(availablePatches,
+          basePatchNum, sortedRevisions, revisions), expectedResult);
     });
 
     test('filesWeblinks', () => {
@@ -264,12 +304,12 @@
       };
 
       assert.equal(element._computePatchSetCommentsString(comments, 1),
-          '(3 comments, 1 unresolved)');
+          ' (3 comments, 1 unresolved)');
 
       // Test string with no unresolved comments.
       delete comments['foo'];
       assert.equal(element._computePatchSetCommentsString(comments, 1),
-          '(2 comments)');
+          ' (2 comments)');
 
       // Test string with no comments.
       delete comments['bar'];
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
index 31a276f..311a5af 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
@@ -60,7 +60,7 @@
         color: #219;
       }
       .gr-syntax-type {
-        color: #00f;
+        color: var(--color-link);
       }
       .gr-syntax-title {
         color: #0000C0;
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index 578989e..c7ab3d9 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -24,6 +24,8 @@
 <link rel="import" href="gr-endpoint-decorator.html">
 <link rel="import" href="../gr-endpoint-param/gr-endpoint-param.html">
 
+<script>void(0);</script>
+
 <test-fixture id="basic">
   <template>
     <gr-endpoint-decorator name="foo">
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index 3a0c898..86c0961 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -47,12 +47,10 @@
      * States that it expects no more than 3 parameters, but that's not true.
      * @todo (beckysiegel) check Polymer annotations and submit change.
      */
-
     _importHtmlPlugins(plugins) {
       for (const url of plugins) {
         this.importHref(
-            this._urlFor(url), Gerrit._pluginInstalled, Gerrit._pluginInstalled,
-            true);
+            this._urlFor(url), null, Gerrit._pluginInstalled, true);
       }
     },
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index 27adbe1..66c7511 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -23,6 +23,8 @@
 <link rel="import" href="../../../test/common-test-setup.html"/>
 <link rel="import" href="gr-plugin-host.html">
 
+<script>void(0);</script>
+
 <test-fixture id="basic">
   <template>
     <gr-plugin-host></gr-plugin-host>
@@ -61,9 +63,9 @@
         plugin: {html_resource_paths: ['foo/bar', 'baz']},
       };
       assert.isTrue(element.importHref.calledWith(
-          '/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
+          '/foo/bar', null, Gerrit._pluginInstalled, true));
       assert.isTrue(element.importHref.calledWith(
-          '/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
+          '/baz', null, Gerrit._pluginInstalled, true));
     });
 
     test('imports relative html plugins from config with a base url', () => {
@@ -71,11 +73,9 @@
       element.config = {
         plugin: {html_resource_paths: ['foo/bar', 'baz']}};
       assert.isTrue(element.importHref.calledWith(
-          '/the-base/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled,
-          true));
+          '/the-base/foo/bar', null, Gerrit._pluginInstalled, true));
       assert.isTrue(element.importHref.calledWith(
-          '/the-base/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled,
-          true));
+          '/the-base/baz', null, Gerrit._pluginInstalled, true));
     });
 
     test('imports absolute html plugins from config', () => {
@@ -88,11 +88,9 @@
         },
       };
       assert.isTrue(element.importHref.calledWith(
-          'http://example.com/foo/bar', Gerrit._pluginInstalled,
-          Gerrit._pluginInstalled, true));
+          'http://example.com/foo/bar', null, Gerrit._pluginInstalled, true));
       assert.isTrue(element.importHref.calledWith(
-          'https://example.com/baz', Gerrit._pluginInstalled,
-          Gerrit._pluginInstalled, true));
+          'https://example.com/baz', null, Gerrit._pluginInstalled, true));
     });
 
     test('adds js plugins from config to the body', () => {
@@ -139,9 +137,9 @@
         },
       };
       assert.isTrue(element.importHref.calledWith(
-          '/oof', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
+          '/oof', null, Gerrit._pluginInstalled, true));
       assert.isTrue(element.importHref.calledWith(
-          '/some', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
+          '/some', null, Gerrit._pluginInstalled, true));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
index fa3428a..6805885 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
@@ -59,18 +59,21 @@
               <td class="urlCell">[[item.url]]</td>
               <td class="buttonColumn">
                 <gr-button
+                    link
                     data-index="[[index]]"
                     on-tap="_handleMoveUpButton"
                     class="moveUpButton">↑</gr-button>
               </td>
               <td class="buttonColumn">
                 <gr-button
+                    link
                     data-index="[[index]]"
                     on-tap="_handleMoveDownButton"
                     class="moveDownButton">↓</gr-button>
               </td>
               <td>
                 <gr-button
+                    link
                     data-index="[[index]]"
                     on-tap="_handleDeleteButton"
                     class="remove-button">Delete</gr-button>
@@ -99,6 +102,7 @@
             <th></th>
             <th>
               <gr-button
+                  link
                   disabled$="[[_computeAddDisabled(_newName, _newUrl)]]"
                   on-tap="_handleAddButton">Add</gr-button>
             </th>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index cacccda..7764d3b 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -353,6 +353,7 @@
               id="emailEditor"
               has-unsaved-changes="{{_emailsChanged}}"></gr-email-editor>
           <gr-button
+              link
               on-tap="_handleSaveEmails"
               disabled$="[[!_emailsChanged]]">Save changes</gr-button>
         </fieldset>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
index a834ea2..68b2c23 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -83,6 +83,7 @@
               </template>
               <td>
                 <gr-button
+                    link
                     data-index$="[[projectIndex]]"
                     on-tap="_handleRemoveProject">Delete</gr-button>
               </td>
@@ -106,7 +107,7 @@
                   placeholder="branch:name, or other search expression">
             </th>
             <th>
-              <gr-button on-tap="_handleAddProject">Add</gr-button>
+              <gr-button link on-tap="_handleAddProject">Add</gr-button>
             </th>
           </tr>
         </tfoot>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
index 90594de..b658025 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -37,24 +37,31 @@
       :host([show-avatar]) .container {
         padding-left: 0;
       }
-      gr-button.remove,
       gr-button.remove:hover,
       gr-button.remove:focus {
-        border-color: transparent;
-        color: #333;
+        --gr-button: {
+          color: #333;
+        }
       }
       gr-button.remove {
-        background: #eee;
-        border: 0;
-        color: #666;
-        font-size: 1.7em;
-        font-weight: normal;
-        height: .6em;
-        line-height: .6em;
-        margin-left: .15em;
-        margin-top: -.05em;
-        padding: 0;
-        text-decoration: none;
+        --gr-button: {
+          border: 0;
+          color: #666;
+          font-size: 1.7em;
+          font-weight: normal;
+          height: .6em;
+          line-height: .6em;
+          margin-left: .15em;
+          margin-top: -.05em;
+          padding: 0;
+          text-decoration: none;
+        }
+        --gr-button-hover-color: {
+          color: #333;
+        }
+        --gr-button-hover-background-color: {
+          color: #333;
+        }
       }
       :host:focus {
         border-color: transparent;
@@ -78,6 +85,7 @@
       <gr-account-link account="[[account]]"></gr-account-link>
       <gr-button
           id="remove"
+          link
           hidden$="[[!removable]]"
           hidden
           tabindex="-1"
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
index dcb38d4..c1c0338 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
@@ -15,7 +15,9 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <script src="../../../scripts/rootElement.js"></script>
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -23,12 +25,6 @@
 <dom-module id="gr-autocomplete-dropdown">
   <template>
     <style include="shared-styles">
-      :host {
-        background: #fff;
-        box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
-        position: absolute;
-        z-index: 104;
-      }
       /* This must be set here vs. the container component because in some cases
       the element is moved in the DOM to a base element and is no longer a
       child of its original parent. */
@@ -48,19 +44,34 @@
       li.selected {
         background-color: #eee;
       }
+      .dropdown-content {
+        background: #fff;
+        box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
+      }
     </style>
-    <div id="suggestions" role="listbox">
-      <ul>
-        <template is="dom-repeat" items="[[suggestions]]">
-          <li data-index$="[[index]]"
-              data-value$="[[item.dataValue]]"
-              tabindex="-1"
-              aria-label$="[[item.name]]"
-              role="option"
-              on-tap="_handleTapItem">[[item.text]]</li>
-        </template>
-      </ul>
-    </div>
+    <iron-dropdown
+        id="dropdown"
+        allow-outside-scroll="true"
+        vertical-align="top"
+        horizontal-align="auto"
+        vertical-offset="[[verticalOffset]]">
+      <div
+          class="dropdown-content"
+          slot="dropdown-content"
+          id="suggestions"
+          role="listbox">
+        <ul>
+          <template is="dom-repeat" items="[[suggestions]]">
+            <li data-index$="[[index]]"
+                data-value$="[[item.dataValue]]"
+                tabindex="-1"
+                aria-label$="[[item.name]]"
+                role="option"
+                on-tap="_handleTapItem">[[item.text]]</li>
+          </template>
+        </ul>
+      </div>
+    </iron-dropdown>
     <gr-cursor-manager
         id="cursor"
         index="{{index}}"
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 12fb074..100b5ca 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -14,6 +14,9 @@
 (function() {
   'use strict';
 
+  const AWAIT_MAX_ITERS = 10;
+  const AWAIT_STEP = 5;
+
   Polymer({
     is: 'gr-autocomplete-dropdown',
 
@@ -31,8 +34,14 @@
 
     properties: {
       index: Number,
-      moveToRoot: Boolean,
-      fixedPosition: Boolean,
+      verticalOffset: {
+        type: Number,
+        value: null,
+      },
+      horizontalOffset: {
+        type: Number,
+        value: null,
+      },
       suggestions: {
         type: Array,
         observer: '_resetCursorStops',
@@ -55,31 +64,47 @@
       tab: '_handleTab',
     },
 
-    attached() {
-      if (this.fixedPosition) {
-        this.classList.add('fixed');
-      }
-    },
-
     close() {
-      if (this.moveToRoot) {
-        Gerrit.getRootElement().removeChild(this);
-      } else {
-        this.hidden = true;
-      }
+      this.$.dropdown.close();
     },
 
     open() {
-      if (this.moveToRoot) {
-        Gerrit.getRootElement().appendChild(this);
-      }
-      this._resetCursorStops();
-      this._resetCursorIndex();
+      this._open().then(() => {
+        this._resetCursorStops();
+        this._resetCursorIndex();
+        this.fire('open-complete');
+      });
     },
 
-    setPosition(top, left) {
-      this.style.top = top;
-      this.style.left = left;
+    // TODO (beckysiegel) look into making this a behavior since it's used
+    // 3 times now.
+    _open(...args) {
+      return new Promise(resolve => {
+        Polymer.IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args);
+        this._awaitOpen(resolve);
+      });
+    },
+
+    /**
+     * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
+     * opening. Eventually replace with a direct way to listen to the overlay.
+     */
+    _awaitOpen(fn) {
+      let iters = 0;
+      const step = () => {
+        this.async(() => {
+          if (this.style.display !== 'none') {
+            fn.call(this);
+          } else if (iters++ < AWAIT_MAX_ITERS) {
+            step.call(this);
+          }
+        }, AWAIT_STEP);
+      };
+      step.call(this);
+    },
+
+    get isHidden() {
+      return !this.$.dropdown.opened;
     },
 
     getCurrentText() {
@@ -134,9 +159,7 @@
 
     _handleEscape() {
       this._fireClose();
-      if (!this.hidden) {
-        this.close();
-      }
+      this.close();
     },
 
     _handleTapItem(e) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
index db9440c..23b27be 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
@@ -27,13 +27,7 @@
 
 <test-fixture id="basic">
   <template>
-    <gr-autocomplete-dropdown id="dropdown"></gr-autocomplete-dropdown>
-  </template>
-</test-fixture>
-
-<test-fixture id="move">
-  <template>
-    <gr-autocomplete-dropdown id="dropdown" move-to-root></gr-autocomplete-dropdown>
+    <gr-autocomplete-dropdown></gr-autocomplete-dropdown>
   </template>
 </test-fixture>
 
@@ -49,6 +43,7 @@
       element.suggestions = [
         {dataValue: 'test value 1', name: 'test name 1', text: 1},
         {dataValue: 'test value 2', name: 'test name 2', text: 2}];
+      flushAsynchronousOperations();
     });
 
     teardown(() => {
@@ -56,26 +51,12 @@
       if (element.isOpen) element.close();
     });
 
-    test('dropdown has not been moved from text fixture to the body', () => {
-      assert.equal(Polymer.dom(document.root)
-          .querySelectorAll('gr-autocomplete-dropdown').length, 1);
-      const dropdown = Polymer.dom(document.root)
-          .querySelector('gr-autocomplete-dropdown');
-      assert.isOk(dropdown);
-      assert.notDeepEqual(dropdown.parentElement,
-          Polymer.dom(document.root).querySelector('body'));
-    });
-
-    test('escape key', () => {
-      const listener = sandbox.spy();
-      element.hidden = false;
-      element.addEventListener('dropdown-closed', listener);
-      const closeSpy = sandbox.spy(element, 'close');
+    test('escape key', done => {
+      const closeSpy = sandbox.spy(element.$.dropdown, 'close');
       MockInteractions.pressAndReleaseKeyOn(element, 27);
       flushAsynchronousOperations();
-      assert.isTrue(listener.called);
       assert.isTrue(closeSpy.called);
-      assert.isTrue(element.hidden);
+      done();
     });
 
     test('tab key', () => {
@@ -150,55 +131,4 @@
     });
   });
 
-  suite('gr-autocomplete-dropdown to root', () => {
-    let element;
-    let sandbox;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      fixture('move').open();
-      // The element was moved to the body, so look for it there.
-      element = Polymer.dom(document.root)
-        .querySelector('gr-autocomplete-dropdown');
-      element.suggestions = [
-        {dataValue: 'test value 1', name: 'test name 1', text: 1},
-        {dataValue: 'test value 2', name: 'test name 2', text: 2}];
-    });
-
-    teardown(() => {
-      sandbox.restore();
-      if (!element.hidden) element.close();
-    });
-
-    test('dropdown has been moved from the text fixture to the body', () => {
-      assert.equal(Polymer.dom(document.root)
-          .querySelectorAll('gr-autocomplete-dropdown').length, 1);
-      const dropdown = Polymer.dom(document.root)
-        .querySelector('gr-autocomplete-dropdown');
-      assert.isOk(dropdown);
-      assert.deepEqual(dropdown.parentElement, Polymer.dom(document.root)
-          .querySelector('body'));
-    });
-
-    test('closing removes from body and adding adds to body', () => {
-      element.close();
-      assert.equal(Polymer.dom(document.root)
-          .querySelectorAll('gr-autocomplete-dropdown').length, 0);
-      element.open();
-      assert.equal(Polymer.dom(document.root)
-          .querySelectorAll('gr-autocomplete-dropdown').length, 1);
-    });
-
-    test('setPosition', () => {
-      const top = '10px';
-      const left = '20px';
-      element.setPosition(top, left);
-      assert.equal(getComputedStyle(element).top, top);
-      assert.equal(getComputedStyle(element).left, left);
-    });
-
-    test('getCurrentText', () => {
-      assert.equal(element.getCurrentText(), 'test value 1');
-    });
-  });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index 33d3cd9..81ac90e 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -57,8 +57,7 @@
           on-keydown="_handleKeydown"
           suggestions="[[_suggestions]]"
           role="listbox"
-          index="[[_index]]"
-          hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]">
+          index="[[_index]]">
       </gr-autocomplete-dropdown>
     </div>
   </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 6df6c98..aa20f96 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -147,6 +147,7 @@
 
     observers: [
       '_textChanged(text)',
+      '_maybeOpenDropdown(_suggestions, _focused)',
     ],
 
     _textChanged() {
@@ -230,8 +231,11 @@
       });
     },
 
-    _computeSuggestionsHidden(suggestions, focused) {
-      return !(suggestions.length && focused);
+    _maybeOpenDropdown(suggestions, focused) {
+      if (suggestions.length > 0 && focused) {
+        return this.$.suggestions.open();
+      }
+      return this.$.suggestions.close();
     },
 
     _computeClass(borderless) {
@@ -280,7 +284,7 @@
 
     _cancel() {
       if (this._suggestions.length) {
-        this._suggestions = [];
+        this.set('_suggestions', []);
       } else {
         this.fire('cancel');
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 038962c..fdfcddb 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -57,7 +57,7 @@
         ]);
       });
       element.query = queryStub;
-      assert.isTrue(element.$.suggestions.hidden);
+      assert.isTrue(element.$.suggestions.isHidden);
       assert.equal(element.$.suggestions.$.cursor.index, -1);
 
       element.text = 'blah';
@@ -66,7 +66,7 @@
       element._focused = true;
 
       promise.then(() => {
-        assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
+        assert.isFalse(element.$.suggestions.isHidden);
         const suggestions =
             Polymer.dom(element.$.suggestions.root).querySelectorAll('li');
         assert.equal(suggestions.length, 5);
@@ -89,20 +89,20 @@
       });
       element.query = queryStub;
 
-      assert.isTrue(element.$.suggestions.hidden);
+      assert.isTrue(element.$.suggestions.isHidden);
 
       element._focused = true;
       element.text = 'blah';
 
       promise.then(() => {
-        assert.isFalse(element.$.suggestions.hidden);
+        assert.isFalse(element.$.suggestions.isHidden);
 
         const cancelHandler = sandbox.spy();
         element.addEventListener('cancel', cancelHandler);
 
         MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
         assert.isFalse(cancelHandler.called);
-        assert.isTrue(element.$.suggestions.hidden);
+        assert.isTrue(element.$.suggestions.isHidden);
         assert.equal(element._suggestions.length, 0);
 
         MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
@@ -124,13 +124,13 @@
       });
       element.query = queryStub;
 
-      assert.isTrue(element.$.suggestions.hidden);
+      assert.isTrue(element.$.suggestions.isHidden);
       assert.equal(element.$.suggestions.$.cursor.index, -1);
       element._focused = true;
       element.text = 'blah';
 
       promise.then(() => {
-        assert.isFalse(element.$.suggestions.hidden);
+        assert.isFalse(element.$.suggestions.isHidden);
 
         const commitHandler = sandbox.spy();
         element.addEventListener('commit', commitHandler);
@@ -157,7 +157,7 @@
         assert.equal(element.value, 1);
         assert.isTrue(commitHandler.called);
         assert.equal(commitHandler.getCall(0).args[0].detail.value, 1);
-        assert.isTrue(element.$.suggestions.hidden);
+        assert.isTrue(element.$.suggestions.isHidden);
         assert.isTrue(element._focused);
         done();
       });
@@ -291,9 +291,16 @@
     });
 
     test('_focused flag shows/hides the suggestions', () => {
-      const suggestions = ['hello', 'its me'];
-      assert.isTrue(element._computeSuggestionsHidden(suggestions, false));
-      assert.isFalse(element._computeSuggestionsHidden(suggestions, true));
+      const openStub = sandbox.stub(element.$.suggestions, 'open');
+      const closedStub = sandbox.stub(element.$.suggestions, 'close');
+      element._suggestions = ['hello', 'its me'];
+      assert.isFalse(openStub.called);
+      assert.isTrue(closedStub.calledOnce);
+      element._focused = true;
+      assert.isTrue(openStub.calledOnce);
+      element._suggestions = [];
+      assert.isTrue(closedStub.calledTwice);
+      assert.isTrue(openStub.calledOnce);
     });
 
     test('changing input sets _textChangedSinceCommit', () => {
@@ -375,7 +382,7 @@
         element.tabComplete = false;
         focusSpy = sandbox.spy(element, 'focus');
         Polymer.dom.flush();
-        assert.isFalse(element.$.suggestions.hidden);
+        assert.isFalse(element.$.suggestions.isHidden);
 
         MockInteractions.pressAndReleaseKeyOn(
             element.$.suggestions.$$('li:first-child'), 9, null, 'tab');
@@ -391,7 +398,7 @@
         element.tabComplete = true;
         focusSpy = sandbox.spy(element, 'focus');
         Polymer.dom.flush();
-        assert.isFalse(element.$.suggestions.hidden);
+        assert.isFalse(element.$.suggestions.isHidden);
 
         MockInteractions.pressAndReleaseKeyOn(
             element.$.suggestions.$$('li:first-child'), 9, null, 'tab');
@@ -406,13 +413,13 @@
         element._focused = true;
         element._suggestions = [{name: 'first suggestion'}];
         Polymer.dom.flush();
-        assert.isFalse(element.$.suggestions.hidden);
+        assert.isFalse(element.$.suggestions.isHidden);
         MockInteractions.tap(element.$.suggestions.$$('li:first-child'));
         flushAsynchronousOperations();
 
         assert.isFalse(focusSpy.called);
         assert.isTrue(commitSpy.called);
-        assert.isTrue(element.$.suggestions.hidden);
+        assert.isTrue(element.$.suggestions.isHidden);
       });
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index c194bcb..f63d7923 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -18,122 +18,100 @@
 
 <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../bower_components/paper-button/paper-button.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-button">
   <template strip-whitespace>
     <style include="shared-styles">
       :host {
-        background-color: #f5f5f5;
-        border: 1px solid #d1d2d3;
-        border-radius: 2px;
-        box-sizing: border-box;
-        color: #333;
-        cursor: pointer;
         display: inline-block;
         font-family: var(--font-family-bold);
         font-size: 12px;
-        outline-width: 0;
-        padding: .4em .85em;
         position: relative;
-        text-align: center;
-        -moz-user-select: none;
-        -ms-user-select: none;
-        -webkit-user-select: none;
-        user-select: none;
       }
       :host([hidden]) {
         display: none;
       }
-      :host([primary]),
-      :host([secondary]) {
-        color: #fff;
-      }
-      :host([primary]) {
-        background-color: #4d90fe;
-        border-color: #3079ed;
-      }
-      :host([secondary]) {
-        background-color: #d14836;
-        border-color: transparent;
-      }
-      :host([small]) {
-        font-size: 12px;
-      }
       :host([link]) {
         background-color: transparent;
         border: none;
-        color: #00f;
+        color: var(--color-link);
         font-size: inherit;
-        font-family: var(--font-family);
-        padding: 0;
-        text-decoration: underline;
+        font-family: var(--font-family-bold);
+        text-transform: none;
       }
-      :host([loading]),
-      :host([disabled]) {
+      :host([link]) paper-button {
+        margin: 0;
+        padding: 0;
+        @apply --gr-button;
+      }
+      paper-button[raised] {
+        background-color: var(--gr-button-background, #fff);
+        color: var(--gr-button-color, --color-link);
+      }
+      /* todo (beckysiegel) switch all secondary to primary as there is no color
+        distinction anymore. */
+      :host([primary]) paper-button[raised],
+      :host([secondary]) paper-button[raised] {
+        background-color: var(--color-link);
+        color: #fff;
+      }
+      :host([link]) paper-button:hover,
+      :host([link]) paper-button:focus,
+      paper-button[raised]:hover,
+      paper-button[raised]:focus  {
+        color: var(--gr-button-hover-color, --color-button-hover);
+      }
+      :host([primary]) paper-button[raised]:hover,
+      :host([primary]) paper-button[raised]:focus,
+      :host([secondary]) paper-button[raised]:hover,
+      :host([secondary]) paper-button[raised]:focus {
+        background-color: var(--gr-button-hover-background-color, --color-button-hover);
+        color: var(--gr-button-color, #fff);
+      }
+      paper-button,
+      paper-button[raised],
+      paper-button[link] {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin: 0;
+        min-width: 0;
+        padding: .4em .85em;
+        @apply --gr-button;
+      }
+      :host([link]) paper-button {
+        --paper-button: {
+          padding: 0;
+        }
+      }
+      :host:not([down-arrow]) .downArrow {display: none; }
+      :host([down-arrow]) .downArrow {
+        border-left: .36em solid transparent;
+        border-right: .36em solid transparent;
+        border-top: .36em solid #ccc;
+        margin-left: .5em;
+        transition: border-top-color 200ms;
+      }
+      :host([down-arrow]):hover .downArrow {
+        border-top-color: #666;
+      }
+      :host([loading]) paper-button,
+      :host([disabled]) paper-button {
+        color: #aaa;
+      }
+      :host([loading]) paper-button,
+      :host([loading][disabled]) paper-button {
+        cursor: wait;
         background-color: #efefef;
         color: #aaa;
       }
-      :host([disabled]) {
-        cursor: default;
-      }
-      :host([loading]),
-      :host([loading][disabled]) {
-        cursor: wait;
-      }
-      :host:focus:not([link]),
-      :host:hover:not([link]) {
-        background-color: #f8f8f8;
-        border-color: #aaa;
-      }
-      :host(:active) {
-        border-color: #d1d2d3;
-        color: #aaa;
-      }
-      :host([primary]:focus),
-      :host([secondary]:focus),
-      :host([primary]:active),
-      :host([secondary]:active) {
-        color: #fff;
-      }
-      :host([primary]:focus) {
-        box-shadow: 0 0 1px #00f;
-        background-color: #4d90fe;
-      }
-      :host([primary]:not([disabled]):hover) {
-        background-color: #4d90fe;
-        border-color: #00F;
-      }
-      :host([primary]:active),
-      :host([secondary]:active) {
-        box-shadow: none;
-      }
-      :host([primary]:active) {
-        border-color: #0c2188;
-      }
-      :host([secondary]:focus) {
-        box-shadow: 0 0 1px #f00;
-        background-color: #d14836;
-      }
-      :host([secondary]:not([disabled]):hover) {
-        background-color: #c53727;
-        border: 1px solid #b0281a;
-      }
-      :host([secondary]:active) {
-        border-color: #941c0c;
-      }
-      :host([primary][loading]) {
-        background-color: #7caeff;
-        border-color: transparent;
-        color: #fff;
-      }
-      :host([primary][disabled]) {
-        background-color: #4d90fe;
-        color: #fff;
-        opacity: .5;
-      }
     </style>
-    <content></content>
+    <paper-button raised="[[!link]]">
+      <content></content>
+      <i class="downArrow"></i>
+    </paper-button>
   </template>
   <script src="gr-button.js"></script>
-</dom-module>
+</dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index ddb2bc3..80c0a9b 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -18,8 +18,13 @@
     is: 'gr-button',
 
     properties: {
+      downArrow: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
       link: {
         type: Boolean,
+        value: false,
         reflectToAttribute: true,
       },
       disabled: {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
index 59f63fa..8a3de33 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -62,6 +62,7 @@
       paper-item {
         cursor: pointer;
         flex-direction: column;
+        font-size: 1em;
         --paper-item: {
           min-height: 0;
           padding: 10px 16px;
@@ -79,7 +80,7 @@
       paper-item:not(:last-of-type) {
         border-bottom: 1px solid #ddd;
       }
-      gr-button {
+      #trigger {
         color: black;
         font: inherit;
         padding: .3em 0;
@@ -87,7 +88,7 @@
       }
       .bottomContent {
         color: rgba(0,0,0,.54);
-        font-size: .85em;
+        font-size: .9em;
         line-height: 16px;
       }
       .bottomContent,
@@ -105,6 +106,16 @@
       gr-select {
         display: none;
       }
+      /* Because the iron dropdown 'area' includes the trigger, and the entire
+       width of the dropdown, we want to treat tapping the area above the
+       dropdown content as if it is tapping whatever content is underneath it.
+       The next two styles allow this to happen. */
+      iron-dropdown {
+        pointer-events: none;
+      }
+      paper-listbox {
+        pointer-events: auto;
+      }
       @media only screen and (max-width: 50em) {
         gr-select {
           display: inline;
@@ -122,7 +133,8 @@
         link
         id="trigger"
         class="dropdown-trigger"
-        on-tap="_showDropdownTapHandler">
+        on-tap="_showDropdownTapHandler"
+        slot="dropdown-trigger">
       <span>[[text]]</span>
       <span
           class="downArrow"
@@ -131,13 +143,14 @@
     <iron-dropdown
         id="dropdown"
         vertical-align="top"
-        allow-outside-scroll="true">
+        allow-outside-scroll="true"
+        on-tap="_handleDropdownTap">
       <paper-listbox
           class="dropdown-content"
           slot="dropdown-content"
           attr-for-selected="value"
-          on-tap="_handleDropdownTap"
-          selected="{{value}}">
+          selected="{{value}}"
+          on-tap="_handleDropdownTap">
         <template is="dom-repeat" items="[[items]]">
             <paper-item
                 disabled="[[item.disabled]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
index 27c6ba8..dc8b44e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -95,6 +95,7 @@
       const selectedObj = items.find(item => {
         return item.value + '' === value + '';
       });
+      if (!selectedObj) { return; }
       this.text = selectedObj.triggerText? selectedObj.triggerText :
           selectedObj.text;
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index f4813fa..b821909 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -43,9 +43,6 @@
         font: inherit;
         padding: .3em 0;
       }
-      :host([down-arrow]) .dropdown-trigger {
-        padding-right: 1.4em;
-      }
       gr-avatar {
         height: 2em;
         width: 2em;
@@ -75,7 +72,6 @@
       }
       li .itemAction:link,
       li .itemAction:visited {
-        color: #00e;
         text-decoration: none;
       }
       li .itemAction:not(.disabled):hover {
@@ -94,26 +90,13 @@
       .bold-text {
         font-family: var(--font-family-bold);
       }
-      :host:not([down-arrow]) .downArrow { display: none; }
-      :host([down-arrow]) .downArrow {
-        border-left: .36em solid transparent;
-        border-right: .36em solid transparent;
-        border-top: .36em solid #ccc;
-        height: 0;
-        position: absolute;
-        right: .3em;
-        top: calc(50% - .05em);
-        transition: border-top-color 200ms;
-        width: 0;
-      }
-      .dropdown-trigger:hover .downArrow {
-        border-top-color: #666;
-      }
     </style>
-    <gr-button link="[[link]]" class="dropdown-trigger" id="trigger"
+    <gr-button
+        link="[[link]]"
+        class="dropdown-trigger" id="trigger"
+        down-arrow="[[downArrow]]"
         on-tap="_showDropdownTapHandler">
       <content></content>
-       <i class="downArrow"></i>
     </gr-button>
     <iron-dropdown id="dropdown"
         vertical-align="top"
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index ec8f04c..49e5a5e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -37,6 +37,7 @@
         type: Array,
         observer: '_resetCursorStops',
       },
+      downArrow: Boolean,
       topContent: Object,
       horizontalAlign: {
         type: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
index 79d69f5..6eb7c8d 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -27,6 +27,9 @@
         align-items: center;
         display: inline-flex;
       }
+      :host([uppercase]) label {
+        text-transform: uppercase;
+      }
       input,
       label {
         width: 100%;
@@ -37,14 +40,14 @@
       label {
         color: #777;
         display: inline-block;
+        font-family: var(--font-family-bold);
         overflow: hidden;
         text-overflow: ellipsis;
         white-space: nowrap;
       }
       label.editable {
-        color: #00f;
+        color: var(--color-link);
         cursor: pointer;
-        text-decoration: underline;
       }
       #dropdown {
         box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index f87a546..b0e8516 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -46,6 +46,11 @@
         type: Boolean,
         value: false,
       },
+      uppercase: {
+        type: Boolean,
+        reflectToAttribute: true,
+        value: false,
+      },
       _inputText: String,
       // This is used to push the iron-input element up on the page, so
       // the input is placed in approximately the same position as the
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
index be71eb6..d30bad2 100644
--- 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
@@ -34,24 +34,31 @@
         display: inline-flex;
         padding: 0 .5em;
       }
-      gr-button.remove,
       gr-button.remove:hover,
       gr-button.remove:focus {
-        border-color: transparent;
-        color: #333;
+        --gr-button: {
+          color: #333;
+        }
       }
       gr-button.remove {
-        background: #eee;
-        border: 0;
-        color: #666;
-        font-size: 1.7em;
-        font-weight: normal;
-        height: .6em;
-        line-height: .6em;
-        margin-left: .15em;
-        margin-top: -.05em;
-        padding: 0;
-        text-decoration: none;
+        --gr-button: {
+          border: 0;
+          color: #666;
+          font-size: 1.7em;
+          font-weight: normal;
+          height: .6em;
+          line-height: .6em;
+          margin-left: .15em;
+          margin-top: -.05em;
+          padding: 0;
+          text-decoration: none;
+        }
+        --gr-button-hover-color: {
+          color: #333;
+        }
+        --gr-button-hover-background-color: {
+          color: #333;
+        }
       }
       .transparentBackground,
       gr-button.transparentBackground {
@@ -68,6 +75,7 @@
       </a>
       <gr-button
           id="remove"
+          link
           hidden$="[[!removable]]"
           hidden
           class$="remove [[_getBackgroundClass(transparentBackground)]]"
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 4e0f3b7..5153fb0 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
@@ -486,6 +486,14 @@
       });
     },
 
+    /**
+     * @param {string} userId the ID of the user usch as an email address.
+     * @return {!Promise<!Object>}
+     */
+    getAccountDetails(userId) {
+      return this.fetchJSON(`/accounts/${encodeURIComponent(userId)}/detail`);
+    },
+
     getAccountEmails() {
       return this._fetchSharedCacheURL('/accounts/self/emails');
     },
@@ -579,6 +587,10 @@
           });
     },
 
+    getAccountStatus(userId) {
+      return this.fetchJSON(`/accounts/${encodeURIComponent(userId)}/status`);
+    },
+
     getAccountGroups() {
       return this._fetchSharedCacheURL('/accounts/self/groups');
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
index 59daf07..95df7c9 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
@@ -28,10 +28,14 @@
     <style include="shared-styles">
       :host {
         display: block;
+        position: relative;
       }
       :host(.monospace) {
         font-family: var(--monospace-font-family);
       }
+      #emojiSuggestions {
+        font-family: var(--font-family);
+      }
       gr-autocomplete {
         display: inline-block
       }
@@ -39,11 +43,16 @@
         background-color: var(--background-color, none);
         width: 100%;
       }
+      #hiddenText #emojiSuggestions {
+        visibility: visible;
+        white-space: normal;
+      }
       /*This is needed to not add a scroll bar on the side of gr-textarea
       since there is 2px of padding in iron-autogrow-textarea for the
       native textarea*/
       iron-autogrow-textarea {
         padding: 2px;
+        position: relative;
       }
       #textarea.noBorder {
         border: none;
@@ -53,17 +62,19 @@
         float: left;
         position: absolute;
         visibility: hidden;
-        white-space: pre-wrap
+        width: 100%;
+        white-space: pre-wrap;
       }
     </style>
-    <gr-autocomplete-dropdown id="emojiSuggestions"
+    <div id="hiddenText"></div>
+    <gr-autocomplete-dropdown
+        id="emojiSuggestions"
         suggestions="[[_suggestions]]"
         index="[[_index]]"
-        move-to-root
-        fixed-position="[[fixedPositionDropdown]]"
-        hidden>
+        vertical-offset="[[_verticalOffset]]"
+        on-dropdown-closed="_resetAndFocus"
+        on-item-selected="_handleEmojiSelect">
     </gr-autocomplete-dropdown>
-    <div id="hiddenText"></div>
     <iron-autogrow-textarea
         id="textarea"
         autocomplete="[[autocomplete]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index a172a09..2d796bd 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -15,7 +15,6 @@
   'use strict';
 
   const MAX_ITEMS_DROPDOWN = 10;
-  const VERTICAL_OFFSET = 7;
 
   const ALL_SUGGESTIONS = [
     {value: '💯', match: '100'},
@@ -63,8 +62,6 @@
       rows: Number,
       maxRows: Number,
       placeholder: String,
-      fixedPositionDropdown: Boolean,
-      moveToRoot: Boolean,
       text: {
         type: String,
         notify: true,
@@ -95,6 +92,12 @@
       },
       _index: Number,
       _suggestions: Array,
+      // Offset makes dropdown appear below text.
+      _verticalOffset: {
+        type: Number,
+        value: 20,
+        readOnly: true,
+      },
     },
 
     behaviors: [
@@ -120,22 +123,11 @@
       if (this.backgroundColor) {
         this.updateStyles({'--background-color': this.backgroundColor});
       }
-      this.listen(this.$.emojiSuggestions, 'dropdown-closed', '_resetAndFocus');
-      this.listen(this.$.emojiSuggestions, 'item-selected',
-          '_handleEmojiSelect');
     },
 
-    detached() {
-      this.closeDropdown();
-      this.listen(this.$.emojiSuggestions, 'dropdown-closed', '_resetAndFocus');
-      this.listen(this.$.emojiSuggestions, 'item-selected',
-          '_handleEmojiSelect');
-    },
 
     closeDropdown() {
-      if (!this.$.emojiSuggestions.hidden) {
-        this._closeEmojiDropdown();
-      }
+      return this.$.emojiSuggestions.close();
     },
 
     getNativeTextarea() {
@@ -161,7 +153,6 @@
 
     _resetAndFocus() {
       this._resetEmojiDropdown();
-      this.$.textarea.textarea.focus();
     },
 
     _handleUpKey(e) {
@@ -197,14 +188,17 @@
       return this.text.substr(0, this._colonIndex || 0) +
           value + this.text.substr(this.$.textarea.selectionStart) + ' ';
     },
-
-    _getPositionOfCursor() {
+    /**
+     * Uses a hidden element with the same width and styling of the textarea and
+     * the text up until the point of interest. Then the emoji selection
+     * element is added to the end so that they are correctly positioned by the
+     * end of the last character entered.
+     */
+    _updateCaratPosition() {
       this.$.hiddenText.textContent = this.$.textarea.value.substr(0,
           this.$.textarea.selectionStart);
 
-      const caratSpan = document.createElement('span');
-      this.$.hiddenText.appendChild(caratSpan);
-      return caratSpan.getBoundingClientRect();
+      this.$.hiddenText.appendChild(this.$.emojiSuggestions);
     },
 
     _getFontSize() {
@@ -218,29 +212,6 @@
     },
 
     /**
-     * This positions the dropdown to be just below the cursor position. It is
-     * calculated by having a hidden element with the same width and styling of
-     * the tetarea and the text up until the point of interest. Then a span
-     * element is added to the end so that there is a specific element to get
-     * the position of.  Line height is determined (or falls back to 12px) as
-     * extra height to add.
-     */
-    _updateSelectorPosition() {
-      // These are broken out into separate functions for testability.
-      const caratPosition = this._getPositionOfCursor();
-      const fontSize = this._getFontSize();
-
-      let top = caratPosition.top + fontSize + VERTICAL_OFFSET;
-
-      if (!this.fixedPositionDropdown) {
-        top += this._getScrollTop();
-      }
-      top += 'px';
-      const left = caratPosition.left + 'px';
-      this.$.emojiSuggestions.setPosition(top, left);
-    },
-
-    /**
      * _handleKeydown used for key handling in the this.$.textarea AND all child
      * autocomplete options.
      */
@@ -278,23 +249,16 @@
           this._resetEmojiDropdown();
         // Otherwise open the dropdown and set the position to be just below the
         // cursor.
-        } else if (this.$.emojiSuggestions.hidden) {
+        } else if (this.$.emojiSuggestions.isHidden) {
           this._hideAutocomplete = false;
           this._openEmojiDropdown();
-          this._updateSelectorPosition();
+          this._updateCaratPosition();
         }
         this.$.textarea.textarea.focus();
       }
     },
-
-    _closeEmojiDropdown() {
-      this.$.emojiSuggestions.close();
-      this.$.emojiSuggestions.hidden = true;
-    },
-
     _openEmojiDropdown() {
       this.$.emojiSuggestions.open();
-      this.$.emojiSuggestions.hidden = false;
     },
 
     _formatSuggestions(matchedSuggestions) {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index 95e3a8d..493dd5d 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -72,7 +72,7 @@
       element.$.textarea.selectionStart = 1;
       element.$.textarea.selectionEnd = 1;
       element.text = ':';
-      assert.isFalse(!element.$.emojiSuggestions.hidden);
+      assert.isFalse(!element.$.emojiSuggestions.isHidden);
     });
 
     test('emoji selector is not open when a general text is entered', () => {
@@ -80,7 +80,7 @@
       element.$.textarea.selectionStart = 9;
       element.$.textarea.selectionEnd = 9;
       element.text = 'some text';
-      assert.isFalse(!element.$.emojiSuggestions.hidden);
+      assert.isFalse(!element.$.emojiSuggestions.isHidden);
     });
 
     test('emoji selector opens when a colon is typed & the textarea has focus',
@@ -95,7 +95,7 @@
           element.$.textarea.selectionEnd = 2;
           element.text = ':t';
           flushAsynchronousOperations();
-          assert.isFalse(element.$.emojiSuggestions.hidden);
+          assert.isFalse(element.$.emojiSuggestions.isHidden);
           assert.equal(element._colonIndex, 0);
           assert.isFalse(element._hideAutocomplete);
           assert.equal(element._currentSearchString, 't');
@@ -165,32 +165,16 @@
       assert.equal(element.text, 'test test 😂 ');
     });
 
-    test('_getPositionOfCursor', () => {
+    test('_updateCaratPosition', () => {
       element.$.textarea.selectionStart = 4;
       element.$.textarea.selectionEnd = 4;
       element.text = 'test';
-      element._getPositionOfCursor();
+      element._updateCaratPosition();
       assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
-          '<span></span>');
+          element.$.emojiSuggestions.outerHTML);
     });
 
-    test('_updateSelectorPosition', () => {
-      const setPositionSpy =
-          sandbox.spy(element.$.emojiSuggestions, 'setPosition');
-      sandbox.stub(element, '_getPositionOfCursor', () => {
-        return {top: 100, left: 30};
-      });
-      sandbox.stub(element, '_getFontSize', () => 12);
-      sandbox.stub(element, '_getScrollTop', () => 100);
-      element._updateSelectorPosition();
-      assert.isTrue(setPositionSpy.lastCall.calledWithExactly('219px', '30px'));
-
-      element.fixedPositionDropdown = true;
-      element._updateSelectorPosition();
-      assert.isTrue(setPositionSpy.lastCall.calledWithExactly('119px', '30px'));
-    });
-
-    test('emoji dropdown is closed when dropdown-closed is fired', () => {
+    test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
       const resetSpy = sandbox.spy(element, '_resetAndFocus');
       element.$.emojiSuggestions.fire('dropdown-closed');
       assert.isTrue(resetSpy.called);
@@ -205,56 +189,67 @@
     });
 
     suite('keyboard shortcuts', () => {
-      function setupDropdown() {
-        MockInteractions.focus(element.$.textarea);
+      function setupDropdown(callback) {
+        element.$.emojiSuggestions.addEventListener('open-complete', () => {
+          callback();
+        });
         flushAsynchronousOperations();
+        MockInteractions.focus(element.$.textarea);
         element.$.textarea.selectionStart = 1;
         element.$.textarea.selectionEnd = 1;
         element.text = ':';
         element.$.textarea.selectionStart = 1;
-        element.$.textarea.selectionEnd = 1;
+        element.$.textarea.selectionEnd = 2;
         element.text = ':1';
       }
 
-      test('escape key', () => {
+      test('escape key', done => {
         const resestSpy = sandbox.spy(element, '_resetAndFocus');
         MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
         assert.isFalse(resestSpy.called);
-        setupDropdown();
-        MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
-        assert.isTrue(resestSpy.called);
-        assert.isFalse(!element.$.emojiSuggestions.hidden);
+        setupDropdown(() => {
+          MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+          assert.isTrue(resestSpy.called);
+          assert.isFalse(!element.$.emojiSuggestions.isHidden);
+          done();
+        });
       });
 
-      test('up key', () => {
+      test('up key', done => {
         const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp');
         MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
         assert.isFalse(upSpy.called);
-        setupDropdown();
-        MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
-        assert.isTrue(upSpy.called);
+        setupDropdown(() => {
+          MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+          assert.isTrue(upSpy.called);
+          done();
+        });
       });
 
-      test('down key', () => {
+      test('down key', done => {
         const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown');
         MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
         assert.isFalse(downSpy.called);
-        setupDropdown();
-        MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
-        assert.isTrue(downSpy.called);
+        setupDropdown(() => {
+          MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+          assert.isTrue(downSpy.called);
+          done();
+        });
       });
 
-      test('enter key', () => {
+      test('enter key', done => {
         const enterSpy = sandbox.spy(element.$.emojiSuggestions,
             'getCursorTarget');
         MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
         assert.isFalse(enterSpy.called);
-        setupDropdown();
-        MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
-        assert.isTrue(enterSpy.called);
-        flushAsynchronousOperations();
-        // A space is automatically added at the end.
-        assert.equal(element.text, '💯 ');
+        setupDropdown(() => {
+          MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+          assert.isTrue(enterSpy.called);
+          flushAsynchronousOperations();
+          // A space is automatically added at the end.
+          assert.equal(element.text, '💯 ');
+          done();
+        });
       });
     });
   });
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index d68433c..573335c 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -38,25 +38,5 @@
     }
     return '';
   };
-
-  /**
-   * Truncates URLs to display filename only
-   * Example
-   * // returns '.../text.html'
-   * util.truncatePath.('dir/text.html');
-   * Example
-   * // returns 'text.html'
-   * util.truncatePath.('text.html');
-   * @return {string} Returns the truncated value of a URL.
-   */
-  util.truncatePath = function(path) {
-    const pathPieces = path.split('/');
-
-    if (pathPieces.length < 2) {
-      return path;
-    }
-    // Character is an ellipsis.
-    return '\u2026/' + pathPieces[pathPieces.length - 1];
-  };
   window.util = util;
 })(window);
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index 4318757..6a81158 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -35,6 +35,11 @@
   --iron-overlay-backdrop: {
     transition: none;
   }
+
+  /* Follow are a part of the design refresh */
+  --color-link: #2a66d9;
+  /* 12% darker */
+  --color-button-hover: #0B47BA;
 }
 @media screen and (max-width: 50em) {
   :root {
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index 5c11d60..7389fa4 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -38,6 +38,9 @@
         margin: 0;
         padding: 0;
       }
+      a {
+        color: var(--color-link);
+      }
       input,
       textarea,
       select,
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index c748a9b..7080eb7 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -52,6 +52,7 @@
     'change-list/gr-change-list-item/gr-change-list-item_test.html',
     'change-list/gr-change-list-view/gr-change-list-view_test.html',
     'change-list/gr-change-list/gr-change-list_test.html',
+    'change-list/gr-user-header/gr-user-header_test.html',
     'change/gr-account-entry/gr-account-entry_test.html',
     'change/gr-account-list/gr-account-list_test.html',
     'change/gr-change-actions/gr-change-actions_test.html',
@@ -160,6 +161,7 @@
 
   // Behaviors tests.
   const behaviors = [
+    'async-foreach-behavior/async-foreach-behavior_test.html',
     'base-url-behavior/base-url-behavior_test.html',
     'docs-url-behavior/docs-url-behavior_test.html',
     'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html',