Merge "Update label vote within reply-dialog when modified externally"
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 5483d85..34f39c8 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -84,6 +84,11 @@
 also redefine the text and behavior of the built in label types `Code-Review`
 and `Verified`.
 
+Optionally a +commentlink+ section can be added to define project-specific
+comment links. The +commentlink+ section has the same format as the
+link:config-gerrit.html#commentlink[+commentlink+ section in gerrit.config]
+which is used to define global comment links.
+
 [[project-section]]
 === Project section
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index bed3760..ccfc440 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2417,18 +2417,21 @@
 
 [source, java]
 ----
-import com.google.gerrit.server.change.ReviewerSuggestion;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 
 import java.util.Set;
 
 public class MyPlugin implements ReviewerSuggestion {
-  public Set<SuggestedReviewer> suggestReviewers(
-      Change.Id changeId, String query, Set<Account.Id> candidates){
-      Set<SuggestedReviewer> suggestions = new HashSet<>();
-      // Implement your ranking logic here
-      return suggestions;
+  public Set<SuggestedReviewer> suggestReviewers(Project.NameKey project,
+      @Nullable Change.Id changeId, @Nullable String query,
+      Set<Account.Id> candidates) {
+    Set<SuggestedReviewer> suggestions = new HashSet<>();
+    // Implement your ranking logic here
+    return suggestions;
   }
 }
 ----
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 4959ced..bd7f6e9 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -90,22 +90,49 @@
   java -jar buck-out/gen/gerrit/gerrit.war init -d ../gerrit_testsite
 ----
 
-Accept defaults by pressing Enter until 'init' completes, or add
-the '--batch' command line option to avoid them entirely.  It is
-recommended to change the listen addresses from '*' to 'localhost' to
-prevent outside connections from contacting the development instance.
+During initialization, make two changes to the default settings:
 
-The daemon will automatically start in the background and a web
-browser will launch to the start page, enabling login via OpenID.
+* Change the listen addresses from '*' to 'localhost' to prevent outside
+  connections from contacting the development instance; and
+* Change the auth type from 'OPENID' to 'DEVELOPMENT_BECOME_ANY_ACCOUNT' to
+  allow yourself to create and act as arbitrary test accounts on your
+  development instance.
 
-Shutdown the daemon after registering the administrator account
-through the web interface:
+Continue through init until it completes. The daemon will automatically start in
+the background and a web browser will launch to the start page. From here you
+can sign in as the account created during init, register additional accounts,
+create projects, and more.
+
+When you want to shut down the daemon, simply run:
 
 ----
   ../gerrit_testsite/bin/gerrit.sh stop
 ----
 
 
+[[localdev]]
+== Working with the Local Server
+
+If you need to create additional accounts on your development instance, click
+'become' in the upper right corner, select 'Switch User', and then register
+a new account.
+
+Use the `ssh` protocol to clone from and push to the local server. For
+example, to clone a repository that you've created through the admin
+interface, run:
+
+----
+git clone ssh://username@localhost:29418/projectname
+----
+
+Then you'll be able to create changes the same way users do, with
+
+----
+git push origin HEAD:refs/for/master
+----
+
+
+
 == Testing
 
 
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index 7a724f7..72fe717 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -70,8 +70,8 @@
 commands:
 
 ----
-  $ git fetch origin refs/meta/config:config
-  $ git checkout config
+  $ git fetch ssh://localhost:29418/project refs/meta/config
+  $ git checkout FETCH_HEAD
   $ git log project.config
 ----
 
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index ababc16..47030cf 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1382,7 +1382,8 @@
     "show_tabs": true,
     "show_whitespace_errors": true,
     "syntax_highlighting": true,
-    "tab_size": 8
+    "tab_size": 8,
+    "font_size": 12
   }
 ----
 
@@ -1413,7 +1414,8 @@
     "show_tabs": true,
     "show_whitespace_errors": true,
     "syntax_highlighting": true,
-    "tab_size": 8
+    "tab_size": 8,
+    "font_size": 12
   }
 ----
 
@@ -1437,7 +1439,8 @@
     "show_tabs": true,
     "show_whitespace_errors": true,
     "syntax_highlighting": true,
-    "tab_size": 8
+    "tab_size": 8,
+    "font_size": 12
   }
 ----
 
@@ -2209,6 +2212,8 @@
 If true the line numbers are hidden.
 |`tab_size`                    ||
 Number of spaces that should be used to display one tab.
+|`font_size`                    ||
+Default font size in pixels for change to be displayed in the diff view.
 |'hide_empty_pane'             |not set if `false`|
 Whether empty panes should be hidden. The left pane is empty when a
 file was added; the right pane is empty when a file was deleted.
@@ -2269,6 +2274,8 @@
 True if the line numbers should be hidden.
 |`tab_size`                    |optional|
 Number of spaces that should be used to display one tab.
+|`font_size`                   |optional|
+Default font size in pixels for change to be displayed in the diff view.
 |`line_wrapping`               |optional|
 Whether to enable line wrapping or not.
 |===========================================
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index ae02475..d8214c2 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -4644,6 +4644,8 @@
 while posting the review.
 NOTE: To apply different tags on on different votes/comments multiple
 invocations of the REST call are required.
+|`post_submit` |not set if `false`|
+If true, this vote was made after the change was submitted.
 |===========================
 
 [[assignee-input]]
@@ -4860,11 +4862,13 @@
 === CherryPickInput
 The `CherryPickInput` entity contains information for cherry-picking a change to a new branch.
 
-[options="header",cols="1,6"]
+[options="header",cols="1,^1,5"]
 |===========================
-|Field Name    |Description
-|`message`     |Commit message for the cherry-picked change
-|`destination` |Destination branch
+|Field Name         ||Description
+|`message`          ||Commit message for the cherry-picked change
+|`destination`      ||Destination branch
+|`parent`           |optional, defaults to 1|
+Number of the parent relative to which the cherry-pick should be considered.
 |===========================
 
 [[comment-info]]
diff --git a/WORKSPACE b/WORKSPACE
index 17fbfea..4487821 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -672,8 +672,8 @@
 
 maven_jar(
   name = 'truth',
-  artifact = 'com.google.truth:truth:0.28',
-  sha1 = '0a388c7877c845ff4b8e19689dda5ac9d34622c4',
+  artifact = 'com.google.truth:truth:0.30',
+  sha1 = '9d591b5a66eda81f0b88cf1c748ab8853d99b18b',
 )
 
 maven_jar(
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index c0d6f18..300eb74 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -22,7 +22,6 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -144,6 +143,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.regex.Pattern;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
index ceab04fe..2f1463d 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.base.Optional;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.reviewdb.client.Project;
@@ -50,6 +50,7 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
 import java.util.Properties;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -239,10 +240,7 @@
       throws IOException {
     RevCommit c = tr.getRevWalk().parseCommit(id);
     tr.getRevWalk().parseBody(c);
-    List<String> ids = c.getFooterLines(FooterConstants.CHANGE_ID);
-    if (ids.isEmpty()) {
-      return Optional.absent();
-    }
-    return Optional.of(ids.get(ids.size() - 1));
+    return Lists.reverse(c.getFooterLines(FooterConstants.CHANGE_ID)).stream()
+        .findFirst();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
index 9236176..bce9861 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
@@ -78,6 +78,7 @@
     // change all default values
     i.context *= -1;
     i.tabSize *= -1;
+    i.fontSize *= -1;
     i.lineLength *= -1;
     i.cursorBlinkRate = 500;
     i.theme = Theme.MIDNIGHT;
@@ -121,9 +122,11 @@
     DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
     int newLineLength = d.lineLength + 10;
     int newTabSize = d.tabSize * 2;
+    int newFontSize = d.fontSize - 2;
     DiffPreferencesInfo update = new DiffPreferencesInfo();
     update.lineLength = newLineLength;
     update.tabSize = newTabSize;
+    update.fontSize = newFontSize;
     gApi.config().server().setDefaultDiffPreferences(update);
 
     DiffPreferencesInfo o = gApi.accounts()
@@ -133,8 +136,9 @@
     // assert configured defaults
     assertThat(o.lineLength).isEqualTo(newLineLength);
     assertThat(o.tabSize).isEqualTo(newTabSize);
+    assertThat(o.fontSize).isEqualTo(newFontSize);
 
     // assert hard-coded defaults
-    assertPrefs(o, d, "lineLength", "tabSize");
+    assertPrefs(o, d, "lineLength", "tabSize", "fontSize");
   }
 }
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 3dd1ff3..dc829dc 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
@@ -406,15 +406,6 @@
   }
 
   @Test
-  public void voteOnClosedChange() throws Exception {
-    PushOneCommit.Result r = createChange();
-    merge(r);
-    exception.expect(ResourceConflictException.class);
-    exception.expectMessage("change is closed");
-    revision(r).review(ReviewInput.reject());
-  }
-
-  @Test
   public void rebaseUpToDateChange() throws Exception {
     PushOneCommit.Result r = createChange();
     exception.expect(ResourceConflictException.class);
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 7f5c6bc..f383daf 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
@@ -26,10 +26,14 @@
 import static org.junit.Assert.fail;
 
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.DraftApi;
@@ -39,25 +43,34 @@
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 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.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.change.GetRevisionActions;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
@@ -66,6 +79,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -127,6 +141,115 @@
   }
 
   @Test
+  public void postSubmitApproval() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = project.get() + "~master~" + r.getChangeId();
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .review(ReviewInput.recommend());
+
+    String label = "Code-Review";
+    ApprovalInfo approval = getApproval(changeId, label);
+    assertThat(approval.value).isEqualTo(1);
+    assertThat(approval.postSubmit).isNull();
+
+    // Submit by direct push.
+    git().push()
+        .setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master"))
+        .call();
+    assertThat(gApi.changes().id(changeId).get().status)
+        .isEqualTo(ChangeStatus.MERGED);
+
+    approval = getApproval(changeId, label);
+    assertThat(approval.value).isEqualTo(1);
+    assertThat(approval.postSubmit).isNull();
+
+    // Repeating the current label is allowed. Does not flip the postSubmit bit
+    // due to deduplication codepath.
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .review(ReviewInput.recommend());
+    approval = getApproval(changeId, label);
+    assertThat(approval.value).isEqualTo(1);
+    assertThat(approval.postSubmit).isNull();
+
+    // Reducing vote is not allowed.
+    try {
+      gApi.changes()
+          .id(changeId)
+          .current()
+          .review(ReviewInput.dislike());
+      fail("expected ResourceConflictException");
+    } catch (ResourceConflictException e) {
+      assertThat(e).hasMessage(
+          "Cannot reduce vote on labels for closed change: Code-Review");
+    }
+    approval = getApproval(changeId, label);
+    assertThat(approval.value).isEqualTo(1);
+    assertThat(approval.postSubmit).isNull();
+
+    // Increasing vote is allowed.
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .review(ReviewInput.approve());
+    approval = getApproval(changeId, label);
+    assertThat(approval.value).isEqualTo(2);
+    assertThat(approval.postSubmit).isTrue();
+
+    // Decreasing to previous post-submit vote is still not allowed.
+    try {
+      gApi.changes()
+          .id(changeId)
+          .current()
+          .review(ReviewInput.dislike());
+      fail("expected ResourceConflictException");
+    } catch (ResourceConflictException e) {
+      assertThat(e).hasMessage(
+          "Cannot reduce vote on labels for closed change: Code-Review");
+    }
+    approval = getApproval(changeId, label);
+    assertThat(approval.value).isEqualTo(2);
+    assertThat(approval.postSubmit).isTrue();
+  }
+
+  @TestProjectInput(submitType = SubmitType.CHERRY_PICK)
+  @Test
+  public void approvalCopiedDuringSubmitIsNotPostSubmit() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getChange().getId();
+    gApi.changes()
+        .id(id.get())
+        .current()
+        .review(ReviewInput.approve());
+    gApi.changes()
+        .id(id.get())
+        .current()
+        .submit();
+
+    ChangeData cd = r.getChange();
+    assertThat(cd.patchSets()).hasSize(2);
+    PatchSetApproval psa = Iterators.getOnlyElement(
+        cd.currentApprovals().stream()
+            .filter(a -> !a.isLegacySubmit()).iterator());
+    assertThat(psa.getPatchSetId().get()).isEqualTo(2);
+    assertThat(psa.getLabel()).isEqualTo("Code-Review");
+    assertThat(psa.getValue()).isEqualTo(2);
+    assertThat(psa.isPostSubmit()).isFalse();
+  }
+
+  @Test
+  public void voteOnAbandonedChange() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).abandon();
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("change is closed");
+    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.reject());
+  }
+
+  @Test
   public void deleteDraft() throws Exception {
     PushOneCommit.Result r = createDraft();
     gApi.changes()
@@ -357,6 +480,111 @@
   }
 
   @Test
+  public void cherryPickMergeRelativeToDefaultParent() throws Exception {
+    String parent1FileName = "a.txt";
+    String parent2FileName = "b.txt";
+    PushOneCommit.Result mergeChangeResult =
+        createCherryPickableMerge(parent1FileName, parent2FileName);
+
+    String cherryPickBranchName = "branch_for_cherry_pick";
+    createBranch(new Branch.NameKey(project, cherryPickBranchName));
+
+    CherryPickInput cherryPickInput = new CherryPickInput();
+    cherryPickInput.destination = cherryPickBranchName;
+    cherryPickInput.message = "Cherry-pick a merge commit to another branch";
+
+    ChangeInfo cherryPickedChangeInfo = gApi.changes()
+        .id(mergeChangeResult.getChangeId())
+        .current()
+        .cherryPick(cherryPickInput)
+        .get();
+
+    Map<String, FileInfo> cherryPickedFilesByName =
+        cherryPickedChangeInfo.revisions
+            .get(cherryPickedChangeInfo.currentRevision)
+            .files;
+    assertThat(cherryPickedFilesByName).containsKey(parent2FileName);
+    assertThat(cherryPickedFilesByName).doesNotContainKey(parent1FileName);
+  }
+
+  @Test
+  public void cherryPickMergeRelativeToSpecificParent() throws Exception {
+    String parent1FileName = "a.txt";
+    String parent2FileName = "b.txt";
+    PushOneCommit.Result mergeChangeResult =
+        createCherryPickableMerge(parent1FileName, parent2FileName);
+
+    String cherryPickBranchName = "branch_for_cherry_pick";
+    createBranch(new Branch.NameKey(project, cherryPickBranchName));
+
+    CherryPickInput cherryPickInput = new CherryPickInput();
+    cherryPickInput.destination = cherryPickBranchName;
+    cherryPickInput.message = "Cherry-pick a merge commit to another branch";
+    cherryPickInput.parent = 2;
+
+    ChangeInfo cherryPickedChangeInfo = gApi.changes()
+        .id(mergeChangeResult.getChangeId())
+        .current()
+        .cherryPick(cherryPickInput)
+        .get();
+
+    Map<String, FileInfo> cherryPickedFilesByName =
+        cherryPickedChangeInfo.revisions
+            .get(cherryPickedChangeInfo.currentRevision)
+            .files;
+    assertThat(cherryPickedFilesByName).containsKey(parent1FileName);
+    assertThat(cherryPickedFilesByName).doesNotContainKey(parent2FileName);
+  }
+
+  @Test
+  public void cherryPickMergeUsingInvalidParent() throws Exception {
+    String parent1FileName = "a.txt";
+    String parent2FileName = "b.txt";
+    PushOneCommit.Result mergeChangeResult =
+        createCherryPickableMerge(parent1FileName, parent2FileName);
+
+    String cherryPickBranchName = "branch_for_cherry_pick";
+    createBranch(new Branch.NameKey(project, cherryPickBranchName));
+
+    CherryPickInput cherryPickInput = new CherryPickInput();
+    cherryPickInput.destination = cherryPickBranchName;
+    cherryPickInput.message = "Cherry-pick a merge commit to another branch";
+    cherryPickInput.parent = 0;
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("Cherry Pick: Parent 0 does not exist. Please"
+        + " specify a parent in range [1, 2].");
+    gApi.changes()
+        .id(mergeChangeResult.getChangeId())
+        .current()
+        .cherryPick(cherryPickInput);
+  }
+
+  @Test
+  public void cherryPickMergeUsingNonExistentParent() throws Exception {
+    String parent1FileName = "a.txt";
+    String parent2FileName = "b.txt";
+    PushOneCommit.Result mergeChangeResult =
+        createCherryPickableMerge(parent1FileName, parent2FileName);
+
+    String cherryPickBranchName = "branch_for_cherry_pick";
+    createBranch(new Branch.NameKey(project, cherryPickBranchName));
+
+    CherryPickInput cherryPickInput = new CherryPickInput();
+    cherryPickInput.destination = cherryPickBranchName;
+    cherryPickInput.message = "Cherry-pick a merge commit to another branch";
+    cherryPickInput.parent = 3;
+
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("Cherry Pick: Parent 3 does not exist. Please"
+        + " specify a parent in range [1, 2].");
+    gApi.changes()
+        .id(mergeChangeResult.getChangeId())
+        .current()
+        .cherryPick(cherryPickInput);
+  }
+
+  @Test
   public void canRebase() throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
@@ -806,4 +1034,49 @@
     assertDiffForNewFile(diff, pushResult.getCommit(), path,
         expectedContentSideB);
   }
