Merge changes from topic 'guava-20.0'

* changes:
  Remove usage of to-be-deprecated Throwables
  Update Guava to 20.0 snapshot version 20160818.201422-323


*submodules:
* Update plugins/replication from branch 'master'
  - Remove usage of to-be-deprecated Throwables methods
    
    The propagate and propagateIfPossible methods will be deprecated
    in Guava 20. Replace them with the recommended alternatives.
    
    Change-Id: I11d8adcae3000189c16f60179e02a79440ef99d6
    
diff --git a/Documentation/config-cla.txt b/Documentation/config-cla.txt
index c07a24f..2234808 100644
--- a/Documentation/config-cla.txt
+++ b/Documentation/config-cla.txt
@@ -37,8 +37,13 @@
 Each `contributor-agreement` section within the `project.config` file must
 have a unique name. The section name will appear in the web UI.
 
-If not already present, add the UUID of the groups used in the
-`autoVerify` and `accepted` variables in the groups file.
+If not already present, add the group(s) used in the `autoVerify` and
+`accepted` variables in the `groups` file:
+----
+    # UUID                                  	Group Name
+    #
+    3dedb32915ecdbef5fced9f0a2587d164cd614d4	CLA Accepted - Individual
+----
 
 Commit the configuration change, and push it back:
 ----
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 686491c..7836b09 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3329,7 +3329,9 @@
 Full Name and Preferred Email.  This may cause messages to be
 classified as spam if the user's domain has SPF or DKIM enabled
 and <<sendemail.smtpServer,sendemail.smtpServer>> is not a trusted
-relay for that domain.
+relay for that domain. You can specify
+<<sendemail.allowedDomain,sendemail.allowedDomain>> to instruct Gerrit to only
+send as USER if USER is from those domains.
 +
 * `MIXED`
 +
@@ -3355,6 +3357,16 @@
 +
 By default, MIXED.
 
+[[sendemail.allowedDomain]]sendemail.allowedDomain::
++
+Only used when `sendemail.from` is set to `USER`.
+List of allowed domains. If user's email matches one of the domains, emails will
+be sent as USER, otherwise as MIXED mode. Wildcards may be specified by
+including `*` to match any number of characters, for example `*.example.com`
+matches any subdomain of `example.com`.
++
+By default, `*`.
+
 [[sendemail.smtpServer]]sendemail.smtpServer::
 +
 Hostname (or IP address) of a SMTP server that will relay
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py
index bc2d657..15f470c 100755
--- a/Documentation/gen_licenses.py
+++ b/Documentation/gen_licenses.py
@@ -95,16 +95,14 @@
 
 if args.asciidoc:
   print("""\
-Gerrit Code Review - Licenses
-=============================
+= Gerrit Code Review - Licenses
 
 Gerrit open source software is licensed under the <<Apache2_0,Apache
 License 2.0>>.  Executable distributions also include other software
 components that are provided under additional licenses.
 
 [[cryptography]]
-Cryptography Notice
--------------------
+== Cryptography Notice
 
 This distribution includes cryptographic software.  The country
 in which you currently reside may have restrictions on the import,
@@ -139,8 +137,7 @@
 link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
 to be installed by the end-user.
 
-Licenses
---------
+== Licenses
 """)
 
 for n in used:
@@ -149,13 +146,13 @@
   if args.asciidoc:
     print()
     print('[[%s]]' % name.replace('.', '_'))
-    print(name)
-    print('~' * len(name))
+    print("=== " + name)
     print()
   else:
     print()
     print(name)
-    print('--')
+    print()
+    print('----')
   for d in libs:
     if d.startswith('//lib:') or d.startswith('//lib/'):
       p = d[len('//lib:'):]
@@ -166,12 +163,12 @@
     print('* ' + p)
   if args.asciidoc:
     print()
-    print('[[license]]')
-    print('[verse]')
-    print('--')
+    print('[[%s_license]]' % name.replace('.', '_'))
+    print('----')
   with open(n[2:].replace(':', '/')) as fd:
     copyfileobj(fd, stdout)