+
+  private PushOneCommit.Result createCherryPickableMerge(String parent1FileName,
+      String parent2FileName) throws Exception {
+    RevCommit initialCommit = getHead(repo());
+
+    String branchAName = "branchA";
+    createBranch(new Branch.NameKey(project, branchAName));
+    String branchBName = "branchB";
+    createBranch(new Branch.NameKey(project, branchBName));
+
+    PushOneCommit.Result changeAResult = pushFactory
+        .create(db, admin.getIdent(), testRepo, "change a",
+            parent1FileName, "Content of a")
+        .to("refs/for/" + branchAName);
+
+    testRepo.reset(initialCommit);
+    PushOneCommit.Result changeBResult = pushFactory
+        .create(db, admin.getIdent(), testRepo, "change b",
+            parent2FileName, "Content of b")
+        .to("refs/for/" + branchBName);
+
+    PushOneCommit pushableMergeCommit = pushFactory.create(db, admin.getIdent(),
+        testRepo, "merge", ImmutableMap.of(parent1FileName, "Content of a",
+            parent2FileName, "Content of b"));
+    pushableMergeCommit.setParents(ImmutableList.of(changeAResult.getCommit(),
+        changeBResult.getCommit()));
+    PushOneCommit.Result mergeChangeResult =
+        pushableMergeCommit.to("refs/for/" + branchAName);
+    mergeChangeResult.assertOkStatus();
+    return mergeChangeResult;
+  }
+
+  private ApprovalInfo getApproval(String changeId, String label)
+      throws Exception {
+    ChangeInfo info = gApi.changes()
+        .id(changeId)
+        .get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+    LabelInfo li = info.labels.get(label);
+    assertThat(li).isNotNull();
+    int accountId = atrScope.get().getUser().getAccountId().get();
+    return li.all.stream()
+        .filter(a -> a._accountId == accountId)
+        .findFirst()
+        .get();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index c611f2c..7830b17e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -20,7 +20,6 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
@@ -87,6 +86,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 public class ChangeEditIT extends AbstractDaemonTest {
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
similarity index 62%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index fd2385b..521ccc4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
@@ -34,18 +35,25 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.ReceiveCommitsAdvertiseRefsHook;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testutil.DisabledReviewDb;
+import com.google.gerrit.testutil.TestChanges;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
@@ -53,11 +61,12 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
 @NoHttpd
-public class VisibleRefFilterIT extends AbstractDaemonTest {
+public class RefAdvertisementIT extends AbstractDaemonTest {
   @Inject
   private ChangeEditModifier editModifier;
 
@@ -74,12 +83,23 @@
   @Inject
   private Provider<CurrentUser> userProvider;
 
+  @Inject
+  private ChangeNoteUtil noteUtil;
+
+  @Inject
+  @AnonymousCowardName
+  private String anonymousCowardName;
+
   private AccountGroup.UUID admins;
 
-  private Change.Id c1;
-  private Change.Id c2;
+  private ChangeData c1;
+  private ChangeData c2;
+  private ChangeData c3;
+  private ChangeData c4;
   private String r1;
   private String r2;
+  private String r3;
+  private String r4;
 
   @Before
   public void setUp() throws Exception {
@@ -111,17 +131,31 @@
         .branch("branch")
         .create(new BranchInput());
 
+    // First 2 changes are merged, which means the tags pointing to them are
+    // visible.
     allow(Permission.SUBMIT, admins, "refs/for/refs/heads/*");
     PushOneCommit.Result mr = pushFactory.create(db, admin.getIdent(), testRepo)
         .to("refs/for/master%submit");
     mr.assertOkStatus();
-    c1 = mr.getChange().getId();
-    r1 = changeRefPrefix(c1);
+    c1 = mr.getChange();
+    r1 = changeRefPrefix(c1.getId());
     PushOneCommit.Result br = pushFactory.create(db, admin.getIdent(), testRepo)
         .to("refs/for/branch%submit");
     br.assertOkStatus();
-    c2 = br.getChange().getId();
-    r2 = changeRefPrefix(c2);
+    c2 = br.getChange();
+    r2 = changeRefPrefix(c2.getId());
+
+    // Second 2 changes are unmerged.
+    mr = pushFactory.create(db, admin.getIdent(), testRepo)
+        .to("refs/for/master");
+    mr.assertOkStatus();
+    c3 = mr.getChange();
+    r3 = changeRefPrefix(c3.getId());
+    br = pushFactory.create(db, admin.getIdent(), testRepo)
+        .to("refs/for/branch");
+    br.assertOkStatus();
+    c4 = br.getChange();
+    r4 = changeRefPrefix(c4.getId());
 
     try (Repository repo = repoManager.openRepository(project)) {
       // master-tag -> master
@@ -139,7 +173,7 @@
   }
 
   @Test
-  public void allRefsVisibleNoRefsMetaConfig() throws Exception {
+  public void uploadPackAllRefsVisibleNoRefsMetaConfig() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
     Util.allow(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
     Util.allow(cfg, Permission.READ, admins, RefNames.REFS_CONFIG);
@@ -147,12 +181,16 @@
     saveProjectConfig(project, cfg);
 
     setApiUser(user);
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
         r2 + "1",
         r2 + "meta",
+        r3 + "1",
+        r3 + "meta",
+        r4 + "1",
+        r4 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
         "refs/tags/branch-tag",
@@ -160,16 +198,20 @@
   }
 
   @Test
-  public void allRefsVisibleWithRefsMetaConfig() throws Exception {
+  public void uploadPackAllRefsVisibleWithRefsMetaConfig() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/*");
     allow(Permission.READ, REGISTERED_USERS, RefNames.REFS_CONFIG);
 
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
         r2 + "1",
         r2 + "meta",
+        r3 + "1",
+        r3 + "meta",
+        r4 + "1",
+        r4 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
         RefNames.REFS_CONFIG,
@@ -178,28 +220,32 @@
   }
 
   @Test
-  public void subsetOfBranchesVisibleIncludingHead() throws Exception {
+  public void uploadPackSubsetOfBranchesVisibleIncludingHead() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
     setApiUser(user);
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
+        r3 + "1",
+        r3 + "meta",
         "refs/heads/master",
         "refs/tags/master-tag");
   }
 
   @Test
-  public void subsetOfBranchesVisibleNotIncludingHead() throws Exception {
+  public void uploadPackSubsetOfBranchesVisibleNotIncludingHead() throws Exception {
     deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
     setApiUser(user);
-    assertRefs(
+    assertUploadPackRefs(
         r2 + "1",
         r2 + "meta",
+        r4 + "1",
+        r4 + "meta",
         "refs/heads/branch",
         "refs/tags/branch-tag",
         // master branch is not visible but master-tag is reachable from branch
@@ -208,12 +254,12 @@
   }
 
   @Test
-  public void subsetOfBranchesVisibleWithEdit() throws Exception {
+  public void uploadPackSubsetOfBranchesVisibleWithEdit() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
     deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
-    Change c = notesFactory.createChecked(db, project, c1).getChange();
-    PatchSet ps1 = getPatchSet(new PatchSet.Id(c1, 1));
+    Change c = notesFactory.createChecked(db, project, c1.getId()).getChange();
+    PatchSet ps1 = getPatchSet(new PatchSet.Id(c1.getId(), 1));
 
     // Admin's edit is not visible.
     setApiUser(admin);
@@ -223,59 +269,64 @@
     setApiUser(user);
     editModifier.createEdit(c, ps1);
 
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
+        r3 + "1",
+        r3 + "meta",
         "refs/heads/master",
         "refs/tags/master-tag",
-        "refs/users/01/1000001/edit-" + c1.get() + "/1");
+        "refs/users/01/1000001/edit-" + c1.getId() + "/1");
   }
 
   @Test
-  public void subsetOfRefsVisibleWithAccessDatabase() throws Exception {
+  public void uploadPackSubsetOfRefsVisibleWithAccessDatabase() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     try {
       deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
       allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
 
-      Change c = notesFactory.createChecked(db, project, c1).getChange();
-      PatchSet ps1 = getPatchSet(new PatchSet.Id(c1, 1));
+      PatchSet ps1 = getPatchSet(new PatchSet.Id(c1.getId(), 1));
       setApiUser(admin);
-      editModifier.createEdit(c, ps1);
+      editModifier.createEdit(c1.change(), ps1);
       setApiUser(user);
 
-      assertRefs(
+      assertUploadPackRefs(
           // Change 1 is visible due to accessDatabase capability, even though
           // refs/heads/master is not.
           r1 + "1",
           r1 + "meta",
           r2 + "1",
           r2 + "meta",
+          r3 + "1",
+          r3 + "meta",
+          r4 + "1",
+          r4 + "meta",
           "refs/heads/branch",
           "refs/tags/branch-tag",
           // See comment in subsetOfBranchesVisibleNotIncludingHead.
           "refs/tags/master-tag",
           // All edits are visible due to accessDatabase capability.
-          "refs/users/00/1000000/edit-" + c1.get() + "/1");
+          "refs/users/00/1000000/edit-" + c1.getId() + "/1");
     } finally {
       removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     }
   }
 
   @Test
-  public void draftRefs() throws Exception {
+  public void uploadPackDraftRefs() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
 
     PushOneCommit.Result br = pushFactory.create(db, admin.getIdent(), testRepo)
         .to("refs/drafts/master");
     br.assertOkStatus();
-    Change.Id c3 = br.getChange().getId();
-    String r3 = changeRefPrefix(c3);
+    Change.Id c5 = br.getChange().getId();
+    String r5 = changeRefPrefix(c5);
 
-    // Only admin can see admin's draft change.
+    // Only admin can see admin's draft change (5).
     setApiUser(admin);
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
@@ -283,6 +334,10 @@
         r2 + "meta",
         r3 + "1",
         r3 + "meta",
+        r4 + "1",
+        r4 + "meta",
+        r5 + "1",
+        r5 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
         RefNames.REFS_CONFIG,
@@ -291,12 +346,16 @@
 
     // user can't.
     setApiUser(user);
-    assertRefs(
+    assertUploadPackRefs(
         "HEAD",
         r1 + "1",
         r1 + "meta",
         r2 + "1",
         r2 + "meta",
+        r3 + "1",
+        r3 + "meta",
+        r4 + "1",
+        r4 + "meta",
         "refs/heads/branch",
         "refs/heads/master",
         "refs/tags/branch-tag",
@@ -304,7 +363,7 @@
   }
 
   @Test
-  public void noSearchingChangeCacheImpl() throws Exception {
+  public void uploadPackNoSearchingChangeCacheImpl() throws Exception {
     allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
 
     setApiUser(user);
@@ -320,6 +379,10 @@
           r1 + "meta",
           r2 + "1",
           r2 + "meta",
+          r3 + "1",
+          r3 + "meta",
+          r4 + "1",
+          r4 + "meta",
           "refs/heads/branch",
           "refs/heads/master",
           "refs/tags/branch-tag",
@@ -328,7 +391,7 @@
   }
 
   @Test
-  public void sequencesWithAccessDatabase() throws Exception {
+  public void uploadPackSequencesWithAccessDatabase() throws Exception {
     assume().that(notesMigration.readChangeSequence()).isTrue();
     try (Repository repo = repoManager.openRepository(allProjects)) {
       setApiUser(user);
@@ -348,6 +411,82 @@
     }
   }
 
+  @Test
+  public void receivePackListsOpenChangesAsAdditionalHaves() throws Exception {
+    ReceiveCommitsAdvertiseRefsHook.Result r = getReceivePackRefs();
+    assertThat(r.allRefs().keySet()).containsExactly(
+        // meta refs are excluded even when NoteDb is enabled.
+        "HEAD",
+        "refs/heads/branch",
+        "refs/heads/master",
+        "refs/meta/config",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
+    assertThat(r.additionalHaves()).containsExactly(obj(c3, 1), obj(c4, 1));
+  }
+
+  @Test
+  public void receivePackRespectsVisibilityOfOpenChanges() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
+    deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
+    setApiUser(user);
+
+    assertThat(getReceivePackRefs().additionalHaves())
+        .containsExactly(obj(c3, 1));
+  }
+
+  @Test
+  public void receivePackListsOnlyLatestPatchSet() throws Exception {
+    testRepo.reset(obj(c3, 1));
+    PushOneCommit.Result r = amendChange(c3.change().getKey().get());
+    r.assertOkStatus();
+    c3 = r.getChange();
+    assertThat(getReceivePackRefs().additionalHaves())
+        .containsExactly(obj(c3, 2), obj(c4, 1));
+  }
+
+  @Test
+  public void receivePackOmitsMissingObject() throws Exception {
+    // Use the tactic from ConsistencyCheckerIT to insert a new patch set with a
+    // missing object.
+    String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    try (Repository repo = repoManager.openRepository(project)) {
+      TestRepository<?> tr = new TestRepository<>(repo);
+      String subject = "Subject for missing commit";
+      Change c = new Change(c3.change());
+      PatchSet.Id psId = new PatchSet.Id(c3.getId(), 2);
+      c.setCurrentPatchSet(psId, subject, c.getOriginalSubject());
+
+      PatchSet ps = TestChanges.newPatchSet(psId, rev, admin.getId());
+      db.patchSets().insert(Collections.singleton(ps));
+      db.changes().update(Collections.singleton(c));
+
+      if (notesMigration.commitChangeWrites()) {
+        PersonIdent committer = serverIdent.get();
+        PersonIdent author = noteUtil.newIdent(
+            accountCache.get(admin.getId()).getAccount(),
+            committer.getWhen(),
+            committer,
+            anonymousCowardName);
+        tr.branch(RefNames.changeMetaRef(c3.getId()))
+            .commit()
+            .author(author)
+            .committer(committer)
+            .message(
+                "Update patch set " + psId.get() + "\n"
+                    + "\n"
+                    + "Patch-set: " + psId.get() + "\n"
+                    + "Commit: " + rev + "\n"
+                    + "Subject: " + subject + "\n")
+            .create();
+      }
+      indexer.index(db, c.getProject(), c.getId());
+    }
+
+    assertThat(getReceivePackRefs().additionalHaves())
+        .containsExactly(obj(c4, 1));
+  }
+
   /**
    * Assert that refs seen by a non-admin user match expected.
    *
@@ -356,7 +495,8 @@
    *     from the expected list before comparing to the actual results.
    * @throws Exception
    */
-  private void assertRefs(String... expectedWithMeta) throws Exception {
+  private void assertUploadPackRefs(String... expectedWithMeta)
+      throws Exception {
     try (Repository repo = repoManager.openRepository(project)) {
       assertRefs(
           repo,
@@ -391,6 +531,15 @@
     }
   }
 
+  private ReceiveCommitsAdvertiseRefsHook.Result getReceivePackRefs()
+      throws Exception {
+    ReceiveCommitsAdvertiseRefsHook hook =
+        new ReceiveCommitsAdvertiseRefsHook(queryProvider, project);
+    try (Repository repo = repoManager.openRepository(project)) {
+      return hook.advertiseRefs(repo.getAllRefs());
+    }
+  }
+
   private ProjectControl projectControl() throws Exception {
     return projectControlFactory.controlFor(project, userProvider.get());
   }
@@ -402,4 +551,12 @@
         projectControlFactory.controlFor(project, userProvider.get()),
         db, true);
   }
+
+  private static ObjectId obj(ChangeData cd, int psNum) throws Exception {
+    PatchSet.Id psId = new PatchSet.Id(cd.getId(), psNum);
+    PatchSet ps = cd.patchSet(psId);
+    assertWithMessage("%s not found in %s", psId, cd.patchSets()).that(ps)
+        .isNotNull();
+    return ObjectId.fromString(ps.getRevision().get());
+  }
 }
diff --git a/gerrit-acceptance-tests/tests.bzl b/gerrit-acceptance-tests/tests.bzl
index 62a99e3..160234f 100644
--- a/gerrit-acceptance-tests/tests.bzl
+++ b/gerrit-acceptance-tests/tests.bzl
@@ -11,7 +11,8 @@
     flaky = 0,
     deps = [],
     labels = [],
-    vm_args = ['-Xmx256m']):
+    vm_args = ['-Xmx256m'],
+    **kwargs):
   junit_tests(
     name = group,
     srcs = srcs,
@@ -24,4 +25,5 @@
       'slow',
     ],
     jvm_flags = vm_args,
+    **kwargs
   )
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
index 7ae7ef1..2e1bb13 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
@@ -17,4 +17,5 @@
 public class CherryPickInput {
   public String message;
   public String destination;
+  public Integer parent;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
index d246996..3f5fa31 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -22,6 +22,9 @@
   /** Default tab size. */
   public static final int DEFAULT_TAB_SIZE = 8;
 
+  /** Default font size. */
+  public static final int DEFAULT_FONT_SIZE = 12;
+
   /** Default line length. */
   public static final int DEFAULT_LINE_LENGTH = 100;
 
@@ -41,6 +44,7 @@
 
   public Integer context;
   public Integer tabSize;
+  public Integer fontSize;
   public Integer lineLength;
   public Integer cursorBlinkRate;
   public Boolean expandAllComments;
@@ -68,6 +72,7 @@
     DiffPreferencesInfo i = new DiffPreferencesInfo();
     i.context = DEFAULT_CONTEXT;
     i.tabSize = DEFAULT_TAB_SIZE;
+    i.fontSize = DEFAULT_FONT_SIZE;
     i.lineLength = DEFAULT_LINE_LENGTH;
     i.cursorBlinkRate = 0;
     i.ignoreWhitespace = Whitespace.IGNORE_NONE;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index 6d28dbc..d59e813 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -20,6 +20,7 @@
   public String tag;
   public Integer value;
   public Timestamp date;
+  public Boolean postSubmit;
 
   public ApprovalInfo(Integer id) {
     super(id);
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 cd70143..77c8bb4 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
@@ -21,7 +21,6 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
-import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
@@ -72,6 +71,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentMap;
 import java.util.function.Predicate;
 import java.util.jar.Attributes;
@@ -556,7 +556,7 @@
       int d = file.lastIndexOf('.');
       return scanner.getEntry(file.substring(0, d) + ".md");
     }
-    return Optional.absent();
+    return Optional.empty();
   }
 
   private void sendMarkdownAsHtml(PluginContentScanner scanner, PluginEntry entry,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
index 4047279..42b5e7e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.httpd.raw;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Change;
@@ -33,6 +32,7 @@
 import com.google.inject.Singleton;
 
 import java.io.IOException;
+import java.util.Optional;
 
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
index 030ac30..755ab1b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
@@ -28,6 +29,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.common.FormatUtil;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -42,12 +44,16 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.ChainedReceiveCommands;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.DummyIndexModule;
 import com.google.gerrit.server.index.change.ReindexAfterUpdate;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -57,9 +63,12 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.kohsuke.args4j.Option;
@@ -67,6 +76,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -107,6 +117,9 @@
   private GitRepositoryManager repoManager;
 
   @Inject
+  private NoteDbUpdateManager.Factory updateManagerFactory;
+
+  @Inject
   private NotesMigration notesMigration;
 
   @Inject
@@ -115,6 +128,9 @@
   @Inject
   private WorkQueue workQueue;
 
+  @Inject
+  private ChangeBundleReader bundleReader;
+
   @Override
   public int run() throws Exception {
     mustHaveValidSite();
@@ -153,7 +169,7 @@
               @Override
               public Boolean call() {
                 try (ReviewDb db = unwrapDb(schemaFactory.open())) {
-                  return rebuilder.rebuildProject(
+                  return rebuildProject(
                       db, changesByProject, project, allUsersRepo);
                 } catch (Exception e) {
                   log.error("Error rebuilding project " + project, e);
@@ -257,4 +273,37 @@
       return ImmutableMultimap.copyOf(changesByProject);
     }
   }
+
+  private boolean rebuildProject(ReviewDb db,
+      ImmutableMultimap<Project.NameKey, Change.Id> allChanges,
+      Project.NameKey project, Repository allUsersRepo)
+      throws IOException, OrmException {
+    checkArgument(allChanges.containsKey(project));
+    boolean ok = true;
+    ProgressMonitor pm = new TextProgressMonitor(new PrintWriter(System.out));
+    pm.beginTask(
+        FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
+    try (NoteDbUpdateManager manager = updateManagerFactory.create(project);
+        ObjectInserter allUsersInserter = allUsersRepo.newObjectInserter();
+        RevWalk allUsersRw = new RevWalk(allUsersInserter.newReader())) {
+      manager.setAllUsersRepo(allUsersRepo, allUsersRw, allUsersInserter,
+          new ChainedReceiveCommands(allUsersRepo));
+      for (Change.Id changeId : allChanges.get(project)) {
+        try {
+          rebuilder.buildUpdates(
+              manager, bundleReader.fromReviewDb(db, changeId));
+        } catch (NoPatchSetsException e) {
+          log.warn(e.getMessage());
+        } catch (Throwable t) {
+          log.error("Failed to rebuild change " + changeId, t);
+          ok = false;
+        }
+        pm.update(1);
+      }
+      manager.execute();
+    } finally {
+      pm.endTask();
+    }
+    return ok;
+  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
index 6739ce0..e47f23a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.VersionedMetaDataOnInit;
@@ -34,6 +33,7 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
 
 public class VersionedAuthorizedKeysOnInit extends VersionedMetaDataOnInit {
   public interface Factory {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index a2becc2..30f2e1d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -100,6 +100,9 @@
   @Column(id = 7, notNull = false)
   protected Account.Id realAccountId;
 
+  @Column(id = 8)
+  protected boolean postSubmit;
+
   // DELETED: id = 4 (changeOpen)
   // DELETED: id = 5 (changeSortKey)
 
@@ -119,6 +122,7 @@
     granted = src.granted;
     realAccountId = src.realAccountId;
     tag = src.tag;
+    postSubmit = src.postSubmit;
   }
 
   public PatchSetApproval(PatchSetApproval src) {
@@ -186,14 +190,24 @@
     return tag;
   }
 
+  public void setPostSubmit(boolean postSubmit) {
+    this.postSubmit = postSubmit;
+  }
+
+  public boolean isPostSubmit() {
+    return postSubmit;
+  }
+
   @Override
   public String toString() {
-    return "["
-        + key + ": "
-        + value
-        + ",tag:" + tag
-        + ",realAccountId:" + realAccountId
-        + ']';
+    StringBuilder sb = new StringBuilder("[")
+        .append(key).append(": ").append(value)
+        .append(",tag:").append(tag)
+        .append(",realAccountId:").append(realAccountId);
+    if (postSubmit) {
+      sb.append(",postSubmit");
+    }
+    return sb.append(']').toString();
   }
 
   @Override
@@ -204,7 +218,8 @@
           && Objects.equals(value, p.value)
           && Objects.equals(granted, p.granted)
           && Objects.equals(tag, p.tag)
-          && Objects.equals(realAccountId, p.realAccountId);
+          && Objects.equals(realAccountId, p.realAccountId)
+          && postSubmit == p.postSubmit;
     }
     return false;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
index bc6f732..2cd4df6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server;
 
-import com.google.common.base.Optional;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeTriplet;
@@ -30,6 +29,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 @Singleton
 public class ChangeFinder {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
index 81ec4eb..d1e0ae5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -18,7 +18,6 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.stream.Collectors.toList;
 
-import com.google.common.base.Optional;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
@@ -64,6 +63,8 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
 import java.util.stream.StreamSupport;
 
 /**
@@ -153,22 +154,18 @@
   public Optional<Comment> get(ReviewDb db, ChangeNotes notes,
       Comment.Key key) throws OrmException {
     if (!migration.readChanges()) {
-      PatchLineComment plc = db.patchComments()
-          .get(PatchLineComment.Key.from(notes.getChangeId(), key));
-      Comment c = plc != null ? plc.asComment(serverId) : null;
-      return Optional.fromNullable(c);
+      return Optional.ofNullable(
+              db.patchComments()
+                  .get(PatchLineComment.Key.from(notes.getChangeId(), key)))
+          .map(plc -> plc.asComment(serverId));
     }
-    for (Comment c : publishedByChange(db, notes)) {
-      if (key.equals(c.key)) {
-        return Optional.of(c);
-      }
+    Predicate<Comment> p = c -> key.equals(c.key);
+    Optional<Comment> c =
+        publishedByChange(db, notes).stream().filter(p).findFirst();
+    if (c.isPresent()) {
+      return c;
     }
-    for (Comment c : draftByChange(db, notes)) {
-      if (key.equals(c.key)) {
-        return Optional.of(c);
-      }
-    }
-    return Optional.absent();
+    return draftByChange(db, notes).stream().filter(p).findFirst();
   }
 
   public List<Comment> publishedByChange(ReviewDb db, ChangeNotes notes)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
index 7f6460d..af85201 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
@@ -50,8 +50,8 @@
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.List;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
@@ -68,6 +68,8 @@
   private static final double BASE_REVIEWER_WEIGHT = 10;
   private static final double BASE_OWNER_WEIGHT = 1;
   private static final double BASE_COMMENT_WEIGHT = 0.5;
+  private static final double[] WEIGHTS = new double[] {
+      BASE_REVIEWER_WEIGHT, BASE_OWNER_WEIGHT, BASE_COMMENT_WEIGHT,};
   private static final long PLUGIN_QUERY_TIMEOUT = 500; //ms
 
   private final ChangeQueryBuilder changeQueryBuilder;
@@ -117,8 +119,9 @@
 
     for (DynamicMap.Entry<ReviewerSuggestion> plugin :
         reviewerSuggestionPluginMap) {
-      tasks.add(() -> plugin.getProvider().get().suggestReviewers(
-          changeNotes.getChangeId(), query, reviewerScores.keySet()));
+      tasks.add(() -> plugin.getProvider().get()
+          .suggestReviewers(projectControl.getProject().getNameKey(),
+              changeNotes.getChangeId(), query, reviewerScores.keySet()));
       String pluginWeight = config.getString("addReviewer",
           plugin.getPluginName() + "-" + plugin.getExportName(), "weight");
       if (Strings.isNullOrEmpty(pluginWeight)) {
@@ -154,7 +157,9 @@
     }
 
     // Remove change owner
-    reviewerScores.remove(changeNotes.getChange().getOwner());
+    if (changeNotes != null) {
+      reviewerScores.remove(changeNotes.getChange().getOwner());
+    }
 
     // Sort results
     Stream<Entry<Account.Id, MutableDouble>> sorted =
@@ -247,18 +252,15 @@
     Iterator<List<ChangeData>> queryResultIterator = result.iterator();
     Iterator<Account.Id> reviewersIterator = reviewers.keySet().iterator();
 
-    double[] weights = new double[]{
-        BASE_REVIEWER_WEIGHT, BASE_OWNER_WEIGHT, BASE_COMMENT_WEIGHT};
-
     int i = 0;
     Account.Id currentId = null;
     while (queryResultIterator.hasNext()) {
       List<ChangeData> currentResult = queryResultIterator.next();
-      if (i % weights.length == 0) {
+      if (i % WEIGHTS.length == 0) {
         currentId = reviewersIterator.next();
       }
 
-      reviewers.get(currentId).add(weights[i % weights.length] *
+      reviewers.get(currentId).add(WEIGHTS[i % WEIGHTS.length] *
           baseWeight * currentResult.size());
       i++;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 149931d..846b44b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.common.base.Optional;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableSet;
@@ -52,6 +51,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
@@ -252,17 +252,13 @@
       if (accountIndexes.getSearchIndex() != null) {
         AccountState accountState =
             accountQueryProvider.get().oneByExternalId(key.get());
-        return accountState != null
-            ? Optional.of(accountState.getAccount().getId())
-            : Optional.<Account.Id>absent();
+        return Optional.ofNullable(accountState)
+            .map(s -> s.getAccount().getId());
       }
 
       try (ReviewDb db = schema.open()) {
-        AccountExternalId id = db.accountExternalIds().get(key);
-        if (id != null) {
-          return Optional.of(id.getAccountId());
-        }
-        return Optional.absent();
+        return Optional.ofNullable(db.accountExternalIds().get(key))
+            .map(AccountExternalId::getAccountId);
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java
index 0e8c051..45dbe60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Optional;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 
 public class AuthorizedKeys {
   public static final String FILE_NAME = "authorized_keys";
@@ -47,7 +47,7 @@
         key.setInvalid();
         keys.add(Optional.of(key));
       } else if (line.startsWith(DELETED_KEY_COMMENT)) {
-        keys.add(Optional.<AccountSshKey> absent());
+        keys.add(Optional.empty());
         seq++;
       } else if (line.startsWith("#")) {
         continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index e5e2f99..a2f4b65 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.common.base.Optional;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -35,6 +34,7 @@
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 
 /** Tracks group objects in memory for efficient access. */
@@ -130,7 +130,7 @@
       return null;
     }
     try {
-      return byName.get(name.get()).orNull();
+      return byName.get(name.get()).orElse(null);
     } catch (ExecutionException e) {
       log.warn(String.format("Cannot lookup group %s by name", name.get()), e);
       return null;
@@ -143,7 +143,7 @@
       return null;
     }
     try {
-      return byUUID.get(uuid.get()).orNull();
+      return byUUID.get(uuid.get()).orElse(null);
     } catch (ExecutionException e) {
       log.warn(String.format("Cannot lookup group %s by name", uuid.get()), e);
       return null;
@@ -183,7 +183,7 @@
     public Optional<AccountGroup> load(final AccountGroup.Id key)
         throws Exception {
       try (ReviewDb db = schema.open()) {
-        return Optional.fromNullable(db.accountGroups().get(key));
+        return Optional.ofNullable(db.accountGroups().get(key));
       }
     }
   }
@@ -203,9 +203,9 @@
         AccountGroup.NameKey key = new AccountGroup.NameKey(name);
         AccountGroupName r = db.accountGroupNames().get(key);
         if (r != null) {
-          return Optional.fromNullable(db.accountGroups().get(r.getId()));
+          return Optional.ofNullable(db.accountGroups().get(r.getId()));
         }
-        return Optional.absent();
+        return Optional.empty();
       }
     }
   }
@@ -228,7 +228,7 @@
         if (r.size() == 1) {
           return Optional.of(r.get(0));
         } else if (r.size() == 0) {
-          return Optional.absent();
+          return Optional.empty();
         } else {
           throw new OrmDuplicateKeyException("Duplicate group UUID " + uuid);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index aa32d27..41ae498 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -16,10 +16,9 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
 
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -46,6 +45,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 /**
  * 'authorized_keys' file in the refs/users/CD/ABCD branches of the All-Users
@@ -192,7 +192,8 @@
   /** Returns all SSH keys. */
   private List<AccountSshKey> getKeys() {
     checkLoaded();
-    return Lists.newArrayList(Optional.presentInstances(keys));
+    return keys.stream().filter(Optional::isPresent).map(Optional::get)
+        .collect(toList());
   }
 
   /**
@@ -205,8 +206,7 @@
    */
   private AccountSshKey getKey(int seq) {
     checkLoaded();
-    Optional<AccountSshKey> key = keys.get(seq - 1);
-    return key.orNull();
+    return keys.get(seq - 1).orElse(null);
   }
 
   /**
@@ -246,7 +246,7 @@
   private boolean deleteKey(int seq) {
     checkLoaded();
     if (seq <= keys.size() && keys.get(seq - 1).isPresent()) {
-      keys.set(seq - 1, Optional.<AccountSshKey> absent());
+      keys.set(seq - 1, Optional.empty());
       return true;
     }
     return false;
@@ -279,8 +279,9 @@
    */
   public void setKeys(Collection<AccountSshKey> newKeys) {
     Ordering<AccountSshKey> o = Ordering.from(comparing(k -> k.getKey().get()));
-    keys = new ArrayList<>(Collections.nCopies(o.max(newKeys).getKey().get(),
-        Optional.<AccountSshKey> absent()));
+    keys = new ArrayList<>(
+        Collections.nCopies(o.max(newKeys).getKey().get(),
+        Optional.empty()));
     for (AccountSshKey key : newKeys) {
       keys.set(key.getKey().get() - 1, Optional.of(key));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java
index a3cd0c9..8f96e92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java
@@ -21,7 +21,6 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Enums;
 import com.google.common.base.Joiner;
-import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
@@ -319,9 +318,9 @@
       if (i + 1 < notifyValue.length() - 2) {
         for (String nt : Splitter.on(',').trimResults().splitToList(
             notifyValue.substring(i + 1, notifyValue.length() - 1))) {
-          Optional<NotifyType> notifyType =
-              Enums.getIfPresent(NotifyType.class, nt);
-          if (!notifyType.isPresent()) {
+          NotifyType notifyType =
+              Enums.getIfPresent(NotifyType.class, nt).orNull();
+          if (notifyType == null) {
             validationErrorSink.error(new ValidationError(WATCH_CONFIG,
                 String.format(
                     "Invalid notify type %s in project watch "
@@ -329,7 +328,7 @@
                     nt, accountId.get(), project, notifyValue)));
             continue;
           }
-          notifyTypes.add(notifyType.get());
+          notifyTypes.add(notifyType);
         }
       }
       return create(filter, notifyTypes);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index eaaafd6..217df2f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -16,7 +16,6 @@
 
 import static java.util.concurrent.TimeUnit.HOURS;
 
-import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Account;
@@ -27,6 +26,7 @@
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 
+import java.util.Optional;
 import java.util.Set;
 
 public class LdapModule extends CacheModule {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 603efe0..4148e7a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
 
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
@@ -49,6 +48,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
@@ -295,7 +295,7 @@
     }
     try {
       Optional<Account.Id> id = usernameCache.get(accountName);
-      return id != null ? id.orNull() : null;
+      return id != null ? id.orElse(null) : null;
     } catch (ExecutionException e) {
       log.warn(String.format("Cannot lookup account %s in LDAP", accountName), e);
       return null;
@@ -313,13 +313,10 @@
     @Override
     public Optional<Account.Id> load(String username) throws Exception {
       try (ReviewDb db = schema.open()) {
-        final AccountExternalId extId =
-            db.accountExternalIds().get(
-                new AccountExternalId.Key(SCHEME_GERRIT, username));
-        if (extId != null) {
-          return Optional.of(extId.getAccountId());
-        }
-        return Optional.absent();
+        return Optional.ofNullable(
+                db.accountExternalIds().get(
+                    new AccountExternalId.Key(SCHEME_GERRIT, username)))
+            .map(AccountExternalId::getAccountId);
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index 878cc81..ad07e30 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
 import com.google.gerrit.common.Nullable;
@@ -65,6 +64,7 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
 
 @Singleton
 public class ChangeEdits implements
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 bf7e6b8..771b049 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_FILES;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
@@ -40,7 +41,6 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
-import com.google.common.base.Optional;
 import com.google.common.base.Throwables;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashBasedTable;
@@ -137,6 +137,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -266,7 +267,7 @@
   }
 
   public ChangeInfo format(ChangeData cd) throws OrmException {
-    return format(cd, Optional.<PatchSet.Id> absent(), true);
+    return format(cd, Optional.empty(), true);
   }
 
   private ChangeInfo format(ChangeData cd, Optional<PatchSet.Id> limitToPsId,
@@ -356,7 +357,7 @@
       ChangeInfo i = out.get(cd.getId());
       if (i == null) {
         try {
-          i = toChangeInfo(cd, Optional.<PatchSet.Id> absent());
+          i = toChangeInfo(cd, Optional.empty());
         } catch (PatchListNotAvailableException | GpgException | OrmException
             | IOException | RuntimeException e) {
           if (has(CHECK)) {
@@ -695,6 +696,10 @@
 
   private void setAllApprovals(ChangeControl baseCtrl, ChangeData cd,
       Map<String, LabelWithStatus> labels) throws OrmException {
+    Change.Status status = cd.change().getStatus();
+    checkState(status.isOpen(),
+        "should not call setAllApprovals on %s change", status);
+
     // Include a user in the output for this label if either:
     //  - They are an explicit reviewer.
     //  - They ever voted on this change.
@@ -734,6 +739,9 @@
           }
           tag = psa.getTag();
           date = psa.getGranted();
+          if (psa.isPostSubmit()) {
+            log.warn("unexpected post-submit approval on open change: {}", psa);
+          }
         } else {
           // Either the user cannot vote on this label, or they were added as a
           // reviewer but have not responded yet. Explicitly check whether the
@@ -816,6 +824,9 @@
           info.value = Integer.valueOf(val);
           info.date = psa.getGranted();
           info.tag = psa.getTag();
+          if (psa.isPostSubmit()) {
+            info.postSubmit = true;
+          }
         }
         if (!standard) {
           continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
index 7069e6d..fc3e70a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
@@ -15,12 +15,13 @@
 package com.google.gerrit.server.change;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.base.Optional;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 
+import java.util.Optional;
+
 @AutoValue
 public abstract class ChangeTriplet {
   public static String format(Change change) {
@@ -44,7 +45,7 @@
     int t2 = triplet.lastIndexOf('~');
     int t1 = triplet.lastIndexOf('~', t2 - 1);
     if (t1 < 0 || t2 < 0) {
-      return Optional.absent();
+      return Optional.empty();
     }
 
     String project = Url.decode(triplet.substring(0, t1));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index 1a063f4..b5eb193 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -60,10 +60,12 @@
   public ChangeInfo apply(RevisionResource revision, CherryPickInput input)
       throws OrmException, IOException, UpdateException, RestApiException {
     final ChangeControl control = revision.getControl();
+    int parent = input.parent == null ? 1 : input.parent;
 
     if (input.message == null || input.message.trim().isEmpty()) {
       throw new BadRequestException("message must be non-empty");
-    } else if (input.destination == null || input.destination.trim().isEmpty()) {
+    } else if (input.destination == null
+        || input.destination.trim().isEmpty()) {
       throw new BadRequestException("destination must be non-empty");
     }
 
@@ -91,7 +93,7 @@
       Change.Id cherryPickedChangeId =
           cherryPickChange.cherryPick(revision.getChange(),
               revision.getPatchSet(), input.message, refName,
-              refControl);
+              refControl, parent);
       return json.create(ChangeJson.NO_OPTIONS).format(revision.getProject(),
           cherryPickedChangeId);
     } catch (InvalidChangeOperationException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index 248acd3..1a18357 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -114,8 +114,8 @@
   }
 
   public Change.Id cherryPick(Change change, PatchSet patch,
-      final String message, final String ref,
-      final RefControl refControl) throws NoSuchChangeException,
+      final String message, final String ref, final RefControl refControl,
+      int parent) throws NoSuchChangeException,
       OrmException, MissingObjectException,
       IncorrectObjectTypeException, IOException,
       InvalidChangeOperationException, IntegrationException, UpdateException,
@@ -147,6 +147,13 @@
       CodeReviewCommit commitToCherryPick =
           revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
 
+      if (parent <= 0 || parent > commitToCherryPick.getParentCount()) {
+        throw new InvalidChangeOperationException(String.format(
+            "Cherry Pick: Parent %s does not exist. Please specify a parent in"
+                + " range [1, %s].",
+            parent, commitToCherryPick.getParentCount()));
+      }
+
       Timestamp now = TimeUtil.nowTs();
       PersonIdent committerIdent =
           identifiedUser.newCommitterIdent(now, serverTimeZone);
@@ -160,10 +167,12 @@
 
       CodeReviewCommit cherryPickCommit;
       try {
-        ProjectState projectState = refControl.getProjectControl().getProjectState();
-        cherryPickCommit =
-            mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
-                commitToCherryPick, committerIdent, commitMessage, revWalk);
+        ProjectState projectState = refControl.getProjectControl()
+            .getProjectState();
+        cherryPickCommit = mergeUtilFactory.create(projectState)
+            .createCherryPickFromCommit(git, oi, mergeTip,
+                commitToCherryPick, committerIdent, commitMessage, revWalk,
+                parent - 1);
 
         Change.Key changeKey;
         final List<String> idList = cherryPickCommit.getFooterLines(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
index 7c1e959..604b615 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -27,6 +26,7 @@
 import com.google.inject.Singleton;
 
 import java.io.IOException;
+import java.util.Optional;
 
 @Singleton
 public class DeleteChangeEdit implements RestModifyView<ChangeResource, Input> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
index 9a4c02d..37930dd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -39,6 +38,7 @@
 import com.google.inject.Singleton;
 
 import java.util.Collections;
+import java.util.Optional;
 
 @Singleton
 public class DeleteDraftComment
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetAssignee.java
index d57107b..5ad259b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetAssignee.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetAssignee.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -25,6 +24,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import java.util.Optional;
+
 @Singleton
 public class GetAssignee implements RestReadView<ChangeResource> {
   private final AccountInfoCacheFactory.Factory accountInfo;
@@ -36,9 +37,8 @@
 
   @Override
   public Response<AccountInfo> apply(ChangeResource rsrc) throws OrmException {
-
     Optional<Account.Id> assignee =
-        Optional.fromNullable(rsrc.getChange().getAssignee());
+        Optional.ofNullable(rsrc.getChange().getAssignee());
     if (assignee.isPresent()) {
       Account account = accountInfo.create().get(assignee.get());
       return Response.ok(AccountJson.toAccountInfo(account));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 9210d2b..92617f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -15,9 +15,11 @@
 package com.google.gerrit.server.change;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toSet;
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 
@@ -55,6 +57,7 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.client.LabelId;
@@ -266,6 +269,10 @@
         continue;
       }
 
+      if (caller.getUser().isInternalUser()) {
+        continue;
+      }
+
       PermissionRange r = caller.getRange(Permission.forLabelAs(type.getName()));
       if (r == null || r.isEmpty() || !r.contains(ent.getValue())) {
         throw new AuthException(String.format(
@@ -795,10 +802,7 @@
         }
       }
 
-      if ((!del.isEmpty() || !ups.isEmpty())
-          && ctx.getChange().getStatus().isClosed()) {
-        throw new ResourceConflictException("change is closed");
-      }
+      validatePostSubmitLabels(ctx, labelTypes, previous, ups, del);
 
       // Return early if user is not a reviewer and not posting any labels.
       // This allows us to preserve their CC status.
@@ -813,6 +817,49 @@
       return !del.isEmpty() || !ups.isEmpty();
     }
 
+    private void validatePostSubmitLabels(ChangeContext ctx,
+        LabelTypes labelTypes, Map<String, Short> previous,
+        List<PatchSetApproval> ups, List<PatchSetApproval> del)
+        throws ResourceConflictException {
+      if (ctx.getChange().getStatus().isOpen()) {
+        return; // Not closed, nothing to validate.
+      } else if (del.isEmpty() && ups.isEmpty()) {
+        return; // No new votes.
+      } else if (ctx.getChange().getStatus() != Change.Status.MERGED) {
+        throw new ResourceConflictException("change is closed");
+      }
+
+      // Disallow reducing votes on any labels post-submit. This assumes the
+      // high values were broadly necessary to submit, so reducing them would
+      // make it possible to take a merged change and make it no longer
+      // submittable.
+      List<PatchSetApproval> reduced = new ArrayList<>(ups.size() + del.size());
+      reduced.addAll(del);
+      for (PatchSetApproval psa : ups) {
+        LabelType lt = checkNotNull(labelTypes.byLabel(psa.getLabel()));
+        String normName = lt.getName();
+        Short prev = previous.get(normName);
+        if (prev == null) {
+          continue;
+        }
+        checkState(prev != psa.getValue()); // Should be filtered out above.
+        if (prev > psa.getValue()) {
+          reduced.add(psa);
+        } else {
+          // Set postSubmit bit in ReviewDb; not required for NoteDb, which sets
+          // it automatically.
+          psa.setPostSubmit(true);
+        }
+      }
+
+      if (!reduced.isEmpty()) {
+        throw new ResourceConflictException(
+            "Cannot reduce vote on labels for closed change: "
+                + reduced.stream().map(p -> p.getLabel()).distinct().sorted()
+                    .collect(joining(", ")));
+      }
+    }
+
     private void forceCallerAsReviewer(ChangeContext ctx,
         Map<String, PatchSetApproval> current, List<PatchSetApproval> ups,
         List<PatchSetApproval> del) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
index 76ff15e..6875287 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -37,6 +36,7 @@
 import com.google.inject.Singleton;
 
 import java.io.IOException;
+import java.util.Optional;
 
 @Singleton
 public class PublishChangeEdit implements
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
index 4298937..5002436 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AssigneeInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -59,10 +62,17 @@
   @Override
   public Response<AccountInfo> apply(ChangeResource rsrc, AssigneeInput input)
       throws RestApiException, UpdateException, OrmException, IOException {
+    if (!rsrc.getControl().canEditAssignee()) {
+      throw new AuthException("Changing Assignee not permitted");
+    }
+    if (Strings.isNullOrEmpty(input.assignee)) {
+      throw new BadRequestException("missing assignee field");
+    }
+
     try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
         rsrc.getChange().getProject(), rsrc.getControl().getUser(),
         TimeUtil.nowTs())) {
-      SetAssigneeOp op = assigneeFactory.create(input);
+      SetAssigneeOp op = assigneeFactory.create(input.assignee);
       bu.addOp(rsrc.getId(), op);
 
       PostReviewers.Addition reviewersAddition =
@@ -70,7 +80,6 @@
       bu.addOp(rsrc.getId(), reviewersAddition.op);
 
       bu.execute();
-      reviewersAddition.gatherResults();
       return Response.ok(AccountJson.toAccountInfo(op.getNewAssignee()));
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 91ad849..0742c6d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -44,6 +43,7 @@
 
 import java.sql.Timestamp;
 import java.util.Collections;
+import java.util.Optional;
 
 @Singleton
 public class PutDraftComment implements RestModifyView<DraftCommentResource, DraftInput> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
index baa0990..ac1b770 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsPost;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -39,6 +38,7 @@
 import com.google.inject.Singleton;
 
 import java.io.IOException;
+import java.util.Optional;
 
 @Singleton
 public class RebaseChangeEdit implements
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestion.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestion.java
index 379d2bd..6affd9f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestion.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 
 import java.util.Set;
 
@@ -30,12 +32,14 @@
   /**
    * Reviewer suggestion.
    *
-   * @param changeId The changeId that the suggestion is for.
-   * @param query The query as typed by the user. Can be an empty string.
-   * @param candidates A set of candidates for the ranking.
+   * @param project The name key of the project the suggestion is for.
+   * @param changeId The changeId that the suggestion is for. Can be an {@code null}.
+   * @param query The query as typed by the user. Can be an {@code null}.
+   * @param candidates A set of candidates for the ranking. Can be empty.
    * @return Set of suggested reviewers as a tuple of account id and score.
    *         The account ids listed here don't have to be a part of candidates.
    */
-  Set<SuggestedReviewer> suggestReviewers(
-      Change.Id changeId, String query, Set<Account.Id> candidates);
+  Set<SuggestedReviewer> suggestReviewers(Project.NameKey project,
+      @Nullable Change.Id changeId, @Nullable String query,
+      Set<Account.Id> candidates);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
index a8fd013..152563b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestResource.HasETag;
 import com.google.gerrit.extensions.restapi.RestView;
@@ -28,6 +27,8 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.TypeLiteral;
 
+import java.util.Optional;
+
 public class RevisionResource implements RestResource, HasETag {
   public static final TypeLiteral<RestView<RevisionResource>> REVISION_KIND =
       new TypeLiteral<RestView<RevisionResource>>() {};
@@ -38,7 +39,7 @@
   private boolean cacheable = true;
 
   public RevisionResource(ChangeResource change, PatchSet ps) {
-    this(change, ps, Optional.<ChangeEdit> absent());
+    this(change, ps, Optional.empty());
   }
 
   public RevisionResource(ChangeResource change, PatchSet ps,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
index 30a09cf..4572994 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Joiner;
-import com.google.common.base.Optional;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -38,6 +37,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 @Singleton
 public class Revisions implements ChildCollection<ChangeResource, RevisionResource> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
index dba5358..43c1dee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Optional;
-import com.google.gerrit.extensions.api.changes.AssigneeInput;
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -39,16 +39,18 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
+import java.util.Optional;
+
 public class SetAssigneeOp extends BatchUpdate.Op {
   public interface Factory {
-    SetAssigneeOp create(AssigneeInput input);
+    SetAssigneeOp create(String assignee);
   }
 
   private final AccountsCollection accounts;
   private final ChangeMessagesUtil cmUtil;
   private final AccountInfoCacheFactory.Factory accountInfosFactory;
   private final DynamicSet<AssigneeValidationListener> validationListeners;
-  private final AssigneeInput input;
+  private final String assignee;
   private final String anonymousCowardName;
   private final AssigneeChanged assigneeChanged;
 
@@ -63,37 +65,28 @@
       DynamicSet<AssigneeValidationListener> validationListeners,
       AssigneeChanged assigneeChanged,
       @AnonymousCowardName String anonymousCowardName,
-      @Assisted AssigneeInput input) {
+      @Assisted String assignee) {
     this.accounts = accounts;
     this.cmUtil = cmUtil;
     this.accountInfosFactory = accountInfosFactory;
     this.validationListeners = validationListeners;
     this.assigneeChanged = assigneeChanged;
     this.anonymousCowardName = anonymousCowardName;
-    this.input = input;
+    this.assignee = checkNotNull(assignee);
   }
 
   @Override
   public boolean updateChange(BatchUpdate.ChangeContext ctx)
       throws OrmException, RestApiException {
-    if (!ctx.getControl().canEditAssignee()) {
-      throw new AuthException("Changing Assignee not permitted");
-    }
     change = ctx.getChange();
     ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
     Optional<Account.Id> oldAssigneeId =
-        Optional.fromNullable(change.getAssignee());
-    if (input.assignee == null) {
-      if (oldAssigneeId.isPresent()) {
-        throw new BadRequestException("Cannot set Assignee to empty");
-      }
-      return false;
-    }
+        Optional.ofNullable(change.getAssignee());
     oldAssignee = null;
     if (oldAssigneeId.isPresent()) {
       oldAssignee = accountInfosFactory.create().get(oldAssigneeId.get());
     }
-    IdentifiedUser newAssigneeUser = accounts.parse(input.assignee);
+    IdentifiedUser newAssigneeUser = accounts.parse(assignee);
     if (oldAssigneeId.isPresent() &&
         oldAssigneeId.get().equals(newAssigneeUser.getAccountId())) {
       newAssignee = oldAssignee;
@@ -101,20 +94,20 @@
     }
     if (!newAssigneeUser.getAccount().isActive()) {
       throw new UnprocessableEntityException(String.format(
-          "Account of %s is not active", input.assignee));
+          "Account of %s is not active", assignee));
     }
     if (!ctx.getControl().forUser(newAssigneeUser).isRefVisible()) {
       throw new AuthException(String.format(
           "Change %s is not visible to %s.",
           change.getChangeId(),
-          input.assignee));
+          assignee));
     }
     try {
       for (AssigneeValidationListener validator : validationListeners) {
         validator.validateAssignee(change, newAssigneeUser.getAccount());
       }
     } catch (ValidationException e) {
-      throw new BadRequestException(e.getMessage());
+      throw new ResourceConflictException(e.getMessage());
     }
     // notedb
     update.setAssignee(newAssigneeUser.getAccountId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
index cf6d307..2b1c97c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -85,9 +86,11 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx)
-      throws AuthException, BadRequestException, OrmException, IOException {
+      throws AuthException, BadRequestException, MethodNotAllowedException,
+      OrmException, IOException {
     if (!notesMigration.readChanges()) {
-      throw new BadRequestException("Cannot add hashtags; NoteDb is disabled");
+      throw new MethodNotAllowedException(
+          "Cannot add hashtags; NoteDb is disabled");
     }
     if (input == null
         || (input.add == null && input.remove == null)) {
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 447d1be..54fc3fa 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
@@ -17,7 +17,6 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.ContributorAgreement;
@@ -62,6 +61,7 @@
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 public class GetServerInfo implements RestReadView<ConfigResource> {
@@ -215,11 +215,11 @@
                 .hasField(ChangeField.ASSIGNEE));
     info.largeChange = cfg.getInt("change", "largeChange", 500);
     info.replyTooltip =
-        Optional.fromNullable(cfg.getString("change", null, "replyTooltip"))
-            .or("Reply and score") + " (Shortcut: a)";
+        Optional.ofNullable(cfg.getString("change", null, "replyTooltip"))
+            .orElse("Reply and score") + " (Shortcut: a)";
     info.replyLabel =
-        Optional.fromNullable(cfg.getString("change", null, "replyLabel"))
-            .or("Reply") + "\u2026";
+        Optional.ofNullable(cfg.getString("change", null, "replyLabel"))
+            .orElse("Reply") + "\u2026";
     info.updateDelay = (int) ConfigUtil.getTimeUnit(
         cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS);
     info.submitWholeTopic = Submit.wholeTopicEnabled(cfg);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
index c7390d7..d8485fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
@@ -151,6 +151,6 @@
   }
 
   public Set<String> getNames() {
-    return cfg.getNames(PLUGIN, pluginName);
+    return cfg.getNames(PLUGIN, pluginName, true);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 8fca453..6beff6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ChangeKind;
@@ -56,6 +55,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
+import java.util.Optional;
 
 /**
  * Utility functions to manipulate change edits.
@@ -144,7 +144,7 @@
       }
       Ref ref = repo.getRefDatabase().firstExactRef(refNames);
       if (ref == null) {
-        return Optional.absent();
+        return Optional.empty();
       }
       try (RevWalk rw = new RevWalk(repo)) {
         RevCommit commit = rw.parseCommit(ref.getObjectId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java
index 2d187c0..c910a7a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java
@@ -20,13 +20,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.inject.Inject;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 public class AgreementSignup {
-  private static final Logger log =
-      LoggerFactory.getLogger(AgreementSignup.class);
-
   private final DynamicSet<AgreementSignupListener> listeners;
   private final EventUtil util;
 
@@ -46,7 +40,7 @@
       try {
         l.onAgreementSignup(event);
       } catch (Exception e) {
-        util.logEventListenerError(log, e);
+        util.logEventListenerError(this, l, e);
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
index 2234556..53d837f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
@@ -43,31 +43,24 @@
     this.util = util;
   }
 
-  public void fire(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee,
-      Timestamp when) {
-    if (!listeners.iterator().hasNext()) {
-      return;
-    }
-    Event event = new Event(change, editor, oldAssignee, when);
-    for (AssigneeChangedListener l : listeners) {
-      try {
-        l.onAssigneeChanged(event);
-      } catch (Exception e) {
-        log.warn("Error in event listener", e);
-      }
-    }
-  }
-
   public void fire(Change change, Account account, Account oldAssignee,
       Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
     try {
-      fire(util.changeInfo(change),
+      Event event = new Event(
+          util.changeInfo(change),
           util.accountInfo(account),
           util.accountInfo(oldAssignee),
           when);
+      for (AssigneeChangedListener l : listeners) {
+        try {
+          l.onAssigneeChanged(event);
+        } catch (Exception e) {
+          util.logEventListenerError(event, l, e);
+        }
+      }
     } catch (OrmException e) {
       log.error("Couldn't fire event", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
index ad5bb98..5a7aec2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -63,7 +63,7 @@
         try {
           l.onChangeAbandoned(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch (PatchListNotAvailableException | GpgException | IOException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
index ebfe7d6..8b4a6a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -63,7 +63,7 @@
         try {
           l.onChangeMerged(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch (PatchListNotAvailableException | GpgException | IOException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
index 6087070..1d2682a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -63,7 +63,7 @@
         try {
           l.onChangeRestored(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch (PatchListNotAvailableException | GpgException | IOException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
index c4f2fc6..d963a47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
@@ -52,7 +52,7 @@
         try {
           l.onChangeReverted(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch (OrmException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
index 35f2c7d..f1bb50a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -68,7 +68,7 @@
         try {
           l.onCommentAdded(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch (PatchListNotAvailableException | GpgException | IOException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
index b5dba77..4f6d298 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
@@ -60,7 +60,7 @@
         try {
           l.onDraftPublished(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch (PatchListNotAvailableException | GpgException | IOException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
index c5eaa0c..682d9ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -35,6 +35,7 @@
 import com.google.inject.Provider;
 
 import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -43,6 +44,7 @@
 import java.util.Map;
 
 public class EventUtil {
+  private static final Logger log = LoggerFactory.getLogger(EventUtil.class);
 
   private final ChangeData.Factory changeDataFactory;
   private final Provider<ReviewDb> db;
@@ -54,8 +56,9 @@
       Provider<ReviewDb> db) {
     this.changeDataFactory = changeDataFactory;
     this.db = db;
-    this.changeJson = changeJsonFactory.create(
-        EnumSet.allOf(ListChangesOption.class));
+    EnumSet<ListChangesOption> opts = EnumSet.allOf(ListChangesOption.class);
+    opts.remove(ListChangesOption.CHECK);
+    this.changeJson = changeJsonFactory.create(opts);
   }
 
   public ChangeInfo changeInfo(Change change) throws OrmException {
@@ -95,11 +98,16 @@
     return result;
   }
 
-  public void logEventListenerError(Logger log, Exception error) {
+  public void logEventListenerError(Object event, Object listener,
+      Exception error) {
     if (log.isDebugEnabled()) {
-      log.debug("Error in event listener", error);
+      log.debug(String.format(
+          "Error in event listener %s for event %s",
+          listener.getClass().getName(), event.getClass().getName()), error);
     } else {
-      log.warn("Error in event listener: {}", error.getMessage());
+      log.warn("Error in listener {} for event {}: {}",
+          listener.getClass().getName(), event.getClass().getName(),
+          error.getMessage());
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 63fa391..381dced 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -26,13 +26,8 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class GitReferenceUpdated {
-  private static final Logger log = LoggerFactory
-      .getLogger(GitReferenceUpdated.class);
-
   public static final GitReferenceUpdated DISABLED = new GitReferenceUpdated() {
     @Override
     public void fire(Project.NameKey project, RefUpdate refUpdate,
@@ -123,7 +118,7 @@
       try {
         l.onGitReferenceUpdated(event);
       } catch (Exception e) {
-        util.logEventListenerError(log, e);
+        util.logEventListenerError(this, l, e);
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
index c9cf5f1..233a89e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -62,7 +62,7 @@
         try {
           l.onHashtagsEdited(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch (OrmException 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 337982f..8860a42 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
@@ -67,7 +67,7 @@
         try {
           l.onReviewersAdded(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch (PatchListNotAvailableException | GpgException | IOException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
index 00950b7..4bc4764 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -71,7 +71,7 @@
         try {
           listener.onReviewerDeleted(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, listener, e);
         }
       }
     } catch (PatchListNotAvailableException | GpgException | IOException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index c7d1ef6..7f03c63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -63,7 +63,7 @@
         try {
           l.onRevisionCreated(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch ( PatchListNotAvailableException | GpgException | IOException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
index 837d730..2e583a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -58,7 +58,7 @@
         try {
           l.onTopicEdited(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch (OrmException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
index 9d1e7f2..e421ea6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
@@ -71,7 +71,7 @@
         try {
           l.onVoteDeleted(event);
         } catch (Exception e) {
-          util.logEventListenerError(log, e);
+          util.logEventListenerError(this, l, e);
         }
       }
     } catch (PatchListNotAvailableException | GpgException | IOException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java
index cfbaa41..a3b30d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java
@@ -17,8 +17,6 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.google.common.base.Optional;
-
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -28,6 +26,7 @@
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * Collection of {@link ReceiveCommand}s that supports multiple updates per ref.
@@ -96,7 +95,7 @@
     if (cmd != null) {
       return !cmd.getNewId().equals(ObjectId.zeroId())
           ? Optional.of(cmd.getNewId())
-          : Optional.<ObjectId>absent();
+          : Optional.empty();
     }
     return refCache.get(refName);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 71b29a1..5aaf41a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -21,13 +21,11 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Joiner;
-import com.google.common.base.Optional;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
@@ -83,6 +81,7 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -256,9 +255,10 @@
   private static Optional<SubmitRecord> findOkRecord(
       Collection<SubmitRecord> in) {
     if (in == null) {
-      return Optional.absent();
+      return Optional.empty();
     }
-    return Iterables.tryFind(in, r -> r.status == SubmitRecord.Status.OK);
+    return in.stream().filter(r -> r.status == SubmitRecord.Status.OK)
+        .findAny();
   }
 
   public static void checkSubmitRule(ChangeData cd)
@@ -479,7 +479,9 @@
       BatchUpdate.execute(orm.batchUpdates(allProjects),
           new SubmitStrategyListener(submitInput, strategies, commits),
           submissionId, dryrun);
-    } catch (UpdateException | SubmoduleException e) {
+    } catch (SubmoduleException e) {
+      throw new IntegrationException(e);
+    } catch (UpdateException e) {
       // BatchUpdate may have inadvertently wrapped an IntegrationException
       // thrown by some legacy SubmitStrategyOp code that intended the error
       // message to be user-visible. Copy the message from the wrapped
@@ -491,8 +493,7 @@
       if (e.getCause() instanceof IntegrationException) {
         msg = e.getCause().getMessage();
       } else {
-        msg = "Error submitting change" + (cs.size() != 1 ? "s" : "") + ": \n"
-            + e.getMessage();
+        msg = "Error submitting change" + (cs.size() != 1 ? "s" : "");
       }
       throw new IntegrationException(msg, e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
index 108572f..751933c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -18,7 +18,6 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -61,6 +60,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -310,8 +310,8 @@
     if (head == null) {
       Ref ref = or.repo.getRefDatabase().exactRef(b.get());
       head = ref != null
-          ? Optional.<RevCommit>of(or.rw.parseCommit(ref.getObjectId()))
-          : Optional.<RevCommit>absent();
+          ? Optional.of(or.rw.parseCommit(ref.getObjectId()))
+          : Optional.empty();
       heads.put(b, head);
     }
     if (head.isPresent()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 0667e14..90edfb1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -184,13 +184,13 @@
   public CodeReviewCommit createCherryPickFromCommit(Repository repo,
       ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit,
       PersonIdent cherryPickCommitterIdent, String commitMsg,
-      CodeReviewRevWalk rw)
+      CodeReviewRevWalk rw, int parentIndex)
       throws MissingObjectException, IncorrectObjectTypeException, IOException,
       MergeIdenticalTreeException, MergeConflictException {
 
     final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
 
-    m.setBase(originalCommit.getParent(0));
+    m.setBase(originalCommit.getParent(parentIndex));
     if (m.merge(mergeTip, originalCommit)) {
       ObjectId tree = m.getResultTreeId();
       if (tree.equals(mergeTip.getTree())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 4139fc5..a23b78c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -33,7 +33,6 @@
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
 
 import com.google.common.base.Function;
-import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
@@ -168,6 +167,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.regex.Matcher;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index 51c2a80..5df6caf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -16,11 +16,15 @@
 
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.util.MagicBranch;
@@ -46,6 +50,13 @@
   private static final Logger log = LoggerFactory
       .getLogger(ReceiveCommitsAdvertiseRefsHook.class);
 
+  @VisibleForTesting
+  @AutoValue
+  public abstract static class Result {
+    public abstract Map<String, Ref> allRefs();
+    public abstract Set<ObjectId> additionalHaves();
+  }
+
   private final Provider<InternalChangeQuery> queryProvider;
   private final Project.NameKey projectName;
 
@@ -77,28 +88,46 @@
         throw ex;
       }
     }
+    Result r = advertiseRefs(oldRefs);
+    rp.setAdvertisedRefs(r.allRefs(), r.additionalHaves());
+  }
+
+  @VisibleForTesting
+  public Result advertiseRefs(Map<String, Ref> oldRefs) {
     Map<String, Ref> r = Maps.newHashMapWithExpectedSize(oldRefs.size());
+    Set<ObjectId> allPatchSets = Sets.newHashSetWithExpectedSize(oldRefs.size());
     for (Map.Entry<String, Ref> e : oldRefs.entrySet()) {
       String name = e.getKey();
       if (!skip(name)) {
         r.put(name, e.getValue());
       }
+      if (name.startsWith(RefNames.REFS_CHANGES)) {
+        allPatchSets.add(e.getValue().getObjectId());
+      }
     }
-    rp.setAdvertisedRefs(r, advertiseOpenChanges());
+    return new AutoValue_ReceiveCommitsAdvertiseRefsHook_Result(
+        r, advertiseOpenChanges(allPatchSets));
   }
 
-  private Set<ObjectId> advertiseOpenChanges() {
+  private Set<ObjectId> advertiseOpenChanges(Set<ObjectId> allPatchSets) {
     // Advertise some recent open changes, in case a commit is based on one.
     int limit = 32;
     try {
       Set<ObjectId> r = Sets.newHashSetWithExpectedSize(limit);
       for (ChangeData cd : queryProvider.get()
+          .setRequestedFields(ImmutableSet.of(ChangeField.PATCH_SET.getName()))
           .enforceVisibility(true)
           .setLimit(limit)
           .byProjectOpen(projectName)) {
         PatchSet ps = cd.currentPatchSet();
         if (ps != null) {
-          r.add(ObjectId.fromString(ps.getRevision().get()));
+          ObjectId id = ObjectId.fromString(ps.getRevision().get());
+          // Ensure we actually observed a patch set ref pointing to this
+          // object, in case the database is out of sync with the repo and the
+          // object doesn't actually exist.
+          if (allPatchSets.contains(id)) {
+            r.add(id);
+          }
         }
       }
       return r;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java
index 562db08..96593ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.common.base.Optional;
-
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.io.IOException;
+import java.util.Optional;
 
 /**
  * Simple short-lived cache of individual refs read from a repo.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java
index 1dfa51e..77f697a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.common.base.Optional;
-
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefDatabase;
@@ -24,6 +22,7 @@
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 
 /** {@link RefCache} backed directly by a repository. */
 public class RepoRefCache implements RefCache {
@@ -42,9 +41,7 @@
       return id;
     }
     Ref ref = refdb.exactRef(refName);
-    id = ref != null
-        ? Optional.of(ref.getObjectId())
-        : Optional.<ObjectId>absent();
+    id = Optional.ofNullable(ref).map(Ref::getObjectId);
     ids.put(refName, id);
     return id;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index 00ab31b..a0f729a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -38,6 +38,7 @@
 import java.util.concurrent.Delayed;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.concurrent.RunnableScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.ThreadFactory;
@@ -356,6 +357,15 @@
             ((CanceledWhileRunning) runnable).setCanceledWhileRunning();
           }
         }
+        if (runnable instanceof Future<?>) {
+          // Creating new futures eventually passes through
+          // AbstractExecutorService#schedule, which will convert the Guava
+          // Future to a Runnable, thereby making it impossible for the
+          // cancellation to propagate from ScheduledThreadPool's task back to
+          // the Guava future, so kludge it here.
+          ((Future<?>) runnable).cancel(mayInterruptIfRunning);
+        }
+
         executor.remove(this);
         executor.purge();
         return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 31da05c..c0d96c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -107,7 +107,7 @@
       try {
         newCommit = args.mergeUtil.createCherryPickFromCommit(
             args.repo, args.inserter, args.mergeTip.getCurrentTip(), toMerge,
-            committer, cherryPickCmtMsg, args.rw);
+            committer, cherryPickCmtMsg, args.rw, 0);
       } catch (MergeConflictException mce) {
         // Keep going in the case of a single merge failure; the goal is to
         // cherry-pick as many commits as possible.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
index 10f5ecb..faa6934 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
@@ -18,7 +18,6 @@
 
 import com.google.common.base.Function;
 import com.google.common.base.MoreObjects;
-import com.google.common.base.Optional;
 import com.google.common.base.Predicates;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
@@ -33,6 +32,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 /** Specific version of a secondary index schema. */
 public class Schema<T> {
@@ -149,15 +149,15 @@
       FieldDef<T, ?>... rest) {
     FieldDef<T, ?> field = fields.get(first.getName());
     if (field != null) {
-      return Optional.<FieldDef<T, ?>> of(checkSame(field, first));
+      return Optional.of(checkSame(field, first));
     }
     for (FieldDef<T, ?> f : rest) {
       field = fields.get(f.getName());
       if (field != null) {
-        return Optional.<FieldDef<T, ?>> of(checkSame(field, f));
+        return Optional.of(checkSame(field, f));
       }
     }
-    return Optional.absent();
+    return Optional.empty();
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index c683513..03f5df8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -19,7 +19,6 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -41,7 +40,6 @@
 import com.google.gerrit.server.index.SchemaUtil;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
 import com.google.gwtorm.protobuf.CodecFactory;
@@ -632,10 +630,9 @@
         @Override
         public Integer get(ChangeData input, FillArgs args)
             throws OrmException {
-          Optional<ChangedLines> changedLines = input.changedLines();
-          return changedLines.isPresent()
-              ? changedLines.get().insertions + changedLines.get().deletions
-              : null;
+          return input.changedLines()
+              .map(c -> c.insertions + c.deletions)
+              .orElse(null);
         }
       };
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index dd8bbaa..70a4888 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.errors.EmailException;
@@ -43,6 +42,7 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 
 /** Send comments, after the author of them hit used Publish Comments in the UI.
@@ -252,7 +252,7 @@
       } catch (OrmException e) {
         log.warn("Could not find the parent of this comment: "
             + child.toString());
-        parent = Optional.absent();
+        parent = Optional.empty();
       }
       if (parent.isPresent()) {
         String msg = parent.get().message.trim();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
index 3322776..61ebfae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -230,7 +230,7 @@
     checkColumns(PatchSet.Id.class, 1, 2);
     checkColumns(PatchSet.class, 1, 2, 3, 4, 5, 6, 8);
     checkColumns(PatchSetApproval.Key.class, 1, 2, 3);
-    checkColumns(PatchSetApproval.class, 1, 2, 3, 6, 7);
+    checkColumns(PatchSetApproval.class, 1, 2, 3, 6, 7, 8);
     checkColumns(PatchLineComment.Key.class, 1, 2);
     checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
   }
@@ -692,6 +692,11 @@
 
       // ReviewDb allows timestamps before patch set was created, but NoteDb
       // truncates this to the patch set creation timestamp.
+      //
+      // ChangeRebuilder ensures all post-submit approvals happen after the
+      // actual submit, so the timestamps may not line up. This shouldn't really
+      // happen, because postSubmit shouldn't be set in ReviewDb until after the
+      // change is submitted in ReviewDb, but you never know.
       Timestamp ta = a.getGranted();
       Timestamp tb = b.getGranted();
       PatchSet psa = checkNotNull(bundleA.patchSets.get(a.getPatchSetId()));
@@ -700,10 +705,12 @@
       List<String> exclude = new ArrayList<>(1);
       if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
         excludeGranted =
-            ta.before(psa.getCreatedOn()) && tb.equals(psb.getCreatedOn());
+            (ta.before(psa.getCreatedOn()) && tb.equals(psb.getCreatedOn()))
+            || ta.compareTo(tb) < 0;
       } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
         excludeGranted =
-            tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn());
+            tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn())
+            || tb.compareTo(ta) < 0;
       }
       if (excludeGranted) {
         exclude.add("granted");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index edb5b4e..68be2c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -25,12 +25,11 @@
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Ordering;
@@ -349,7 +348,7 @@
 
   // Lazy defensive copies of mutable ReviewDb types, to avoid polluting the
   // ChangeNotesCache from handlers.
-  private ImmutableMap<PatchSet.Id, PatchSet> patchSets;
+  private ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets;
   private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals;
 
   @VisibleForTesting
@@ -368,18 +367,26 @@
     return change;
   }
 
-  public ImmutableMap<PatchSet.Id, PatchSet> getPatchSets() {
+  public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() {
     if (patchSets == null) {
-      patchSets = ImmutableMap.copyOf(
-          Maps.transformValues(state.patchSets(), PatchSet::new));
+      ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b =
+          ImmutableSortedMap.orderedBy(comparing(PatchSet.Id::get));
+      for (Map.Entry<PatchSet.Id, PatchSet> e : state.patchSets()) {
+        b.put(e.getKey(), new PatchSet(e.getValue()));
+      }
+      patchSets = b.build();
     }
     return patchSets;
   }
 
   public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovals() {
     if (approvals == null) {
-      approvals = ImmutableListMultimap.copyOf(
-          Multimaps.transformValues(state.approvals(), PatchSetApproval::new));
+      ImmutableListMultimap.Builder<PatchSet.Id, PatchSetApproval> b =
+          ImmutableListMultimap.builder();
+      for (Map.Entry<PatchSet.Id, PatchSetApproval> e : state.approvals()) {
+        b.put(e.getKey(), new PatchSetApproval(e.getValue()));
+      }
+      approvals = b.build();
     }
     return approvals;
   }
@@ -526,7 +533,7 @@
 
   public PatchSet getCurrentPatchSet() {
     PatchSet.Id psId = change.currentPatchSetId();
-    return checkNotNull(state.patchSets().get(psId),
+    return checkNotNull(getPatchSets().get(psId),
         "missing current patch set %s", psId.get());
   }
 
@@ -559,7 +566,7 @@
   @Override
   protected ObjectId readRef(Repository repo) throws IOException {
     return refs != null
-        ? refs.get(getRefName()).orNull()
+        ? refs.get(getRefName()).orElse(null)
         : super.readRef(repo);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index 198eb2f..73b7aec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -101,7 +101,8 @@
           + P // status
           + P + set(state.pastAssignees(), K)
           + P + set(state.hashtags(), str(10))
-          + P + map(state.patchSets(), patchSet())
+          + P + list(state.patchSets(), patchSet())
+          + P + list(state.allPastReviewers(), approval())
           + P + list(state.reviewerUpdates(), 4 * O + K + K + P)
           + P + list(state.submitRecords(), P + list(2, str(4) + P + K) + P)
           + P + list(state.allChangeMessages(), changeMessage())
@@ -172,6 +173,15 @@
           + P; // pushCertificate
     }
 
+    private static int approval() {
+      return O
+          + P + patchSetId() + P + K + P + O + str(10)
+          + 2 // value
+          + P + T // granted
+          + P // tag
+          + P; // realAccountId
+    }
+
     private static int changeMessage() {
       int key = K + str(20);
       return O
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index a3ef2ce..bd20423 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -35,7 +35,6 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Enums;
-import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.HashBasedTable;
@@ -90,6 +89,7 @@
 import java.util.Map;
 import java.util.NavigableSet;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -131,6 +131,7 @@
   private final Set<PatchSet.Id> deletedPatchSets;
   private final Map<PatchSet.Id, PatchSetState> patchSetStates;
   private final Map<ApprovalKey, PatchSetApproval> approvals;
+  private final List<PatchSetApproval> bufferedApprovals;
   private final List<ChangeMessage> allChangeMessages;
   private final Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet;
 
@@ -160,6 +161,7 @@
     this.noteUtil = noteUtil;
     this.metrics = metrics;
     approvals = new LinkedHashMap<>();
+    bufferedApprovals = new ArrayList<>();
     reviewers = HashBasedTable.create();
     allPastReviewers = new ArrayList<>();
     reviewerUpdates = new ArrayList<>();
@@ -213,7 +215,7 @@
         topic,
         originalSubject,
         submissionId,
-        assignee != null ? assignee.orNull() : null,
+        assignee != null ? assignee.orElse(null) : null,
         status,
 
         Sets.newLinkedHashSet(Lists.reverse(pastAssignees)),
@@ -281,9 +283,6 @@
     if (branch == null) {
       branch = parseBranch(commit);
     }
-    if (status == null) {
-      status = parseStatus(commit);
-    }
 
     PatchSet.Id psId = parsePatchSetId(commit);
     if (currentPatchSetId == null || psId.get() > currentPatchSetId.get()) {
@@ -343,6 +342,12 @@
       parseSubmitRecords(commit.getFooterLineValues(FOOTER_SUBMITTED_WITH));
     }
 
+    if (status == null) {
+      status = parseStatus(commit);
+    }
+
+    // Parse approvals after status to treat approvals in the same commit as
+    // "Status: merged" as non-post-submit.
     for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
       parseApproval(psId, accountId, realAccountId, ts, line);
     }
@@ -503,10 +508,10 @@
       Optional<Account.Id> parsedAssignee;
       if (assigneeValue.equals("")) {
         // Empty footer found, assignee deleted
-        parsedAssignee = Optional.absent();
+        parsedAssignee = Optional.empty();
       } else {
         PersonIdent ident = RawParseUtils.parsePersonIdent(assigneeValue);
-        parsedAssignee = Optional.fromNullable(noteUtil.parseIdent(ident, id));
+        parsedAssignee = Optional.ofNullable(noteUtil.parseIdent(ident, id));
       }
       if (assignee == null) {
         assignee = parsedAssignee;
@@ -538,12 +543,21 @@
     } else if (statusLines.size() > 1) {
       throw expectedOneFooter(FOOTER_STATUS, statusLines);
     }
-    Optional<Change.Status> status = Enums.getIfPresent(
-        Change.Status.class, statusLines.get(0).toUpperCase());
-    if (!status.isPresent()) {
+    Change.Status status = Enums.getIfPresent(
+        Change.Status.class, statusLines.get(0).toUpperCase()).orNull();
+    if (status == null) {
       throw invalidFooter(FOOTER_STATUS, statusLines.get(0));
     }
-    return status.get();
+    // All approvals after MERGED and before the next status change get the
+    // postSubmit bit. (Currently the state can't change from MERGED to
+    // something else, but just in case.)
+    if (status == Change.Status.MERGED) {
+      for (PatchSetApproval psa : bufferedApprovals) {
+        psa.setPostSubmit(true);
+      }
+    }
+    bufferedApprovals.clear();
+    return status;
   }
 
   private PatchSet.Id parsePatchSetId(ChangeNotesCommit commit)
@@ -567,10 +581,11 @@
     }
     String withParens = psIdLine.substring(s + 1);
     if (withParens.startsWith("(") && withParens.endsWith(")")) {
-      Optional<PatchSetState> state = Enums.getIfPresent(PatchSetState.class,
-          withParens.substring(1, withParens.length() - 1).toUpperCase());
-      if (state.isPresent()) {
-        return state.get();
+      PatchSetState state = Enums.getIfPresent(PatchSetState.class,
+              withParens.substring(1, withParens.length() - 1).toUpperCase())
+          .orNull();
+      if (state != null) {
+        return state;
       }
     }
     throw invalidFooter(FOOTER_PATCH_SET, psIdLine);
@@ -665,15 +680,18 @@
       throw parseException(
           "patch set %s requires an identified user as uploader", psId.get());
     }
+    PatchSetApproval psa;
     if (line.startsWith("-")) {
-      parseRemoveApproval(psId, accountId, realAccountId, ts, line);
+      psa = parseRemoveApproval(psId, accountId, realAccountId, ts, line);
     } else {
-      parseAddApproval(psId, accountId, realAccountId, ts, line);
+      psa = parseAddApproval(psId, accountId, realAccountId, ts, line);
     }
+    bufferedApprovals.add(psa);
   }
 
-  private void parseAddApproval(PatchSet.Id psId, Account.Id committerId,
-      Account.Id realAccountId, Timestamp ts, String line)
+  private PatchSetApproval parseAddApproval(PatchSet.Id psId,
+      Account.Id committerId, Account.Id realAccountId, Timestamp ts,
+      String line)
       throws ConfigInvalidException {
     // There are potentially 3 accounts involved here:
     //  1. The account from the commit, which is the effective IdentifiedUser
@@ -726,11 +744,12 @@
     if (!approvals.containsKey(k)) {
       approvals.put(k, psa);
     }
+    return psa;
   }
 
-  private void parseRemoveApproval(PatchSet.Id psId, Account.Id committerId,
-      Account.Id realAccountId, Timestamp ts, String line)
-      throws ConfigInvalidException {
+  private PatchSetApproval parseRemoveApproval(PatchSet.Id psId,
+      Account.Id committerId, Account.Id realAccountId, Timestamp ts,
+      String line) throws ConfigInvalidException {
     // See comments in parseAddApproval about the various users involved.
     Account.Id effectiveAccountId;
     String label;
@@ -774,6 +793,7 @@
     if (!approvals.containsKey(k)) {
       approvals.put(k, remove);
     }
+    return remove;
   }
 
   private void parseSubmitRecords(List<String> lines)
@@ -787,10 +807,9 @@
         submitRecords.add(rec);
         int s = line.indexOf(' ');
         String statusStr = s >= 0 ? line.substring(0, s) : line;
-        Optional<SubmitRecord.Status> status =
-            Enums.getIfPresent(SubmitRecord.Status.class, statusStr);
-        checkFooter(status.isPresent(), FOOTER_SUBMITTED_WITH, line);
-        rec.status = status.get();
+        rec.status =
+            Enums.getIfPresent(SubmitRecord.Status.class, statusStr).orNull();
+        checkFooter(rec.status != null, FOOTER_SUBMITTED_WITH, line);
         if (s >= 0) {
           rec.errorMessage = line.substring(s);
         }
@@ -802,10 +821,9 @@
         }
         rec.labels.add(label);
 
-        Optional<SubmitRecord.Label.Status> status = Enums.getIfPresent(
-            SubmitRecord.Label.Status.class, line.substring(0, c));
-        checkFooter(status.isPresent(), FOOTER_SUBMITTED_WITH, line);
-        label.status = status.get();
+        label.status = Enums.getIfPresent(
+            SubmitRecord.Label.Status.class, line.substring(0, c)).orNull();
+        checkFooter(label.status != null, FOOTER_SUBMITTED_WITH, line);
         int c2 = line.indexOf(": ", c + 2);
         if (c2 >= 0) {
           label.label = line.substring(c + 2, c2);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 67eb328..e0f7920 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -15,14 +15,12 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkNotNull;
-import static java.util.Comparator.comparing;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Multimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -59,17 +57,17 @@
     return new AutoValue_ChangeNotesState(
         change.getId(),
         null,
-        ImmutableSet.<Account.Id>of(),
-        ImmutableSet.<String>of(),
-        ImmutableSortedMap.<PatchSet.Id, PatchSet>of(),
-        ImmutableListMultimap.<PatchSet.Id, PatchSetApproval>of(),
+        ImmutableSet.of(),
+        ImmutableSet.of(),
+        ImmutableList.of(),
+        ImmutableList.of(),
         ReviewerSet.empty(),
-        ImmutableList.<Account.Id>of(),
-        ImmutableList.<ReviewerStatusUpdate>of(),
-        ImmutableList.<SubmitRecord>of(),
-        ImmutableList.<ChangeMessage>of(),
-        ImmutableListMultimap.<PatchSet.Id, ChangeMessage>of(),
-        ImmutableListMultimap.<RevId, Comment>of());
+        ImmutableList.of(),
+        ImmutableList.of(),
+        ImmutableList.of(),
+        ImmutableList.of(),
+        ImmutableListMultimap.of(),
+        ImmutableListMultimap.of());
   }
 
   static ChangeNotesState create(
@@ -117,8 +115,8 @@
             status),
         ImmutableSet.copyOf(pastAssignees),
         ImmutableSet.copyOf(hashtags),
-        ImmutableSortedMap.copyOf(patchSets, comparing(PatchSet.Id::get)),
-        ImmutableListMultimap.copyOf(approvals),
+        ImmutableList.copyOf(patchSets.entrySet()),
+        ImmutableList.copyOf(approvals.entries()),
         reviewers,
         ImmutableList.copyOf(allPastReviewers),
         ImmutableList.copyOf(reviewerUpdates),
@@ -161,8 +159,8 @@
   // Other related to this Change.
   abstract ImmutableSet<Account.Id> pastAssignees();
   abstract ImmutableSet<String> hashtags();
-  abstract ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets();
-  abstract ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals();
+  abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSet>> patchSets();
+  abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSetApproval>> approvals();
 
   abstract ReviewerSet reviewers();
   abstract ImmutableList<Account.Id> allPastReviewers();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 3642dff..1a417e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -39,7 +39,6 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Table;
@@ -84,6 +83,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -284,7 +284,7 @@
   }
 
   public void removeApprovalFor(Account.Id reviewer, String label) {
-    approvals.put(label, reviewer, Optional.<Short> absent());
+    approvals.put(label, reviewer, Optional.empty());
   }
 
   public void merge(RequestId submissionId,
@@ -414,7 +414,7 @@
   }
 
   public void removeAssignee() {
-    this.assignee = Optional.absent();
+    this.assignee = Optional.empty();
   }
 
   public Map<Account.Id, ReviewerStateInternal> getReviewers() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
index 4aed71e..ad54f02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
@@ -22,7 +22,6 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Optional;
 import com.google.common.base.Predicates;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableMap;
@@ -40,6 +39,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * The state of all relevant NoteDb refs across all repos corresponding to a
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
index 72a1f1b..c4d7277 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -16,11 +16,8 @@
 
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
-import com.google.common.collect.ImmutableMultimap;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
@@ -29,7 +26,6 @@
 import com.google.inject.name.Names;
 
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Repository;
 
 public class NoteDbModule extends FactoryModule {
   private final Config cfg;
@@ -79,13 +75,6 @@
         }
 
         @Override
-        public boolean rebuildProject(ReviewDb db,
-            ImmutableMultimap<NameKey, Id> allChanges, NameKey project,
-            Repository allUsersRepo) {
-          return false;
-        }
-
-        @Override
         public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId) {
           return null;
         }
@@ -95,6 +84,12 @@
             NoteDbUpdateManager manager) {
           return null;
         }
+
+        @Override
+        public void buildUpdates(NoteDbUpdateManager manager,
+            ChangeBundle bundle) {
+          // Do nothing.
+        }
       });
       bind(new TypeLiteral<Cache<ChangeNotesCache.Key, ChangeNotesState>>() {})
           .annotatedWith(Names.named(ChangeNotesCache.CACHE_NAME))
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 18cac47..1f64bcc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -21,7 +21,6 @@
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.base.Optional;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableList;
@@ -58,6 +57,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -362,7 +362,7 @@
         StagedResult r = StagedResult.create(
             e.getKey(),
             NoteDbChangeState.Delta.create(
-                e.getKey(), Optional.<ObjectId>absent(), e.getValue()),
+                e.getKey(), Optional.empty(), e.getValue()),
             changeRepo, allUsersRepo);
         checkState(r.changeCommands().isEmpty(),
             "should not have change commands when updating only drafts: %s", r);
@@ -550,7 +550,7 @@
     for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) {
       String refName = e.getKey();
       Collection<U> updates = e.getValue();
-      ObjectId old = or.cmds.get(refName).or(ObjectId.zeroId());
+      ObjectId old = or.cmds.get(refName).orElse(ObjectId.zeroId());
       // Only actually write to the ref if one of the updates explicitly allows
       // us to do so, i.e. it is known to represent a new change. This avoids
       // writing partial change meta if the change hasn't been backfilled yet.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
index 7bfb20c..1c585fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
@@ -15,9 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableMultimap;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
@@ -29,7 +27,6 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -83,23 +80,6 @@
   }
 
   @Override
-  public boolean rebuildProject(ReviewDb db,
-      ImmutableMultimap<Project.NameKey, Change.Id> allChanges,
-      Project.NameKey project, Repository allUsersRepo)
-      throws NoSuchChangeException, IOException, OrmException,
-      ConfigInvalidException {
-    if (failNextUpdate.getAndSet(false)) {
-      throw new IOException("Update failed");
-    }
-    boolean result =
-        delegate.rebuildProject(db, allChanges, project, allUsersRepo);
-    if (stealNextUpdate.getAndSet(false)) {
-      throw new IOException("Update stolen");
-    }
-    return result;
-  }
-
-  @Override
   public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
       throws NoSuchChangeException, IOException, OrmException {
     // Don't inspect stealNextUpdate; that happens in execute() below.
@@ -119,4 +99,11 @@
     }
     return result;
   }
+
+  @Override
+  public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
+      throws IOException, OrmException {
+    // Don't check for manual failure; that happens in execute().
+    delegate.buildUpdates(manager, bundle);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
index 3aba7c5..4fed25d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
@@ -38,4 +38,9 @@
     checkUpdate(update);
     update.putApproval(psa.getLabel(), psa.getValue());
   }
+
+  @Override
+  protected boolean isPostSubmitApproval() {
+    return psa.isPostSubmit();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
index ea96012..a63fbad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
@@ -14,11 +14,9 @@
 
 package com.google.gerrit.server.notedb.rebuild;
 
-import com.google.common.collect.ImmutableMultimap;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.ChangeBundle;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager;
@@ -28,7 +26,6 @@
 import com.google.gwtorm.server.SchemaFactory;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
 import java.util.concurrent.Callable;
@@ -69,11 +66,8 @@
       ChangeBundle bundle) throws NoSuchChangeException, IOException,
       OrmException, ConfigInvalidException;
 
-  public abstract boolean rebuildProject(ReviewDb db,
-      ImmutableMultimap<Project.NameKey, Change.Id> allChanges,
-      Project.NameKey project, Repository allUsersRepo)
-      throws NoSuchChangeException, IOException, OrmException,
-      ConfigInvalidException;
+  public abstract void buildUpdates(NoteDbUpdateManager manager,
+      ChangeBundle bundle) throws IOException, OrmException;
 
   public abstract NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
       throws NoSuchChangeException, IOException, OrmException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index 1916610..b6e25a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.notedb.rebuild;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
@@ -23,18 +22,16 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.toList;
 
-import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Table;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.common.FormatUtil;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -44,7 +41,6 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
@@ -75,12 +71,8 @@
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -88,7 +80,6 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -96,6 +87,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 public class ChangeRebuilderImpl extends ChangeRebuilder {
@@ -166,6 +158,8 @@
       throws NoSuchChangeException, IOException, OrmException,
       ConfigInvalidException {
     db = ReviewDbUtil.unwrapDb(db);
+    // Read change just to get project; this instance is then discarded so we
+    // can read a consistent ChangeBundle inside a transaction.
     Change change = db.changes().get(changeId);
     if (change == null) {
       throw new NoSuchChangeException(changeId);
@@ -260,40 +254,7 @@
   }
 
   @Override
-  public boolean rebuildProject(ReviewDb db,
-      ImmutableMultimap<Project.NameKey, Change.Id> allChanges,
-      Project.NameKey project, Repository allUsersRepo)
-      throws NoSuchChangeException, IOException, OrmException,
-      ConfigInvalidException {
-    checkArgument(allChanges.containsKey(project));
-    boolean ok = true;
-    ProgressMonitor pm = new TextProgressMonitor(new PrintWriter(System.out));
-    pm.beginTask(
-        FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
-    try (NoteDbUpdateManager manager = updateManagerFactory.create(project);
-        ObjectInserter allUsersInserter = allUsersRepo.newObjectInserter();
-        RevWalk allUsersRw = new RevWalk(allUsersInserter.newReader())) {
-      manager.setAllUsersRepo(allUsersRepo, allUsersRw, allUsersInserter,
-          new ChainedReceiveCommands(allUsersRepo));
-      for (Change.Id changeId : allChanges.get(project)) {
-        try {
-          buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
-        } catch (NoPatchSetsException e) {
-          log.warn(e.getMessage());
-        } catch (Throwable t) {
-          log.error("Failed to rebuild change " + changeId, t);
-          ok = false;
-        }
-        pm.update(1);
-      }
-      manager.execute();
-    } finally {
-      pm.endTask();
-    }
-    return ok;
-  }
-
-  private void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
+  public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
       throws IOException, OrmException {
     manager.setCheckExpectedState(false);
     Change change = new Change(bundle.getChange());
@@ -427,7 +388,9 @@
 
   private void sortAndFillEvents(Change change, Change noteDbChange,
       List<Event> events, Integer minPsNum) {
-    events.add(new FinalUpdatesEvent(change, noteDbChange));
+    Event finalUpdates = new FinalUpdatesEvent(change, noteDbChange);
+    events.add(finalUpdates);
+    setPostSubmitDeps(events);
     new EventSorter(events).sort();
 
     // Ensure the first event in the list creates the change, setting the author
@@ -476,6 +439,17 @@
     }
   }
 
+  private void setPostSubmitDeps(List<Event> events) {
+    Optional<Event> submitEvent = Lists.reverse(events).stream()
+        .filter(Event::isSubmit)
+        .findFirst();
+    if (submitEvent.isPresent()) {
+      events.stream()
+          .filter(Event::isPostSubmitApproval)
+          .forEach(e -> e.addDep(submitEvent.get()));
+    }
+  }
+
   private void flushEventsToUpdate(NoteDbUpdateManager manager,
       EventList<Event> events, Change change) throws OrmException, IOException {
     if (events.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
index 78df2d2..4dcdec6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
@@ -86,6 +86,14 @@
 
   abstract void apply(ChangeUpdate update) throws OrmException, IOException;
 
+  protected boolean isPostSubmitApproval() {
+    return false;
+  }
+
+  protected boolean isSubmit() {
+    return false;
+  }
+
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
index 4f6e2e8..59ff49e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
@@ -23,21 +23,32 @@
 
 import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.Objects;
 
-class EventList<E extends Event> extends ArrayList<E> {
-  private static final long serialVersionUID = 1L;
+class EventList<E extends Event> implements Iterable<E> {
+  private final ArrayList<E> list = new ArrayList<>();
+  private boolean isSubmit;
 
-  private E getLast() {
-    return get(size() - 1);
+  @Override
+  public Iterator<E> iterator() {
+    return list.iterator();
   }
 
-  private long getLastTime() {
-    return getLast().when.getTime();
+  void add(E e) {
+    list.add(e);
+    if (e.isSubmit()) {
+      isSubmit = true;
+    }
   }
 
-  private long getFirstTime() {
-    return get(0).when.getTime();
+  void clear() {
+    list.clear();
+    isSubmit = false;
+  }
+
+  boolean isEmpty() {
+    return list.isEmpty();
   }
 
   boolean canAdd(E e) {
@@ -55,6 +66,10 @@
         || !Objects.equals(e.tag, last.tag)) {
       return false; // Different patch set, author, or tag.
     }
+    if (e.isPostSubmitApproval() && isSubmit) {
+      // Post-submit approvals must come after the update that submits.
+      return false;
+    }
 
     long t = e.when.getTime();
     long tFirst = getFirstTime();
@@ -114,4 +129,24 @@
   String getTag() {
     return getLast().tag;
   }
+
+  private E get(int i) {
+    return list.get(i);
+  }
+
+  private int size() {
+    return list.size();
+  }
+
+  private E getLast() {
+    return list.get(list.size() - 1);
+  }
+
+  private long getLastTime() {
+    return getLast().when.getTime();
+  }
+
+  private long getFirstTime() {
+    return list.get(0).when.getTime();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
index 9babc28..17cf8ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
@@ -58,4 +58,9 @@
       update.setSubjectForCommit("Final NoteDb migration updates");
     }
   }
+
+  @Override
+  protected boolean isSubmit() {
+    return change.getStatus() == Change.Status.MERGED;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java
index c7632e8..5bc05d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.notedb.rebuild;
 
-import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -25,6 +24,7 @@
 
 import java.sql.Timestamp;
 import java.util.Map;
+import java.util.Optional;
 import java.util.regex.Pattern;
 
 class StatusChangeEvent extends Event {
@@ -40,7 +40,7 @@
       Change change, Change noteDbChange) {
     String msg = message.getMessage();
     if (msg == null) {
-      return Optional.absent();
+      return Optional.empty();
     }
     for (Map.Entry<Change.Status, Pattern> e : PATTERNS.entrySet()) {
       if (e.getValue().matcher(msg).matches()) {
@@ -48,12 +48,12 @@
             message, change, noteDbChange, e.getKey()));
       }
     }
-    return Optional.absent();
+    return Optional.empty();
   }
 
+  private final Change.Status status;
   private final Change change;
   private final Change noteDbChange;
-  private final Change.Status status;
 
   private StatusChangeEvent(ChangeMessage message, Change change,
       Change noteDbChange, Change.Status status) {
@@ -87,4 +87,9 @@
       noteDbChange.setSubmissionId(change.getSubmissionId());
     }
   }
+
+  @Override
+  protected boolean isSubmit() {
+    return status == Change.Status.MERGED;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 7db206f..42324f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
@@ -59,6 +58,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.Callable;
 
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
index 73d7b1e..354ccf9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -17,12 +17,9 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.collect.Iterables.transform;
 
-import com.google.common.base.Optional;
-import com.google.common.base.Predicates;
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
@@ -49,6 +46,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.jar.Attributes;
 import java.util.jar.JarEntry;
@@ -192,9 +190,9 @@
     String annotationName;
     String annotationValue;
     String[] interfaces;
-    Iterable<String> exports;
+    Collection<String> exports;
 
-    private ClassData(Iterable<String> exports) {
+    private ClassData(Collection<String> exports) {
       super(Opcodes.ASM5);
       this.exports = exports;
     }
@@ -214,9 +212,12 @@
 
     @Override
     public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+      if (!visible) {
+        return null;
+      }
       Optional<String> found =
-          Iterables.tryFind(exports, Predicates.equalTo(desc));
-      if (visible && found.isPresent()) {
+          exports.stream().filter(x -> x.equals(desc)).findAny();
+      if (found.isPresent()) {
         annotationName = desc;
         return new AbstractAnnotationVisitor() {
           @Override
@@ -287,10 +288,11 @@
   }
 
   @Override
-  public Optional<PluginEntry> getEntry(String resourcePath) throws IOException {
+  public Optional<PluginEntry> getEntry(String resourcePath)
+      throws IOException {
     JarEntry jarEntry = jarFile.getJarEntry(resourcePath);
     if (jarEntry == null || jarEntry.getSize() == 0) {
-      return Optional.absent();
+      return Optional.empty();
     }
 
     return Optional.of(resourceOf(jarEntry));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
index 15bb92f..c333638 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.plugins;
 
-import com.google.common.base.Optional;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
@@ -23,6 +21,7 @@
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.Map;
+import java.util.Optional;
 import java.util.jar.Manifest;
 
 /**
@@ -51,9 +50,8 @@
     }
 
     @Override
-    public Optional<PluginEntry> getEntry(String resourcePath)
-        throws IOException {
-      return Optional.absent();
+    public Optional<PluginEntry> getEntry(String resourcePath) {
+      return Optional.empty();
     }
 
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
index 74ded73..c6077f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
@@ -11,13 +11,13 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.gerrit.server.plugins;
 
-import com.google.common.base.Optional;
+package com.google.gerrit.server.plugins;
 
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * Plugin static resource entry
@@ -38,7 +38,7 @@
       };
 
   private static final Map<Object, String> EMPTY_ATTRS = Collections.emptyMap();
-  private static final Optional<Long> NO_SIZE = Optional.absent();
+  private static final Optional<Long> NO_SIZE = Optional.empty();
 
   private final String name;
   private final long time;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 407d113..4d55c80 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -20,7 +20,6 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
-import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
@@ -92,6 +91,7 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 public class ChangeData {
@@ -611,12 +611,12 @@
     Optional<PatchList> r = patchLists.get(psId);
     if (r == null) {
       if (!lazyLoad) {
-        return Optional.absent();
+        return Optional.empty();
       }
       try {
         r = Optional.of(patchListCache.get(c, ps));
       } catch (PatchListNotAvailableException e) {
-        r = Optional.absent();
+        r = Optional.empty();
       }
       patchLists.put(psId, r);
     }
@@ -631,12 +631,12 @@
     Optional<DiffSummary> r = diffSummaries.get(psId);
     if (r == null) {
       if (!lazyLoad) {
-        return Optional.absent();
+        return Optional.empty();
       }
       try {
         r = Optional.of(patchListCache.getDiffSummary(c, ps));
       } catch (PatchListNotAvailableException e) {
-        r = Optional.absent();
+        r = Optional.empty();
       }
       diffSummaries.put(psId, r);
     }
@@ -646,24 +646,20 @@
   private Optional<ChangedLines> computeChangedLines() throws OrmException {
     Change c = change();
     if (c == null) {
-      return Optional.absent();
+      return Optional.empty();
     }
     PatchSet ps = currentPatchSet();
     if (ps == null) {
-      return Optional.absent();
+      return Optional.empty();
     }
-    Optional<PatchList> p = getPatchList(c, ps);
-    if (!p.isPresent()) {
-      return Optional.absent();
-    }
-    return Optional.of(
-        new ChangedLines(p.get().getInsertions(), p.get().getDeletions()));
+    return getPatchList(c, ps).map(
+        p -> new ChangedLines(p.getInsertions(), p.getDeletions()));
   }
 
   public Optional<ChangedLines> changedLines() throws OrmException {
     if (changedLines == null) {
       if (!lazyLoad) {
-        return Optional.absent();
+        return Optional.empty();
       }
       changedLines = computeChangedLines();
     }
@@ -675,7 +671,7 @@
   }
 
   public void setNoChangedLines() {
-    changedLines = Optional.absent();
+    changedLines = Optional.empty();
   }
 
   public Change.Id getId() {
@@ -951,13 +947,10 @@
    * @throws OrmException an error occurred reading the database.
    */
   public Optional<PatchSetApproval> getSubmitApproval()
-    throws OrmException {
-    for (PatchSetApproval psa : currentApprovals()) {
-      if (psa.isLegacySubmit()) {
-        return Optional.fromNullable(psa);
-      }
-    }
-    return Optional.absent();
+      throws OrmException {
+    return currentApprovals().stream()
+        .filter(PatchSetApproval::isLegacySubmit)
+        .findFirst();
   }
 
   public ReviewerSet reviewers() throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 57f5710..390f8b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -19,7 +19,6 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -85,6 +84,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index a7a8191..a514642 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -33,7 +33,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_133> C = Schema_133.class;
+  public static final Class<Schema_134> C = Schema_134.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_134.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_134.java
new file mode 100644
index 0000000..fa01ff3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_134.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_134 extends SchemaVersion {
+  @Inject
+  Schema_134(Provider<Schema_133> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java
index f5849c1..2a86401 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.base.Optional;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 
@@ -24,6 +23,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 public class AuthorizedKeysTest {
   private static final String KEY1 =
@@ -168,7 +168,7 @@
    * @return the expected line for this key in the authorized_keys file
    */
   private static String addDeletedKey(List<Optional<AccountSshKey>> keys) {
-    keys.add(Optional.<AccountSshKey> absent());
+    keys.add(Optional.empty());
     return AuthorizedKeys.DELETED_KEY_COMMENT + "\n";
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index ea33e65..d750bc4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -399,6 +399,81 @@
   }
 
   @Test
+  public void approvalsPostSubmit() throws Exception {
+    Change c = newChange();
+    RequestId submissionId = RequestId.forChange(c);
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putApproval("Code-Review", (short) 1);
+    update.putApproval("Verified", (short) 1);
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.merge(submissionId, ImmutableList.of(
+        submitRecord("NOT_READY", null,
+          submitLabel("Verified", "OK", changeOwner.getAccountId()),
+          submitLabel("Code-Review", "NEED", null))));
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.putApproval("Code-Review", (short) 2);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    List<PatchSetApproval> approvals =
+        Lists.newArrayList(notes.getApprovals().values());
+    assertThat(approvals).hasSize(2);
+    assertThat(approvals.get(0).getLabel()).isEqualTo("Verified");
+    assertThat(approvals.get(0).getValue()).isEqualTo((short) 1);
+    assertThat(approvals.get(0).isPostSubmit()).isFalse();
+    assertThat(approvals.get(1).getLabel()).isEqualTo("Code-Review");
+    assertThat(approvals.get(1).getValue()).isEqualTo((short) 2);
+    assertThat(approvals.get(1).isPostSubmit()).isTrue();
+  }
+
+  @Test
+  public void approvalsDuringSubmit() throws Exception {
+    Change c = newChange();
+    RequestId submissionId = RequestId.forChange(c);
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putApproval("Code-Review", (short) 1);
+    update.putApproval("Verified", (short) 1);
+    update.commit();
+
+    Account.Id ownerId = changeOwner.getAccountId();
+    Account.Id otherId = otherUser.getAccountId();
+    update = newUpdate(c, otherUser);
+    update.merge(submissionId, ImmutableList.of(
+        submitRecord("NOT_READY", null,
+          submitLabel("Verified", "OK", ownerId),
+          submitLabel("Code-Review", "NEED", null))));
+    update.putApproval("Other-Label", (short) 1);
+    update.putApprovalFor(ownerId, "Code-Review", (short) 2);
+    update.commit();
+
+    update = newUpdate(c, otherUser);
+    update.putApproval("Other-Label", (short) 2);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+
+    List<PatchSetApproval> approvals =
+        Lists.newArrayList(notes.getApprovals().values());
+    assertThat(approvals).hasSize(3);
+    assertThat(approvals.get(0).getAccountId()).isEqualTo(ownerId);
+    assertThat(approvals.get(0).getLabel()).isEqualTo("Verified");
+    assertThat(approvals.get(0).getValue()).isEqualTo(1);
+    assertThat(approvals.get(0).isPostSubmit()).isFalse();
+    assertThat(approvals.get(1).getAccountId()).isEqualTo(ownerId);
+    assertThat(approvals.get(1).getLabel()).isEqualTo("Code-Review");
+    assertThat(approvals.get(1).getValue()).isEqualTo(2);
+    assertThat(approvals.get(1).isPostSubmit()).isFalse(); // During submit.
+    assertThat(approvals.get(2).getAccountId()).isEqualTo(otherId);
+    assertThat(approvals.get(2).getLabel()).isEqualTo("Other-Label");
+    assertThat(approvals.get(2).getValue()).isEqualTo(2);
+    assertThat(approvals.get(2).isPostSubmit()).isTrue();
+  }
+
+  @Test
   public void multipleReviewers() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
index 216f71b..f2bf2be 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
@@ -20,7 +20,6 @@
 import static com.google.gerrit.server.notedb.NoteDbChangeState.parse;
 import static org.eclipse.jgit.lib.ObjectId.zeroId;
 
-import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -33,6 +32,8 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
+import java.util.Optional;
+
 /** Unit tests for {@link NoteDbChangeState}. */
 public class NoteDbChangeStateTest {
   static {
@@ -134,7 +135,7 @@
   // Static factory methods to avoid type arguments when using as method args.
 
   private static Optional<ObjectId> noMetaId() {
-    return Optional.absent();
+    return Optional.empty();
   }
 
   private static Optional<ObjectId> metaId(ObjectId id) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
index 103fee3..23950d4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.testutil;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.base.Enums;
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 
@@ -47,12 +48,10 @@
       return OFF;
     }
     value = value.toUpperCase().replace("-", "_");
-    Optional<NoteDbMode> mode = Enums.getIfPresent(NoteDbMode.class, value);
-    if (!mode.isPresent()) {
-      throw new IllegalArgumentException(
-          "Invalid value for " + VAR + ": " + System.getenv(VAR));
-    }
-    return mode.get();
+    NoteDbMode mode = Enums.getIfPresent(NoteDbMode.class, value).orNull();
+    checkArgument(mode != null,
+        "Invalid value for %s: %s", VAR, System.getenv(VAR));
+    return mode;
   }
 
   public static boolean readWrite() {
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index c66c4aa..1871b0c 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -24,6 +24,7 @@
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.CmdLineParser;
 import org.kohsuke.args4j.Option;
+
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
diff --git a/lib/prolog/prolog.bzl b/lib/prolog/prolog.bzl
index d4e9e08..995d81c 100644
--- a/lib/prolog/prolog.bzl
+++ b/lib/prolog/prolog.bzl
@@ -18,7 +18,7 @@
     name,
     srcs,
     deps = [],
-    visibility = []):
+    **kwargs):
   genrule2(
     name = name + '__pl2j',
     cmd = '$(location //lib/prolog:compiler_bin) ' +
@@ -32,5 +32,5 @@
     name = name,
     srcs = [':' + name + '__pl2j'],
     deps = ['//lib/prolog:runtime'] + deps,
-    visibility = visibility,
+    **kwargs
   )
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index 09981c0..6cb2fcd 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 09981c0638f7241a4f435baaa96bd6112a1edaa9
+Subproject commit 6cb2fcdb54d73abf572a5fbb45db644c56f657ce
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index e9c66c6..45f6975 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit e9c66c6f08edb641d3c935c2fdcaa3fbf3a85d29
+Subproject commit 45f69757be42ac61dfba915b6e2801525416f1af
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index 530b7be..adf0bf1 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -13,6 +13,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
+<!-- Polymer included for the html import polyfill. -->
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../bower_components/web-component-tester/browser.js"></script>
 <title>gr-path-list-behavior</title>
 
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 09d5b84..96c6193 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
@@ -66,6 +66,7 @@
       <section hidden$="[[!_actionCount(actions.*, _additionalActions.*)]]">
         <template is="dom-repeat" items="[[_changeActionValues]]" as="action">
           <gr-button title$="[[action.title]]"
+              hidden$="[[_computeActionHidden(action.__key, _hiddenChangeActions.*)]]"
               primary$="[[action.__primary]]"
               hidden$="[[!action.enabled]]"
               data-action-key$="[[action.__key]]"
@@ -78,6 +79,7 @@
       <section hidden$="[[!_actionCount(_revisionActions.*, _additionalActions.*)]]">
         <template is="dom-repeat" items="[[_revisionActionValues]]" as="action">
           <gr-button title$="[[action.title]]"
+              hidden$="[[_computeActionHidden(action.__key, _hiddenRevisionActions.*)]]"
               primary$="[[action.__primary]]"
               disabled$="[[!action.enabled]]"
               data-action-key$="[[action.__key]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 9960f90..e35d3cb 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -102,6 +102,14 @@
         type: Array,
         value: function() { return []; },
       },
+      _hiddenChangeActions: {
+        type: Array,
+        value: function() { return []; },
+      },
+      _hiddenRevisionActions: {
+        type: Array,
+        value: function() { return []; },
+      },
     },
 
     ActionType: ActionType,
@@ -169,6 +177,24 @@
       ], value);
     },
 
+    setActionHidden: function(type, key, hidden) {
+      var path;
+      if (type === ActionType.CHANGE) {
+        path = '_hiddenChangeActions';
+      } else if (type === ActionType.REVISION) {
+        path = '_hiddenRevisionActions';
+      } else {
+        throw Error('Invalid action type given: ' + type);
+      }
+
+      var idx = this.get(path).indexOf(key);
+      if (hidden && idx === -1) {
+        this.push(path, key);
+      } else if (!hidden && idx !== -1) {
+        this.splice(path, idx, 1);
+      }
+    },
+
     _indexOfActionButtonWithKey: function(key) {
       for (var i = 0; i < this._additionalActions.length; i++) {
         if (this._additionalActions[i].__key === key) {
@@ -270,6 +296,12 @@
           this._getRevision(this.change, this.patchNum));
     },
 
+    _computeActionHidden: function(key, hiddenActionsChangeRecord) {
+      var hiddenActions =
+          (hiddenActionsChangeRecord && hiddenActionsChangeRecord.base) || [];
+      return hiddenActions.indexOf(key) !== -1;
+    },
+
     _getRevision: function(change, patchNum) {
       var num = window.parseInt(patchNum, 10);
       for (var hash in change.revisions) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index a342c87..5fe41ec 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -97,6 +97,64 @@
       return element.reload();
     });
 
+    test('hide revision action', function(done) {
+      flush(function() {
+        var buttonEl = element.$$('[data-action-key="submit"]');
+        assert.isOk(buttonEl);
+        assert.isFalse(buttonEl.hasAttribute('hidden'));
+        assert.throws(element.setActionHidden.bind(element, 'invalid type'));
+        element.setActionHidden(element.ActionType.REVISION,
+            element.RevisionActions.SUBMIT, true);
+        assert.lengthOf(element._hiddenRevisionActions, 1);
+        element.setActionHidden(element.ActionType.REVISION,
+            element.RevisionActions.SUBMIT, true);
+        assert.lengthOf(element._hiddenRevisionActions, 1);
+        flush(function() {
+          var buttonEl = element.$$('[data-action-key="submit"]');
+          assert.isOk(buttonEl);
+          assert.isTrue(buttonEl.hasAttribute('hidden'));
+
+          element.setActionHidden(element.ActionType.REVISION,
+            element.RevisionActions.SUBMIT, false);
+          flush(function() {
+            var buttonEl = element.$$('[data-action-key="submit"]');
+            assert.isOk(buttonEl);
+            assert.isFalse(buttonEl.hasAttribute('hidden'));
+            done();
+          });
+        });
+      });
+    });
+
+    test('hide change action', function(done) {
+      flush(function() {
+        var buttonEl = element.$$('[data-action-key="/"]');
+        assert.isOk(buttonEl);
+        assert.isFalse(buttonEl.hasAttribute('hidden'));
+        assert.throws(element.setActionHidden.bind(element, 'invalid type'));
+        element.setActionHidden(element.ActionType.CHANGE,
+            element.ChangeActions.DELETE, true);
+        assert.lengthOf(element._hiddenChangeActions, 1);
+        element.setActionHidden(element.ActionType.CHANGE,
+            element.ChangeActions.DELETE, true);
+        assert.lengthOf(element._hiddenChangeActions, 1);
+        flush(function() {
+          var buttonEl = element.$$('[data-action-key="/"]');
+          assert.isOk(buttonEl);
+          assert.isTrue(buttonEl.hasAttribute('hidden'));
+
+          element.setActionHidden(element.ActionType.CHANGE,
+            element.RevisionActions.DELETE, false);
+          flush(function() {
+            var buttonEl = element.$$('[data-action-key="/"]');
+            assert.isOk(buttonEl);
+            assert.isFalse(buttonEl.hasAttribute('hidden'));
+            done();
+          });
+        });
+      });
+    });
+
     test('buttons show', function(done) {
       flush(function() {
         var buttonEls = Polymer.dom(element.root).querySelectorAll('gr-button');
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 108ac92..3ad2d257 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -59,6 +59,9 @@
       .notApproved {
         background-color: #ffd4d4;
       }
+      .labelStatus {
+        max-width: 9em;
+      }
       @media screen and (max-width: 50em), screen and (min-width: 75em) {
         :host {
           display: table;
@@ -174,6 +177,14 @@
         </span>
       </section>
     </template>
+    <template is="dom-if" if="[[_showLabelStatus]]">
+      <section>
+        <span class="title">Label Status</span>
+        <span class="value labelStatus">
+          [[_computeSubmitStatus(change.labels)]]
+        </span>
+      </section>
+    </template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-change-metadata.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 661a296..184215e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -37,6 +37,10 @@
         type: Boolean,
         computed: '_computeShowReviewersByState(serverConfig)',
       },
+      _showLabelStatus: {
+        type: Boolean,
+        computed: '_computeShowLabelStatus(change)',
+      },
     },
 
     behaviors: [
@@ -143,5 +147,30 @@
         }
       }.bind(this));
     },
+
+    _computeShowLabelStatus: function(change) {
+      var isNewChange = change.status === this.ChangeStatus.NEW;
+      var hasLabels = Object.keys(change.labels).length > 0;
+      return isNewChange && hasLabels;
+    },
+
+    _computeSubmitStatus: function(labels) {
+      var missingLabels = [];
+      var output = '';
+      for (var label in labels) {
+        var obj = labels[label];
+        if (!obj.optional && !obj.approved) {
+          missingLabels.push(label);
+        }
+      }
+      if (missingLabels.length) {
+        output += 'Needs ';
+        output += missingLabels.join(' and ');
+        output += missingLabels.length > 1 ? ' labels' : ' label';
+      } else {
+        output = 'Ready to ubmit';
+      }
+      return output;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 22080e8..281636c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -78,6 +78,22 @@
       assert.isTrue(hasCc());
     });
 
+    test('computes submit status', function() {
+      var labels = {};
+      assert.equal(element._computeSubmitStatus(labels), 'Ready to Submit');
+      labels = { test: {} };
+      assert.equal(element._computeSubmitStatus(labels), 'Needs test Label');
+      labels.test.approved = true;
+      assert.equal(element._computeSubmitStatus(labels), 'Ready to Submit');
+      labels.test.approved = false;
+      labels.test.optional = true;
+      assert.equal(element._computeSubmitStatus(labels), 'Ready to Submit');
+      labels.test.optional = false;
+      labels.test2 = {};
+      assert.equal(element._computeSubmitStatus(labels),
+          'Needs test and test2 Labels');
+    });
+
     suite('remove reviewer votes', function() {
       var sandbox;
       setup(function() {
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index e4ab44b..48a3d60 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -24,6 +24,7 @@
       :host {
         display: block;
         font-family: var(--monospace-font-family);
+        word-wrap: break-word;
       }
       .file {
         border-top: 1px solid #ddd;
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 78e4634..5c950dd 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
@@ -40,7 +40,13 @@
         margin-bottom: .5em;
       }
       .rightControls {
+        display: flex;
+        flex-wrap: wrap;
         font-weight: normal;
+        justify-content: flex-end;
+      }
+      .separator {
+        margin: 0 .25em;
       }
       .reviewed,
       .status {
@@ -150,9 +156,9 @@
       <div>Files</div>
       <div class="rightControls">
         <gr-button link on-tap="_expandAllDiffs">Show diffs</gr-button>
-        /
+        <span class="separator">/</span>
         <gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button>
-        /
+        <span class="separator">/</span>
         <select
             id="modeSelect"
             is="gr-select"
@@ -161,7 +167,7 @@
           <option value="SIDE_BY_SIDE">Side By Side</option>
           <option value="UNIFIED_DIFF">Unified</option>
         </select>
-        /
+        <span class="separator">/</span>
         <label>
           Diff against
           <select id="patchChange" on-change="_handlePatchChange">
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 6e88267..7a7e761 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -98,13 +98,6 @@
             <td><span class="key">u</span></td>
             <td>Up to change list</td>
           </tr>
-          <tr>
-            <td>
-              <span class="key modifier">Shift</span>
-              <span class="key">i</span>
-            </td>
-            <td>Show/hide inline diffs</td>
-          </tr>
         </tbody>
         <!-- Diff View -->
         <tbody hidden$="[[!_computeInView(view, 'gr-diff-view')]]" hidden>
@@ -177,6 +170,13 @@
             <td>Show selected file</td>
           </tr>
           <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">i</span>
+            </td>
+            <td>Show/hide all inline diffs</td>
+          </tr>
+          <tr>
             <td><span class="key">i</span></td>
             <td>Show/hide selected inline diff</td>
           </tr>
@@ -214,6 +214,17 @@
             <td>Go to previous comment thread</td>
           </tr>
           <tr>
+            <td><span class="key">e</span></td>
+            <td>Expand all comment threads</td>
+          </tr>
+          <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">e</span>
+            </td>
+            <td>Collapse all comment threads</td>
+          </tr>
+          <tr>
             <td>
               <span class="key modifier">Shift</span>
               <span class="key">←</span>
@@ -277,6 +288,17 @@
             <td>Show previous comment thread</td>
           </tr>
           <tr>
+            <td><span class="key">e</span></td>
+            <td>Expand all comment threads</td>
+          </tr>
+          <tr>
+            <td>
+              <span class="key modifier">Shift</span>
+              <span class="key">e</span>
+            </td>
+            <td>Collapse all comment threads</td>
+          </tr>
+          <tr>
             <td>
               <span class="key modifier">Shift</span>
               <span class="key">←</span>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 32dd687..9c0e902 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -111,7 +111,7 @@
     locationChanged: function() {
       var page = '';
       var pathname = this._getPathname();
-      if (pathname.startsWith('/q/')) {
+      if (pathname.indexOf('/q/') === 0) {
         page = '/q/';
       } else if (pathname.match(CHANGE_VIEW_REGEX)) { // change view
         page = '/c/';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 3ad7c74..0a595c7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -394,7 +394,7 @@
         },
 
         /**
-         * @returns {Boolean} whether any of the lines in _groups are longer
+         * @return {Boolean} whether any of the lines in _groups are longer
          * than SYNTAX_MAX_LINE_LENGTH.
          */
         _anyLineTooLong: function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index 305c36a..963084a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -29,6 +29,10 @@
         type: Array,
         value: function() { return []; },
       },
+      keyEventTarget: {
+        type: Object,
+        value: function() { return document.body; },
+      },
       patchNum: String,
       path: String,
       projectConfig: Object,
@@ -41,6 +45,10 @@
       _orderedComments: Array,
     },
 
+    behaviors: [
+      Gerrit.KeyboardShortcutBehavior,
+    ],
+
     listeners: {
       'comment-update': '_handleCommentUpdate',
     },
@@ -80,6 +88,22 @@
       this._orderedComments = this._sortedComments(this.comments);
     },
 
+    _handleKey: function(e) {
+      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+      if (e.keyCode === 69) { // 'e'
+        e.preventDefault();
+        this._expandCollapseComments(e.shiftKey);
+      }
+    },
+
+    _expandCollapseComments: function(actionIsCollapse) {
+      var comments =
+          Polymer.dom(this.root).querySelectorAll('gr-diff-comment');
+      comments.forEach(function(comment) {
+        comment.collapsed = actionIsCollapse;
+      });
+    },
+
     _sortedComments: function(comments) {
       comments.sort(function(c1, c2) {
         var c1Date = c1.__date || util.parseDate(c1.updated);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index 641dc0f..eb87f56 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -54,30 +54,25 @@
           message: 'i like you, too',
           in_reply_to: 'sallys_confession',
           updated: '2015-12-25 15:00:20.396000000',
-        },
-        {
+        }, {
           id: 'sallys_confession',
           message: 'i like you, jack',
           updated: '2015-12-24 15:00:20.396000000',
-        },
-        {
+        }, {
           id: 'sally_to_dr_finklestein',
           message: 'i’m running away',
           updated: '2015-10-31 09:00:20.396000000',
-        },
-        {
+        }, {
           id: 'sallys_defiance',
           in_reply_to: 'sally_to_dr_finklestein',
           message: 'i will poison you so i can get away',
           updated: '2015-10-31 15:00:20.396000000',
-        },
-        {
+        }, {
           id: 'dr_finklesteins_response',
           in_reply_to: 'sally_to_dr_finklestein',
           message: 'no i will pull a thread and your arm will fall off',
           updated: '2015-10-31 11:00:20.396000000'
-        },
-        {
+        }, {
           id: 'sallys_mission',
           message: 'i have to find santa',
           updated: '2015-12-24 21:00:20.396000000'
@@ -89,31 +84,26 @@
           id: 'sally_to_dr_finklestein',
           message: 'i’m running away',
           updated: '2015-10-31 09:00:20.396000000',
-        },
-        {
+        }, {
           id: 'dr_finklesteins_response',
           in_reply_to: 'sally_to_dr_finklestein',
           message: 'no i will pull a thread and your arm will fall off',
           updated: '2015-10-31 11:00:20.396000000'
-        },
-        {
+        }, {
           id: 'sallys_defiance',
           in_reply_to: 'sally_to_dr_finklestein',
           message: 'i will poison you so i can get away',
           updated: '2015-10-31 15:00:20.396000000',
-        },
-        {
+        }, {
           id: 'sallys_confession',
           message: 'i like you, jack',
           updated: '2015-12-24 15:00:20.396000000',
-        },
-        {
+        }, {
           id: 'jacks_reply',
           message: 'i like you, too',
           in_reply_to: 'sallys_confession',
           updated: '2015-12-25 15:00:20.396000000',
-        },
-        {
+        }, {
           id: 'sallys_mission',
           message: 'i have to find santa',
           updated: '2015-12-24 21:00:20.396000000'
@@ -247,20 +237,17 @@
           message: 'i like you, too',
           in_reply_to: 'sallys_confession',
           updated: '2015-12-25 15:00:20.396000000',
-        },
-        {
+        }, {
           id: 'sallys_confession',
           in_reply_to: 'nonexistent_comment',
           message: 'i like you, jack',
           updated: '2015-12-24 15:00:20.396000000',
-        },
-        {
+        }, {
           id: 'sally_to_dr_finklestein',
           in_reply_to: 'nonexistent_comment',
           message: 'i’m running away',
           updated: '2015-10-31 09:00:20.396000000',
-        },
-        {
+        }, {
           id: 'sallys_defiance',
           message: 'i will poison you so i can get away',
           updated: '2015-10-31 15:00:20.396000000',
@@ -268,5 +255,37 @@
       element.comments = comments;
       assert.equal(4, element._orderedComments.length);
     });
+
+    test('keyboard shortcuts', function() {
+      var comments = [
+        {
+          id: 'jacks_reply',
+          message: 'i like you, too',
+          in_reply_to: 'sallys_confession',
+          updated: '2015-12-25 15:00:20.396000000',
+        }, {
+          id: 'sallys_confession',
+          in_reply_to: 'nonexistent_comment',
+          message: 'i like you, jack',
+          updated: '2015-12-24 15:00:20.396000000',
+        }, {
+          id: 'sally_to_dr_finklestein',
+          in_reply_to: 'nonexistent_comment',
+          message: 'i’m running away',
+          updated: '2015-10-31 09:00:20.396000000',
+        }, {
+          id: 'sallys_defiance',
+          message: 'i will poison you so i can get away',
+          updated: '2015-10-31 15:00:20.396000000',
+        }];
+      element.comments = comments;
+      var expandCollapseStub = sinon.stub(element, '_expandCollapseComments');
+      MockInteractions.pressAndReleaseKeyOn(element, 69);  // 'e'
+      assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
+
+      MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift');  // 'e'
+      assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
+      expandCollapseStub.restore();
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index 864712d..ebb1f87 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -170,9 +170,9 @@
         <div class="show-hide">
           <label class="show-hide">
             <input type="checkbox" class="show-hide"
-               checked$="[[_commentCollapsed]]"
+               checked$="[[collapsed]]"
                on-change="_handleToggleCollapsed">
-            [[_computeShowHideText(_commentCollapsed)]]
+            [[_computeShowHideText(collapsed)]]
           </label>
         </div>
       </div>
@@ -186,7 +186,7 @@
       <gr-linked-text class="message"
           pre
           content="[[comment.message]]"
-          collapsed="[[_commentCollapsed]]"
+          collapsed="[[collapsed]]"
           config="[[projectConfig.commentlinks]]"></gr-linked-text>
       <div class="actions" hidden$="[[!showActions]]">
         <gr-button class="action reply" on-tap="_handleReply">Reply</gr-button>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 791f949..2abd0a8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -81,7 +81,7 @@
       },
       patchNum: String,
       showActions: Boolean,
-      _commentCollapsed: {
+      collapsed: {
         type: Boolean,
         value: true,
         observer: '_toggleCollapseClass',
@@ -103,7 +103,7 @@
 
     attached: function() {
       if (this.editing) {
-        this._commentCollapsed = false;
+        this.collapsed = false;
       }
     },
 
@@ -226,11 +226,11 @@
     },
 
     _handleToggleCollapsed: function() {
-      this._commentCollapsed = !this._commentCollapsed;
+      this.collapsed = !this.collapsed;
     },
 
-    _toggleCollapseClass: function(_commentCollapsed) {
-      if (_commentCollapsed) {
+    _toggleCollapseClass: function(collapsed) {
+      if (collapsed) {
         this.$.container.classList.add('collapsed');
       } else {
         this.$.container.classList.remove('collapsed');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index b05746e..0a591ae 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -66,6 +66,7 @@
 
     test('collapsible comments', function() {
       // When a comment (not draft) is loaded, it should be collapsed
+      assert.isTrue(element.collapsed);
       assert.isFalse(isVisible(element.$$('gr-linked-text')),
           'gr-linked-text is not visible');
       assert.isFalse(isVisible(element.$$('.actions')),
@@ -80,6 +81,7 @@
 
       // When the header row is clicked, the comment should expand
       MockInteractions.tap(element.$.header);
+      assert.isFalse(element.collapsed);
       assert.isTrue(isVisible(element.$$('gr-linked-text')),
           'gr-linked-text is visible');
       assert.isTrue(isVisible(element.$$('.actions')),
@@ -124,6 +126,29 @@
           'Should navigate to ' + dest + ' without triggering nav');
       showStub.restore();
     });
+
+    test('comment expand and collapse', function() {
+      element.collapsed = true;
+      assert.isFalse(isVisible(element.$$('gr-linked-text')),
+          'gr-linked-text is not visible');
+      assert.isFalse(isVisible(element.$$('.actions')),
+          'actions are not visible');
+      assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+          'textarea is not visible');
+      assert.isTrue(isVisible(element.$$('.collapsedContent')),
+          'header middle content is visible');
+
+      element.collapsed = false;
+      assert.isFalse(element.collapsed);
+      assert.isTrue(isVisible(element.$$('gr-linked-text')),
+          'gr-linked-text is visible');
+      assert.isTrue(isVisible(element.$$('.actions')),
+          'actions are visible');
+      assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+          'textarea is not visible');
+      assert.isFalse(isVisible(element.$$('.collapsedContent')),
+          'header middle content is is not visible');
+    });
   });
 
   suite('gr-diff-comment draft tests', function() {
@@ -213,6 +238,7 @@
         assert.ok(e.detail.comment);
         done();
       });
+      assert.isTrue(element.collapsed);
       assert.isFalse(isVisible(element.$$('gr-linked-text')),
           'gr-linked-text is not visible');
       assert.isFalse(isVisible(element.$$('.actions')),
@@ -223,6 +249,7 @@
           'header middle content is visible');
 
       MockInteractions.tap(element.$.header);
+      assert.isFalse(element.collapsed);
       assert.isTrue(isVisible(element.$$('gr-linked-text')),
           'gr-linked-text is visible');
       assert.isTrue(isVisible(element.$$('.actions')),
@@ -235,6 +262,7 @@
       // When the edit button is pressed, should still see the actions
       // and also textarea
       MockInteractions.tap(element.$$('.edit'));
+      assert.isFalse(element.collapsed);
       assert.isFalse(isVisible(element.$$('gr-linked-text')),
           'gr-linked-text is not visible');
       assert.isTrue(isVisible(element.$$('.actions')),
@@ -247,6 +275,7 @@
       // When toggle again, everything should be hidden except for textarea
       // and header middle content should be visible
       MockInteractions.tap(element.$.header);
+      assert.isTrue(element.collapsed);
       assert.isFalse(isVisible(element.$$('gr-linked-text')),
           'gr-linked-text is not visible');
       assert.isFalse(isVisible(element.$$('.actions')),
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index 57a2e1f..7a263d0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -101,6 +101,13 @@
             bind-value="{{_newPrefs.tab_size}}">
       </div>
       <div class="pref">
+        <label for="fontSizeInput">Font Size</label>
+        <input is="iron-input" type="number" id="fontSizeInput"
+               prevent-invalid-input
+               allowed-pattern="[0-9]"
+               bind-value="{{_newPrefs.font_size}}">
+      </div>
+      <div class="pref">
         <label for="showTabsInput">Show tabs</label>
         <input is="iron-input" type="checkbox" id="showTabsInput"
             on-tap="_handleShowTabsTap">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
index 3c4d457..c595663 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -41,6 +41,7 @@
     test('model changes', function() {
       element.prefs = {
         context: 10,
+        font_size: 12,
         line_length: 100,
         show_tabs: true,
         tab_size: 8,
@@ -51,11 +52,13 @@
       element.$.contextSelect.value = '50';
       element.fire('change', {}, {node: element.$.contextSelect});
       element.$.columnsInput.bindValue = 80;
+      element.$.fontSizeInput.bindValue = 10;
       element.$.tabSizeInput.bindValue = 4;
       MockInteractions.tap(element.$.showTabsInput);
       MockInteractions.tap(element.$.syntaxHighlightInput);
 
       assert.equal(element._newPrefs.context, 50);
+      assert.equal(element._newPrefs.font_size, 10);
       assert.equal(element._newPrefs.line_length, 80);
       assert.equal(element._newPrefs.tab_size, 4);
       assert.isFalse(element._newPrefs.show_tabs);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index f91c8ef..d3cc461 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -258,7 +258,7 @@
       }
 
       // If there is a range to hide.
-      if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 0) {
+      if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 1) {
         var linesBeforeCtx = lines.slice(0, hiddenRange[0]);
         var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
         var linesAfterCtx = lines.slice(hiddenRange[1]);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index c89d9b5..f6d0e37 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -511,6 +511,17 @@
           assert.equal(result[0].lines.length, rows.length);
         });
 
+        test('_sharedGroupsFromRows no single line collapse', function() {
+          rows = rows.slice(0, 7);
+          var context = 3;
+          var result = element._sharedGroupsFromRows(
+              rows, context, 10, 100);
+
+          // Results in one uncollapsed group with all rows.
+          assert.equal(result.length, 1, 'Results in one group');
+          assert.equal(result[0].lines.length, rows.length);
+        });
+
         test('_deltaLinesFromRows', function() {
           var startLineNum = 10;
           var result = element._deltaLinesFromRows(GrDiffLine.Type.ADD, rows,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index e6d3653..c68e925 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -112,12 +112,10 @@
     _handleCopy: function(e) {
       var commentSelected = false;
       var target = this._getCopyEventTarget(e);
+      if (target.type === 'textarea') { return; }
+      if (!this._elementDescendedFromClass(target, 'content')) { return; }
       if (this.classList.contains(SelectionClass.COMMENT)) {
         commentSelected = true;
-      } else {
-        if (!this._elementDescendedFromClass(target, 'content')) {
-          return;
-        }
       }
       var lineEl = this.diffBuilder.getLineElByChild(target);
       if (!lineEl) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index 71fa233..774430a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -79,6 +79,18 @@
             <div class="contentText" data-side="right">some other text</div>
           </td>
         </tr>
+        <tr>
+          <td class="lineNum left" data-value="4">4</td>
+          <td class="content">
+            <div class="contentText" data-side="left">ga ga</div>
+            <div data-side="left">
+              <div class="gr-diff-comment-thread">
+                <textarea data-side="right">test for textarea copying</textarea>
+              </div>
+            </div>
+          </td>
+          <td class="lineNum right" data-value="4">4</td>
+        </tr>
       </table>
     </gr-diff-selection>
   </template>
@@ -87,14 +99,14 @@
 <script>
   suite('gr-diff-selection', function() {
     var element;
-    var copyEventStub;
+    var sandbox;
 
     var emulateCopyOn = function(target) {
       var fakeEvent = {
         target: target,
-        preventDefault: sinon.stub(),
+        preventDefault: sandbox.stub(),
         clipboardData: {
-          setData: sinon.stub(),
+          setData: sandbox.stub(),
         },
       };
       element._getCopyEventTarget.returns(target);
@@ -104,10 +116,11 @@
 
     setup(function() {
       element = fixture('basic');
-      sinon.stub(element, '_getCopyEventTarget');
+      sandbox = sinon.sandbox.create();
+      sandbox.stub(element, '_getCopyEventTarget');
       element._cachedDiffBuilder = {
-        getLineElByChild: sinon.stub().returns({}),
-        getSideByLineEl: sinon.stub(),
+        getLineElByChild: sandbox.stub().returns({}),
+        getSideByLineEl: sandbox.stub(),
         diffElement: element.querySelector('#diffTable'),
       };
       element.diff = {
@@ -128,6 +141,10 @@
       };
     });
 
+    teardown(function() {
+      sandbox.restore();
+    });
+
     test('applies selected-left on left side click', function() {
       element.classList.add('selected-right');
       element._cachedDiffBuilder.getSideByLineEl.returns('left');
@@ -150,26 +167,26 @@
     });
 
     test('ignores copy for non-content Element', function() {
-      sinon.stub(element, '_getSelectedText');
+      sandbox.stub(element, '_getSelectedText');
       emulateCopyOn(element.querySelector('.other'));
       assert.isFalse(element._getSelectedText.called);
     });
 
     test('asks for text for left side Elements', function() {
       element._cachedDiffBuilder.getSideByLineEl.returns('left');
-      sinon.stub(element, '_getSelectedText');
+      sandbox.stub(element, '_getSelectedText');
       emulateCopyOn(element.querySelector('div.contentText'));
       assert.deepEqual(['left', false], element._getSelectedText.lastCall.args);
     });
 
     test('reacts to copy for content Elements', function() {
-      sinon.stub(element, '_getSelectedText');
+      sandbox.stub(element, '_getSelectedText');
       emulateCopyOn(element.querySelector('div.contentText'));
       assert.isTrue(element._getSelectedText.called);
     });
 
     test('copy event is prevented for content Elements', function() {
-      sinon.stub(element, '_getSelectedText');
+      sandbox.stub(element, '_getSelectedText');
       element._cachedDiffBuilder.getSideByLineEl.returns('left');
       element._getSelectedText.returns('test');
       var event = emulateCopyOn(element.querySelector('div.contentText'));
@@ -177,7 +194,7 @@
     });
 
     test('inserts text into clipboard on copy', function() {
-      sinon.stub(element, '_getSelectedText').returns('the text');
+      sandbox.stub(element, '_getSelectedText').returns('the text');
       var event = emulateCopyOn(element.querySelector('div.contentText'));
       assert.deepEqual(
           ['Text', 'the text'], event.clipboardData.setData.lastCall.args);
@@ -219,5 +236,13 @@
           element._getSelectedText('left', true));
       selection.removeAllRanges();
     });
+
+    test('defers to default behavior for textarea', function() {
+      element.classList.add('selected-left');
+      element.classList.remove('selected-right');
+      var selectedTextSpy = sandbox.spy(element, '_getSelectedText');
+      emulateCopyOn(element.querySelector('textarea'));
+      assert.isFalse(selectedTextSpy.called);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index a2af8c5..7ae5c34 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -129,6 +129,9 @@
           display: block;
           width: 100%;
         }
+        .mobileJumpToFileContainer select {
+          width: 100%;
+        }
       }
     </style>
     <header>
@@ -168,7 +171,7 @@
               <option
                   value$="[[path]]"
                   selected$="[[_computeFileSelected(path, _path)]]">
-                [[_computeFileDisplayName(path)]]
+                [[_computeTruncatedFileDisplayName(path)]]
               </option>
             </template>
           </select>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 03609c0..c5779a4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -16,6 +16,8 @@
 
   var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
 
+  var MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900;
+
   var DiffViewMode = {
     SIDE_BY_SIDE: 'SIDE_BY_SIDE',
     UNIFIED: 'UNIFIED_DIFF',
@@ -105,12 +107,17 @@
         }
       }.bind(this));
       if (this.changeViewState.diffMode === null) {
-        // Initialize with user's diff mode preference. Default to
-        // SIDE_BY_SIDE in the meantime.
-        this.set('changeViewState.diffMode', DiffViewMode.SIDE_BY_SIDE);
-        this.$.restAPI.getPreferences().then(function(prefs) {
-          this.set('changeViewState.diffMode', prefs.diff_view);
-        }.bind(this));
+        // If screen size is small, always default to unified view.
+        if (this._getWindowWidth() < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX) {
+          this.set('changeViewState.diffMode', DiffViewMode.UNIFIED);
+        } else {
+          // Initialize with user's diff mode preference. Default to
+          // SIDE_BY_SIDE in the meantime.
+          this.set('changeViewState.diffMode', DiffViewMode.SIDE_BY_SIDE);
+          this.$.restAPI.getPreferences().then(function(prefs) {
+            this.set('changeViewState.diffMode', prefs.diff_view);
+          }.bind(this));
+        }
       }
 
       if (this._path) {
@@ -155,6 +162,10 @@
       return this.$.restAPI.getPreferences();
     },
 
+    _getWindowWidth: function() {
+      return window.innerWidth;
+    },
+
     _handleReviewedChange: function(e) {
       this._setReviewed(Polymer.dom(e).rootTarget.checked);
     },
@@ -427,7 +438,22 @@
     },
 
     _computeFileDisplayName: function(path) {
-      return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
+      return path === COMMIT_MESSAGE_PATH ? 'Commit message' : path;
+    },
+
+    _computeTruncatedFileDisplayName: function(path) {
+      return path === COMMIT_MESSAGE_PATH ?
+          'Commit message' : this._shortenPath(path);
+    },
+
+    _shortenPath: function(path) {
+      var pathPieces = path.split('/');
+
+      if (pathPieces.length < 2) {
+        return path;
+      }
+      // Character is an ellipsis.
+      return '\u2026/' + pathPieces.pop();
     },
 
     _computeFileSelected: function(path, currentPath) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 566c6a1..ec57726 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -491,6 +491,39 @@
       assert.equal(select.value, 'SIDE_BY_SIDE');
     });
 
+    test('unified view is always default on small screens', function() {
+      var resolvePrefs;
+      var prefsPromise = new Promise(function(resolve) {
+        resolvePrefs = resolve;
+      });
+
+      var getPreferencesStub = sinon.stub(element.$.restAPI, 'getPreferences',
+          function() { return prefsPromise; });
+
+      // Attach a new gr-diff-view so we can intercept the preferences fetch.
+      var view = document.createElement('gr-diff-view');
+
+      view.changeViewState = {diffMode: null};
+
+      sinon.stub(view, '_getWindowWidth', function() {
+        return 800;
+      });
+
+      var select = view.$.modeSelect;
+      fixture('blank').appendChild(view);
+      flushAsynchronousOperations();
+
+      // At this point the diff mode doesn't yet have the user's preference.
+      assert.equal(select.value, 'UNIFIED_DIFF');
+
+      // Receive the overriding preference.
+      resolvePrefs({diff_view: 'SIDE_BY_SIDE'});
+      flushAsynchronousOperations();
+
+      // On small screens, unified should override user perferences
+      assert.equal(select.value, 'UNIFIED_DIFF');
+    });
+
     test('_loadHash', function() {
       assert.isNotOk(element.$.cursor.initialLineNumber);
 
@@ -516,5 +549,25 @@
       assert.isTrue(element._checkForModifiers({shiftKey: true}));
       assert.isFalse(element._checkForModifiers({}));
     });
+
+    test('_shortenPath with long path should add ellipsis', function() {
+      var path =
+          'level1/level2/level3/level4/file.js';
+      var shortenedPath = element._shortenPath(path);
+      // The expected path is truncated with an ellipsis.
+      var expectedPath = '\u2026/file.js';
+      assert.equal(shortenedPath, expectedPath);
+
+      var path = 'level2/file.js';
+      var shortenedPath = element._shortenPath(path);
+      assert.equal(shortenedPath, expectedPath);
+    });
+
+    test('_shortenPath with short path should not add ellipsis', function() {
+      var path = 'file.js';
+      var expectedPath = 'file.js';
+      var shortenedPath = element._shortenPath(path);
+      assert.equal(shortenedPath, expectedPath);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 4672b31..625d6e8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -113,6 +113,11 @@
         max-width: var(--content-width, 80ch);
         min-width: var(--content-width, 80ch);
       }
+      .content,
+      .lineNum {
+        /* Set font size based the user's diff preference. */
+        font-size: var(--font-size, 12px);
+      }
       .content.add .intraline,
       .content.add.darkHighlight {
         background-color: var(--dark-add-highlight-color);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 1a6759b..8886005 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -359,6 +359,7 @@
     _prefsChanged: function(prefs) {
       if (!prefs) { return; }
       this.customStyle['--content-width'] = prefs.line_length + 'ch';
+      this.customStyle['--font-size'] = prefs.font_size + 'px';
       this.updateStyles();
 
       if (this._diff && this._comments) {
@@ -375,6 +376,12 @@
     },
 
     _handleGetDiffError: function(response) {
+      // Loading the diff may respond with 409 if the file is too large. In this
+      // case, use a toast error..
+      if (response.status === 409) {
+        this.fire('server-error', {response: response});
+        return;
+      }
       this.fire('page-error', {response: response});
     },
 
@@ -386,8 +393,8 @@
           this.path,
           this._handleGetDiffError.bind(this)).then(function(diff) {
                this.filesWeblinks = {
-                 meta_a: diff.meta_a && diff.meta_a.web_links,
-                 meta_b: diff.meta_b && diff.meta_b.web_links,
+                 meta_a: diff && diff.meta_a && diff.meta_a.web_links,
+                 meta_b: diff && diff.meta_b && diff.meta_b.web_links,
                };
                return diff;
              }.bind(this));
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index b9b49c0..20ed61f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -327,6 +327,16 @@
         });
         content.click();
       });
+
+      test('_getDiff handles null diff responses', function(done) {
+        stub('gr-rest-api-interface', {
+          getDiff: function() { return Promise.resolve(null); },
+        });
+        element.changeNum = 123;
+        element.patchRange = {basePatchNum: 1, patchNum: 2};
+        element.path = 'file.txt';
+        element._getDiff().then(done);
+      });
     });
 
     suite('logged in', function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
index 9a8ea37..98f6b01 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
@@ -21,26 +21,25 @@
   <template>
     <style>
       :host {
-        --gr-arrow-size: .6em;
+        --gr-arrow-size: .65em;
 
-        background-color: #fff;
-        border: 1px solid #000;
-        border-radius: .5em;
+        background-color: rgba(22, 22, 22, .9);
+        border-radius: 3px;
+        color: #fff;
         cursor: pointer;
-        padding: .3em;
+        font-family: var(--font-family);
+        padding: .5em .75em;
         position: absolute;
         white-space: nowrap;
       }
       .arrow {
-        background: #fff;
-        border: var(--gr-arrow-size) solid #000;
-        border-width: 0 1px 1px 0;
-        height: var(--gr-arrow-size);
-        left: calc(50% - 1em);
-        margin-top: .05em;
+        border: var(--gr-arrow-size) solid transparent;
+        border-top: var(--gr-arrow-size) solid rgba(22, 22, 22, 0.9);
+        height: 0;
+        left: calc(50% - var(--gr-arrow-size));
+        margin-top: .5em;
         position: absolute;
-        transform: rotate(45deg);
-        width: var(--gr-arrow-size);
+        width: 0;
       }
     </style>
     Press <strong>C</strong> to comment.
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index fbd1ef3..29b3c19 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -48,7 +48,7 @@
     ],
 
     listeners: {
-      'tap': '_handleTap',
+      'mousedown': '_handleMouseDown', // See https://crbug.com/gerrit/4767
     },
 
     placeAbove: function(el) {
@@ -87,7 +87,9 @@
       }
     },
 
-    _handleTap: function() {
+    _handleMouseDown: function(e) {
+      e.preventDefault();
+      e.stopPropagation();
       this._fireCreateComment();
     },
 
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 8ac268e..a188ea1 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -166,7 +166,7 @@
     _handleLocationChange: function(e) {
       var hash = e.detail.hash.substring(1);
       var pathname = e.detail.pathname;
-      if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) {
+      if (pathname.indexOf('/c/') === 0 && parseInt(hash, 10) > 0) {
         pathname += '@' + hash;
       }
       this.set('_path', pathname);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 8194c21..45bc6df 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -232,6 +232,17 @@
             </span>
           </section>
           <section>
+            <span class="title">Font Size</span>
+            <span class="value">
+              <input
+                  is="iron-input"
+                  type="number"
+                  prevent-invalid-input
+                  allowed-pattern="[0-9]"
+                  bind-value="{{_diffPrefs.font_size}}">
+            </span>
+          </section>
+          <section>
             <span class="title">Show Tabs</span>
             <span class="value">
               <input
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index 4e98b43..2862226 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -92,6 +92,7 @@
       diffPreferences = {
         context: 10,
         tab_size: 8,
+        font_size: 12,
         line_length: 100,
         cursor_blink_rate: 0,
         intraline_difference: true,
@@ -192,6 +193,8 @@
           .firstElementChild.bindValue, diffPreferences.line_length);
       assert.equal(valueOf('Tab Width', 'diffPreferences')
           .firstElementChild.bindValue, diffPreferences.tab_size);
+      assert.equal(valueOf('Font Size', 'diffPreferences')
+          .firstElementChild.bindValue, diffPreferences.font_size);
       assert.equal(valueOf('Show Tabs', 'diffPreferences')
           .firstElementChild.checked, diffPreferences.show_tabs);
 
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 31918dd..601a056 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -118,11 +118,11 @@
     },
 
     attached: function() {
-      this.listen(document.body, 'click', '_handleBodyClick');
+      this.listen(document.body, 'tap', '_handleBodyTap');
     },
 
     detached: function() {
-      this.unlisten(document.body, 'click', '_handleBodyClick');
+      this.unlisten(document.body, 'tap', '_handleBodyTap');
     },
 
     get focusStart() {
@@ -233,7 +233,7 @@
       }
     },
 
-    _handleBodyClick: function(e) {
+    _handleBodyTap: function(e) {
       var eventPath = Polymer.dom(e).path;
       for (var i = 0; i < eventPath.length; i++) {
         if (eventPath[i] === this) {
@@ -244,6 +244,7 @@
     },
 
     _handleSuggestionTap: function(e) {
+      e.stopPropagation();
       this.$.cursor.setCursor(e.target);
       this._commit();
       this.focus();
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 7a26e72..685f568 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -290,6 +290,7 @@
       assert.isTrue(focusSpy.called);
       assert.isTrue(commitSpy.called);
       assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+      assert.isTrue(element._focused);
       focusSpy.restore();
       commitSpy.restore();
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
index f7c337b..72c7f6e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -33,6 +33,11 @@
     });
   };
 
+  GrChangeActionsInterface.prototype.setActionHidden = function(type, key,
+      hidden) {
+    return this._el.setActionHidden(type, key, hidden);
+  };
+
   GrChangeActionsInterface.prototype.add = function(type, label) {
     return this._el.addActionButton(type, label);
   };
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 4919a5a..93e676c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -119,5 +119,22 @@
         });
       });
     });
+
+    test('hide action buttons', function(done) {
+      var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+      flush(function() {
+        var button = element.$$('[data-action-key="' + key + '"]');
+        assert.isOk(button);
+        assert.isFalse(button.hasAttribute('hidden'));
+        changeActions.setActionHidden(changeActions.ActionType.REVISION, key,
+            true);
+        flush(function() {
+          var button = element.$$('[data-action-key="' + key + '"]');
+          assert.isOk(button);
+          assert.isTrue(button.hasAttribute('hidden'));
+          done();
+        });
+      });
+    });
   });
 </script>
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 6195272..f46404a 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
@@ -191,6 +191,7 @@
           auto_hide_diff_table_header: true,
           context: 10,
           cursor_blink_rate: 0,
+          font_size: 12,
           ignore_whitespace: 'IGNORE_NONE',
           intraline_difference: true,
           line_length: 100,
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index ef0fe51..aa7d07f 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -124,7 +124,7 @@
   outputs = {'war' : '%{name}.war'},
 )
 
-def pkg_war(name, ui = 'ui_optdbg', context = []):
+def pkg_war(name, ui = 'ui_optdbg', context = [], **kwargs):
   ui_deps = []
   if ui:
     ui_deps.append('//gerrit-gwtui:%s' % ui)
@@ -136,4 +136,5 @@
       '//gerrit-main:main_bin_deploy.jar',
       '//gerrit-war:webapp_assets',
     ],
+    **kwargs
   )
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index bdd1794..32f57e86 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -4,7 +4,8 @@
     deps = [],
     srcs = [],
     resources = [],
-    manifest_entries = []):
+    manifest_entries = [],
+    **kwargs):
   # TODO(davido): Fix stamping: run git describe in plugin directory
   # https://github.com/bazelbuild/bazel/issues/1758
   manifest_lines = [
@@ -31,4 +32,5 @@
       ':%s__plugin' % name,
     ],
     visibility = ['//visibility:public'],
+    **kwargs
   )
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
index dfd271d..a8b3f01 100644
--- a/tools/eclipse/BUCK
+++ b/tools/eclipse/BUCK
@@ -15,7 +15,6 @@
     '//gerrit-reviewdb:client_tests',
     '//gerrit-server:server',
     '//gerrit-server:server_tests',
-    '//lib:jimfs',
     '//lib/asciidoctor:asciidoc_lib',
     '//lib/asciidoctor:doc_indexer_lib',
     '//lib/auto:auto-value',