-  print('--')
+  print()
+  print('----')
 
 if args.asciidoc:
   print("""
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 29b8c5f..ec1335b 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -192,6 +192,7 @@
 get::/changes/?q=is:open+owner:self&q=is:open+reviewer:self+-owner:self&q=is:closed+owner:self+limit:5&o=LABELS
 ****
 
+[[query-options]]
 Additional fields can be obtained by adding `o` parameters, each
 option requires more database lookups and slows down the query
 response time to the client so they are generally disabled by
diff --git a/gerrit-acceptance-framework/BUCK b/gerrit-acceptance-framework/BUCK
index ba68fa3..7ef71c4 100644
--- a/gerrit-acceptance-framework/BUCK
+++ b/gerrit-acceptance-framework/BUCK
@@ -37,10 +37,20 @@
 
 java_binary(
   name = 'acceptance-framework',
+  merge_manifests = False,
+  manifest_file = ':manifest',
   deps = [':lib'],
   visibility = ['PUBLIC'],
 )
 
+genrule(
+  name = 'manifest',
+  cmd = 'echo "Manifest-Version: 1.0" >$OUT;' +
+    'echo "Implementation-Title: Gerrit Acceptance Test Framework" >>$OUT;' +
+    'echo "Implementation-Vendor: Gerrit Code Review Project" >>$OUT',
+  out = 'manifest.txt',
+)
+
 java_library(
   name = 'lib',
   srcs = SRCS,
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
index c71e13f..dde1875 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
@@ -48,9 +48,9 @@
   private Path pluginsSitePath;
   private Path pluginSubPath;
   private Path pluginSource;
-  private String pluginName;
   private boolean standalone;
 
+  protected String pluginName;
   protected Path testSite;
 
   @Override
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 47d97e3..7bad261 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
@@ -41,6 +41,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
@@ -58,6 +59,7 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
@@ -538,6 +540,149 @@
   }
 
   @Test
+  public void pushCommitOfOtherUser() throws Exception {
+    // admin pushes commit of user
+    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    CommitInfo commit = change.revisions.get(change.currentRevision).commit;
+    assertThat(commit.author.email).isEqualTo(user.email);
+    assertThat(commit.committer.email).isEqualTo(user.email);
+
+    // check that the author/committer was added as reviewer
+    Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(user.getId().get());
+    assertThat(change.reviewers.get(CC)).isNull();
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.body())
+        .contains(admin.fullName + " has uploaded a new change for review");
+    assertThat(m.body())
+        .contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+    assertMailFrom(m, admin.email);
+  }
+
+  @Test
+  public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception {
+    // create hidden project that is only visible to administrators
+    Project.NameKey p = createProject("p");
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+    Util.allow(cfg,
+        Permission.READ,
+        groupCache.get(new AccountGroup.NameKey("Administrators"))
+            .getGroupUUID(),
+        "refs/*");
+    Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(p, cfg);
+
+    // admin pushes commit of user
+    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
+    PushOneCommit push = pushFactory.create(db, user.getIdent(), repo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    CommitInfo commit = change.revisions.get(change.currentRevision).commit;
+    assertThat(commit.author.email).isEqualTo(user.email);
+    assertThat(commit.committer.email).isEqualTo(user.email);
+
+    // check the user cannot see the change
+    setApiUser(user);
+    try {
+      gApi.changes().id(result.getChangeId()).get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException e) {
+      // Expected.
+    }
+
+    // check that the author/committer was NOT added as reviewer (he can't see
+    // the change)
+    assertThat(change.reviewers.get(REVIEWER)).isNull();
+    assertThat(change.reviewers.get(CC)).isNull();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  public void pushCommitWithFooterOfOtherUser() throws Exception {
+    // admin pushes commit that references 'user' in a footer
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT + "\n\n"
+            + FooterConstants.REVIEWED_BY.getName() + ": "
+            + user.getIdent().toExternalString(),
+        PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    // check that 'user' was added as reviewer
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(user.getId().get());
+    assertThat(change.reviewers.get(CC)).isNull();
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.body()).contains("I'd like you to do a code review.");
+    assertThat(m.body())
+        .contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+    assertMailFrom(m, admin.email);
+  }
+
+  @Test
+  public void pushCommitWithFooterOfOtherUserThatCannotSeeChange()
+      throws Exception {
+    // create hidden project that is only visible to administrators
+    Project.NameKey p = createProject("p");
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+    Util.allow(cfg,
+        Permission.READ, groupCache
+            .get(new AccountGroup.NameKey("Administrators")).getGroupUUID(),
+        "refs/*");
+    Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(p, cfg);
+
+    // admin pushes commit that references 'user' in a footer
+    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo,
+        PushOneCommit.SUBJECT + "\n\n" + FooterConstants.REVIEWED_BY.getName()
+            + ": " + user.getIdent().toExternalString(),
+        PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    // check that 'user' cannot see the change
+    setApiUser(user);
+    try {
+      gApi.changes().id(result.getChangeId()).get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException e) {
+      // Expected.
+    }
+
+    // check that 'user' was NOT added as cc ('user' can't see the change)
+    setApiUser(admin);
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.reviewers.get(REVIEWER)).isNull();
+    assertThat(change.reviewers.get(CC)).isNull();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
   public void addReviewerThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
     Project.NameKey p = createProject("p");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index ee2dbfe..3629e29 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -52,6 +52,7 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ETagView;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.server.change.GetRevisionActions;
@@ -566,6 +567,19 @@
   }
 
   @Test
+  public void diffNonExistingFile() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    exception.expect(ResourceNotFoundException.class);
+    exception.expectMessage("non-existing");
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .file("non-existing")
+        .diff();
+  }
+
+  @Test
   public void diffOnMergeCommitChange() throws Exception {
     PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index ca669b4..7dee60f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -783,6 +783,23 @@
     pushWithReviewerInFooter("Notauser", null);
   }
 
+  @Test
+  public void pushNewPatchsetOverridingStickyLabel() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    LabelType codeReview = Util.codeReview();
+    codeReview.setCopyMaxScore(true);
+    cfg.getLabelSections().put(codeReview.getName(), codeReview);
+    saveProjectConfig(cfg);
+
+    PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review+2");
+    r.assertOkStatus();
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent", r.getChangeId());
+    r = push.to("refs/for/master%l=Code-Review+1");
+    r.assertOkStatus();
+  }
+
   private void pushWithReviewerInFooter(String nameEmail,
       TestAccount expectedReviewer) throws Exception {
     int n = 5;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index 752f0d2..afd6734 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -16,13 +16,11 @@
 
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.common.RpcImpl;
 import com.google.gwtjsonrpc.common.RpcImpl.Version;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.List;
 import java.util.Set;
@@ -36,14 +34,4 @@
   @SignInRequired
   void deleteExternalIds(Set<AccountExternalId.Key> keys,
       AsyncCallback<Set<AccountExternalId.Key>> callback);
-
-  @Audit
-  @SignInRequired
-  void updateContact(String fullName, String emailAddr,
-      AsyncCallback<Account> callback);
-
-  @Audit
-  @SignInRequired
-  void enterAgreement(String agreementName,
-      AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
deleted file mode 100644
index 22482c7..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2008 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.common.data;
-
-import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-
-@RpcImpl(version = Version.V2_0)
-public interface AccountService extends RemoteJsonService {
-  @SignInRequired
-  void myAgreements(AsyncCallback<AgreementInfo> callback);
-}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 2731476..a71ab37 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -47,7 +47,7 @@
   Map<String, FileInfo> files() throws RestApiException;
   Map<String, FileInfo> files(String base) throws RestApiException;
   Map<String, FileInfo> files(int parentNum) throws RestApiException;
-  FileApi file(String path);
+  FileApi file(String path) throws RestApiException;
   MergeableInfo mergeable() throws RestApiException;
   MergeableInfo mergeableOtherBranches() throws RestApiException;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
index 3cc3fdc..bb4ac9d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
@@ -17,12 +17,14 @@
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.extensions.common.AccountInfo;
 
-/** Notified whenever a Reviewer is added to a change. */
+import java.util.List;
+
+/** Notified whenever one or more Reviewers are added to a change. */
 @ExtensionPoint
 public interface ReviewerAddedListener {
   interface Event extends ChangeEvent {
-    AccountInfo getReviewer();
+    List<AccountInfo> getReviewers();
   }
 
-  void onReviewerAdded(Event event);
+  void onReviewersAdded(Event event);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index acd2e78..15668ca 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -83,6 +83,14 @@
     new RestApi("/accounts/").id(account).view("name").get(cb);
   }
 
+  /** Set the account name */
+  public static void setName(String account, String name,
+      AsyncCallback<NativeString> cb) {
+    AccountNameInput input = AccountNameInput.create();
+    input.name(name);
+    new RestApi("/accounts/").id(account).view("name").put(input, cb);
+  }
+
   /** Retrieve email addresses */
   public static void getEmails(String account,
       AsyncCallback<JsArray<EmailInfo>> cb) {
@@ -97,6 +105,13 @@
         .ifNoneMatch().put(in, cb);
   }
 
+  /** Set preferred email address */
+  public static void setPreferredEmail(String account, String email,
+      AsyncCallback<NativeString> cb) {
+    new RestApi("/accounts/").id(account).view("emails")
+        .id(email).view("preferred").put(cb);
+  }
+
   /** Retrieve SSH keys */
   public static void getSshKeys(String account,
       AsyncCallback<JsArray<SshKeyInfo>> cb) {
@@ -196,6 +211,14 @@
     new RestApi("/accounts/").id(account).view("password.http").delete(cb);
   }
 
+  /** Enter a contributor agreement */
+  public static void enterAgreement(String account, String name,
+      AsyncCallback<NativeString> cb) {
+    AgreementInput in = AgreementInput.create();
+    in.name(name);
+    new RestApi("/accounts/").id(account).view("agreements").put(in, cb);
+  }
+
   private static JsArray<ProjectWatchInfo> projectWatchArrayFromSet(
       Set<ProjectWatchInfo> set) {
     JsArray<ProjectWatchInfo> jsArray = JsArray.createArray().cast();
@@ -205,6 +228,17 @@
     return jsArray;
   }
 
+  private static class AgreementInput extends JavaScriptObject {
+    final native void name(String n) /*-{ if(n)this.name=n; }-*/;
+
+    static AgreementInput create() {
+      return createObject().cast();
+    }
+
+    protected AgreementInput() {
+    }
+  }
+
   private static class HttpPasswordInput extends JavaScriptObject {
     final native void generate(boolean g) /*-{ if(g)this.generate=g; }-*/;
 
@@ -227,6 +261,17 @@
     }
   }
 
+  private static class AccountNameInput extends JavaScriptObject {
+    final native void name(String n) /*-{ if(n)this.name=n; }-*/;
+
+    static AccountNameInput create() {
+      return createObject().cast();
+    }
+
+    protected AccountNameInput() {
+    }
+  }
+
   public static void addGpgKey(String account, String armored,
       AsyncCallback<NativeMap<GpgKeyInfo>> cb) {
     new RestApi("/accounts/")
@@ -243,6 +288,12 @@
       .post(GpgKeysInput.delete(fingerprints), cb);
   }
 
+  /** List contributor agreements */
+  public static void getAgreements(String account,
+      AsyncCallback<JsArray<AgreementInfo>> cb) {
+    new RestApi("/accounts/").id(account).view("agreements").get(cb);
+  }
+
   private static class GpgKeysInput extends JavaScriptObject {
     static GpgKeysInput add(String key) {
       return createWithAdd(Natives.arrayOf(key));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index a084612..01b5af3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -141,11 +141,8 @@
   String errorDialogTitleRegisterNewEmail();
 
   String newAgreement();
-  String agreementStatus();
   String agreementName();
   String agreementDescription();
-  String agreementStatus_EXPIRED();
-  String agreementStatus_VERIFIED();
 
   String newAgreementSelectTypeHeading();
   String newAgreementNoneAvailable();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index ca2d316..f74d34b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -151,10 +151,7 @@
 
 
 newAgreement = New Contributor Agreement
-agreementStatus = Status
 agreementName = Name
-agreementStatus_EXPIRED = Expired
-agreementStatus_VERIFIED = Verified
 agreementDescription = Description
 
 newAgreementSelectTypeHeading = Select an agreement type:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AgreementInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AgreementInfo.java
new file mode 100644
index 0000000..f038818
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AgreementInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class AgreementInfo extends JavaScriptObject {
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String description() /*-{ return this.description; }-*/;
+  public final native String url() /*-{ return this.url; }-*/;
+
+  protected AgreementInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index 136de64..e078e60 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -46,7 +45,6 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 
 class ContactPanelShort extends Composite {
   protected final FlowPanel body;
@@ -61,6 +59,7 @@
   NpTextBox nameTxt;
   private ListBox emailPick;
   private Button registerNewEmail;
+  private OnEditEnabler onEditEnabler;
 
   ContactPanelShort() {
     body = new FlowPanel();
@@ -145,7 +144,7 @@
     save.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        doSave(null);
+        doSave();
       }
     });
 
@@ -167,6 +166,8 @@
         }
       }
     });
+
+    onEditEnabler = new OnEditEnabler(save, nameTxt);
   }
 
   private boolean canEditFullName() {
@@ -230,7 +231,7 @@
       updateEmailList();
       registerNewEmail.setEnabled(true);
       save.setEnabled(false);
-      new OnEditEnabler(save, nameTxt);
+      onEditEnabler.updateOriginalValue(nameTxt);
     }
     display();
   }
@@ -249,7 +250,7 @@
     currentEmail = account.email();
     nameTxt.setText(account.name());
     save.setEnabled(false);
-    new OnEditEnabler(save, nameTxt);
+    onEditEnabler.updateOriginalValue(nameTxt);
   }
 
   private void doRegisterNewEmail() {
@@ -344,10 +345,13 @@
     inEmail.setFocus(true);
   }
 
-  void doSave(final AsyncCallback<Account> onSave) {
-    String newName = canEditFullName() ? nameTxt.getText() : null;
-    if (newName != null && newName.trim().isEmpty()) {
+  void doSave() {
+    final String newName;
+    String name = canEditFullName() ? nameTxt.getText() : null;
+    if (name != null && name.trim().isEmpty()) {
       newName = null;
+    } else {
+      newName = name;
     }
 
     final String newEmail;
@@ -365,24 +369,40 @@
     save.setEnabled(false);
     registerNewEmail.setEnabled(false);
 
-    Util.ACCOUNT_SEC.updateContact(newName, newEmail,
-        new GerritCallback<Account>() {
-          @Override
-          public void onSuccess(Account result) {
-            registerNewEmail.setEnabled(true);
-            onSaveSuccess(FormatUtil.asInfo(result));
-            if (onSave != null) {
-              onSave.onSuccess(result);
-            }
-          }
+    CallbackGroup group = new CallbackGroup();
+    if (!newEmail.equals(currentEmail)) {
+      AccountApi.setPreferredEmail("self", newEmail,
+          group.add(new GerritCallback<NativeString>() {
+        @Override
+        public void onSuccess(NativeString result) {
+        }
+      }));
+    }
+    AccountApi.setName("self", newName,
+        group.add(new GerritCallback<NativeString>() {
+      @Override
+      public void onSuccess(NativeString result) {
+      }
 
-          @Override
-          public void onFailure(final Throwable caught) {
-            save.setEnabled(true);
-            registerNewEmail.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
+      @Override
+      public void onFailure(Throwable caught) {
+        save.setEnabled(true);
+        registerNewEmail.setEnabled(true);
+        super.onFailure(caught);
+      }
+    }));
+    group.done();
+    group.addListener(new GerritCallback<Void>() {
+      @Override
+      public void onSuccess(Void result) {
+        currentEmail = newEmail;
+        AccountInfo me = Gerrit.getUserAccount();
+        me.email(currentEmail);
+        me.name(newName);
+        onSaveSuccess(me);
+        registerNewEmail.setEnabled(true);
+      }
+    });
   }
 
   void onSaveSuccess(AccountInfo result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
index 308cf30..5396b4f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -16,14 +16,17 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 
+import java.util.List;
+
 public class MyAgreementsScreen extends SettingsScreen {
   private AgreementTable agreements;
 
@@ -39,71 +42,54 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SVC.myAgreements(new ScreenLoadCallback<AgreementInfo>(this) {
+    AccountApi.getAgreements(
+        "self", new ScreenLoadCallback<JsArray<AgreementInfo>>(this) {
       @Override
-      public void preDisplay(final AgreementInfo result) {
-        agreements.display(result);
-      }
-    });
+      public void preDisplay(JsArray<AgreementInfo> result) {
+        agreements.display(Natives.asList(result));
+      }});
   }
 
   private static class AgreementTable extends FancyFlexTable<ContributorAgreement> {
     AgreementTable() {
       table.setWidth("");
-      table.setText(0, 1, Util.C.agreementStatus());
-      table.setText(0, 2, Util.C.agreementName());
-      table.setText(0, 3, Util.C.agreementDescription());
+      table.setText(0, 1, Util.C.agreementName());
+      table.setText(0, 2, Util.C.agreementDescription());
 
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      for (int c = 1; c < 4; c++) {
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      for (int c = 1; c < 3; c++) {
         fmt.addStyleName(0, c, Gerrit.RESOURCES.css().dataHeader());
       }
     }
 
-    void display(final AgreementInfo result) {
+    void display(List<AgreementInfo> result) {
       while (1 < table.getRowCount()) {
         table.removeRow(table.getRowCount() - 1);
       }
 
-      for (final String k : result.accepted) {
-        addOne(result, k);
+      for (AgreementInfo info : result) {
+        addOne(info);
       }
     }
 
-    void addOne(final AgreementInfo info, final String k) {
-      final int row = table.getRowCount();
+    void addOne(AgreementInfo info) {
+      int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
 
-      final ContributorAgreement cla = info.agreements.get(k);
-      final String statusName;
-      if (cla == null) {
-        statusName = Util.C.agreementStatus_EXPIRED();
+      String url = info.url();
+      if (url != null && url.length() > 0) {
+        Anchor a = new Anchor(info.name(), url);
+        a.setTarget("_blank");
+        table.setWidget(row, 1, a);
       } else {
-        statusName = Util.C.agreementStatus_VERIFIED();
+        table.setText(row, 1, info.name());
       }
-      table.setText(row, 1, statusName);
-
-      if (cla == null) {
-        table.setText(row, 2, "");
-        table.setText(row, 3, "");
-      } else {
-        final String url = cla.getAgreementUrl();
-        if (url != null && url.length() > 0) {
-          final Anchor a = new Anchor(cla.getName(), url);
-          a.setTarget("_blank");
-          table.setWidget(row, 2, a);
-        } else {
-          table.setText(row, 2, cla.getName());
-        }
-        table.setText(row, 3, cla.getDescription());
-      }
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      for (int c = 1; c < 4; c++) {
+      table.setText(row, 2, info.description());
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      for (int c = 1; c < 3; c++) {
         fmt.addStyleName(row, c, Gerrit.RESOURCES.css().dataCell());
       }
-
-      setRowItem(row, cla);
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
index 14f8e2f..27a6cc7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
@@ -17,13 +17,15 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountScreen;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.http.client.Request;
@@ -41,7 +43,6 @@
 import com.google.gwt.user.client.ui.RadioButton;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.HashSet;
 import java.util.List;
@@ -73,15 +74,19 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SVC.myAgreements(new GerritCallback<AgreementInfo>() {
+    AccountApi.getAgreements(
+        "self", new GerritCallback<JsArray<AgreementInfo>>() {
       @Override
-      public void onSuccess(AgreementInfo result) {
+      public void onSuccess(JsArray<AgreementInfo> result) {
         if (isAttached()) {
-          mySigned = new HashSet<>(result.accepted);
+          mySigned = new HashSet<>();
+          for (AgreementInfo info: Natives.asList(result)) {
+            mySigned.add(info.name());
+          }
           postRPC();
         }
-      }
-    });
+      }});
+
     Gerrit.SYSTEM_SVC
         .contributorAgreements(new GerritCallback<List<ContributorAgreement>>() {
           @Override
@@ -199,15 +204,15 @@
   }
 
   private void doEnterAgreement() {
-    Util.ACCOUNT_SEC.enterAgreement(current.getName(),
-        new GerritCallback<VoidResult>() {
+    AccountApi.enterAgreement("self", current.getName(),
+        new GerritCallback<NativeString>() {
           @Override
-          public void onSuccess(final VoidResult result) {
+          public void onSuccess(NativeString result) {
             Gerrit.display(nextToken);
           }
 
           @Override
-          public void onFailure(final Throwable caught) {
+          public void onFailure(Throwable caught) {
             yesIAgreeBox.setText("");
             super.onFailure(caught);
           }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
index a0f36b9..b4b4390 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.common.data.AccountSecurity;
-import com.google.gerrit.common.data.AccountService;
 import com.google.gerrit.common.data.ProjectAdminService;
 import com.google.gwt.core.client.GWT;
 import com.google.gwtjsonrpc.client.JsonUtil;
@@ -23,14 +22,10 @@
 public class Util {
   public static final AccountConstants C = GWT.create(AccountConstants.class);
   public static final AccountMessages M = GWT.create(AccountMessages.class);
-  public static final AccountService ACCOUNT_SVC;
   public static final AccountSecurity ACCOUNT_SEC;
   public static final ProjectAdminService PROJECT_SVC;
 
   static {
-    ACCOUNT_SVC = GWT.create(AccountService.class);
-    JsonUtil.bind(ACCOUNT_SVC, "rpc/AccountService");
-
     ACCOUNT_SEC = GWT.create(AccountSecurity.class);
     JsonUtil.bind(ACCOUNT_SEC, "rpc/AccountSecurity");
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
index 819a11f..4391477 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
@@ -73,6 +73,9 @@
     widget = w;
   }
 
+  public void updateOriginalValue(final TextBoxBase tb) {
+    originalValue = tb.getValue().trim();
+  }
 
   // Register input widgets to be listened to
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 2c67182..c1f4da0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -64,6 +64,7 @@
 
     if (options.enableDefaultUi()) {
       filter("/").through(XsrfCookieFilter.class);
+      filter("/accounts/self/detail").through(XsrfCookieFilter.class);
       serve("/").with(HostPageServlet.class);
       serve("/Gerrit").with(LegacyGerritServlet.class);
       serve("/Gerrit/*").with(legacyGerritScreen());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 8594e30..27aff21 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -359,7 +359,6 @@
           if (Strings.isNullOrEmpty(entryTitle)) {
             entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' ');
           }
-          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
         } else {
           entryTitle = rsrc.substring(nameOffset).replace('-', ' ');
         }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
index 62778eb..d32fdaf 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
@@ -28,12 +28,10 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
-        factory(AgreementInfoFactory.Factory.class);
         factory(DeleteExternalIds.Factory.class);
         factory(ExternalIdDetailFactory.Factory.class);
       }
     });
     rpc(AccountSecurityImpl.class);
-    rpc(AccountServiceImpl.class);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index 8fcf9ea..3d05548 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -14,74 +14,31 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
-import com.google.common.base.Strings;
-import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.data.AccountSecurity;
-import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.extensions.events.AgreementSignup;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.io.IOException;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
 class AccountSecurityImpl extends BaseServiceImplementation implements
     AccountSecurity {
-  private final Realm realm;
-  private final ProjectCache projectCache;
-  private final Provider<IdentifiedUser> user;
-  private final AccountByEmailCache byEmailCache;
-  private final AccountCache accountCache;
-
   private final DeleteExternalIds.Factory deleteExternalIdsFactory;
   private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
 
-  private final GroupCache groupCache;
-  private final AuditService auditService;
-  private final AgreementSignup agreementSignup;
-
   @Inject
   AccountSecurityImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser,
-      final Realm r, final Provider<IdentifiedUser> u,
-      final ProjectCache pc,
-      final AccountByEmailCache abec, final AccountCache uac,
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
-      final ExternalIdDetailFactory.Factory externalIdDetailFactory,
-      final GroupCache groupCache,
-      final AuditService auditService,
-      AgreementSignup agreementSignup) {
+      final ExternalIdDetailFactory.Factory externalIdDetailFactory) {
     super(schema, currentUser);
-    realm = r;
-    user = u;
-    projectCache = pc;
-    byEmailCache = abec;
-    accountCache = uac;
-    this.auditService = auditService;
     this.deleteExternalIdsFactory = deleteExternalIdsFactory;
     this.externalIdDetailFactory = externalIdDetailFactory;
-    this.groupCache = groupCache;
-    this.agreementSignup = agreementSignup;
   }
 
   @Override
@@ -94,84 +51,4 @@
       final AsyncCallback<Set<AccountExternalId.Key>> callback) {
     deleteExternalIdsFactory.create(keys).to(callback);
   }
-
-  @Override
-  public void updateContact(final String name, final String emailAddr,
-      final AsyncCallback<Account> callback) {
-    run(callback, new Action<Account>() {
-      @Override
-      public Account run(ReviewDb db)
-          throws OrmException, Failure, IOException {
-        IdentifiedUser self = user.get();
-        final Account me = db.accounts().get(self.getAccountId());
-        final String oldEmail = me.getPreferredEmail();
-        if (realm.allowsEdit(Account.FieldName.FULL_NAME)) {
-          me.setFullName(Strings.emptyToNull(name));
-        }
-        if (!Strings.isNullOrEmpty(emailAddr)
-            && !self.hasEmailAddress(emailAddr)) {
-          throw new Failure(new PermissionDeniedException("Email address must be verified"));
-        }
-        me.setPreferredEmail(Strings.emptyToNull(emailAddr));
-        db.accounts().update(Collections.singleton(me));
-        if (!eq(oldEmail, me.getPreferredEmail())) {
-          byEmailCache.evict(oldEmail);
-          byEmailCache.evict(me.getPreferredEmail());
-        }
-        accountCache.evict(me.getId());
-        return me;
-      }
-    });
-  }
-
-  private static boolean eq(final String a, final String b) {
-    if (a == null && b == null) {
-      return true;
-    }
-    return a != null && a.equals(b);
-  }
-
-  @Override
-  public void enterAgreement(final String agreementName,
-      final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      @Override
-      public VoidResult run(final ReviewDb db)
-          throws OrmException, Failure, IOException {
-        ContributorAgreement ca = projectCache.getAllProjects().getConfig()
-            .getContributorAgreement(agreementName);
-        if (ca == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        if (ca.getAutoVerify() == null) {
-          throw new Failure(new IllegalStateException(
-              "cannot enter a non-autoVerify agreement"));
-        } else if (ca.getAutoVerify().getUUID() == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        AccountGroup group = groupCache.get(ca.getAutoVerify().getUUID());
-        if (group == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        Account account = user.get().getAccount();
-        agreementSignup.fire(account, ca.getName());
-
-        final AccountGroupMember.Key key =
-            new AccountGroupMember.Key(account.getId(), group.getId());
-        AccountGroupMember m = db.accountGroupMembers().get(key);
-        if (m == null) {
-          m = new AccountGroupMember(key);
-          auditService.dispatchAddAccountsToGroup(account.getId(), Collections
-              .singleton(m));
-          db.accountGroupMembers().insert(Collections.singleton(m));
-          accountCache.evict(m.getAccountId());
-        }
-
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
deleted file mode 100644
index 8fba47d..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2008 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.httpd.rpc.account;
-
-import com.google.gerrit.common.data.AccountService;
-import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-class AccountServiceImpl extends BaseServiceImplementation implements
-    AccountService {
-  private final AgreementInfoFactory.Factory agreementInfoFactory;
-
-  @Inject
-  AccountServiceImpl(final Provider<ReviewDb> schema,
-      final Provider<IdentifiedUser> identifiedUser,
-      final AgreementInfoFactory.Factory agreementInfoFactory) {
-    super(schema, identifiedUser);
-    this.agreementInfoFactory = agreementInfoFactory;
-  }
-
-  @Override
-  public void myAgreements(final AsyncCallback<AgreementInfo> callback) {
-    agreementInfoFactory.create().to(callback);
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
deleted file mode 100644
index 91afd97..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2009 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.httpd.rpc.account;
-
-import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-class AgreementInfoFactory extends Handler<AgreementInfo> {
-  private static final Logger log = LoggerFactory.getLogger(AgreementInfoFactory.class);
-
-  interface Factory {
-    AgreementInfoFactory create();
-  }
-
-  private final IdentifiedUser user;
-  private final ProjectCache projectCache;
-
-  private AgreementInfo info;
-
-  @Inject
-  AgreementInfoFactory(final IdentifiedUser user,
-      final ProjectCache projectCache) {
-    this.user = user;
-    this.projectCache = projectCache;
-  }
-
-  @Override
-  public AgreementInfo call() throws Exception {
-    List<String> accepted = new ArrayList<>();
-    Map<String, ContributorAgreement> agreements = new HashMap<>();
-    Collection<ContributorAgreement> cas =
-        projectCache.getAllProjects().getConfig().getContributorAgreements();
-    for (ContributorAgreement ca : cas) {
-      agreements.put(ca.getName(), ca.forUi());
-
-      List<AccountGroup.UUID> groupIds = new ArrayList<>();
-      for (PermissionRule rule : ca.getAccepted()) {
-        if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)) {
-          if (rule.getGroup().getUUID() == null) {
-            log.warn("group \"" + rule.getGroup().getName() + "\" does not " +
-                " exist, referenced in CLA \"" + ca.getName() + "\"");
-          } else {
-            groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
-          }
-        }
-      }
-      if (user.getEffectiveGroups().containsAnyOf(groupIds)) {
-        accepted.add(ca.getName());
-      }
-    }
-
-    info = new AgreementInfo();
-    info.setAccepted(accepted);
-    info.setAgreements(agreements);
-    return info;
-  }
-}
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index 777f3a8..7e12f232 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -13,10 +13,20 @@
 
 java_binary(
   name = 'plugin-api',
+  merge_manifests = False,
+  manifest_file = ':manifest',
   deps = [':lib'],
   visibility = ['PUBLIC'],
 )
 
+genrule(
+  name = 'manifest',
+  cmd = 'echo "Manifest-Version: 1.0" >$OUT;' +
+    'echo "Implementation-Title: Gerrit Plugin API" >>$OUT;' +
+    'echo "Implementation-Vendor: Gerrit Code Review Project" >>$OUT',
+  out = 'manifest.txt',
+)
+
 java_library(
   name = 'lib',
   exported_deps = PLUGIN_API + [
@@ -29,7 +39,7 @@
     '//gerrit-reviewdb:server',
     '//lib:args4j',
     '//lib:blame-cache',
-    '//lib/dropwizard:dropwizard-core',
+    '//lib:gson',
     '//lib:guava',
     '//lib:gwtorm',
     '//lib:jsch',
@@ -37,6 +47,7 @@
     '//lib:servlet-api-3_1',
     '//lib:velocity',
     '//lib/commons:lang',
+    '//lib/dropwizard:dropwizard-core',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index d723667..bdceb7b 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -9,7 +9,6 @@
 ALTER TABLE patch_set_approvals CLUSTER ON patch_set_approvals_pkey;
 
 ALTER TABLE account_group_members CLUSTER ON account_group_members_pkey;
-ALTER TABLE starred_changes CLUSTER ON starred_changes_pkey;
 CLUSTER;
 
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index bc2ec06..d69ad3f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -77,22 +77,54 @@
     this.psUtil = psUtil;
   }
 
+  /**
+   * Apply approval copy settings from prior PatchSets to a new PatchSet.
+   *
+   * @param db review database.
+   * @param ctl change control for user uploading PatchSet
+   * @param ps new PatchSet
+   * @throws OrmException
+   */
   public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps)
       throws OrmException {
-    db.patchSetApprovals().insert(getForPatchSet(db, ctl, ps));
+    copy(db, ctl, ps, Collections.<PatchSetApproval>emptyList());
+  }
+
+  /**
+   * Apply approval copy settings from prior PatchSets to a new PatchSet.
+   *
+   * @param db review database.
+   * @param ctl change control for user uploading PatchSet
+   * @param ps new PatchSet
+   * @param dontCopy PatchSetApprovals indicating which (account, label) pairs
+   *        should not be copied
+   * @throws OrmException
+   */
+  public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps,
+      Iterable<PatchSetApproval> dontCopy) throws OrmException {
+    db.patchSetApprovals().insert(
+        getForPatchSet(db, ctl, ps, dontCopy));
   }
 
   Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
       ChangeControl ctl, PatchSet.Id psId) throws OrmException {
+    return getForPatchSet(db, ctl, psId,
+        Collections.<PatchSetApproval>emptyList());
+  }
+
+  Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
+      ChangeControl ctl, PatchSet.Id psId,
+      Iterable<PatchSetApproval> dontCopy) throws OrmException {
     PatchSet ps = psUtil.get(db, ctl.getNotes(), psId);
     if (ps == null) {
       return Collections.emptyList();
     }
-    return getForPatchSet(db, ctl, ps);
+    return getForPatchSet(db, ctl, ps, dontCopy);
   }
 
   private Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
-      ChangeControl ctl, PatchSet ps) throws OrmException {
+      ChangeControl ctl, PatchSet ps,
+      Iterable<PatchSetApproval> dontCopy) throws OrmException {
     checkNotNull(ps, "ps should not be null");
     ChangeData cd = changeDataFactory.create(db, ctl);
     try {
@@ -103,10 +135,16 @@
 
       Table<String, Account.Id, PatchSetApproval> wontCopy =
           HashBasedTable.create();
+      for (PatchSetApproval psa : dontCopy) {
+        wontCopy.put(psa.getLabel(), psa.getAccountId(), psa);
+      }
+
       Table<String, Account.Id, PatchSetApproval> byUser =
           HashBasedTable.create();
       for (PatchSetApproval psa : all.get(ps.getId())) {
-        byUser.put(psa.getLabel(), psa.getAccountId(), psa);
+        if (!wontCopy.contains(psa.getLabel(), psa.getAccountId())) {
+          byUser.put(psa.getLabel(), psa.getAccountId(), psa);
+        }
       }
 
       TreeMap<Integer, PatchSet> patchSets = getPatchSets(cd);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index e0526e4..5a5d16c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -43,11 +43,15 @@
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -73,6 +77,9 @@
  */
 @Singleton
 public class ApprovalsUtil {
+  private static final Logger log =
+      LoggerFactory.getLogger(ApprovalsUtil.class);
+
   private static final Ordering<PatchSetApproval> SORT_APPROVALS =
       Ordering.natural()
           .onResultOf(
@@ -99,13 +106,19 @@
   }
 
   private final NotesMigration migration;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final ApprovalCopier copier;
 
   @VisibleForTesting
   @Inject
   public ApprovalsUtil(NotesMigration migration,
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeControl.GenericFactory changeControlFactory,
       ApprovalCopier copier) {
     this.migration = migration;
+    this.userFactory = userFactory;
+    this.changeControlFactory = changeControlFactory;
     this.copier = copier;
   }
 
@@ -164,8 +177,8 @@
       PatchSetInfo info, Iterable<Account.Id> wantReviewers,
       Collection<Account.Id> existingReviewers) throws OrmException {
     return addReviewers(db, update, labelTypes, change, ps.getId(),
-        ps.isDraft(), info.getAuthor().getAccount(),
-        info.getCommitter().getAccount(), wantReviewers, existingReviewers);
+        info.getAuthor().getAccount(), info.getCommitter().getAccount(),
+        wantReviewers, existingReviewers);
   }
 
   public List<PatchSetApproval> addReviewers(ReviewDb db, ChangeNotes notes,
@@ -189,12 +202,12 @@
         existingReviewers.add(entry.getKey());
       }
     }
-    return addReviewers(db, update, labelTypes, change, psId, false, null, null,
+    return addReviewers(db, update, labelTypes, change, psId, null, null,
         wantReviewers, existingReviewers);
   }
 
   private List<PatchSetApproval> addReviewers(ReviewDb db, ChangeUpdate update,
-      LabelTypes labelTypes, Change change, PatchSet.Id psId, boolean isDraft,
+      LabelTypes labelTypes, Change change, PatchSet.Id psId,
       Account.Id authorId, Account.Id committerId,
       Iterable<Account.Id> wantReviewers,
       Collection<Account.Id> existingReviewers) throws OrmException {
@@ -204,11 +217,11 @@
     }
 
     Set<Account.Id> need = Sets.newLinkedHashSet(wantReviewers);
-    if (authorId != null && !isDraft) {
+    if (authorId != null && canSee(db, update.getNotes(), authorId)) {
       need.add(authorId);
     }
 
-    if (committerId != null && !isDraft) {
+    if (committerId != null && canSee(db, update.getNotes(), authorId)) {
       need.add(committerId);
     }
     need.remove(change.getOwner());
@@ -229,6 +242,17 @@
     return Collections.unmodifiableList(cells);
   }
 
+  private boolean canSee(ReviewDb db, ChangeNotes notes, Account.Id accountId) {
+    try {
+      IdentifiedUser user = userFactory.create(accountId);
+      return changeControlFactory.controlFor(notes, user).isVisible(db);
+    } catch (OrmException | NoSuchChangeException e) {
+      log.warn(String.format("Failed to check if account %d can see change %d",
+          accountId.get(), notes.getChangeId().get()), e);
+      return false;
+    }
+  }
+
   /**
    * Adds accounts to a change as reviewers in the CC state.
    *
@@ -254,25 +278,40 @@
     return need;
   }
 
-  public void addApprovals(ReviewDb db, ChangeUpdate update,
+  /**
+   * Adds approvals to ChangeUpdate and writes to ReviewDb.
+   *
+   * @param db review database.
+   * @param update change update.
+   * @param labelTypes label types for the containing project.
+   * @param ps patch set being approved.
+   * @param changeCtl change control for user adding approvals.
+   * @param approvals approvals to add.
+   * @throws OrmException
+   */
+  public Iterable<PatchSetApproval> addApprovals(ReviewDb db, ChangeUpdate update,
       LabelTypes labelTypes, PatchSet ps, ChangeControl changeCtl,
       Map<String, Short> approvals) throws OrmException {
-    if (!approvals.isEmpty()) {
-      checkApprovals(approvals, changeCtl);
-      List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
-      Date ts = update.getWhen();
-      for (Map.Entry<String, Short> vote : approvals.entrySet()) {
-        LabelType lt = labelTypes.byLabel(vote.getKey());
-        cells.add(new PatchSetApproval(new PatchSetApproval.Key(
-            ps.getId(),
-            ps.getUploader(),
-            lt.getLabelId()),
-            vote.getValue(),
-            ts));
-        update.putApproval(vote.getKey(), vote.getValue());
-      }
-      db.patchSetApprovals().insert(cells);
+    if (approvals.isEmpty()) {
+      return Collections.emptyList();
     }
+    checkApprovals(approvals, changeCtl);
+    List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
+    Date ts = update.getWhen();
+    for (Map.Entry<String, Short> vote : approvals.entrySet()) {
+      LabelType lt = labelTypes.byLabel(vote.getKey());
+      cells.add(new PatchSetApproval(new PatchSetApproval.Key(
+          ps.getId(),
+          ps.getUploader(),
+          lt.getLabelId()),
+          vote.getValue(),
+          ts));
+    }
+    for (PatchSetApproval psa : cells) {
+      update.putApproval(psa.getLabel(), psa.getValue());
+    }
+    db.patchSetApprovals().insert(cells);
+    return cells;
   }
 
   public static void checkLabel(LabelTypes labelTypes, String name, Short value) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
index 14cc74e..99e6bd8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import java.util.ArrayList;
@@ -24,11 +26,20 @@
 
 @Singleton
 public class GetEmails implements RestReadView<AccountResource> {
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+
+  @Inject
+  GetEmails(IdentifiedUser.GenericFactory identifiedUserFactory) {
+    this.identifiedUserFactory = identifiedUserFactory;
+  }
 
   @Override
   public List<EmailInfo> apply(AccountResource rsrc) {
+    IdentifiedUser user =
+        identifiedUserFactory.create(rsrc.getUser().getAccountId());
+
     List<EmailInfo> emails = new ArrayList<>();
-    for (String email : rsrc.getUser().getEmailAddresses()) {
+    for (String email : user.getEmailAddresses()) {
       if (email != null) {
         EmailInfo e = new EmailInfo();
         e.email = email;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 6b5e83c..213f90d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -321,9 +321,13 @@
   }
 
   @Override
-  public FileApi file(String path) {
-    return fileApi.create(files.parse(revision,
-        IdString.fromDecoded(path)));
+  public FileApi file(String path) throws RestApiException {
+    try {
+      return fileApi.create(files.parse(revision,
+          IdString.fromDecoded(path)));
+    } catch (IOException e) {
+      throw new RestApiException("Cannot retrieve file", e);
+    }
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 6333809..84781f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -19,6 +19,8 @@
 import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -34,6 +36,7 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.CommentAdded;
@@ -48,6 +51,7 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
@@ -86,6 +90,7 @@
       LoggerFactory.getLogger(ChangeInserter.class);
 
   private final ProjectControl.GenericFactory projectControlFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final PatchSetUtil psUtil;
@@ -128,6 +133,7 @@
 
   @Inject
   ChangeInserter(ProjectControl.GenericFactory projectControlFactory,
+      IdentifiedUser.GenericFactory userFactory,
       ChangeControl.GenericFactory changeControlFactory,
       PatchSetInfoFactory patchSetInfoFactory,
       PatchSetUtil psUtil,
@@ -142,6 +148,7 @@
       @Assisted RevCommit commit,
       @Assisted String refName) {
     this.projectControlFactory = projectControlFactory;
+    this.userFactory = userFactory;
     this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.psUtil = psUtil;
@@ -353,8 +360,10 @@
     update.fixStatus(change.getStatus());
 
     LabelTypes labelTypes = ctl.getProjectControl().getLabelTypes();
-    approvalsUtil.addReviewers(db, update, labelTypes, change,
-        patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
+    approvalsUtil.addReviewers(db, update, labelTypes, change, patchSet,
+        patchSetInfo,
+        filterOnChangeVisibility(db, ctx.getNotes(), reviewers),
+        Collections.<Account.Id> emptySet());
     approvalsUtil.addApprovals(db, update, labelTypes, patchSet,
         ctx.getControl(), approvals);
     if (message != null) {
@@ -368,6 +377,25 @@
     return true;
   }
 
+  private Set<Account.Id> filterOnChangeVisibility(final ReviewDb db,
+      final ChangeNotes notes, Set<Account.Id> accounts) {
+    return Sets.filter(accounts, new Predicate<Account.Id>() {
+      @Override
+      public boolean apply(Account.Id accountId) {
+        try {
+          IdentifiedUser user = userFactory.create(accountId);
+          return changeControlFactory.controlFor(notes, user).isVisible(db);
+        } catch (OrmException | NoSuchChangeException e) {
+          log.warn(
+              String.format("Failed to check if account %d can see change %d",
+                  accountId.get(), notes.getChangeId().get()),
+              e);
+          return false;
+        }
+      }
+    });
+  }
+
   @Override
   public void postUpdate(Context ctx) throws OrmException, NoSuchChangeException {
     if (sendMail) {
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 48f00c3..ab92a97 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
@@ -926,12 +926,19 @@
         .toSortedList(AccountInfoComparator.ORDER_NULLS_FIRST);
   }
 
+  @Nullable
+  private Repository openRepoIfNecessary(ChangeControl ctl) throws IOException {
+    if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
+      return repoManager.openRepository(ctl.getProject().getNameKey());
+    }
+    return null;
+  }
+
   private Map<String, RevisionInfo> revisions(ChangeControl ctl, ChangeData cd,
       Map<PatchSet.Id, PatchSet> map) throws PatchListNotAvailableException,
       GpgException, OrmException, IOException {
     Map<String, RevisionInfo> res = new LinkedHashMap<>();
-    try (Repository repo =
-        repoManager.openRepository(ctl.getProject().getNameKey())) {
+    try (Repository repo = openRepoIfNecessary(ctl)) {
       for (PatchSet in : map.values()) {
         if ((has(ALL_REVISIONS)
             || in.getId().equals(ctl.getChange().currentPatchSetId()))
@@ -975,8 +982,7 @@
       throws PatchListNotAvailableException, GpgException, OrmException,
       IOException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-    try (Repository repo =
-        repoManager.openRepository(ctl.getProject().getNameKey())) {
+    try (Repository repo = openRepoIfNecessary(ctl)) {
       RevisionInfo rev = toRevisionInfo(
           ctl, changeDataFactory.create(db.get(), ctl), in, repo, true);
       accountLoader.fill();
@@ -985,7 +991,7 @@
   }
 
   private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd,
-      PatchSet in, Repository repo, boolean fillCommit)
+      PatchSet in, @Nullable Repository repo, boolean fillCommit)
       throws PatchListNotAvailableException, GpgException, OrmException,
       IOException {
     Change c = ctl.getChange();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
index 2302b70..f0075ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -31,10 +32,11 @@
  * implementation changes, which might invalidate old entries).
  */
 public interface ChangeKindCache {
-  ChangeKind getChangeKind(ProjectState project, Repository repo,
+  ChangeKind getChangeKind(ProjectState project, @Nullable Repository repo,
       ObjectId prior, ObjectId next);
 
   ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch);
 
-  ChangeKind getChangeKind(Repository repo, ChangeData cd, PatchSet patch);
+  ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd,
+      PatchSet patch);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index edc1b12..b23bcf8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -22,9 +22,11 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.Weigher;
 import com.google.common.collect.FluentIterable;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -100,11 +102,13 @@
     }
 
     @Override
-    public ChangeKind getChangeKind(ProjectState project, Repository repo,
-        ObjectId prior, ObjectId next) {
+    public ChangeKind getChangeKind(ProjectState project,
+        @Nullable Repository repo, ObjectId prior, ObjectId next) {
       try {
         Key key = new Key(prior, next, useRecursiveMerge);
-        return new Loader(key, repo).call();
+        return new Loader(
+                key, repoManager, project.getProject().getNameKey(), repo)
+            .call();
       } catch (IOException e) {
         log.warn("Cannot check trivial rebase of new patch set " + next.name()
             + " in " + project.getProject().getName(), e);
@@ -120,7 +124,7 @@
     }
 
     @Override
-    public ChangeKind getChangeKind(Repository repo, ChangeData cd,
+    public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd,
         PatchSet patch) {
       return getChangeKindInternal(this, repo, cd, patch, projectCache);
     }
@@ -191,11 +195,16 @@
 
   private static class Loader implements Callable<ChangeKind> {
     private final Key key;
-    private final Repository repo;
+    private final GitRepositoryManager repoManager;
+    private final Project.NameKey projectName;
+    private final Repository alreadyOpenRepo;
 
-    private Loader(Key key, Repository repo) {
+    private Loader(Key key, GitRepositoryManager repoManager,
+        Project.NameKey projectName, @Nullable Repository alreadyOpenRepo) {
       this.key = key;
-      this.repo = repo;
+      this.repoManager = repoManager;
+      this.projectName = projectName;
+      this.alreadyOpenRepo = alreadyOpenRepo;
     }
 
     @Override
@@ -204,6 +213,12 @@
         return ChangeKind.NO_CODE_CHANGE;
       }
 
+      Repository repo = alreadyOpenRepo;
+      boolean close = false;
+      if (repo == null) {
+        repo = repoManager.openRepository(projectName);
+        close = true;
+      }
       try (RevWalk walk = new RevWalk(repo)) {
         RevCommit prior = walk.parseCommit(key.prior);
         walk.parseBody(prior);
@@ -246,6 +261,10 @@
           // it was a rework.
         }
         return ChangeKind.REWORK;
+      } finally {
+        if (close) {
+          repo.close();
+        }
       }
     }
 
@@ -321,11 +340,14 @@
   }
 
   @Override
-  public ChangeKind getChangeKind(ProjectState project, Repository repo,
-      ObjectId prior, ObjectId next) {
+  public ChangeKind getChangeKind(ProjectState project,
+      @Nullable Repository repo, ObjectId prior, ObjectId next) {
     try {
       Key key = new Key(prior, next, useRecursiveMerge);
-      return cache.get(key, new Loader(key, repo));
+      return cache.get(
+          key,
+          new Loader(
+                key, repoManager, project.getProject().getNameKey(), repo));
     } catch (ExecutionException e) {
       log.warn("Cannot check trivial rebase of new patch set " + next.name()
           + " in " + project.getProject().getName(), e);
@@ -340,14 +362,14 @@
   }
 
   @Override
-  public ChangeKind getChangeKind(Repository repo, ChangeData cd,
+  public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd,
       PatchSet patch) {
     return getChangeKindInternal(this, repo, cd, patch, projectCache);
   }
 
   private static ChangeKind getChangeKindInternal(
       ChangeKindCache cache,
-      Repository repo,
+      @Nullable Repository repo,
       ChangeData change,
       PatchSet patch,
       ProjectCache projectCache) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index 35dbec1..1034c99c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -48,6 +48,7 @@
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
@@ -67,11 +68,15 @@
 public class Files implements ChildCollection<RevisionResource, FileResource> {
   private final DynamicMap<RestView<FileResource>> views;
   private final Provider<ListFiles> list;
+  private final GitRepositoryManager repoManager;
 
   @Inject
-  Files(DynamicMap<RestView<FileResource>> views, Provider<ListFiles> list) {
+  Files(DynamicMap<RestView<FileResource>> views,
+      Provider<ListFiles> list,
+      GitRepositoryManager repoManager) {
     this.views = views;
     this.list = list;
+    this.repoManager = repoManager;
   }
 
   @Override
@@ -85,8 +90,17 @@
   }
 
   @Override
-  public FileResource parse(RevisionResource rev, IdString id) {
-    return new FileResource(rev, id.get());
+  public FileResource parse(RevisionResource rev, IdString id)
+      throws ResourceNotFoundException, IOException {
+    try (Repository repo = repoManager.openRepository(rev.getProject());
+        RevWalk rw = new RevWalk(repo)) {
+      RevTree tree = rw.parseTree(
+          ObjectId.fromString(rev.getPatchSet().getRevision().get()));
+      if (TreeWalk.forPath(repo, id.get(), tree) != null) {
+        return new FileResource(rev, id.get());
+      }
+    }
+    throw new ResourceNotFoundException(id);
   }
 
   public static final class ListFiles implements RestReadView<RevisionResource> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index fb37d9d..4166ca3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.extensions.client.ReviewerState.CC;
 import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
 
+import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
@@ -40,7 +41,6 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.GroupMembers;
@@ -96,7 +96,6 @@
   private final Provider<IdentifiedUser> user;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Config cfg;
-  private final AccountCache accountCache;
   private final ReviewerJson json;
   private final ReviewerAdded reviewerAdded;
   private final NotesMigration migration;
@@ -115,7 +114,6 @@
       Provider<IdentifiedUser> user,
       IdentifiedUser.GenericFactory identifiedUserFactory,
       @GerritServerConfig Config cfg,
-      AccountCache accountCache,
       ReviewerJson json,
       ReviewerAdded reviewerAdded,
       NotesMigration migration) {
@@ -132,7 +130,6 @@
     this.user = user;
     this.identifiedUserFactory = identifiedUserFactory;
     this.cfg = cfg;
-    this.accountCache = accountCache;
     this.json = json;
     this.reviewerAdded = reviewerAdded;
     this.migration = migration;
@@ -355,11 +352,15 @@
         }
         emailReviewers(rsrc.getChange(), addedReviewers, addedCCs);
         if (!addedReviewers.isEmpty()) {
-          for (PatchSetApproval psa : addedReviewers) {
-            Account account = accountCache.get(psa.getAccountId()).getAccount();
-            reviewerAdded.fire(rsrc.getChange(), patchSet, account,
+          List<Account.Id> reviewers = Lists.transform(addedReviewers,
+              new Function<PatchSetApproval, Account.Id>() {
+                @Override
+                public Account.Id apply(PatchSetApproval psa) {
+                  return psa.getAccountId();
+                }
+              });
+          reviewerAdded.fire(rsrc.getChange(), patchSet, reviewers,
               ctx.getAccount(), ctx.getWhen());
-          }
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
index 2b621c2..1dc910c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -20,7 +20,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.gerrit.common.data.GitwebType;
 import com.google.gerrit.extensions.config.CloneCommand;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
@@ -385,11 +384,6 @@
     public String reportBugText;
   }
 
-  public static class GitwebInfo {
-    public String url;
-    public GitwebType type;
-  }
-
   public static class PluginConfigInfo {
     public Boolean hasAvatars;
     public List<String> jsResourcePaths;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 1368bf3..8d093f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -321,7 +321,7 @@
   }
 
   @Override
-  public void onReviewerAdded(ReviewerAddedListener.Event ev) {
+  public void onReviewersAdded(ReviewerAddedListener.Event ev) {
     try {
       ChangeNotes notes = getNotes(ev.getChange());
       Change change = notes.getChange();
@@ -330,9 +330,10 @@
       event.change = changeAttributeSupplier(change);
       event.patchSet = patchSetAttributeSupplier(change,
           psUtil.current(db.get(), notes));
-      event.reviewer = accountAttributeSupplier(ev.getReviewer());
-
-      dispatcher.get().postEvent(change, event);
+      for (AccountInfo reviewer : ev.getReviewers()) {
+        event.reviewer = accountAttributeSupplier(reviewer);
+        dispatcher.get().postEvent(change, event);
+      }
     } catch (OrmException e) {
       log.error("Failed to dispatch event", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
index 3ae8135..35b14dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -33,6 +35,7 @@
 
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.List;
 
 public class ReviewerAdded {
   private static final Logger log =
@@ -49,29 +52,38 @@
   }
 
   public void fire(ChangeInfo change, RevisionInfo revision,
-      AccountInfo reviewer, AccountInfo adder, Timestamp when) {
+      List<AccountInfo> reviewers, AccountInfo adder, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, revision, reviewer, adder, when);
+    Event event = new Event(change, revision, reviewers, adder, when);
     for (ReviewerAddedListener l : listeners) {
       try {
-        l.onReviewerAdded(event);
+        l.onReviewersAdded(event);
       } catch (Exception e) {
         log.warn("Error in event listener, e");
       }
     }
   }
 
-  public void fire(Change change, PatchSet patchSet, Account account,
+  public void fire(Change change, PatchSet patchSet, List<Account.Id> reviewers,
       Account adder, Timestamp when) {
-    if (!listeners.iterator().hasNext()) {
+    if (!listeners.iterator().hasNext() || reviewers.isEmpty()) {
       return;
     }
+
+    List<AccountInfo> transformed = Lists.transform(reviewers,
+        new Function<Account.Id, AccountInfo>() {
+          @Override
+          public AccountInfo apply(Account.Id account) {
+            return util.accountInfo(account);
+          }
+        });
+
     try {
       fire(util.changeInfo(change),
           util.revisionInfo(change.getProject(), patchSet),
-          util.accountInfo(account),
+          transformed,
           util.accountInfo(adder),
           when);
     } catch (PatchListNotAvailableException | GpgException | IOException
@@ -82,17 +94,17 @@
 
   private static class Event extends AbstractRevisionEvent
       implements ReviewerAddedListener.Event {
-    private final AccountInfo reviewer;
+    private final List<AccountInfo> reviewers;
 
-    Event(ChangeInfo change, RevisionInfo revision, AccountInfo reviewer,
+    Event(ChangeInfo change, RevisionInfo revision, List<AccountInfo> reviewers,
         AccountInfo adder, Timestamp when) {
       super(change, revision, adder, when, NotifyHandling.ALL);
-      this.reviewer = reviewer;
+      this.reviewers = reviewers;
     }
 
     @Override
-    public AccountInfo getReviewer() {
-      return reviewer;
+    public List<AccountInfo> getReviewers() {
+      return reviewers;
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index fe568c8..a70fa7a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -255,13 +255,15 @@
     ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getControl());
     MailRecipients oldRecipients =
         getRecipientsFromReviewers(cd.reviewers());
-    approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet);
+    Iterable<PatchSetApproval> newApprovals =
+        approvalsUtil.addApprovals(ctx.getDb(), update,
+            projectControl.getLabelTypes(), newPatchSet, ctx.getControl(),
+            approvals);
+    approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet,
+        newApprovals);
     approvalsUtil.addReviewers(ctx.getDb(), update,
         projectControl.getLabelTypes(), change, newPatchSet, info,
         recipients.getReviewers(), oldRecipients.getAll());
-    approvalsUtil.addApprovals(ctx.getDb(), update,
-        projectControl.getLabelTypes(), newPatchSet, ctx.getControl(),
-        approvals);
     recipients.add(oldRecipients);
 
     String approvalMessage = ApprovalsUtil.renderMessageWithApprovals(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java
index 95b3ff4..eedfe70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java
@@ -64,12 +64,6 @@
     if (failAfterRefUpdates) {
       throw new ResourceConflictException("Failing after ref updates");
     }
-    for (SubmitStrategy strategy : strategies) {
-      SubmitStrategy.Arguments args = strategy.args;
-      if (args.mergeTip.getCurrentTip().equals(args.mergeTip.getInitialTip())) {
-        continue;
-      }
-    }
   }
 
   private void findUnmergedChanges(List<Change.Id> alreadyMerged)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
index 51f7ad1..0bc65bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -32,6 +32,7 @@
 
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.regex.Pattern;
 
 /** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */
 @Singleton
@@ -53,13 +54,15 @@
       generator =
           new PatternGen(srvAddr, accountCache, anonymousCowardName, name,
               srvAddr.email);
-
     } else if ("USER".equalsIgnoreCase(from)) {
-      generator = new UserGen(accountCache, srvAddr);
-
+      String[] domains = cfg.getStringList("sendemail", null, "allowedDomain");
+      Pattern domainPattern = MailUtil.glob(domains);
+      ParameterizedString namePattern =
+          new ParameterizedString("${user} (Code Review)");
+      generator = new UserGen(accountCache, domainPattern, anonymousCowardName,
+          namePattern, srvAddr);
     } else if ("SERVER".equalsIgnoreCase(from)) {
       generator = new ServerGen(srvAddr);
-
     } else {
       final Address a = Address.parse(from);
       final ParameterizedString name = a.name != null ? new ParameterizedString(a.name) : null;
@@ -84,11 +87,31 @@
 
   static final class UserGen implements FromAddressGenerator {
     private final AccountCache accountCache;
-    private final Address srvAddr;
+    private final Pattern domainPattern;
+    private final String anonymousCowardName;
+    private final ParameterizedString nameRewriteTmpl;
+    private final Address serverAddress;
 
-    UserGen(AccountCache accountCache, Address srvAddr) {
+    /**
+     * From address generator for USER mode
+     *
+     * @param accountCache get user account from id
+     * @param domainPattern allowed user domain pattern that Gerrit can send as
+     *        the user
+     * @param anonymousCowardName name used when user's full name is missing
+     * @param nameRewriteTmpl name template used for rewriting the sender's name
+     *        when Gerrit can not send as the user
+     * @param serverAddress serverAddress.name is used when fromId is null and
+     *        serverAddress.email is used when Gerrit can not send as the user
+     */
+    UserGen(AccountCache accountCache, Pattern domainPattern,
+        String anonymousCowardName, ParameterizedString nameRewriteTmpl,
+        Address serverAddress) {
       this.accountCache = accountCache;
-      this.srvAddr = srvAddr;
+      this.domainPattern = domainPattern;
+      this.anonymousCowardName = anonymousCowardName;
+      this.nameRewriteTmpl = nameRewriteTmpl;
+      this.serverAddress = serverAddress;
     }
 
     @Override
@@ -98,14 +121,44 @@
 
     @Override
     public Address from(final Account.Id fromId) {
+      String senderName;
       if (fromId != null) {
         Account a = accountCache.get(fromId).getAccount();
+        String fullName = a.getFullName();
         String userEmail = a.getPreferredEmail();
-        return new Address(
-            a.getFullName(),
-            userEmail != null ? userEmail : srvAddr.getEmail());
+        if (canRelay(userEmail)) {
+          return new Address(fullName, userEmail);
+        }
+
+        if (fullName == null || "".equals(fullName.trim())) {
+          fullName = anonymousCowardName;
+        }
+        senderName = nameRewriteTmpl.replace("user", fullName).toString();
+      } else {
+        senderName = serverAddress.name;
       }
-      return srvAddr;
+
+      String senderEmail;
+      ParameterizedString senderEmailPattern =
+          new ParameterizedString(serverAddress.email);
+      if (senderEmailPattern.getParameterNames().isEmpty()) {
+        senderEmail = senderEmailPattern.getRawPattern();
+      } else {
+        senderEmail = senderEmailPattern.replace("userHash", hashOf(senderName))
+            .toString();
+      }
+      return new Address(senderName, senderEmail);
+    }
+
+    /** check if Gerrit is allowed to send from {@code userEmail}. */
+    private boolean canRelay(String userEmail) {
+      if (userEmail != null) {
+        int index = userEmail.indexOf('@');
+        if (index > 0 && index < userEmail.length() - 1) {
+          return domainPattern.matcher(userEmail.substring(index + 1)).matches();
+        }
+      }
+      return false;
     }
   }
 
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 048a4a4..8a132cd 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
@@ -32,6 +32,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 public class MailUtil {
   public static MailRecipients getRecipientsFromFooters(
@@ -124,4 +125,19 @@
       return Collections.unmodifiableSet(all);
     }
   }
+
+  /** allow wildcard matching for {@code domains} */
+  public static Pattern glob(String[] domains) {
+    // if domains is not set, match anything
+    if (domains == null || domains.length == 0) {
+      return Pattern.compile(".*");
+    }
+
+    StringBuilder sb = new StringBuilder("");
+    for (String domain : domains) {
+      String quoted = "\\Q" + domain.replace("\\E", "\\E\\\\E\\Q") + "\\E|";
+      sb.append(quoted.replace("*", "\\E.*\\Q"));
+    }
+    return Pattern.compile(sb.substring(0, sb.length() - 1));
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
index 47942be..82ea155 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
@@ -14,16 +14,39 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.TypeLiteral;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import java.io.IOException;
 
 public class FileResource implements RestResource {
   public static final TypeLiteral<RestView<FileResource>> FILE_KIND =
       new TypeLiteral<RestView<FileResource>>() {};
 
+  public static FileResource create(GitRepositoryManager repoManager,
+      ProjectControl project, ObjectId rev, String path)
+          throws ResourceNotFoundException, IOException {
+    try (Repository repo =
+            repoManager.openRepository(project.getProject().getNameKey());
+        RevWalk rw = new RevWalk(repo)) {
+      RevTree tree = rw.parseTree(rev);
+      if (TreeWalk.forPath(repo, path, tree) != null) {
+        return new FileResource(project, rev, path);
+      }
+    }
+    throw new ResourceNotFoundException(IdString.fromDecoded(path));
+  }
+
   private final ProjectControl project;
   private final ObjectId rev;
   private final String path;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
index d0460d5..dcb8747 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
@@ -19,19 +19,25 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectId;
 
+import java.io.IOException;
+
 @Singleton
 public class FilesCollection implements
     ChildCollection<BranchResource, FileResource> {
   private final DynamicMap<RestView<FileResource>> views;
+  private final GitRepositoryManager repoManager;
 
   @Inject
-  FilesCollection(DynamicMap<RestView<FileResource>> views) {
+  FilesCollection(DynamicMap<RestView<FileResource>> views,
+      GitRepositoryManager repoManager) {
     this.views = views;
+    this.repoManager = repoManager;
   }
 
   @Override
@@ -40,11 +46,10 @@
   }
 
   @Override
-  public FileResource parse(BranchResource parent, IdString id) {
-    return new FileResource(
-        parent.getControl(),
-        ObjectId.fromString(parent.getRevision()),
-        id.get());
+  public FileResource parse(BranchResource parent, IdString id)
+      throws ResourceNotFoundException, IOException {
+    return FileResource.create(repoManager, parent.getControl(),
+        ObjectId.fromString(parent.getRevision()), id.get());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
index 8e0aab8..736fe5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
@@ -19,17 +19,23 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import java.io.IOException;
+
 @Singleton
 public class FilesInCommitCollection implements
     ChildCollection<CommitResource, FileResource> {
   private final DynamicMap<RestView<FileResource>> views;
+  private final GitRepositoryManager repoManager;
 
   @Inject
-  FilesInCommitCollection(DynamicMap<RestView<FileResource>> views) {
+  FilesInCommitCollection(DynamicMap<RestView<FileResource>> views,
+      GitRepositoryManager repoManager) {
     this.views = views;
+    this.repoManager = repoManager;
   }
 
   @Override
@@ -39,8 +45,9 @@
 
   @Override
   public FileResource parse(CommitResource parent, IdString id)
-      throws ResourceNotFoundException {
-    return new FileResource(parent.getProject(), parent.getCommit(), id.get());
+      throws ResourceNotFoundException, IOException {
+    return FileResource.create(repoManager, parent.getProject(),
+        parent.getCommit(), id.get());
   }
 
   @Override
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
index 11f1d54..87dd3d9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -35,8 +35,10 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Set;
 
 public class FromAddressGeneratorProviderTest {
@@ -60,6 +62,10 @@
     config.setString("sendemail", null, "from", newFrom);
   }
 
+  private void setDomains(List<String> domains) {
+    config.setStringList("sendemail", null, "allowedDomain", domains);
+  }
+
   @Test
   public void testDefaultIsMIXED() {
     assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class);
@@ -118,7 +124,7 @@
     replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.name).isEqualTo(name);
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
     assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
@@ -135,6 +141,88 @@
   }
 
   @Test
+  public void testUSERAllowDomain() {
+    setFrom("USER");
+    setDomains(Arrays.asList("*.example.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.example.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(email);
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERNoAllowDomain() {
+    setFrom("USER");
+    setDomains(Arrays.asList("example.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERAllowDomainTwice() {
+    setFrom("USER");
+    setDomains(Arrays.asList("example.com"));
+    setDomains(Arrays.asList("test.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(email);
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERAllowDomainTwiceReverse() {
+    setFrom("USER");
+    setDomains(Arrays.asList("test.com"));
+    setDomains(Arrays.asList("example.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERAllowTwoDomains() {
+    setFrom("USER");
+    setDomains(Arrays.asList("example.com", "test.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(email);
+    verify(accountCache);
+  }
+
+  @Test
   public void testSelectSERVER() {
     setFrom("SERVER");
     assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class);
diff --git a/lib/BUCK b/lib/BUCK
index 20ac74c..2496df7 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -59,8 +59,8 @@
 
 maven_jar(
   name = 'gson',
-  id = 'com.google.code.gson:gson:2.6.2',
-  sha1 = 'f1bc476cc167b18e66c297df599b2377131a8947',
+  id = 'com.google.code.gson:gson:2.7',
+  sha1 = '751f548c85fa49f330cecbb1875893f971b33c4e',
   license = 'Apache2.0',
 )
 
diff --git a/lib/JGIT_VERSION b/lib/JGIT_VERSION
index 58b677a..6457323 100644
--- a/lib/JGIT_VERSION
+++ b/lib/JGIT_VERSION
@@ -1,4 +1,4 @@
 include_defs('//lib/maven.defs')
 
 REPO = GERRIT # Leave here even if set to MAVEN_CENTRAL.
-VERS = '4.4.1.201607150455-r.109-g0b4751e'
+VERS = '4.4.1.201607150455-r.118-g1096652'
diff --git a/lib/guice/BUCK b/lib/guice/BUCK
index 3893b80..867b521 100644
--- a/lib/guice/BUCK
+++ b/lib/guice/BUCK
@@ -1,6 +1,6 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.0'
+VERSION = '4.1.0'
 EXCLUDE = [
   'META-INF/DEPENDENCIES',
   'META-INF/LICENSE',
@@ -19,7 +19,7 @@
 maven_jar(
   name = 'guice_library',
   id = 'com.google.inject:guice:' + VERSION,
-  sha1 = '0f990a43d3725781b6db7cd0acf0a8b62dfd1649',
+  sha1 = 'eeb69005da379a10071aa4948c48d89250febb07',
   license = 'Apache2.0',
   deps = [':aopalliance'],
   exclude_java_sources = True,
@@ -33,7 +33,7 @@
 maven_jar(
   name = 'guice-assistedinject',
   id = 'com.google.inject.extensions:guice-assistedinject:' + VERSION,
-  sha1 = '8fa6431da1a2187817e3e52e967535899e2e46ca',
+  sha1 = 'af799dd7e23e6fe8c988da12314582072b07edcb',
   license = 'Apache2.0',
   deps = [':guice'],
   exclude = EXCLUDE,
@@ -42,7 +42,7 @@
 maven_jar(
   name = 'guice-servlet',
   id = 'com.google.inject.extensions:guice-servlet:' + VERSION,
-  sha1 = '4503da866f4c402b5090579b40c1c4aaefabb164',
+  sha1 = '90ac2db772d9b85e2b05417b74f7464bcc061dcb',
   license = 'Apache2.0',
   deps = [':guice'],
   exclude = EXCLUDE,
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUCK b/lib/jgit/org.eclipse.jgit.archive/BUCK
index fab0634..384a5e0 100644
--- a/lib/jgit/org.eclipse.jgit.archive/BUCK
+++ b/lib/jgit/org.eclipse.jgit.archive/BUCK
@@ -4,7 +4,7 @@
 maven_jar(
   name = 'jgit-archive',
   id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
-  sha1 = '57ebfba05a6023e3d9ed48083ff0dcbac09e8047',
+  sha1 = '3f45cd199e40a7c68ee07a1743c06d1c3d07308a',
   license = 'jgit',
   repository = REPO,
   deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUCK b/lib/jgit/org.eclipse.jgit.http.server/BUCK
index 3371b87..2ade9ff 100644
--- a/lib/jgit/org.eclipse.jgit.http.server/BUCK
+++ b/lib/jgit/org.eclipse.jgit.http.server/BUCK
@@ -4,7 +4,7 @@
 maven_jar(
   name = 'jgit-servlet',
   id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
-  sha1 = '44b864259407c0fe2c7ccdeabcef79db3de5a9ea',
+  sha1 = 'fa67bf925001cfc663bf98772f37d5c5c1abd756',
   license = 'jgit',
   repository = REPO,
   deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUCK b/lib/jgit/org.eclipse.jgit.junit/BUCK
index 68062e7..a31ee6f 100644
--- a/lib/jgit/org.eclipse.jgit.junit/BUCK
+++ b/lib/jgit/org.eclipse.jgit.junit/BUCK
@@ -4,7 +4,7 @@
 maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
-  sha1 = '9637d7ec4775a4e0e2467c8e951a735ad06a8c57',
+  sha1 = 'dc7edb9c3060655c7fb93ab9b9349e815bab266f',
   license = 'DO_NOT_DISTRIBUTE',
   repository = REPO,
   unsign = True,
diff --git a/lib/jgit/org.eclipse.jgit/BUCK b/lib/jgit/org.eclipse.jgit/BUCK
index 17e3bbe..7c06726 100644
--- a/lib/jgit/org.eclipse.jgit/BUCK
+++ b/lib/jgit/org.eclipse.jgit/BUCK
@@ -4,8 +4,8 @@
 maven_jar(
   name = 'jgit',
   id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
-  bin_sha1 = 'de7fb6fa397b2388bfafe17b212d4fe4ad5df72a',
-  src_sha1 = '30665754ae7703018251b2880e0cfba6edcbe189',
+  bin_sha1 = 'cd142b9030910babd119702f1c4eeae13ee90018',
+  src_sha1 = '3e65e476bfb4a529e18752ffcd27b566e7ee7241',
   license = 'jgit',
   repository = REPO,
   unsign = True,
diff --git a/plugins/hooks b/plugins/hooks
index 3acc14d..ac87f18 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 3acc14d10d26678eae6489038fe0d4dad644a9b4
+Subproject commit ac87f187034838e6cf8d5824b2e541cc9b3a0aa4
diff --git a/plugins/replication b/plugins/replication
index c5123d6..9a83958 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit c5123d6a5604cc740d6f42485235c0d3ec141c4e
+Subproject commit 9a8395846516d65158b1b2bb59deb9aa6537e820
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index b741784..1fe09e5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -32,19 +32,14 @@
   <template>
     <style>
       :host {
-        display: block;
+        display: inline-block;
+        font-family: var(--font-family);
       }
       section {
-        margin-top: 1em;
-      }
-      .groupLabel {
-        color: #666;
-        margin-bottom: .15em;
-        text-align: center;
+        display: inline-block;
       }
       gr-button {
-        display: block;
-        margin-bottom: .5em;
+        margin-left: .5em;
       }
       gr-button:before {
         content: attr(data-label);
@@ -53,6 +48,15 @@
         content: attr(data-loading-label);
       }
       @media screen and (max-width: 50em) {
+        :host,
+        section,
+        gr-button {
+          display: block;
+        }
+        gr-button {
+          margin-bottom: .5em;
+          margin-left: 0;
+        }
         .confirmDialog {
           width: 90vw;
         }
@@ -60,7 +64,6 @@
     </style>
     <div>
       <section hidden$="[[!_actionCount(actions.*, _additionalActions.*)]]">
-        <div class="groupLabel">Change</div>
         <template is="dom-repeat" items="[[_changeActionValues]]" as="action">
           <gr-button title$="[[action.title]]"
               primary$="[[action.__primary]]"
@@ -73,7 +76,6 @@
         </template>
       </section>
       <section hidden$="[[!_actionCount(_revisionActions.*, _additionalActions.*)]]">
-        <div class="groupLabel">Revision</div>
         <template is="dom-repeat" items="[[_revisionActionValues]]" as="action">
           <gr-button title$="[[action.title]]"
               primary$="[[action.__primary]]"
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 e3f7fd2..1807ac9 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
@@ -45,18 +45,16 @@
         color: #666;
         padding: 1em var(--default-horizontal-margin);
       }
-      .headerContainer {
-        height: 4.1em;
-        margin-bottom: .5em;
-      }
       .header {
         align-items: center;
         background-color: var(--view-background-color);
-        border-bottom: 1px solid #ddd;
         display: flex;
-        padding: 1em var(--default-horizontal-margin);
+        padding: .65em var(--default-horizontal-margin);
         z-index: 99;  /* Less than gr-overlay's backdrop */
       }
+      .header .download {
+        margin-right: 1em;
+      }
       .header.pinned {
         border-bottom-color: transparent;
         box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
@@ -69,24 +67,11 @@
         flex: 1;
         font-size: 1.2em;
         font-weight: bold;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
       }
       gr-change-star {
         margin-right: .25em;
         vertical-align: -.425em;
       }
-      .download,
-      .patchSelectLabel {
-        margin-left: 1em;
-      }
-      .header select {
-        margin-left: .5em;
-      }
-      .header .reply {
-        margin-left: var(--default-horizontal-margin);
-      }
       gr-reply-dialog {
         width: 50em;
       }
@@ -111,12 +96,8 @@
         padding-right: 1em;
       }
       .changeMetadata {
-        border-right: 1px solid #ddd;
         font-size: .9em;
       }
-      gr-change-actions {
-        margin-top: 1em;
-      }
       .commitMessage {
         font-family: var(--monospace-font-family);
         flex: 0 0 72ch;
@@ -124,15 +105,28 @@
         margin-bottom: 1em;
         overflow-x: hidden;
       }
-      .commitMessage h4 {
-        font-family: var(--font-family);
-        font-weight: bold;
-        margin-bottom: .25em;
-      }
       .commitMessage gr-linked-text {
         --linked-text-white-space: pre;
         overflow: auto;
       }
+      .editCommitMessage {
+        margin-top: 1em;
+      }
+      .commitActions {
+        border-bottom: 1px solid #ddd;
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: .5em;
+        padding-bottom: .5em;
+      }
+      .reply {
+        margin-right: .5em;
+      }
+      .mainChangeInfo {
+        display: flex;
+        flex: 1;
+        flex-direction: column;
+      }
       .commitAndRelated {
         align-content: flex-start;
         display: flex;
@@ -149,9 +143,6 @@
         padding: 0 var(--default-horizontal-margin);
       }
       @media screen and (max-width: 50em) {
-        .headerContainer {
-          height: 5.15em;
-        }
         .header {
           align-items: flex-start;
           flex-direction: column;
@@ -163,12 +154,6 @@
         .header-title {
           font-size: 1.1em;
         }
-        .header-actions {
-          align-items: center;
-          display: flex;
-          justify-content: space-between;
-          margin-top: .5em;
-        }
         gr-reply-dialog {
           min-width: initial;
           width: 90vw;
@@ -176,17 +161,10 @@
         .download {
           display: none;
         }
-        .patchSelectLabel {
-          margin-left: 0;
-          margin-right: .5em;
-        }
-        .header select {
-          margin-left: 0;
-          margin-right: .5em;
-        }
-        .header .reply {
-          margin-left: 0;
-          margin-right: .5em;
+        .reply {
+          display: block;
+          margin-right: 0;
+          margin-bottom: .5em;
         }
         .changeInfo-column:not(:last-of-type) {
           margin-right: 0;
@@ -207,6 +185,9 @@
           margin-top: .25em;
           max-width: none;
         }
+        .commitActions {
+          flex-direction: column;
+        }
         .commitMessage {
           flex: initial;
           margin-right: 0;
@@ -215,35 +196,28 @@
     </style>
     <div class="container loading" hidden$="{{!_loading}}">Loading...</div>
     <div class="container" hidden$="{{_loading}}">
-      <div class="headerContainer">
-        <div class="header">
-          <span class="header-title">
-            <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
-            <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
-            <span>[[_change.subject]]</span>
-            <span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span>
+      <div class="header">
+        <span class="header-title">
+          <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
+          <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
+          <span>[[_change.subject]]</span>
+          <span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span>
+        </span>
+        <span class="header-actions">
+          <gr-button class="download" on-tap="_handleDownloadTap">Download</gr-button>
+          <span>
+            <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
+            <select id="patchSetSelect" on-change="_handlePatchChange">
+              <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
+                <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
+                  <span>[[patchNumber]]</span>
+                  /
+                  <span>[[_computeLatestPatchNum(_allPatchSets)]]</span>
+                </option>
+              </template>
+            </select>
           </span>
-          <span class="header-actions">
-            <gr-button hidden
-                class="reply"
-                primary$="[[_computeReplyButtonHighlighted(_diffDrafts.*)]]"
-                hidden$="[[!_loggedIn]]"
-                on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
-            <gr-button class="download" on-tap="_handleDownloadTap">Download</gr-button>
-            <span>
-              <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
-              <select id="patchSetSelect" on-change="_handlePatchChange">
-                <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
-                  <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
-                    <span>[[patchNumber]]</span>
-                    /
-                    <span>[[_computeLatestPatchNum(_allPatchSets)]]</span>
-                  </option>
-                </template>
-              </select>
-            </span>
-          </span>
-        </div>
+        </span>
       </div>
       <section class="changeInfo">
         <div class="changeInfo-column changeMetadata">
@@ -254,34 +228,40 @@
               mutable="[[_loggedIn]]"
               on-show-reply-dialog="_handleShowReplyDialog">
           </gr-change-metadata>
-          <gr-change-actions id="actions"
-              change="[[_change]]"
-              actions="[[_change.actions]]"
-              change-num="[[_changeNum]]"
-              patch-num="[[_patchRange.patchNum]]"
-              commit-info="[[_commitInfo]]"
-              on-reload-change="_handleReloadChange"></gr-change-actions>
         </div>
-        <div class="changeInfo-column commitAndRelated">
-          <div class="commitMessage">
-            <h4>
-              Commit message
+        <div class="changeInfo-column mainChangeInfo">
+          <div class="commitActions" hidden$="[[!_loggedIn]]"">
+            <gr-button
+                class="reply"
+                secondary
+                on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
+            <gr-change-actions id="actions"
+                change="[[_change]]"
+                actions="[[_change.actions]]"
+                change-num="[[_changeNum]]"
+                patch-num="[[_patchRange.patchNum]]"
+                commit-info="[[_commitInfo]]"
+                on-reload-change="_handleReloadChange"></gr-change-actions>
+          </div>
+          <div class="commitAndRelated">
+            <div class="commitMessage">
+              <gr-editable-content id="commitMessageEditor"
+                  editing="[[_editingCommitMessage]]"
+                  content="{{_commitInfo.message}}">
+                <gr-linked-text pre
+                    content="[[_commitInfo.message]]"
+                    config="[[_projectConfig.commentlinks]]"></gr-linked-text>
+              </gr-editable-content>
               <gr-button link
+                  class="editCommitMessage"
                   on-tap="_handleEditCommitMessage"
                   hidden$="[[_hideEditCommitMessage]]">Edit</gr-button>
-            </h4>
-            <gr-editable-content id="commitMessageEditor"
-                editing="[[_editingCommitMessage]]"
-                content="{{_commitInfo.message}}">
-              <gr-linked-text pre
-                  content="[[_commitInfo.message]]"
-                  config="[[_projectConfig.commentlinks]]"></gr-linked-text>
-            </gr-editable-content>
-          </div>
-          <div class="relatedChanges">
-            <gr-related-changes-list id="relatedChanges"
-                change="[[_change]]"
-                patch-num="[[_patchRange.patchNum]]"></gr-related-changes-list>
+            </div>
+            <div class="relatedChanges">
+              <gr-related-changes-list id="relatedChanges"
+                  change="[[_change]]"
+                  patch-num="[[_patchRange.patchNum]]"></gr-related-changes-list>
+            </div>
           </div>
         </div>
       </section>
@@ -312,6 +292,7 @@
           on-close="_handleDownloadDialogClose"></gr-download-dialog>
     </gr-overlay>
     <gr-overlay id="replyOverlay"
+        no-cancel-on-outside-click
         on-iron-overlay-opened="_handleReplyOverlayOpen"
         with-backdrop>
       <gr-reply-dialog id="replyDialog"
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 14ac4d1..c7cb978 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
@@ -78,8 +78,6 @@
         value: false,
       },
       _loading: Boolean,
-      _headerContainerEl: Object,
-      _headerEl: Object,
       _projectConfig: Object,
       _replyButtonLabel: {
         type: String,
@@ -98,10 +96,6 @@
       '_paramsAndChangeChanged(params, _change)',
     ],
 
-    ready: function() {
-      this._headerEl = this.$$('.header');
-    },
-
     attached: function() {
       this._getLoggedIn().then(function(loggedIn) {
         this._loggedIn = loggedIn;
@@ -114,34 +108,6 @@
           this._handleCommitMessageSave.bind(this));
       this.addEventListener('editable-content-cancel',
           this._handleCommitMessageCancel.bind(this));
-      this.listen(window, 'scroll', '_handleBodyScroll');
-    },
-
-    detached: function() {
-      this.unlisten(window, 'scroll', '_handleBodyScroll');
-    },
-
-    _handleBodyScroll: function(e) {
-      var containerEl = this._headerContainerEl ||
-          this.$$('.headerContainer');
-
-      // Calculate where the header is relative to the window.
-      var top = containerEl.offsetTop;
-      for (var offsetParent = containerEl.offsetParent;
-           offsetParent;
-           offsetParent = offsetParent.offsetParent) {
-        top += offsetParent.offsetTop;
-      }
-      // The element may not be displayed yet, in which case do nothing.
-      if (top == 0) { return; }
-
-      this._headerEl.classList.toggle('pinned', window.scrollY >= top);
-    },
-
-    _resetHeaderEl: function() {
-      var el = this._headerEl || this.$$('.header');
-      this._headerEl = el;
-      el.classList.remove('pinned');
     },
 
     _handleEditCommitMessage: function(e) {
@@ -267,19 +233,7 @@
     },
 
     _handlePatchChange: function(e) {
-      var patchNum = e.target.value;
-      var currentPatchNum;
-      if (this._change.current_revision) {
-        currentPatchNum =
-            this._change.revisions[this._change.current_revision]._number;
-      } else {
-        currentPatchNum = this._computeLatestPatchNum(this._allPatchSets);
-      }
-      if (patchNum == currentPatchNum) {
-        page.show(this.changePath(this._changeNum));
-        return;
-      }
-      page.show(this.changePath(this._changeNum) + '/' + patchNum);
+      this._changePatchNum(parseInt(e.target.value, 10));
     },
 
     _handleReplyTap: function(e) {
@@ -339,9 +293,6 @@
       };
 
       this._reload().then(function() {
-        this.$.messageList.topMargin = this._headerEl.offsetHeight;
-        this.$.fileList.topMargin = this._headerEl.offsetHeight;
-
         // Allow the message list to render before scrolling.
         this.async(function() {
           this._maybeScrollToMessage();
@@ -405,6 +356,25 @@
       this.fire('title-change', {title: title});
     },
 
+    /**
+     * Change active patch to the provided patch num.
+     * @param {int} patchNum the patchn number to be viewed.
+     */
+    _changePatchNum: function(patchNum) {
+      var currentPatchNum;
+      if (this._change.current_revision) {
+        currentPatchNum =
+            this._change.revisions[this._change.current_revision]._number;
+      } else {
+        currentPatchNum = this._computeLatestPatchNum(this._allPatchSets);
+      }
+      if (patchNum === currentPatchNum) {
+        page.show(this.changePath(this._changeNum));
+        return;
+      }
+      page.show(this.changePath(this._changeNum) + '/' + patchNum);
+    },
+
     _computeChangePermalink: function(changeNum) {
       return '/' + changeNum;
     },
@@ -477,11 +447,6 @@
       return result;
     },
 
-    _computeReplyButtonHighlighted: function(changeRecord) {
-      var drafts = (changeRecord && changeRecord.base) || {};
-      return Object.keys(drafts).length > 0;
-    },
-
     _computeReplyButtonLabel: function(changeRecord) {
       var drafts = (changeRecord && changeRecord.base) || {};
       var draftCount = Object.keys(drafts).reduce(function(count, file) {
@@ -495,9 +460,17 @@
       return label;
     },
 
+    _switchToMostRecentPatchNum: function() {
+      this._getChangeDetail().then(function() {
+        var patchNum = this._allPatchSets[this._allPatchSets.length - 1];
+        if (patchNum !== this._patchRange.patchNum) {
+          this._changePatchNum(patchNum);
+        }
+      }.bind(this));
+    },
+
     _handleKey: function(e) {
       if (this.shouldSupressKeyboardShortcut(e)) { return; }
-
       switch (e.keyCode) {
         case 65:  // 'a'
           if (this._loggedIn && !e.shiftKey) {
@@ -505,6 +478,12 @@
             this._openReplyDialog();
           }
           break;
+        case 82: // 'r'
+          if (e.shiftKey) {
+            e.preventDefault();
+            this._switchToMostRecentPatchNum();
+          }
+          break;
         case 85:  // 'u'
           e.preventDefault();
           page.show('/');
@@ -619,8 +598,6 @@
         ]);
       }.bind(this);
 
-      this._resetHeaderEl();
-
       if (this._patchRange.patchNum) {
         return reloadPatchNumDependentResources().then(function() {
           return detailCompletes;
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 c9a687b..2200a63 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
@@ -43,44 +43,89 @@
       element = fixture('basic');
     });
 
-    test('keyboard shortcuts', function() {
-      var showStub = sinon.stub(page, 'show');
+    suite('keyboard shortcuts', function() {
+      test('U should navigate to /', function() {
+        var showStub = sinon.stub(page, 'show');
+        MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'U'
+        assert(showStub.lastCall.calledWithExactly('/'));
+        showStub.restore();
+      });
 
-      MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'U'
-      assert(showStub.lastCall.calledWithExactly('/'),
-          'Should navigate to /');
-      showStub.restore();
+      test('A should toggle overlay', function() {
+        MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
+        var overlayEl = element.$.replyOverlay;
+        assert.isFalse(overlayEl.opened);
+        element._loggedIn = true;
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
-      var overlayEl = element.$.replyOverlay;
-      assert.isFalse(overlayEl.opened);
-      element._loggedIn = true;
+        MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'A'
+        assert.isFalse(overlayEl.opened);
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'A'
-      assert.isFalse(overlayEl.opened);
+        MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
+        assert.isTrue(overlayEl.opened);
+        overlayEl.close();
+        assert.isFalse(overlayEl.opened);
+      });
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
-      assert.isTrue(overlayEl.opened);
-      overlayEl.close();
-      assert.isFalse(overlayEl.opened);
+      test('shift + R should fetch and navigate to the latest patch set',
+          function(done) {
+        element._changeNum = '42';
+        element._patchRange = {
+          basePatchNum: 'PARENT',
+          patchNum: 1,
+        };
+        element._change = {
+          change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+          revisions: {
+            rev1: {_number: 1},
+          },
+          current_revision: 'rev1',
+          status: 'NEW',
+          labels: {},
+          actions: {},
+        };
+
+        sinon.stub(element.$.restAPI, '_getChangeDetail', function() {
+          // Mock change obj.
+          return Promise.resolve({
+            change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+            revisions: {
+              rev1: {_number: 1},
+              rev13: {_number: 13},
+            },
+            current_revision: 'rev1',
+            status: 'NEW',
+            labels: {},
+            actions: {},
+          });
+        });
+
+        var showStub = sinon.stub(page, 'show', function(arg) {
+          assert.equal(arg, '/c/42/13');
+          showStub.restore();
+          element.$.restAPI._getChangeDetail.restore();
+          done();
+        });
+
+        // 'shift + R'
+        MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift');
+      });
     });
 
-    test('reply button is highlighted when there are drafts', function() {
+    test('reply button has updated count when there are drafts', function() {
       var replyButton = element.$$('gr-button.reply');
       assert.ok(replyButton);
-      assert.isFalse(replyButton.hasAttribute('primary'));
+      assert.equal(replyButton.textContent, 'Reply');
 
       element._diffDrafts = null;
-      assert.isFalse(replyButton.hasAttribute('primary'));
+      assert.equal(replyButton.textContent, 'Reply');
 
       element._diffDrafts = {};
-      assert.isFalse(replyButton.hasAttribute('primary'));
+      assert.equal(replyButton.textContent, 'Reply');
 
       element._diffDrafts = {
         'file1.txt': [{}],
         'file2.txt': [{}, {}],
       };
-      assert.isTrue(replyButton.hasAttribute('primary'));
       assert.equal(replyButton.textContent, 'Reply (3)');
     });
 
@@ -306,14 +351,15 @@
           true, false, changeRecord, '2'));
     });
 
-    test('topic is coalesced to null', function() {
+    test('topic is coalesced to null', function(done) {
       sinon.stub(element, '_changeChanged');
-      sinon.stub(element.$.restAPI, 'getChangeDetail', function(num) {
+      sinon.stub(element.$.restAPI, 'getChangeDetail', function() {
         return Promise.resolve({id: '123456789', labels: {}});
       });
 
       element._getChangeDetail().then(function() {
         assert.isNull(element._change.topic);
+        done();
       });
     });
 
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 ef5ceed..201671d 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
@@ -181,9 +181,7 @@
     </template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
-    <gr-diff-cursor
-        id="cursor"
-        fold-offset-top="[[topMargin]]"></gr-diff-cursor>
+    <gr-diff-cursor id="cursor"></gr-diff-cursor>
   </template>
   <script src="gr-file-list.js"></script>
 </dom-module>
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 225d8b3..7bbeab1 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
@@ -27,7 +27,6 @@
       drafts: Object,
       revisions: Object,
       projectConfig: Object,
-      topMargin: Number,
       selectedIndex: {
         type: Number,
         notify: true,
@@ -352,7 +351,7 @@
       }
 
       // Don't scroll if it's already in view.
-      if (top > window.pageYOffset + this.topMargin &&
+      if (top > window.pageYOffset &&
           top < window.pageYOffset + window.innerHeight - el.clientHeight) {
         return;
       }
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index e7a0573..875bf2b 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -29,7 +29,6 @@
       },
       comments: Object,
       projectConfig: Object,
-      topMargin: Number,
       showReplyButtons: {
         type: Boolean,
         value: false,
@@ -52,7 +51,7 @@
            offsetParent = offsetParent.offsetParent) {
         top += offsetParent.offsetTop;
       }
-      window.scrollTo(0, top - this.topMargin);
+      window.scrollTo(0, top);
       this._highlightEl(el);
     },
 
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 930c8cf..916726fd 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
@@ -45,34 +45,38 @@
       .links {
         margin-left: 1em;
       }
-      .links ul {
+      .links .menuContainer {
         display: none;
       }
       .links > li {
         cursor: default;
         display: inline-block;
         margin-left: 1em;
-        padding: .4em 0;
+        padding: .5em 0;
         position: relative;
       }
-      .links li:hover ul {
+      .links li:hover .menuContainer,
+      .links li:active .menuContainer {
         background-color: #fff;
+        border-radius: 3px;
         box-shadow: 0 1px 1px rgba(0, 0, 0, .3);
         display: block;
-        left: -.75em;
+        left: -.5em;
+        padding: .5em 0;
         position: absolute;
-        top: 2em;
+        top: 2.45em;
         z-index: 1000;
       }
       .links li ul li a:link,
       .links li ul li a:visited {
         color: #00e;
         display: block;
-        padding: .5em .75em;
+        padding: .3em 1em;
         text-decoration: none;
         white-space: nowrap;
       }
-      .links li ul li:hover a {
+      .links li ul li:hover a,
+      .links li ul li:active a {
         background-color: var(--selection-background-color);
       }
       .linksTitle {
@@ -87,7 +91,8 @@
         height: 0;
         position: absolute;
         right: 0;
-        top: calc(50% - .1em);
+        top: calc(50% - .05em);
+        transition: border-top-color 200ms;
         width: 0;
       }
       .links li:hover .downArrow {
@@ -137,11 +142,13 @@
             <span class="linksTitle">
               [[linkGroup.title]] <i class="downArrow"></i>
             </span>
-            <ul>
-              <template is="dom-repeat" items="[[linkGroup.links]]" as="link">
-                <li><a href$="[[link.url]]">[[link.name]]</a></li>
-              </template>
-            </ul>
+            <div class="menuContainer">
+              <ul>
+                <template is="dom-repeat" items="[[linkGroup.links]]" as="link">
+                  <li><a href$="[[link.url]]">[[link.name]]</a></li>
+                </template>
+              </ul>
+            </div>
           </li>
         </template>
       </ul>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index fecb376..2b05d99 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -18,6 +18,8 @@
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
 
 <dom-module id="gr-search-bar">
   <template>
@@ -53,6 +55,7 @@
           multi
           borderless></gr-autocomplete>
       <gr-button id="searchButton">Search</gr-button>
+      <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     </form>
   </template>
   <script src="gr-search-bar.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 8e52f8f..e132f44 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -78,6 +78,10 @@
     'tr',
   ];
 
+  var MAX_AUTOCOMPLETE_RESULTS = 10;
+
+  var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+
   Polymer({
     is: 'gr-search-bar',
 
@@ -124,34 +128,144 @@
       page.show('/q/' + encodeURIComponent(encodeURIComponent(this._inputVal)));
     },
 
-    // TODO(kaspern): Flesh this out better.
-    _makeSuggestion: function(str) {
-      return {
-        name: str,
-        value: str,
-      };
+    /**
+     * Fetch from the API the predicted accounts.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'owner'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'kasp'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchAccounts: function(predicate, expression) {
+      if (expression.length === 0) { return Promise.resolve([]); }
+      return this.$.restAPI.getSuggestedAccounts(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(function(accounts) {
+            if (!accounts) { return []; }
+            return accounts.map(function(acct) {
+              return predicate + ':"' + acct.name + ' <' + acct.email + '>"';
+            });
+      });
     },
 
-    // TODO(kaspern): Expand support for more complicated autocomplete features.
+    /**
+     * Fetch from the API the predicted groups.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'ownerin'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'polyger'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchGroups: function(predicate, expression) {
+      if (expression.length === 0) { return Promise.resolve([]); }
+      return this.$.restAPI.getSuggestedGroups(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(function(groups) {
+            if (!groups) { return []; }
+            var keys = Object.keys(groups);
+            return keys.map(function(key) { return predicate + ':' + key; });
+          });
+    },
+
+    /**
+     * Fetch from the API the predicted projects.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'project'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'gerr'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchProjects: function(predicate, expression) {
+      return this.$.restAPI.getSuggestedProjects(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(function(projects) {
+            if (!projects) { return []; }
+            var keys = Object.keys(projects);
+            return keys.map(function(key) { return predicate + ':' + key; });
+          });
+    },
+
+    /**
+     * Determine what array of possible suggestions should be provided
+     *     to _getSearchSuggestions.
+     * @param {string} input - The full search term, in lowercase.
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchSuggestions: function(input) {
+      // Split the input on colon to get a two part predicate/expression.
+      var splitInput = input.split(':');
+      var predicate = splitInput[0];
+      var expression = splitInput[1] || '';
+      // Switch on the predicate to determine what to autocomplete.
+      switch (predicate) {
+        case 'ownerin':
+        case 'reviewerin':
+          // Fetch groups.
+          return this._fetchGroups(predicate, expression);
+
+        case 'parentproject':
+        case 'project':
+          // Fetch projects.
+          return this._fetchProjects(predicate, expression);
+
+        case 'author':
+        case 'commentby':
+        case 'committer':
+        case 'from':
+        case 'owner':
+        case 'reviewedby':
+        case 'reviewer':
+          // Fetch accounts.
+          return this._fetchAccounts(predicate, expression);
+
+        default:
+          return Promise.resolve(SEARCH_OPERATORS
+              .filter(function(operator) {
+                return operator.indexOf(input) !== -1;
+              }));
+      }
+    },
+
+    /**
+     * Get the sorted, pruned list of suggestions for the current search query.
+     * @param {string} input - The complete search query.
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
     _getSearchSuggestions: function(input) {
-      return Promise.resolve(SEARCH_OPERATORS).then(function(operators) {
-        if (!operators) { return []; }
-        var lowerCaseInput = input
-            .substring(input.lastIndexOf(' ') + 1)
-            .toLowerCase();
-        return operators
-            .filter(function(operator) {
-              // Disallow autocomplete values that exactly match the whole str.
-              var opContainsInput = operator.indexOf(lowerCaseInput) !== -1;
-              var inputContainsOp = lowerCaseInput.indexOf(operator) !== -1;
-              return opContainsInput && !inputContainsOp;
-            })
-            // Prioritize results that start with the input.
-            .sort(function(operator) {
-              return operator.indexOf(lowerCaseInput);
-            })
-            .map(this._makeSuggestion);
-      }.bind(this));
+      // Allow spaces within quoted terms.
+      var tokens = input.match(TOKENIZE_REGEX);
+      var trimmedInput = tokens[tokens.length - 1].toLowerCase();
+
+      return this._fetchSuggestions(trimmedInput)
+          .then(function(operators) {
+            if (!operators) { return []; }
+            return operators
+                // Disallow autocomplete values that exactly match the str.
+                .filter(function(operator) {
+                  return input.indexOf(operator.toLowerCase()) == -1;
+                })
+                // Prioritize results that start with the input.
+                .sort(function(operator) {
+                  return operator.indexOf(trimmedInput);
+                })
+                // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results.
+                .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1)
+                // Map to an object to play nice with gr-autocomplete.
+                .map(function(operator) {
+                  return {
+                    name: operator,
+                    value: operator,
+                  };
+                });
+          });
     },
 
     _handleKey: function(e) {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index 0c16774..60e29b2 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -79,22 +79,82 @@
       showStub.restore();
     });
 
-    test('_getSearchSuggestions returns proper set of suggestions',
-        function(done) {
-      element._getSearchSuggestions('is:o')
-          .then(function(suggestions) {
-            assert.equal(suggestions[0].name, 'is:open');
-            assert.equal(suggestions[0].value, 'is:open');
-            assert.equal(suggestions[1].name, 'is:owner');
-            assert.equal(suggestions[1].value, 'is:owner');
-          })
-          .then(function() {
-            element._getSearchSuggestions('asdasdasdasd')
-                .then(function(suggestions) {
-                  assert.equal(suggestions.length, 0);
-                  done();
-                });
+    suite('_getSearchSuggestions',
+        function() {
+      setup(function() {
+        sinon.stub(element.$.restAPI, 'getSuggestedAccounts', function() {
+          return Promise.resolve([
+            {
+              name: 'fred',
+              email: 'fred@goog.co',
+            },
+          ]);
+        });
+        sinon.stub(element.$.restAPI, 'getSuggestedGroups', function() {
+          return Promise.resolve({
+            Polygerrit: 0,
           });
+        });
+        sinon.stub(element.$.restAPI, 'getSuggestedProjects', function() {
+          return Promise.resolve({
+            Polygerrit: 0,
+          });
+        });
+      });
+
+      teardown(function() {
+        element.$.restAPI.getSuggestedAccounts.restore();
+        element.$.restAPI.getSuggestedGroups.restore();
+        element.$.restAPI.getSuggestedProjects.restore();
+      });
+
+      test('Autocompletes accounts',
+          function(done) {
+        return element._getSearchSuggestions('owner:fr')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].value, 'owner:"fred <fred@goog.co>"');
+              done();
+            });
+      });
+
+      test('Autocompletes groups',
+          function(done) {
+        return element._getSearchSuggestions('ownerin:pol')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].value, 'ownerin:Polygerrit');
+              done();
+            });
+      });
+
+      test('Autocompletes projects',
+          function(done) {
+        return element._getSearchSuggestions('project:pol')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].value, 'project:Polygerrit');
+              done();
+            });
+      });
+
+      test('Autocompletes simple searches',
+          function(done) {
+        return element._getSearchSuggestions('is:o')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].name, 'is:open');
+              assert.equal(suggestions[0].value, 'is:open');
+              assert.equal(suggestions[1].name, 'is:owner');
+              assert.equal(suggestions[1].value, 'is:owner');
+              done();
+            });
+      });
+
+      test('Does not autocomplete with no match',
+          function(done) {
+        return element._getSearchSuggestions('asdasdasdasd')
+            .then(function(suggestions) {
+              assert.equal(suggestions.length, 0);
+              done();
+            });
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
index 5a41709..491eded 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
@@ -23,7 +23,6 @@
         id="cursorManager"
         scroll="keep-visible"
         cursor-target-class="target-row"
-        fold-offset-top="[[foldOffsetTop]]"
         target="{{diffRow}}"></gr-cursor-manager>
   </template>
   <script src="gr-diff-cursor.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index 99a0b5c..dd11f2c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -54,11 +54,6 @@
         },
       },
 
-      foldOffsetTop: {
-        type: Number,
-        value: 0,
-      },
-
       /**
        * If set, the cursor will attempt to move to the line number (instead of
        * the first chunk) the next time the diff renders. It is set back to null
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 0fc6b07..6fc09ca0 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -14,6 +14,8 @@
 (function() {
   'use strict';
 
+  var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+
   Polymer({
     is: 'gr-autocomplete',
 
@@ -199,8 +201,10 @@
       var completed = suggestions[index].value;
       if (this.multi) {
         // Append the completed text to the end of the string.
-        var shortStr = this.text.substring(0, this.text.lastIndexOf(' ') + 1);
-        this.value = shortStr + completed;
+        // Allow spaces within quoted terms.
+        var tokens = this.text.match(TOKENIZE_REGEX);
+        tokens[tokens.length - 1] = completed;
+        this.value = tokens.join(' ');
       } else {
         this.value = completed;
       }
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 c815ffd..cc0da66 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -22,7 +22,7 @@
   <template strip-whitespace>
     <style>
       :host {
-        background-color: #fff;
+        background-color: #f5f5f5;
         border: 1px solid #d1d2d3;
         border-radius: 2px;
         box-sizing: border-box;
@@ -30,10 +30,10 @@
         cursor: pointer;
         display: inline-block;
         font-family: var(--font-family);
-        font-size: 13px;
+        font-size: 12px;
         font-weight: bold;
         outline-width: 0;
-        padding: .3em .65em;
+        padding: .4em .85em;
         position: relative;
         text-align: center;
         -moz-user-select: none;
@@ -44,10 +44,17 @@
       :host([hidden]) {
         display: none;
       }
+      :host([primary]),
+      :host([secondary]) {
+        color: #fff;
+      }
       :host([primary]) {
         background-color: #4d90fe;
         border-color: #3079ed;
-        color: #fff;
+      }
+      :host([secondary]) {
+        background-color: #d14836;
+        border-color: transparent;
       }
       :host([small]) {
         font-size: 12px;
@@ -74,25 +81,44 @@
       :host([loading][disabled]) {
         cursor: wait;
       }
-      :host(:focus),
-      :host(:hover) {
-        border-color: #666;
+      :host(:focus:not([primary]:not[secondary])),
+      :host(:hover:not([primary]:not[secondary])) {
+        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) {
-        border-color: #fff;
         box-shadow: 0 0 1px #00f;
       }
       :host([primary]:hover) {
+        background-color: #4d90fe;
         border-color: #00F;
       }
+      :host([primary]:active),
+      :host([secondary]:active) {
+        box-shadow: none;
+      }
       :host([primary]:active) {
         border-color: #0c2188;
-        box-shadow: none;
-        color: #fff;
+      }
+      :host([secondary]:focus) {
+        box-shadow: 0 0 1px #f00;
+      }
+      :host([secondary]:hover) {
+        background-color: #c53727;
+        border: 1px solid #b0281a;
+      }
+      :host([secondary]:active) {
+        border-color: #941c0c;
       }
       :host([primary][loading]),
       :host([primary][disabled]) {
@@ -100,6 +126,7 @@
         border-color: transparent;
         color: #fff;
       }
+
     </style>
     <content></content>
   </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 0d3ea3d..7b3bc23 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -63,15 +63,6 @@
         type: String,
         value: ScrollBehavior.NEVER,
       },
-
-      /**
-       * When using the 'keep-visible' scroll behavior, set an offset to the top
-       * of the window for what is considered above the upper fold.
-       */
-      foldOffsetTop: {
-        type: Number,
-        value: 0,
-      },
     },
 
     detached: function() {
@@ -214,7 +205,7 @@
       }
 
       if (this.scroll === ScrollBehavior.KEEP_VISIBLE &&
-          top > window.pageYOffset + this.foldOffsetTop &&
+          top > window.pageYOffset &&
           top < window.pageYOffset + window.innerHeight) { return; }
 
       // Scroll the element to the middle of the window. Dividing by a third
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 2f109c9..0b07217 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
@@ -467,8 +467,26 @@
       });
     },
 
-    getSuggestedProjects: function(inputVal, opt_errFn, opt_ctx) {
-      return this.fetchJSON('/projects/', opt_errFn, opt_ctx, {p: inputVal});
+    getSuggestedGroups: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+      return this.fetchJSON('/groups/', opt_errFn, opt_ctx, {
+        s: inputVal,
+        n: opt_n,
+      });
+    },
+
+    getSuggestedProjects: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+      return this.fetchJSON('/projects/', opt_errFn, opt_ctx, {
+        p: inputVal,
+        n: opt_n,
+      });
+    },
+
+    getSuggestedAccounts: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+      return this.fetchJSON('/accounts/', opt_errFn, opt_ctx, {
+        q: inputVal,
+        n: opt_n,
+        suggest: null,
+      });
     },
 
     addChangeReviewer: function(changeNum, reviewerID) {